From 2a427809ff521051dd77c4700a898b37fb392a9d Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Wed, 31 Jan 2024 06:44:09 -0500 Subject: [PATCH] feat(plugins): ui for dynamic-plugins-info-backend (#1138) 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 --- plugins/dynamic-plugins-info/.eslintrc.js | 1 + plugins/dynamic-plugins-info/CONTRIBUTING.md | 7 ++ plugins/dynamic-plugins-info/README.md | 45 +++++++++ .../app-config.janus-idp.yaml | 13 +++ plugins/dynamic-plugins-info/dev/index.tsx | 85 +++++++++++++++++ plugins/dynamic-plugins-info/package.json | 65 +++++++++++++ .../src/api/DynamicPluginsInfoClient.ts | 30 ++++++ plugins/dynamic-plugins-info/src/api/types.ts | 16 ++++ .../DynamicPluginsInfoContent.tsx | 14 +++ .../DynamicPluginsTable.tsx | 95 +++++++++++++++++++ plugins/dynamic-plugins-info/src/index.ts | 1 + .../dynamic-plugins-info/src/plugin.test.ts | 7 ++ plugins/dynamic-plugins-info/src/plugin.ts | 40 ++++++++ plugins/dynamic-plugins-info/src/routes.ts | 5 + .../dynamic-plugins-info/src/setupTests.ts | 1 + plugins/dynamic-plugins-info/tsconfig.json | 9 ++ plugins/dynamic-plugins-info/turbo.json | 9 ++ 17 files changed, 443 insertions(+) create mode 100644 plugins/dynamic-plugins-info/.eslintrc.js create mode 100644 plugins/dynamic-plugins-info/CONTRIBUTING.md create mode 100644 plugins/dynamic-plugins-info/README.md create mode 100644 plugins/dynamic-plugins-info/app-config.janus-idp.yaml create mode 100644 plugins/dynamic-plugins-info/dev/index.tsx create mode 100644 plugins/dynamic-plugins-info/package.json create mode 100644 plugins/dynamic-plugins-info/src/api/DynamicPluginsInfoClient.ts create mode 100644 plugins/dynamic-plugins-info/src/api/types.ts create mode 100644 plugins/dynamic-plugins-info/src/components/DynamicPluginsInfoContent/DynamicPluginsInfoContent.tsx create mode 100644 plugins/dynamic-plugins-info/src/components/DynamicPluginsTable/DynamicPluginsTable.tsx create mode 100644 plugins/dynamic-plugins-info/src/index.ts create mode 100644 plugins/dynamic-plugins-info/src/plugin.test.ts create mode 100644 plugins/dynamic-plugins-info/src/plugin.ts create mode 100644 plugins/dynamic-plugins-info/src/routes.ts create mode 100644 plugins/dynamic-plugins-info/src/setupTests.ts create mode 100644 plugins/dynamic-plugins-info/tsconfig.json create mode 100644 plugins/dynamic-plugins-info/turbo.json diff --git a/plugins/dynamic-plugins-info/.eslintrc.js b/plugins/dynamic-plugins-info/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/dynamic-plugins-info/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/dynamic-plugins-info/CONTRIBUTING.md b/plugins/dynamic-plugins-info/CONTRIBUTING.md new file mode 100644 index 0000000000..234259f812 --- /dev/null +++ b/plugins/dynamic-plugins-info/CONTRIBUTING.md @@ -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 +``` diff --git a/plugins/dynamic-plugins-info/README.md b/plugins/dynamic-plugins-info/README.md new file mode 100644 index 0000000000..9b723841f2 --- /dev/null +++ b/plugins/dynamic-plugins-info/README.md @@ -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 +``` diff --git a/plugins/dynamic-plugins-info/app-config.janus-idp.yaml b/plugins/dynamic-plugins-info/app-config.janus-idp.yaml new file mode 100644 index 0000000000..490c0123a9 --- /dev/null +++ b/plugins/dynamic-plugins-info/app-config.janus-idp.yaml @@ -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 diff --git a/plugins/dynamic-plugins-info/dev/index.tsx b/plugins/dynamic-plugins-info/dev/index.tsx new file mode 100644 index 0000000000..c40e8b3503 --- /dev/null +++ b/plugins/dynamic-plugins-info/dev/index.tsx @@ -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: ( + + +
+ + + + + + + ), + title: 'Root Page', + path: '/dynamic-plugins-info', + }) + .render(); diff --git a/plugins/dynamic-plugins-info/package.json b/plugins/dynamic-plugins-info/package.json new file mode 100644 index 0000000000..6899efbecb --- /dev/null +++ b/plugins/dynamic-plugins-info/package.json @@ -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" +} diff --git a/plugins/dynamic-plugins-info/src/api/DynamicPluginsInfoClient.ts b/plugins/dynamic-plugins-info/src/api/DynamicPluginsInfoClient.ts new file mode 100644 index 0000000000..b3c4224dea --- /dev/null +++ b/plugins/dynamic-plugins-info/src/api/DynamicPluginsInfoClient.ts @@ -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 { + 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; + } +} diff --git a/plugins/dynamic-plugins-info/src/api/types.ts b/plugins/dynamic-plugins-info/src/api/types.ts new file mode 100644 index 0000000000..af6b4d908c --- /dev/null +++ b/plugins/dynamic-plugins-info/src/api/types.ts @@ -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; +} + +export const dynamicPluginsInfoApiRef = createApiRef({ + id: 'plugin.dynamic-plugins-info', +}); diff --git a/plugins/dynamic-plugins-info/src/components/DynamicPluginsInfoContent/DynamicPluginsInfoContent.tsx b/plugins/dynamic-plugins-info/src/components/DynamicPluginsInfoContent/DynamicPluginsInfoContent.tsx new file mode 100644 index 0000000000..ad4760dddb --- /dev/null +++ b/plugins/dynamic-plugins-info/src/components/DynamicPluginsInfoContent/DynamicPluginsInfoContent.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { ContentHeader, SupportButton } from '@backstage/core-components'; + +import { DynamicPluginsTable } from '../DynamicPluginsTable/DynamicPluginsTable'; + +export const DynamicPluginsInfoContent = () => ( + <> + + Some placeholder text + + + +); diff --git a/plugins/dynamic-plugins-info/src/components/DynamicPluginsTable/DynamicPluginsTable.tsx b/plugins/dynamic-plugins-info/src/components/DynamicPluginsTable/DynamicPluginsTable.tsx new file mode 100644 index 0000000000..f79b7855cd --- /dev/null +++ b/plugins/dynamic-plugins-info/src/components/DynamicPluginsTable/DynamicPluginsTable.tsx @@ -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(undefined); + const [count, setCount] = useState(0); + const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef); + const columns: TableColumn[] = [ + { + 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, + ): Promise> => { + 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, b: Record) => { + 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 ; + } + return ( + + ); +}; diff --git a/plugins/dynamic-plugins-info/src/index.ts b/plugins/dynamic-plugins-info/src/index.ts new file mode 100644 index 0000000000..46440887c3 --- /dev/null +++ b/plugins/dynamic-plugins-info/src/index.ts @@ -0,0 +1 @@ +export { dynamicPluginsInfoPlugin, DynamicPluginsInfo } from './plugin'; diff --git a/plugins/dynamic-plugins-info/src/plugin.test.ts b/plugins/dynamic-plugins-info/src/plugin.test.ts new file mode 100644 index 0000000000..1b311ce7b4 --- /dev/null +++ b/plugins/dynamic-plugins-info/src/plugin.test.ts @@ -0,0 +1,7 @@ +import { dynamicPluginsInfoPlugin } from './plugin'; + +describe('dynamic-plugins-info', () => { + it('should export plugin', () => { + expect(dynamicPluginsInfoPlugin).toBeDefined(); + }); +}); diff --git a/plugins/dynamic-plugins-info/src/plugin.ts b/plugins/dynamic-plugins-info/src/plugin.ts new file mode 100644 index 0000000000..f603e06ef8 --- /dev/null +++ b/plugins/dynamic-plugins-info/src/plugin.ts @@ -0,0 +1,40 @@ +import { + createApiFactory, + createPlugin, + createRoutableExtension, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; + +import { DynamicPluginsInfoClient } from './api/DynamicPluginsInfoClient'; +import { dynamicPluginsInfoApiRef } from './api/types'; +import { dynamicPluginsInfoRouteRef } from './routes'; + +export const dynamicPluginsInfoPlugin = createPlugin({ + id: 'dynamic-plugins-info', + routes: { + root: dynamicPluginsInfoRouteRef, + }, + apis: [ + createApiFactory({ + api: dynamicPluginsInfoApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new DynamicPluginsInfoClient({ discoveryApi, fetchApi }), + }), + ], +}); + +export const DynamicPluginsInfo = dynamicPluginsInfoPlugin.provide( + createRoutableExtension({ + name: 'DynamicPluginsInfo', + component: () => + import( + './components/DynamicPluginsInfoContent/DynamicPluginsInfoContent' + ).then(m => m.DynamicPluginsInfoContent), + mountPoint: dynamicPluginsInfoRouteRef, + }), +); diff --git a/plugins/dynamic-plugins-info/src/routes.ts b/plugins/dynamic-plugins-info/src/routes.ts new file mode 100644 index 0000000000..478de64255 --- /dev/null +++ b/plugins/dynamic-plugins-info/src/routes.ts @@ -0,0 +1,5 @@ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const dynamicPluginsInfoRouteRef = createRouteRef({ + id: 'dynamic-plugins-info', +}); diff --git a/plugins/dynamic-plugins-info/src/setupTests.ts b/plugins/dynamic-plugins-info/src/setupTests.ts new file mode 100644 index 0000000000..7b0828bfa8 --- /dev/null +++ b/plugins/dynamic-plugins-info/src/setupTests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/plugins/dynamic-plugins-info/tsconfig.json b/plugins/dynamic-plugins-info/tsconfig.json new file mode 100644 index 0000000000..c81a299b6e --- /dev/null +++ b/plugins/dynamic-plugins-info/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src", "dev", "migrations"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "../../dist-types/plugins/dynamic-plugins-info", + "rootDir": "." + } +} diff --git a/plugins/dynamic-plugins-info/turbo.json b/plugins/dynamic-plugins-info/turbo.json new file mode 100644 index 0000000000..aad6de81a6 --- /dev/null +++ b/plugins/dynamic-plugins-info/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "pipeline": { + "tsc": { + "outputs": ["../../dist-types/plugins/dynamic-plugins-info/**"], + "dependsOn": ["^tsc"] + } + } +}