SCM-Manager – Frontend and release for my first plugin
This article is part 3 of the series „Introduction to plugin development for SCM-Manager“
Read the first part now.
In the first post we explained why and how plugins can be used to extend SCM-Manager’s basic functionality, what the basic architecture looks like and how you can share plugins you developed with the community. In the second post we developed the backend for a plugin. In this post we will show you how to create the frontend, connect it to the backend and release the plugin.
Starting with the frontend
The SCM-Manager frontend is written in React with Typescript support. We also use libraries/frameworks like Bulma for the styling and React Query for the data synchronisation. For our plugin it is our goal to configure the custom links globally for SCM-Manager and also to show them in the footer.
Global Configuration
To create the global configuration for the links, we start by adding a dedicated config page to the administration section. For this we create a new React component. With useState
we hold the data of our input fields. Using custom React hooks based on React Query, we can load our data from the server and send our requests away.
A big advantage of React Query is the stale while revalidate
procedure, which leads to a minimization of loading spinners. This is implemented via frontend caches that only exchange data once new data has arrived. The custom hooks manage the loading states for us. We merely use a boolean flag to check if the data has finished loading.
If errors fly, we display them in our Error Boundary in the UI.
Important: You don’t have to create your required components from scratch. We highly recommend the usage of the SCM-Manager provided components and apis.
Structure
We will build an overview for our configuration that consists of two parts:
- A table with all created custom links.
- A small form to create/update custom links.
Our table consists of a <Table/> component that we get from the UI components of the SCM-Manager. Then we add some columns to display our data and functions. Directly into the table we then dump in our custom links data coming from the server. If the table is empty, an info is displayed.
const CustomLinksTable: FC<{ customLinks: CustomLink[] }> = ({ customLinks }) => {
const [t] = useTranslation("plugins");
const { deleteLink, error: deleteError } = useDeleteCustomLink();
return (
<>
<Table data={customLinks} emptyMessage={t("scm-custom-links-plugin.form.table.empty")}>
<TextColumn header={t("scm-custom-links-plugin.form.table.name")} dataKey="name" />
<TextColumn header={t("scm-custom-links-plugin.form.table.url")} dataKey="url" />
<Column header={t("")}>
{(row: any) => (
<Icon
name="trash"
onClick={() => deleteLink((row._links.delete as Link).href)}
title={t("scm-custom-links-plugin.form.table.deleteLink")}
/>
)}
</Column>
</Table>
<ErrorNotification error={deleteError} />
</>
);
};
Translations and Styling
After we have built our config page structurally, we still need the translations of our texts and the CSS finishing touches. For the translations we use I18Next from React and maintain our translations in a JSON file per language.
The CSS can be styled using different approaches. We prefer Bulma as CSS framework and for special cases we like to use styled components. In our case, the components already look good and just need to be positioned harmoniously. For this we use the Bulma Helper classes. Now our config page should look good and fulfill our functional requirements.
Bind the config page
As we have built our configuration page, we still need to integrate it into the administration navigation. This is done by using the configuration binder from ui-components
.
This binder is especially designed to add React components as new configuration entries, in our case into the global administration navigation and also to bind the route.
Example:
import { ConfigurationBinder as configurationBinder } from "@scm-manager/ui-components";
import GlobalConfig from "./GlobalConfig";
configurationBinder.bindGlobal(
"/custom-links", // the link where our new config page will be available
"scm-custom-links-plugin.settings.navLink", // the translation key for the navigation entry which must be translated in the plugins.json files
"customLinksConfig", // the linkname inside the index. If any user does not have this link (not permitted), this page will not be accessible to them.
GlobalConfig // our React component we want to render
);
Now we can already access the config page via the UI and manage our custom links there.
Extension Points
After we have added some custom links, we would like to see them in the footer.
For this we have to create a custom links renderer,
which simply requests them from the server and renders a link list. This renderer now has to be used in the footer so that our links can be reached there.
For this case we have to bind our renderer as an extension against the matching extension point in the footer.
Since we decide to show the custom links in the “Information” column, the extension point footer.information
works for us.
const CustomLinksRenderer: FC<Props> = ({ links }) => {
const { data, isLoading } = useCustomLinks((links.customLinks as Link).href);
if (isLoading) {
return null;
}
return (
<>
{(data?._embedded?.customLinks as CustomLink[]).map(cl => (
<li>
<a href={cl.url} target="_blank">
{cl.name}
</a>
</li>
))}
</>
);
};
The CustomLinkRenderer uses a React hook, which retrieves the data and writes it to our frontend cache using ReactQuery.
export const useCustomLinks = (link: string) => {
const { error, isLoading, data } = useQuery<HalRepresentation, Error>("custom-links", () =>
apiClient.get(link).then(res => res.json())
);
return {
error,
isLoading,
data
};
};
Notes:
- If there is currently no or not the right extension point for your plugin, please contact the SCM-Manager team.
- Extension points should always be bound in the index.tsx of your plugin.
- Our example is pretty simple. Extension points can also provide props to all bound extensions, can sort the extensions by name and priority or can be disabled using a predicate function.
Example:
import { binder } from "@scm-manager/ui-extensions";
import CustomLinksRenderer from "./CustomLinksRenderer";
binder.bind("footer.information", CustomLinksRenderer)
Now we can see the custom links in our footer and as soon as a link is changed, the footer will be updated instantly.
How to release my plugin?
The first iteration of the plugin is done. It works as we expected and can be used. Now it is up to you whether you want to keep it to you or, if you think that it could help other community members, you share it with the community. For the latter, push your plugin sources to a public instance like GitHub and afterwards contact the SCM-Manager team. We will review your plugin and fork it into the SCM-Manager GitHub organization. Once that is done, we will release it into the official SCM-Manager plugin center.
Final words
We hope we were able to give you a little insight into the development of plugins for SCM-Manager. This was a pretty simple examples to show you what a plugin could look like. We already have much bigger and more complex plugins which you can check out at the SCM-Manager GitHub organization.
If you are still not sure whether you could create your own plugin or how to realize your use case the best, contact the SCM-Manager team. Our developers and UX specialist would love to support you during your development.
SCM-Manager
The easiest way to share and manage your Git, Mercurial and Subversion repositories.
Getting started
This article is part 3 of the series „Introduction to plugin development for SCM-Manager“.
Read all articles now: