Skip to content

Commit dad9806

Browse files
authored
feat(marketplace): integrate plugins-info plugin (#1395)
* feat(marketplace): integrate plugins-info plugin Signed-off-by: Yi Cai <[email protected]> * update package and yarn.lock Signed-off-by: Yi Cai <[email protected]> * added changeset Signed-off-by: Yi Cai <[email protected]> * yarn dedupe Signed-off-by: Yi Cai <[email protected]> * fix tsc issue Signed-off-by: Yi Cai <[email protected]> * addressed review comments Signed-off-by: Yi Cai <[email protected]> * addressed feedback Signed-off-by: Yi Cai <[email protected]> * code improvement Signed-off-by: Yi Cai <[email protected]> * updated report.api.md Signed-off-by: Yi Cai <[email protected]> * added link to name column Signed-off-by: Yi Cai <[email protected]> * addressed review comments Signed-off-by: Yi Cai <[email protected]> * fixed saving issue Signed-off-by: Yi Cai <[email protected]> * fixed saving config error Signed-off-by: Yi Cai <[email protected]> * changeset updated to minor Signed-off-by: Yi Cai <[email protected]> --------- Signed-off-by: Yi Cai <[email protected]>
1 parent d581486 commit dad9806

36 files changed

+2961
-698
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-marketplace-backend': minor
3+
'@red-hat-developer-hub/backstage-plugin-marketplace': minor
4+
---
5+
6+
Integrate plugins-info plugin and add `Installed packages` tab with enhanced UI.
7+
8+
BREAKING: The deprecated `InstallationContextProvider` export behavior changed.
9+
10+
- We now export a null component `InstallationContextProvider` from `plugin.ts` solely for backward compatibility. It no longer provides context and will be removed in a future release.
11+
- Migration: There is no replacement API; this was internal-only. Please upgrade to the latest RHDH where features no longer rely on this provider.
12+
13+
Also:
14+
15+
- New `Installed packages` tab with dual-source mapping and client-side filtering/pagination.

workspaces/marketplace/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"minimatch": "3",
5151
"node-gyp": "^9.0.0",
5252
"prettier": "^3.4.2",
53-
"typescript": "~5.3.0"
53+
"typescript": "~5.3.0",
54+
"yaml": "^2.8.1"
5455
},
5556
"resolutions": {
5657
"@types/react": "^18",

workspaces/marketplace/packages/app/src/App.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,7 @@ import { githubAuthApiRef } from '@backstage/core-plugin-api';
5252

5353
import { getAllThemes } from '@red-hat-developer-hub/backstage-plugin-theme';
5454

55-
import {
56-
InstallationContextProvider,
57-
DynamicMarketplacePluginRouter as Marketplace,
58-
} from '@red-hat-developer-hub/backstage-plugin-marketplace';
55+
import { DynamicMarketplacePluginRouter as Marketplace } from '@red-hat-developer-hub/backstage-plugin-marketplace';
5956

6057
import { apis } from './apis';
6158
import { entityPage } from './components/catalog/EntityPage';
@@ -136,14 +133,7 @@ const routes = (
136133
</Route>
137134
<Route path="/settings" element={<UserSettingsPage />} />
138135
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
139-
<Route
140-
path="/extensions"
141-
element={
142-
<InstallationContextProvider>
143-
<Marketplace />
144-
</InstallationContextProvider>
145-
}
146-
/>
136+
<Route path="/extensions" element={<Marketplace />} />
147137
</FlatRoutes>
148138
);
149139

workspaces/marketplace/plugins/marketplace-backend/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
},
3535
"dependencies": {
3636
"@backstage/backend-defaults": "^0.11.1",
37+
"@backstage/backend-dynamic-feature-service": "^0.7.3",
3738
"@backstage/backend-plugin-api": "^1.4.1",
3839
"@backstage/catalog-client": "^1.10.2",
3940
"@backstage/catalog-model": "^1.7.5",
@@ -44,7 +45,6 @@
4445
"@red-hat-developer-hub/backstage-plugin-marketplace-common": "workspace:^",
4546
"express": "^4.17.1",
4647
"express-promise-router": "^4.1.0",
47-
"yaml": "^2.7.1",
4848
"zod": "^3.22.4"
4949
},
5050
"devDependencies": {
@@ -56,7 +56,8 @@
5656
"@types/express": "*",
5757
"@types/supertest": "^2.0.12",
5858
"msw": "^1.0.0",
59-
"supertest": "^6.2.4"
59+
"supertest": "^6.2.4",
60+
"yaml": "^2.7.1"
6061
},
6162
"files": [
6263
"dist"

workspaces/marketplace/plugins/marketplace-backend/src/plugin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
createBackendPlugin,
2020
} from '@backstage/backend-plugin-api';
2121
import { CatalogClient } from '@backstage/catalog-client';
22+
import { dynamicPluginsServiceRef } from '@backstage/backend-dynamic-feature-service';
2223

2324
import {
2425
MarketplaceApi,
@@ -45,8 +46,10 @@ export const marketplacePlugin = createBackendPlugin({
4546
discovery: coreServices.discovery,
4647
logger: coreServices.logger,
4748
permissions: coreServices.permissions,
49+
pluginProvider: dynamicPluginsServiceRef,
4850
},
4951
async init({
52+
pluginProvider,
5053
auth,
5154
config,
5255
httpAuth,
@@ -75,6 +78,8 @@ export const marketplacePlugin = createBackendPlugin({
7578
installationDataService,
7679
marketplaceApi,
7780
permissions,
81+
pluginProvider,
82+
logger,
7883
config,
7984
}),
8085
);

workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@ import request from 'supertest';
2020
import { stringify } from 'yaml';
2121

2222
import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter';
23-
import { BackendFeature } from '@backstage/backend-plugin-api';
23+
import {
24+
BackendFeature,
25+
createServiceFactory,
26+
} from '@backstage/backend-plugin-api';
2427
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
28+
import {
29+
DynamicPluginProvider,
30+
BaseDynamicPlugin,
31+
dynamicPluginsServiceRef,
32+
} from '@backstage/backend-dynamic-feature-service';
2533
import type { JsonObject } from '@backstage/types';
2634
import {
2735
AuthorizeResult,
@@ -67,6 +75,43 @@ const BASE_CONFIG = {
6775
},
6876
};
6977

78+
// Mock dynamic plugins data for testing
79+
const mockDynamicPluginsData: BaseDynamicPlugin[] = [
80+
{
81+
name: '@backstage/plugin-catalog-backend',
82+
version: '1.0.0',
83+
role: 'backend',
84+
platform: 'node',
85+
},
86+
{
87+
name: '@backstage/plugin-catalog',
88+
version: '1.0.0',
89+
role: 'frontend',
90+
platform: 'web',
91+
},
92+
{
93+
name: '@red-hat-developer-hub/backstage-plugin-marketplace',
94+
version: '1.0.0',
95+
role: 'frontend',
96+
platform: 'web',
97+
},
98+
];
99+
100+
// Mock DynamicPluginProvider
101+
const mockDynamicPluginProvider: DynamicPluginProvider = {
102+
plugins: () => mockDynamicPluginsData as any,
103+
getScannedPackage: () => ({}) as any,
104+
frontendPlugins: () => [],
105+
backendPlugins: () => [],
106+
};
107+
108+
// Create mock service factory for dynamicPluginsServiceRef
109+
const mockDynamicPluginsServiceFactory = createServiceFactory({
110+
service: dynamicPluginsServiceRef,
111+
deps: {},
112+
factory: () => mockDynamicPluginProvider,
113+
});
114+
70115
const FILE_INSTALL_CONFIG = {
71116
extensions: {
72117
installation: {
@@ -141,6 +186,7 @@ async function startBackendServer(
141186
mockServices.rootConfig.factory({
142187
data: { ...BASE_CONFIG, ...(config ?? {}) },
143188
}),
189+
mockDynamicPluginsServiceFactory,
144190
];
145191

146192
if (authorizeResult) {
@@ -881,4 +927,31 @@ describe('createRouter', () => {
881927
},
882928
);
883929
});
930+
931+
describe('GET /loaded-plugins', () => {
932+
it('should return the list of loaded dynamic plugins', async () => {
933+
const { backendServer } = await setupTestWithMockCatalog({
934+
mockData: {},
935+
});
936+
937+
const response = await request(backendServer).get(
938+
'/api/extensions/loaded-plugins',
939+
);
940+
941+
expect(response.status).toBe(200);
942+
expect(Array.isArray(response.body)).toBe(true);
943+
// The response should be an array of dynamic plugin info objects
944+
// Each plugin should have the expected structure
945+
response.body.forEach((plugin: any) => {
946+
expect(plugin).toEqual(
947+
expect.objectContaining({
948+
name: expect.any(String),
949+
version: expect.any(String),
950+
role: expect.any(String),
951+
platform: expect.any(String),
952+
}),
953+
);
954+
});
955+
});
956+
});
884957
});

workspaces/marketplace/plugins/marketplace-backend/src/router.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type { Config } from '@backstage/config';
2222
import {
2323
HttpAuthService,
2424
PermissionsService,
25+
LoggerService,
2526
} from '@backstage/backend-plugin-api';
2627
import {
2728
AuthorizeResult,
@@ -48,11 +49,19 @@ import { matches } from './utils/permissionUtils';
4849
import { InstallationDataService } from './installation/InstallationDataService';
4950
import { ConfigFormatError } from './errors/ConfigFormatError';
5051

52+
import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';
53+
import {
54+
BaseDynamicPlugin,
55+
DynamicPluginProvider,
56+
} from '@backstage/backend-dynamic-feature-service';
57+
5158
export type MarketplaceRouterOptions = {
5259
httpAuth: HttpAuthService;
5360
marketplaceApi: MarketplaceApi;
5461
permissions: PermissionsService;
5562
installationDataService: InstallationDataService;
63+
pluginProvider: DynamicPluginProvider;
64+
logger: LoggerService;
5665
config: Config;
5766
};
5867

@@ -64,6 +73,8 @@ export async function createRouter(
6473
marketplaceApi,
6574
permissions,
6675
installationDataService,
76+
pluginProvider,
77+
logger,
6778
config,
6879
} = options;
6980

@@ -457,5 +468,22 @@ export async function createRouter(
457468
res.json(packages);
458469
});
459470

471+
const plugins = pluginProvider.plugins();
472+
const dynamicPlugins = plugins.map(p => {
473+
// Remove the installer details for the dynamic backend plugins
474+
if (p.platform === 'node') {
475+
const { installer, ...rest } = p;
476+
return rest as BaseDynamicPlugin;
477+
}
478+
return p as BaseDynamicPlugin;
479+
});
480+
router.get('/loaded-plugins', async (req, response) => {
481+
await httpAuth.credentials(req, { allow: ['user', 'service'] });
482+
response.send(dynamicPlugins);
483+
});
484+
const middleware = MiddlewareFactory.create({ logger, config });
485+
486+
router.use(middleware.error());
487+
460488
return router;
461489
}

workspaces/marketplace/plugins/marketplace/knip-report.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,3 @@
66
| :-------------------------- | :---------------- | :------- |
77
| @testing-library/user-event | package.json:64:6 | error |
88
| msw | package.json:65:6 | error |
9-

workspaces/marketplace/plugins/marketplace/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@backstage/plugin-permission-react": "^0.4.36",
4242
"@backstage/theme": "^0.6.7",
4343
"@backstage/types": "^1.2.1",
44+
"@material-table/core": "^6.4.4",
4445
"@material-ui/core": "^4.12.2",
4546
"@monaco-editor/react": "^4.7.0",
4647
"@mui/icons-material": "^5.16.7",

workspaces/marketplace/plugins/marketplace/report.api.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,21 @@
44
55
```ts
66

7-
/// <reference types="react" />
8-
97
import { BackstagePlugin } from '@backstage/core-plugin-api';
108
import { IconComponent } from '@backstage/core-plugin-api';
119
import { JSX as JSX_2 } from 'react/jsx-runtime';
12-
import { JSXElementConstructor } from 'react';
1310
import { PathParams } from '@backstage/core-plugin-api';
14-
import { ReactElement } from 'react';
1511
import { RouteRef } from '@backstage/core-plugin-api';
1612
import { SubRouteRef } from '@backstage/core-plugin-api';
1713

1814
// @public (undocumented)
1915
export const DynamicMarketplacePluginContent: () => JSX_2.Element;
2016

21-
// @public (undocumented)
17+
// @public
2218
export const DynamicMarketplacePluginRouter: () => JSX_2.Element;
2319

24-
// @public (undocumented)
25-
export const InstallationContextProvider: ({ children, }: {
26-
children: ReactElement<any, string | JSXElementConstructor<any>>;
27-
}) => JSX_2.Element;
20+
// @public @deprecated (undocumented)
21+
export const InstallationContextProvider: () => null;
2822

2923
// @public
3024
export const MarketplaceFullPageRouter: () => JSX_2.Element;
@@ -43,11 +37,16 @@ packageRouteRef: SubRouteRef<PathParams<"/packages/:namespace/:name">>;
4337
packageInstallRouteRef: SubRouteRef<PathParams<"/packages/:namespace/:name/install">>;
4438
collectionsRouteRef: SubRouteRef<undefined>;
4539
collectionRouteRef: SubRouteRef<PathParams<"/collections/:namespace/:name">>;
40+
catalogTabRouteRef: SubRouteRef<undefined>;
41+
installedTabRouteRef: SubRouteRef<undefined>;
4642
}, {}, {}>;
4743

4844
// @public
4945
export const MarketplaceTabbedPageRouter: () => JSX_2.Element;
5046

47+
// @public (undocumented)
48+
export const PluginsIcon: IconComponent;
49+
5150
// (No @packageDocumentation comment for this package)
5251

5352
```

0 commit comments

Comments
 (0)