Skip to content

Commit

Permalink
feat(plugins): ui for dynamic-plugins-info-backend (#1138)
Browse files Browse the repository at this point in the history
This change adds a dynamic-plugins-info plugin which is a frontend for
the dynamic-plugins-info-backend plugin that is part of the Janus IDP
backstage-showcase instance.  The plugin implements a table view of the
plugin data and includes client-side filtering, sorting and pagination.
A dev mode is available with some test data to populate the table.
This initial implementation only covers installing the plugin
dynamically.

Signed-off-by: Stan Lewis <[email protected]>
  • Loading branch information
gashcrumb authored Jan 31, 2024
1 parent d0285ec commit 2a42780
Show file tree
Hide file tree
Showing 17 changed files with 443 additions and 0 deletions.
1 change: 1 addition & 0 deletions plugins/dynamic-plugins-info/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
7 changes: 7 additions & 0 deletions plugins/dynamic-plugins-info/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Setting up the development environment for Dynamic Plugins Info plugin

In [Backstage plugin terminology](https://backstage.io/docs/local-dev/cli-build-system#package-roles), the Dynamic Plugins Info plugin is a front-end plugin. You can start a live development session from the repository root using the following command:

```console
yarn workspace @janus-idp/backstage-plugin-dynamic-plugins-info run start
```
45 changes: 45 additions & 0 deletions plugins/dynamic-plugins-info/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Dynamic Plugins Info plugin for Backstage

The dynamic-plugins-info plugin is a frontend component for the [dynamic-plugins-info-backend](https://github.com/janus-idp/backstage-showcase/tree/main/plugins/dynamic-plugins-info-backend) plugin. It offers a simple table UI that supports client-side sorting, filtering and pagination.

The plugin is designed to be installed dynamically in the [backstage-showcase](https://github.com/janus-idp/backstage-showcase/tree/main) app.

To build this plugin and the dynamic entrypoint:

`yarn install`

`yarn tsc`

`yarn build`

`yarn export-dynamic`

To install the dynamic plugin from a local build:

```bash
cd dist-scalprum
npm pack .
archive=$(npm pack $pkg)
tar -xzf "$archive" && rm "$archive"
mv package $(echo $archive | sed -e 's:\.tgz$::')
```

Move the resulting directory (`janus-idp-backstage-plugin-dynamic-plugins-info-0.1.0`) into the `dynamic-plugins-root` folder of your [backstage-showcase](https://github.com/janus-idp/backstage-showcase/tree/main) clone.

This configuration will enable the plugin to be visible in the UI:

```yaml
dynamicPlugins:
frontend:
janus-idp.backstage-plugin-dynamic-plugins-info:
dynamicRoutes:
- path: /admin/plugins
importName: DynamicPluginsInfo
mountPoints:
- mountPoint: admin.page.plugins/cards
importName: DynamicPluginsInfo
config:
layout:
gridColumn: '1 / -1'
width: 100vw
```
13 changes: 13 additions & 0 deletions plugins/dynamic-plugins-info/app-config.janus-idp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
dynamicPlugins:
frontend:
janus-idp.backstage-plugin-dynamic-plugins-info:
dynamicRoutes:
- path: /admin/plugins
importName: DynamicPluginsInfo
mountPoints:
- mountPoint: admin.page.plugins/cards
importName: DynamicPluginsInfo
config:
layout:
gridColumn: '1 / -1'
width: 100vw
85 changes: 85 additions & 0 deletions plugins/dynamic-plugins-info/dev/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';

import { Content, Header, HeaderTabs, Page } from '@backstage/core-components';
import { createDevApp } from '@backstage/dev-utils';
import { TestApiProvider } from '@backstage/test-utils';

import { dynamicPluginsInfoApiRef } from '../src/api/types';
import { DynamicPluginsInfoContent } from '../src/components/DynamicPluginsInfoContent/DynamicPluginsInfoContent';
import { dynamicPluginsInfoPlugin } from '../src/plugin';

export const listLoadedPluginsResult = [
{
name: 'some-plugin-one',
version: '0.1.0',
role: 'frontend-plugin',
platform: 'web',
},
{
name: 'some-plugin-two',
version: '1.1.0',
role: 'backend-plugin-module',
platform: 'node',
},
{
name: 'some-plugin-three',
version: '0.1.2',
role: 'backend-plugin',
platform: 'node',
},
{
name: 'some-plugin-four',
version: '1.1.0',
role: 'frontend-plugin',
platform: 'web',
},
{
name: 'some-plugin-five',
version: '1.2.0',
role: 'frontend-plugin',
platform: 'web',
},
{
name: 'some-plugin-six',
version: '0.6.3',
role: 'backend-plugin',
platform: 'node',
},
];

const mockedApi = {
listLoadedPlugins: async () => {
return listLoadedPluginsResult;
},
};

createDevApp()
.registerPlugin(dynamicPluginsInfoPlugin)
.addPage({
element: (
<TestApiProvider apis={[[dynamicPluginsInfoApiRef, mockedApi]]}>
<Page themeId="theme">
<Header title="Administration" />
<HeaderTabs
selectedIndex={1}
tabs={[
{
id: 'rbac',
label: 'RBAC',
},
{
id: 'plugins',
label: 'Plugins',
},
]}
/>
<Content>
<DynamicPluginsInfoContent />
</Content>
</Page>
</TestApiProvider>
),
title: 'Root Page',
path: '/dynamic-plugins-info',
})
.render();
65 changes: 65 additions & 0 deletions plugins/dynamic-plugins-info/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "@janus-idp/backstage-plugin-dynamic-plugins-info",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin"
},
"scripts": {
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"export-dynamic": "janus-cli package export-dynamic-plugin",
"lint": "backstage-cli package lint",
"postpack": "backstage-cli package postpack",
"postversion": "yarn run export-dynamic",
"prepack": "backstage-cli package prepack",
"start": "backstage-cli package start",
"test": "backstage-cli package test --passWithNoTests --coverage",
"tsc": "tsc"
},
"dependencies": {
"@backstage/core-components": "^0.13.6",
"@backstage/core-plugin-api": "^1.7.0",
"@backstage/theme": "^0.4.3",
"@material-table/core": "^3.1.0",
"react-use": "^17.4.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
"react-router-dom": "^6.20.0"
},
"devDependencies": {
"@backstage/cli": "0.23.0",
"@backstage/core-app-api": "1.11.0",
"@backstage/dev-utils": "1.0.22",
"@backstage/test-utils": "1.4.4",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "14.5.1",
"msw": "1.3.2"
},
"files": [
"dist",
"dist-scalprum"
],
"scalprum": {
"name": "janus-idp.backstage-plugin-dynamic-plugins-info",
"exposedModules": {
"PluginRoot": "./src/index.ts"
}
},
"repository": "github:janus-idp/backstage-plugins",
"keywords": [
"backstage",
"plugin"
],
"homepage": "https://janus-idp.io/",
"bugs": "https://github.com/janus-idp/backstage-plugins/issues"
}
30 changes: 30 additions & 0 deletions plugins/dynamic-plugins-info/src/api/DynamicPluginsInfoClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';

import { DynamicPluginInfo, DynamicPluginsInfoApi } from './types';

export interface DynamicPluginsInfoClientOptions {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
}

const loadedPluginsEndpoint = '/loaded-plugins';

export class DynamicPluginsInfoClient implements DynamicPluginsInfoApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;

constructor(options: DynamicPluginsInfoClientOptions) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
}
async listLoadedPlugins(): Promise<DynamicPluginInfo[]> {
const baseUrl = await this.discoveryApi.getBaseUrl('dynamic-plugins-info');
const targetUrl = `${baseUrl}${loadedPluginsEndpoint}`;
const response = await this.fetchApi.fetch(targetUrl);
const data = await response.json();
if (!response.ok) {
throw new Error(`${data.message}`);
}
return data;
}
}
16 changes: 16 additions & 0 deletions plugins/dynamic-plugins-info/src/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createApiRef } from '@backstage/core-plugin-api';

export type DynamicPluginInfo = {
name: string;
version: string;
role: string;
platform: string;
};

export interface DynamicPluginsInfoApi {
listLoadedPlugins(): Promise<DynamicPluginInfo[]>;
}

export const dynamicPluginsInfoApiRef = createApiRef<DynamicPluginsInfoApi>({
id: 'plugin.dynamic-plugins-info',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

import { ContentHeader, SupportButton } from '@backstage/core-components';

import { DynamicPluginsTable } from '../DynamicPluginsTable/DynamicPluginsTable';

export const DynamicPluginsInfoContent = () => (
<>
<ContentHeader title="">
<SupportButton title="Support">Some placeholder text</SupportButton>
</ContentHeader>
<DynamicPluginsTable />
</>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useState } from 'react';

import {
ResponseErrorPanel,
Table,
TableColumn,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';

import { Query, QueryResult } from '@material-table/core';

import { DynamicPluginInfo, dynamicPluginsInfoApiRef } from '../../api/types';

export const DynamicPluginsTable = () => {
const [error, setError] = useState<Error | undefined>(undefined);
const [count, setCount] = useState<number>(0);
const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef);
const columns: TableColumn<DynamicPluginInfo>[] = [
{
title: 'Name',
field: 'name',
defaultSort: 'asc',
},
{
title: 'Version',
field: 'version',
width: '30%',
},
{
title: 'Role',
render: ({ platform, role }) => <>{`${role} (${platform})`}</>,
sorting: false,
},
];
const fetchData = async (
query: Query<DynamicPluginInfo>,
): Promise<QueryResult<DynamicPluginInfo>> => {
const {
orderBy = { field: 'name' },
orderDirection = 'asc',
page = 0,
pageSize = 5,
search = '',
} = query || {};
try {
// for now sorting/searching/pagination is handled client-side
const data = (await dynamicPluginInfo.listLoadedPlugins())
.sort((a: Record<string, string>, b: Record<string, string>) => {
const field = orderBy.field!;
if (!a[field] || !b[field]) {
return 0;
}
return (
a[field].localeCompare(b[field]) *
(orderDirection === 'desc' ? -1 : 1)
);
})
.filter(
value =>
search.trim() === '' ||
JSON.stringify(value).indexOf(search.trim()) > 0,
);
const totalCount = data.length;
let start = 0;
let end = totalCount;
if (totalCount > pageSize) {
start = page * pageSize;
end = start + pageSize;
}
setCount(totalCount);
return { data: data.slice(start, end), page, totalCount };
} catch (loadingError) {
setError(loadingError as Error);
return { data: [], totalCount: 0, page: 0 };
}
};
if (error) {
return <ResponseErrorPanel error={error} />;
}
return (
<Table
title={`Installed Plugins (${count})`}
options={{
draggable: false,
filtering: false,
sorting: true,
paging: true,
thirdSortClick: false,
debounceInterval: 500,
}}
columns={columns}
data={fetchData}
/>
);
};
1 change: 1 addition & 0 deletions plugins/dynamic-plugins-info/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { dynamicPluginsInfoPlugin, DynamicPluginsInfo } from './plugin';
7 changes: 7 additions & 0 deletions plugins/dynamic-plugins-info/src/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { dynamicPluginsInfoPlugin } from './plugin';

describe('dynamic-plugins-info', () => {
it('should export plugin', () => {
expect(dynamicPluginsInfoPlugin).toBeDefined();
});
});
Loading

0 comments on commit 2a42780

Please sign in to comment.