Skip to content

Commit

Permalink
feat(admin): identify external plugins (janus-idp#1202)
Browse files Browse the repository at this point in the history
Signed-off-by: Yi Cai <[email protected]>
  • Loading branch information
ciiay authored May 1, 2024
1 parent e074e89 commit cf3f2c7
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# comma delimited path of files to exclude from copy/paste duplicate checking
sonar.cpd.exclusions=packages/app/src/components/DynamicRoot/DynamicRoot.test.tsx,packages/app/src/components/admin/AdminTabs.test.tsx,packages/app/src/components/catalog/EntityPage/defaultTabs.tsx
sonar.cpd.exclusions=packages/app/src/components/DynamicRoot/DynamicRoot.test.tsx,packages/app/src/components/admin/AdminTabs.test.tsx,packages/app/src/components/catalog/EntityPage/defaultTabs.tsx,plugins/dynamic-plugins-info/src/components/InternalPluginsMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,19 @@ test.describe('dynamic-plugins-info UI tests', () => {
await uiHelper.clickTab('Plugins');
});

test('it should show a table and have a support button, the table should contain techdocs plugins', async ({
test('it should show a table, and the table should contain techdocs plugins', async ({
page,
}) => {
// what shows up in the list depends on how the instance is configured so
// let's check for the main basic elements of the component to verify the
// mount point is working as expected
await uiHelper.verifyText('Installed Plugins', false);
await uiHelper.verifyText('Plugins', false);
await uiHelper.verifyText('5 rows');
await uiHelper.verifyText('Name');
await uiHelper.verifyText('Version');
await uiHelper.verifyText('Enabled');
await uiHelper.verifyText('Preinstalled');
await uiHelper.verifyText('Role');
await uiHelper.clickButton('Support');
await uiHelper.verifyText('All of the installed plugins');
await uiHelper.clickButton('Close');

// Check the filter and use that to verify that the table contains the
// dynamic-plugins-info plugin, which is required for this test to run
Expand Down
1 change: 1 addition & 0 deletions plugins/dynamic-plugins-info/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@backstage/core-plugin-api": "1.9.1",
"@backstage/theme": "0.5.2",
"@material-table/core": "3.1.0",
"@mui/material": "5.15.6",
"react-use": "17.4.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
export const listLoadedPluginsResult = [
{
name: 'some-plugin-one',
name: 'api-returned-some-plugin-one',
version: '0.1.0',
role: 'frontend-plugin',
platform: 'web',
internal: true,
enabled: true,
},
{
name: 'some-plugin-two',
name: 'api-returned-some-plugin-two',
version: '1.1.0',
role: 'backend-plugin-module',
platform: 'node',
internal: false,
enabled: true,
},
{
name: 'some-plugin-three',
name: 'api-returned-some-plugin-three',
version: '0.1.2',
role: 'backend-plugin',
platform: 'node',
internal: false,
enabled: true,
},
{
name: 'some-plugin-four',
name: 'api-returned-some-plugin-four',
version: '1.1.0',
role: 'frontend-plugin',
platform: 'web',
internal: true,
enabled: true,
},
{
name: 'some-plugin-five',
name: 'api-returned-some-plugin-five',
version: '1.2.0',
role: 'frontend-plugin',
platform: 'web',
internal: true,
enabled: true,
},
{
name: 'some-plugin-six',
name: 'api-returned-some-plugin-six',
version: '0.6.3',
role: 'backend-plugin',
platform: 'node',
internal: true,
enabled: true,
},
];
2 changes: 2 additions & 0 deletions plugins/dynamic-plugins-info/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export type DynamicPluginInfo = {
version: string;
role: string;
platform: string;
enabled: boolean;
internal: boolean;
};

export interface DynamicPluginsInfoApi {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import React from 'react';

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

import Box from '@mui/material/Box';
import { DynamicPluginsTable } from '../DynamicPluginsTable/DynamicPluginsTable';

export const DynamicPluginsInfoContent = () => (
<>
<ContentHeader title="">
<SupportButton>All of the installed plugins</SupportButton>
</ContentHeader>
<Box sx={{ marginTop: '1rem' }}>
<DynamicPluginsTable />
</>
</Box>
);
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,38 @@ describe('DynamicPluginsTable', () => {
const { findByText, container } = await renderWithEffects(
<DynamicPluginsTable />,
);
expect(await findByText('Installed Plugins (6)')).toBeInTheDocument();
expect(await findByText('some-plugin-five')).toBeInTheDocument();
// 6 mockapi returned external(enabled) + 52 internal(not enabled)
// mockapi returns enabled plugins
// keys from InternalPluginsMap are internal plugins
expect(await findByText('Plugins (58)')).toBeInTheDocument();
expect(
await findByText('@janus-idp/backstage-plugin-3scale-backend-dynamic'),
).toBeInTheDocument();
const nameCells = Array.from(
container.querySelectorAll('tbody tr > td:first-child'),
);
const versionCells = Array.from(
container.querySelectorAll('tbody tr > td:nth-child(2)'),
);
const enabledCells = Array.from(
container.querySelectorAll('tbody tr > td:nth-child(3)'),
);
const internalCells = Array.from(
container.querySelectorAll('tbody tr > td:nth-child(4)'),
);
expect(nameCells.length).toBe(5);
expect(nameCells[0].textContent).toBe('some-plugin-five');
expect(nameCells[4].textContent).toBe('some-plugin-three');
expect(versionCells[0].textContent).toBe('1.2.0');
expect(versionCells[4].textContent).toBe('0.1.2');
expect(nameCells[0].textContent).toBe(
'@janus-idp/backstage-plugin-3scale-backend-dynamic',
);
expect(nameCells[4].textContent).toBe(
'@janus-idp/backstage-plugin-jfrog-artifactory',
);
expect(versionCells[0].textContent).toBe('');
expect(versionCells[4].textContent).toBe('');
expect(enabledCells[0].textContent).toBe('No');
expect(enabledCells[4].textContent).toBe('No');
expect(internalCells[0].textContent).toBe('Yes');
expect(internalCells[4].textContent).toBe('Yes');
});

it('supports filtering by a simple text search', async () => {
Expand All @@ -52,52 +71,65 @@ describe('DynamicPluginsTable', () => {
container.querySelectorAll('tbody tr > td:first-child'),
);
expect(nameCells.length).toBe(2);
expect(nameCells[0].textContent).toBe('some-plugin-five');
expect(nameCells[1].textContent).toBe('some-plugin-four');
expect(nameCells[0].textContent).toBe('api-returned-some-plugin-five');
expect(nameCells[1].textContent).toBe('api-returned-some-plugin-four');
});

it('supports sorting by name and version columns', async () => {
it('supports sorting by name, version and rhdh embedded columns', async () => {
const { findByText, container } = await renderWithEffects(
<DynamicPluginsTable />,
);
// descending by name
let nameCells = Array.from(
container.querySelectorAll('tbody tr > td:first-child'),
);
let versionCells = Array.from(
container.querySelectorAll('tbody tr > td:nth-child(2)'),
);
expect(nameCells.length).toBe(5);
expect(nameCells[0].textContent).toBe('some-plugin-five');
expect(nameCells[4].textContent).toBe('some-plugin-three');
expect(versionCells[0].textContent).toBe('1.2.0');
expect(versionCells[4].textContent).toBe('0.1.2');
expect(nameCells[0].textContent).toBe(
'@janus-idp/backstage-plugin-3scale-backend-dynamic',
);
expect(nameCells[4].textContent).toBe(
'@janus-idp/backstage-plugin-jfrog-artifactory',
);
await act(() => findByText('Name').then(el => el.click()));
// ascending by name
nameCells = Array.from(
container.querySelectorAll('tbody tr > td:first-child'),
);
versionCells = Array.from(
container.querySelectorAll('tbody tr > td:nth-child(2)'),
);
expect(nameCells.length).toBe(5);
expect(nameCells[0].textContent).toBe('some-plugin-two');
expect(nameCells[4].textContent).toBe('some-plugin-four');
expect(versionCells[0].textContent).toBe('1.1.0');
expect(versionCells[4].textContent).toBe('1.1.0');
expect(nameCells[0].textContent).toBe(
'roadiehq-scaffolder-backend-module-utils-dynamic',
);
expect(nameCells[4].textContent).toBe('roadiehq-backstage-plugin-jira');
// ascending by version
await act(() => findByText('Version').then(el => el.click()));
nameCells = Array.from(
container.querySelectorAll('tbody tr > td:first-child'),
);
versionCells = Array.from(
container.querySelectorAll('tbody tr > td:nth-child(2)'),
expect(nameCells.length).toBe(5);
expect(nameCells[0].textContent).toBe('api-returned-some-plugin-five');
expect(nameCells[4].textContent).toBe('api-returned-some-plugin-three');

// ascending by enabled
await act(() => findByText('Enabled').then(el => el.click()));
nameCells = Array.from(
container.querySelectorAll('tbody tr > td:first-child'),
);
expect(nameCells.length).toBe(5);
expect(nameCells[0].textContent).toBe('some-plugin-five');
expect(nameCells[4].textContent).toBe('some-plugin-three');
expect(versionCells[0].textContent).toBe('1.2.0');
expect(versionCells[4].textContent).toBe('0.1.2');
expect(nameCells[0].textContent).toBe('api-returned-some-plugin-six');
expect(nameCells[4].textContent).toBe('api-returned-some-plugin-two');

// ascending by Preinstalled
await act(() => findByText('Preinstalled').then(el => el.click()));
nameCells = Array.from(
container.querySelectorAll('tbody tr > td:first-child'),
);
expect(nameCells.length).toBe(5);
expect(nameCells[0].textContent).toBe(
'@janus-idp/backstage-plugin-analytics-provider-segment',
);
expect(nameCells[4].textContent).toBe(
'@janus-idp/backstage-plugin-jfrog-artifactory',
);
});

it('supports changing the number of items per page', async () => {
Expand All @@ -116,6 +148,6 @@ describe('DynamicPluginsTable', () => {
nameCells = Array.from(
container.querySelectorAll('tbody tr > td:first-child'),
);
expect(nameCells.length).toBe(6);
expect(nameCells.length).toBe(10);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import { useApi } from '@backstage/core-plugin-api';
import { Query, QueryResult } from '@material-table/core';

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

export const DynamicPluginsTable = () => {
const [error, setError] = useState<Error | undefined>(undefined);
const [count, setCount] = useState<number>(0);
const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef);
let data: DynamicPluginInfo[] = [];
const columns: TableColumn<DynamicPluginInfo>[] = [
{
title: 'Name',
Expand All @@ -24,11 +29,25 @@ export const DynamicPluginsTable = () => {
{
title: 'Version',
field: 'version',
width: '30%',
width: '15%',
},
{
title: 'Enabled',
field: 'enabled',
render: ({ enabled }) => <>{enabled ? 'Yes' : 'No'}</>,
width: '10%',
},
{
title: 'Preinstalled',
field: 'internal',
render: ({ internal }) => <>{internal ? 'Yes' : 'No'}</>,
width: '10%',
},
{
title: 'Role',
render: ({ platform, role }) => <>{`${role} (${platform})`}</>,
render: ({ platform, role }) => (
<>{(role && `${role} (${platform})`) || null}</>
),
sorting: false,
},
];
Expand All @@ -44,21 +63,55 @@ export const DynamicPluginsTable = () => {
} = 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;
const enabledPlugins = (await dynamicPluginInfo.listLoadedPlugins()).map(
plugin => {
if (plugin.name in InternalPluginsMap) {
return {
...plugin,
internal: true,
enabled: true,
};
}
return (
a[field].localeCompare(b[field]) *
(orderDirection === 'desc' ? -1 : 1)
);
})
.filter(
value =>
search.trim() === '' ||
JSON.stringify(value).indexOf(search.trim()) > 0,
return { ...plugin, internal: false, enabled: true };
},
);
const notEnabledInternalPlugins = getNotEnabledInternalPlugins(
enabledPlugins.map(plugin => plugin.name),
);
data = [...enabledPlugins]
// add other internal plugins that are not enabled
.concat(notEnabledInternalPlugins)
.sort(
(
a: Record<string, string | boolean>,
b: Record<string, string | boolean>,
) => {
const field = orderBy.field!;
const orderMultiplier = orderDirection === 'desc' ? -1 : 1;

if (a[field] === null || b[field] === null) {
return 0;
}

// Handle boolean values separately
if (
typeof a[field] === 'boolean' &&
typeof b[field] === 'boolean'
) {
return (a[field] ? 1 : -1) * orderMultiplier;
}

return (
(a[field] as string).localeCompare(b[field] as string) *
orderMultiplier
);
},
)
.filter(plugin =>
plugin.name
.toLowerCase()
.trim()
.includes(search.toLowerCase().trim()),
);
const totalCount = data.length;
let start = 0;
Expand All @@ -79,7 +132,7 @@ export const DynamicPluginsTable = () => {
}
return (
<Table
title={`Installed Plugins (${count})`}
title={`Plugins (${count})`}
options={{
draggable: false,
filtering: false,
Expand Down
Loading

0 comments on commit cf3f2c7

Please sign in to comment.