Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions workspaces/marketplace/.changeset/unlucky-hornets-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@red-hat-developer-hub/backstage-plugin-marketplace-backend': minor
'@red-hat-developer-hub/backstage-plugin-marketplace': minor
---

Integrate plugins-info plugin and add `Installed Plugins` tab with enhanced UI
3 changes: 2 additions & 1 deletion workspaces/marketplace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"minimatch": "3",
"node-gyp": "^9.0.0",
"prettier": "^3.4.2",
"typescript": "~5.3.0"
"typescript": "~5.3.0",
"yaml": "^2.8.1"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed on the workspace level? :)

},
"resolutions": {
"@types/react": "^18",
Expand Down
14 changes: 2 additions & 12 deletions workspaces/marketplace/packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ import { githubAuthApiRef } from '@backstage/core-plugin-api';

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

import {
InstallationContextProvider,
DynamicMarketplacePluginRouter as Marketplace,
} from '@red-hat-developer-hub/backstage-plugin-marketplace';
import { DynamicMarketplacePluginRouter as Marketplace } from '@red-hat-developer-hub/backstage-plugin-marketplace';

import { apis } from './apis';
import { entityPage } from './components/catalog/EntityPage';
Expand Down Expand Up @@ -136,14 +133,7 @@ const routes = (
</Route>
<Route path="/settings" element={<UserSettingsPage />} />
<Route path="/catalog-graph" element={<CatalogGraphPage />} />
<Route
path="/extensions"
element={
<InstallationContextProvider>
<Marketplace />
</InstallationContextProvider>
}
/>
<Route path="/extensions" element={<Marketplace />} />
</FlatRoutes>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"dependencies": {
"@backstage/backend-defaults": "^0.11.1",
"@backstage/backend-dynamic-feature-service": "^0.7.3",
"@backstage/backend-plugin-api": "^1.4.1",
"@backstage/catalog-client": "^1.10.2",
"@backstage/catalog-model": "^1.7.5",
Expand All @@ -44,7 +45,6 @@
"@red-hat-developer-hub/backstage-plugin-marketplace-common": "workspace:^",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"yaml": "^2.7.1",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plugins/marketplace-backend/src/installation/FileInstallationStorage.ts is using the yaml package. Please move this back from devDependencies to dependencies.

"zod": "^3.22.4"
},
"devDependencies": {
Expand All @@ -56,7 +56,8 @@
"@types/express": "*",
"@types/supertest": "^2.0.12",
"msw": "^1.0.0",
"supertest": "^6.2.4"
"supertest": "^6.2.4",
"yaml": "^2.7.1"
},
"files": [
"dist"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { CatalogClient } from '@backstage/catalog-client';
import { dynamicPluginsServiceRef } from '@backstage/backend-dynamic-feature-service';

import {
MarketplaceApi,
Expand All @@ -45,8 +46,10 @@ export const marketplacePlugin = createBackendPlugin({
discovery: coreServices.discovery,
logger: coreServices.logger,
permissions: coreServices.permissions,
pluginProvider: dynamicPluginsServiceRef,
},
async init({
pluginProvider,
auth,
config,
httpAuth,
Expand Down Expand Up @@ -75,6 +78,8 @@ export const marketplacePlugin = createBackendPlugin({
installationDataService,
marketplaceApi,
permissions,
pluginProvider,
logger,
config,
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@ import request from 'supertest';
import { stringify } from 'yaml';

import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter';
import { BackendFeature } from '@backstage/backend-plugin-api';
import {
BackendFeature,
createServiceFactory,
} from '@backstage/backend-plugin-api';
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
import {
DynamicPluginProvider,
BaseDynamicPlugin,
dynamicPluginsServiceRef,
} from '@backstage/backend-dynamic-feature-service';
import type { JsonObject } from '@backstage/types';
import {
AuthorizeResult,
Expand Down Expand Up @@ -67,6 +75,43 @@ const BASE_CONFIG = {
},
};

// Mock dynamic plugins data for testing
const mockDynamicPluginsData: BaseDynamicPlugin[] = [
{
name: '@backstage/plugin-catalog-backend',
version: '1.0.0',
role: 'backend',
platform: 'node',
},
{
name: '@backstage/plugin-catalog',
version: '1.0.0',
role: 'frontend',
platform: 'web',
},
{
name: '@red-hat-developer-hub/backstage-plugin-marketplace',
version: '1.0.0',
role: 'frontend',
platform: 'web',
},
];

// Mock DynamicPluginProvider
const mockDynamicPluginProvider: DynamicPluginProvider = {
plugins: () => mockDynamicPluginsData as any,
getScannedPackage: () => ({}) as any,
frontendPlugins: () => [],
backendPlugins: () => [],
};

// Create mock service factory for dynamicPluginsServiceRef
const mockDynamicPluginsServiceFactory = createServiceFactory({
service: dynamicPluginsServiceRef,
deps: {},
factory: () => mockDynamicPluginProvider,
});

const FILE_INSTALL_CONFIG = {
extensions: {
installation: {
Expand Down Expand Up @@ -141,6 +186,7 @@ async function startBackendServer(
mockServices.rootConfig.factory({
data: { ...BASE_CONFIG, ...(config ?? {}) },
}),
mockDynamicPluginsServiceFactory,
];

if (authorizeResult) {
Expand Down Expand Up @@ -881,4 +927,31 @@ describe('createRouter', () => {
},
);
});

describe('GET /loaded-plugins', () => {
it('should return the list of loaded dynamic plugins', async () => {
const { backendServer } = await setupTestWithMockCatalog({
mockData: {},
});

const response = await request(backendServer).get(
'/api/extensions/loaded-plugins',
);

expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
// The response should be an array of dynamic plugin info objects
// Each plugin should have the expected structure
response.body.forEach((plugin: any) => {
expect(plugin).toEqual(
expect.objectContaining({
name: expect.any(String),
version: expect.any(String),
role: expect.any(String),
platform: expect.any(String),
}),
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { Config } from '@backstage/config';
import {
HttpAuthService,
PermissionsService,
LoggerService,
} from '@backstage/backend-plugin-api';
import {
AuthorizeResult,
Expand All @@ -48,11 +49,19 @@ import { matches } from './utils/permissionUtils';
import { InstallationDataService } from './installation/InstallationDataService';
import { ConfigFormatError } from './errors/ConfigFormatError';

import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';
import {
BaseDynamicPlugin,
DynamicPluginProvider,
} from '@backstage/backend-dynamic-feature-service';

export type MarketplaceRouterOptions = {
httpAuth: HttpAuthService;
marketplaceApi: MarketplaceApi;
permissions: PermissionsService;
installationDataService: InstallationDataService;
pluginProvider: DynamicPluginProvider;
logger: LoggerService;
config: Config;
};

Expand All @@ -64,6 +73,8 @@ export async function createRouter(
marketplaceApi,
permissions,
installationDataService,
pluginProvider,
logger,
config,
} = options;

Expand Down Expand Up @@ -457,5 +468,22 @@ export async function createRouter(
res.json(packages);
});

const plugins = pluginProvider.plugins();
const dynamicPlugins = plugins.map(p => {
// Remove the installer details for the dynamic backend plugins
if (p.platform === 'node') {
const { installer, ...rest } = p;
return rest as BaseDynamicPlugin;
}
return p as BaseDynamicPlugin;
});
router.get('/loaded-plugins', async (req, response) => {
await httpAuth.credentials(req, { allow: ['user', 'service'] });
response.send(dynamicPlugins);
});
const middleware = MiddlewareFactory.create({ logger, config });

router.use(middleware.error());

return router;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@
| :-------------------------- | :---------------- | :------- |
| @testing-library/user-event | package.json:64:6 | error |
| msw | package.json:65:6 | error |

1 change: 1 addition & 0 deletions workspaces/marketplace/plugins/marketplace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@backstage/plugin-permission-react": "^0.4.36",
"@backstage/theme": "^0.6.7",
"@backstage/types": "^1.2.1",
"@material-table/core": "^6.4.4",
"@material-ui/core": "^4.12.2",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^5.16.7",
Expand Down
16 changes: 6 additions & 10 deletions workspaces/marketplace/plugins/marketplace/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,19 @@

```ts

/// <reference types="react" />

import { BackstagePlugin } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { JSXElementConstructor } from 'react';
import { PathParams } from '@backstage/core-plugin-api';
import { ReactElement } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
import { SubRouteRef } from '@backstage/core-plugin-api';

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

// @public (undocumented)
// @public
export const DynamicMarketplacePluginRouter: () => JSX_2.Element;

// @public (undocumented)
export const InstallationContextProvider: ({ children, }: {
children: ReactElement<any, string | JSXElementConstructor<any>>;
}) => JSX_2.Element;

// @public
export const MarketplaceFullPageRouter: () => JSX_2.Element;

Expand All @@ -43,11 +34,16 @@ packageRouteRef: SubRouteRef<PathParams<"/packages/:namespace/:name">>;
packageInstallRouteRef: SubRouteRef<PathParams<"/packages/:namespace/:name/install">>;
collectionsRouteRef: SubRouteRef<undefined>;
collectionRouteRef: SubRouteRef<PathParams<"/collections/:namespace/:name">>;
catalogTabRouteRef: SubRouteRef<undefined>;
installedTabRouteRef: SubRouteRef<undefined>;
}, {}, {}>;

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

// @public (undocumented)
export const PluginsIcon: IconComponent;

// (No @packageDocumentation comment for this package)

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
DiscoveryApi,
FetchApi,
IdentityApi,
} from '@backstage/core-plugin-api';
import { DynamicPluginInfo, DynamicPluginsInfoApi } from '.';

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

const loadedPluginsEndpoint = '/loaded-plugins';

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

constructor(options: DynamicPluginsInfoClientOptions) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
this.identityApi = options.identityApi;
}
async listLoadedPlugins(): Promise<DynamicPluginInfo[]> {
const baseUrl = await this.discoveryApi.getBaseUrl('extensions');
const targetUrl = `${baseUrl}${loadedPluginsEndpoint}`;
const { token } = await this.identityApi.getCredentials();
const response = await this.fetchApi.fetch(targetUrl, {
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
});
const data = await response.json();
if (!response.ok) {
const message = data.error?.message || data.message || data.toString?.();
throw new Error(`Failed to load dynamic plugin info: ${message}`);
}
return data;
}
}
14 changes: 14 additions & 0 deletions workspaces/marketplace/plugins/marketplace/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ import { MarketplaceApi } from '@red-hat-developer-hub/backstage-plugin-marketpl
export const marketplaceApiRef = createApiRef<MarketplaceApi>({
id: 'plugin.extensions.api-ref',
});

export type DynamicPluginInfo = {
name: string;
version: string;
role: string;
platform: string;
};
export interface DynamicPluginsInfoApi {
listLoadedPlugins(): Promise<DynamicPluginInfo[]>;
}

export const dynamicPluginsInfoApiRef = createApiRef<DynamicPluginsInfoApi>({
id: 'plugin.extensions.dynamic-plugins-info',
});
Loading