From db8f890447f5612ec6e77fb495fe32a8cb9f5683 Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 5 Sep 2025 02:56:56 -0400 Subject: [PATCH 01/14] feat(marketplace): integrate plugins-info plugin Signed-off-by: Yi Cai --- workspaces/marketplace/package.json | 3 +- .../plugins/marketplace-backend/package.json | 5 +- .../plugins/marketplace-backend/src/plugin.ts | 5 + .../marketplace-backend/src/router.test.ts | 75 ++- .../plugins/marketplace-backend/src/router.ts | 28 + .../plugins/marketplace/report.api.md | 7 +- .../src/api/DynamicPluginsInfoClient.ts | 56 ++ .../plugins/marketplace/src/api/index.ts | 14 + .../InstalledPluginsTable.tsx | 198 ++++++ .../src/hooks/useInstalledPluginsCount.ts | 47 ++ .../pages/DynamicMarketplacePluginRouter.tsx | 132 ++-- .../plugins/marketplace/src/plugin.ts | 26 +- .../plugins/marketplace/src/routes.ts | 14 + .../src/utils/pluginProcessing.test.ts | 290 +++++++++ .../marketplace/src/utils/pluginProcessing.ts | 90 +++ workspaces/marketplace/yarn.lock | 615 +++++++++++++----- 16 files changed, 1371 insertions(+), 234 deletions(-) create mode 100644 workspaces/marketplace/plugins/marketplace/src/api/DynamicPluginsInfoClient.ts create mode 100644 workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx create mode 100644 workspaces/marketplace/plugins/marketplace/src/hooks/useInstalledPluginsCount.ts create mode 100644 workspaces/marketplace/plugins/marketplace/src/utils/pluginProcessing.test.ts create mode 100644 workspaces/marketplace/plugins/marketplace/src/utils/pluginProcessing.ts diff --git a/workspaces/marketplace/package.json b/workspaces/marketplace/package.json index 6e56ee5509..b1d8cdc032 100644 --- a/workspaces/marketplace/package.json +++ b/workspaces/marketplace/package.json @@ -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" }, "resolutions": { "@types/react": "^18", diff --git a/workspaces/marketplace/plugins/marketplace-backend/package.json b/workspaces/marketplace/plugins/marketplace-backend/package.json index 4c23eafe0c..3bc2969230 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/package.json +++ b/workspaces/marketplace/plugins/marketplace-backend/package.json @@ -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", @@ -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", "zod": "^3.22.4" }, "devDependencies": { @@ -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" diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/plugin.ts b/workspaces/marketplace/plugins/marketplace-backend/src/plugin.ts index 1de979e175..c2d6c41556 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/plugin.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/plugin.ts @@ -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, @@ -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, @@ -75,6 +78,8 @@ export const marketplacePlugin = createBackendPlugin({ installationDataService, marketplaceApi, permissions, + pluginProvider, + logger, config, }), ); diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts index dbf26a9a24..160fde45cd 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.test.ts @@ -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, @@ -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: { @@ -141,6 +186,7 @@ async function startBackendServer( mockServices.rootConfig.factory({ data: { ...BASE_CONFIG, ...(config ?? {}) }, }), + mockDynamicPluginsServiceFactory, ]; if (authorizeResult) { @@ -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), + }), + ); + }); + }); + }); }); diff --git a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts index d5118de128..4aefb0ffc4 100644 --- a/workspaces/marketplace/plugins/marketplace-backend/src/router.ts +++ b/workspaces/marketplace/plugins/marketplace-backend/src/router.ts @@ -22,6 +22,7 @@ import type { Config } from '@backstage/config'; import { HttpAuthService, PermissionsService, + LoggerService, } from '@backstage/backend-plugin-api'; import { AuthorizeResult, @@ -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; }; @@ -64,6 +73,8 @@ export async function createRouter( marketplaceApi, permissions, installationDataService, + pluginProvider, + logger, config, } = options; @@ -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; } diff --git a/workspaces/marketplace/plugins/marketplace/report.api.md b/workspaces/marketplace/plugins/marketplace/report.api.md index e0f2209e30..8f66203193 100644 --- a/workspaces/marketplace/plugins/marketplace/report.api.md +++ b/workspaces/marketplace/plugins/marketplace/report.api.md @@ -18,7 +18,7 @@ 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) @@ -43,11 +43,16 @@ packageRouteRef: SubRouteRef>; packageInstallRouteRef: SubRouteRef>; collectionsRouteRef: SubRouteRef; collectionRouteRef: SubRouteRef>; +catalogTabRouteRef: SubRouteRef; +installedTabRouteRef: SubRouteRef; }, {}, {}>; // @public export const MarketplaceTabbedPageRouter: () => JSX_2.Element; +// @public (undocumented) +export const PluginsIcon: IconComponent; + // (No @packageDocumentation comment for this package) ``` diff --git a/workspaces/marketplace/plugins/marketplace/src/api/DynamicPluginsInfoClient.ts b/workspaces/marketplace/plugins/marketplace/src/api/DynamicPluginsInfoClient.ts new file mode 100644 index 0000000000..b56c0e91b4 --- /dev/null +++ b/workspaces/marketplace/plugins/marketplace/src/api/DynamicPluginsInfoClient.ts @@ -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 { + 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; + } +} diff --git a/workspaces/marketplace/plugins/marketplace/src/api/index.ts b/workspaces/marketplace/plugins/marketplace/src/api/index.ts index 7d9f79ceef..7d3c712353 100644 --- a/workspaces/marketplace/plugins/marketplace/src/api/index.ts +++ b/workspaces/marketplace/plugins/marketplace/src/api/index.ts @@ -21,3 +21,17 @@ import { MarketplaceApi } from '@red-hat-developer-hub/backstage-plugin-marketpl export const marketplaceApiRef = createApiRef({ id: 'plugin.extensions.api-ref', }); + +export type DynamicPluginInfo = { + name: string; + version: string; + role: string; + platform: string; +}; +export interface DynamicPluginsInfoApi { + listLoadedPlugins(): Promise; +} + +export const dynamicPluginsInfoApiRef = createApiRef({ + id: 'plugin.extensions.dynamic-plugins-info', +}); diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx new file mode 100644 index 0000000000..74b6a3a4a5 --- /dev/null +++ b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx @@ -0,0 +1,198 @@ +/* + * 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 { 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'; +import { useInstalledPluginsCount } from '../../hooks/useInstalledPluginsCount'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; + +import { processPluginsForDisplay } from '../../utils/pluginProcessing'; + +export const InstalledPluginsTable = () => { + const [error, setError] = useState(undefined); + const [currentSearchTerm, setCurrentSearchTerm] = useState(''); + const { count } = useInstalledPluginsCount(); + const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef); + let data: DynamicPluginInfo[] = []; + const columns: TableColumn[] = [ + { + title: 'Name', + field: 'name', + defaultSort: 'asc', + }, + { + title: 'Version', + field: 'version', + width: '15%', + }, + { + title: 'Actions', + render: (_rowData: DynamicPluginInfo) => { + return ( + + + { + // TODO: Implement edit functionality + }} + > + + + + + { + // TODO: Implement delete functionality + }} + > + + + + + { + // TODO: Implement download functionality + }} + > + + + + + ); + }, + sorting: false, + }, + ]; + const fetchData = async ( + query: Query, + ): Promise> => { + const { + orderBy = { field: 'name' }, + orderDirection = 'asc', + page = 0, + pageSize = 5, + search = '', + } = query || {}; + + // Track current search term for conditional empty message + setCurrentSearchTerm(search); + + try { + // for now sorting/searching/pagination is handled client-side + const installedPlugins = await dynamicPluginInfo.listLoadedPlugins(); + + // Process plugins to create single rows with readable names + const processedPlugins = processPluginsForDisplay(installedPlugins); + + data = [...processedPlugins] + .sort( + ( + a: Record, + b: Record, + ) => { + 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; + let end = totalCount; + if (totalCount > pageSize) { + start = page * pageSize; + end = start + pageSize; + } + return { data: data.slice(start, end), page, totalCount }; + } catch (loadingError) { + // eslint-disable-next-line no-console + console.error('Failed to load plugins', loadingError); + setError(loadingError as Error); + return { data: [], totalCount: 0, page: 0 }; + } + }; + if (error) { + return ; + } + + // Conditional empty message based on search state + const emptyMessage = currentSearchTerm.trim() + ? 'No results found. Try a different search term.' + : 'No records to display'; + + return ( + + ); +}; diff --git a/workspaces/marketplace/plugins/marketplace/src/hooks/useInstalledPluginsCount.ts b/workspaces/marketplace/plugins/marketplace/src/hooks/useInstalledPluginsCount.ts new file mode 100644 index 0000000000..5fd7a82ff7 --- /dev/null +++ b/workspaces/marketplace/plugins/marketplace/src/hooks/useInstalledPluginsCount.ts @@ -0,0 +1,47 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { useApi } from '@backstage/core-plugin-api'; +import { dynamicPluginsInfoApiRef } from '../api'; +import { getUniquePluginsCount } from '../utils/pluginProcessing'; + +export const useInstalledPluginsCount = () => { + const [count, setCount] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(undefined); + const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef); + + useEffect(() => { + const fetchCount = async () => { + try { + setLoading(true); + setError(undefined); + const plugins = await dynamicPluginInfo.listLoadedPlugins(); + setCount(getUniquePluginsCount(plugins)); + } catch (err) { + setError(err as Error); + setCount(0); + } finally { + setLoading(false); + } + }; + + fetchCount(); + }, [dynamicPluginInfo]); + + return { count, loading, error }; +}; diff --git a/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx b/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx index e13a240182..d54f10b3ee 100644 --- a/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx @@ -14,110 +14,100 @@ * limitations under the License. */ -import type { ComponentType } from 'react'; import { Routes, Route } from 'react-router-dom'; - import { Page, Header, - TabbedLayout, ErrorBoundary, + TabbedLayout, } from '@backstage/core-components'; - -import { useScalprum } from '@scalprum/react-core'; +import FactCheckOutlinedIcon from '@mui/icons-material/FactCheckOutlined'; +import CategoryOutlinedIcon from '@mui/icons-material/CategoryOutlined'; +import Typography from '@mui/material/Typography'; import { themeId } from '../consts'; - import { ReactQueryProvider } from '../components/ReactQueryProvider'; - import { MarketplaceCatalogContent } from '../components/MarketplaceCatalogContent'; - -// import { MarketplaceCollectionsGrid } from '../components/MarketplaceCollectionsGrid'; +import { InstalledPluginsTable } from '../components/InstalledPlugins/InstalledPluginsTable'; +import { useInstalledPluginsCount } from '../hooks/useInstalledPluginsCount'; import { MarketplaceCollectionPage } from './MarketplaceCollectionPage'; - -// import { MarketplacePluginsTable } from '../components/MarketplacePluginsTable'; import { MarketplacePluginDrawer } from '../components/MarketplacePluginDrawer'; import { MarketplacePluginInstallPage } from './MarketplacePluginInstallPage'; - -// import { MarketplacePackagesTable } from '../components/MarketplacePackagesTable'; import { MarketplacePackageDrawer } from '../components/MarketplacePackageDrawer'; import { MarketplacePackageInstallPage } from './MarketplacePackageInstallPage'; -export interface PluginTab { - Component: ComponentType; - config: { - path: string; - title: string; - }; -} +// Constants for consistent styling +const TAB_ICON_STYLE = { + display: 'inline-flex', + alignItems: 'center', + gap: '4px', + fontSize: '14px', +} as const; + +const ICON_PROPS = { + fontSize: 'small' as const, + sx: { pr: '2px' }, +}; -export interface ScalprumState { - api?: { - dynamicRootConfig?: { - mountPoints?: { - 'internal.plugins/tab': PluginTab[]; - }; - }; - }; -} +// Helper component for tab labels with icons +const TabLabel = ({ + icon, + children, +}: { + icon: React.ReactElement; + children: React.ReactNode; +}) => ( + + {icon} {children} + +); -const Tabs = () => { - const scalprum = useScalprum(); +const MarketplacePage = () => { + const { count: installedPluginsCount, loading } = useInstalledPluginsCount(); - const tabs = scalprum.api?.dynamicRootConfig?.mountPoints?.[ - 'internal.plugins/tab' - ] ?? [ - { - Component: MarketplaceCatalogContent, - config: { - path: '', - title: 'Catalog', - }, - }, - ]; + const installedPluginsTitle = loading + ? 'Installed Plugins' + : `Installed Plugins (${installedPluginsCount})`; return ( <>
- {/* + }> + Catalog + + ), + }} + > - */} - - {tabs.map(({ Component, config }) => ( - - - - - - ))} - - {/* - - - - - - - - - - + + }> + {installedPluginsTitle} + + ), + }} + > - + - */} + ( path="/packages/:namespace/:name/install" Component={MarketplacePackageInstallPage} /> - + ); diff --git a/workspaces/marketplace/plugins/marketplace/src/plugin.ts b/workspaces/marketplace/plugins/marketplace/src/plugin.ts index e39742b9ea..b5b1d903a4 100644 --- a/workspaces/marketplace/plugins/marketplace/src/plugin.ts +++ b/workspaces/marketplace/plugins/marketplace/src/plugin.ts @@ -27,10 +27,12 @@ import { } from '@backstage/core-plugin-api'; import MUIMarketplaceIcon from '@mui/icons-material/ShoppingBasketOutlined'; +import MUIPluginsIcon from '@mui/icons-material/PowerOutlined'; import { MarketplaceBackendClient } from '@red-hat-developer-hub/backstage-plugin-marketplace-common'; -import { marketplaceApiRef } from './api'; +import { marketplaceApiRef, dynamicPluginsInfoApiRef } from './api'; +import { DynamicPluginsInfoClient } from './api/DynamicPluginsInfoClient'; import { allRoutes } from './routes'; /** @@ -57,6 +59,20 @@ export const marketplacePlugin = createPlugin({ configApi, }), }), + createApiFactory({ + api: dynamicPluginsInfoApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + identityApi: identityApiRef, + }, + factory: ({ discoveryApi, fetchApi, identityApi }) => + new DynamicPluginsInfoClient({ + discoveryApi, + fetchApi, + identityApi, + }), + }), ], }); @@ -91,11 +107,12 @@ export const MarketplaceTabbedPageRouter = marketplacePlugin.provide( ); /** + * Main marketplace plugin router with tabs and sub-routes. * @public */ export const DynamicMarketplacePluginRouter = marketplacePlugin.provide( createRoutableExtension({ - name: 'DynamicMarketplacePluginRouter', + name: 'MarketplaceRouter', component: () => import('./pages/DynamicMarketplacePluginRouter').then( m => m.DynamicMarketplacePluginRouter, @@ -138,3 +155,8 @@ export const InstallationContextProvider = marketplacePlugin.provide( * @public */ export const MarketplaceIcon: IconComponent = MUIMarketplaceIcon; + +/** + * @public + */ +export const PluginsIcon: IconComponent = MUIPluginsIcon; diff --git a/workspaces/marketplace/plugins/marketplace/src/routes.ts b/workspaces/marketplace/plugins/marketplace/src/routes.ts index f8112e850d..a4d480088d 100644 --- a/workspaces/marketplace/plugins/marketplace/src/routes.ts +++ b/workspaces/marketplace/plugins/marketplace/src/routes.ts @@ -68,6 +68,18 @@ export const collectionRouteRef = createSubRouteRef({ parent: rootRouteRef, }); +export const catalogTabRouteRef = createSubRouteRef({ + id: 'extensions/catalog', + path: '/catalog', + parent: rootRouteRef, +}); + +export const installedTabRouteRef = createSubRouteRef({ + id: 'extensions/installed', + path: '/installed', + parent: rootRouteRef, +}); + export const allRoutes = { rootRouteRef, pluginsRouteRef, @@ -78,4 +90,6 @@ export const allRoutes = { packageInstallRouteRef, collectionsRouteRef, collectionRouteRef, + catalogTabRouteRef, + installedTabRouteRef, }; diff --git a/workspaces/marketplace/plugins/marketplace/src/utils/pluginProcessing.test.ts b/workspaces/marketplace/plugins/marketplace/src/utils/pluginProcessing.test.ts new file mode 100644 index 0000000000..496ae955f4 --- /dev/null +++ b/workspaces/marketplace/plugins/marketplace/src/utils/pluginProcessing.test.ts @@ -0,0 +1,290 @@ +/* + * 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 { DynamicPluginInfo } from '../api'; +import { + getReadableName, + getBasePluginName, + processPluginsForDisplay, + getUniquePluginsCount, +} from './pluginProcessing'; + +describe('pluginProcessing', () => { + describe('getReadableName', () => { + it('should handle Red Hat Developer Hub plugins', () => { + expect( + getReadableName( + '@red-hat-developer-hub/backstage-plugin-marketplace-dynamic', + ), + ).toBe('Marketplace'); + expect( + getReadableName( + '@red-hat-developer-hub/backstage-plugin-global-header-dynamic', + ), + ).toBe('Global Header'); + expect( + getReadableName( + '@red-hat-developer-hub/backstage-plugin-quickstart-dynamic', + ), + ).toBe('Quickstart'); + }); + + it('should handle Red Hat Developer Hub plugins with backend suffix', () => { + expect( + getReadableName( + '@red-hat-developer-hub/backstage-plugin-marketplace-backend-dynamic', + ), + ).toBe('Marketplace'); + }); + + it('should handle Red Hat Developer Hub display names with spaces', () => { + expect( + getReadableName('@Red Hat Developer Hub/Backstage Plugin Marketplace'), + ).toBe('Marketplace'); + expect( + getReadableName('@Red Hat Developer Hub/Backstage Plugin Quickstart'), + ).toBe('Quickstart'); + expect( + getReadableName( + '@Red Hat Developer Hub/Backstage Plugin Dynamic Home Page', + ), + ).toBe('Dynamic Home Page'); + }); + + it('should handle community plugins', () => { + expect(getReadableName('@backstage-community/plugin-acr-dynamic')).toBe( + 'Acr', + ); + expect( + getReadableName( + 'backstage-community-plugin-analytics-provider-segment', + ), + ).toBe('Analytics Provider Segment'); + }); + + it('should handle official Backstage plugins', () => { + expect(getReadableName('@backstage/plugin-catalog-backend')).toBe( + 'Catalog', + ); + expect(getReadableName('backstage-plugin-kubernetes')).toBe('Kubernetes'); + }); + + it('should remove various suffixes', () => { + expect(getReadableName('some-plugin-dynamic')).toBe('Some Plugin'); + expect(getReadableName('some-plugin-backend')).toBe('Some Plugin'); + expect(getReadableName('some-plugin-frontend')).toBe('Some Plugin'); + }); + + it('should handle edge cases', () => { + expect(getReadableName('')).toBe(''); + expect(getReadableName('simple-name')).toBe('Simple Name'); + expect(getReadableName('already-capitalized-name')).toBe( + 'Already Capitalized Name', + ); + }); + }); + + describe('getBasePluginName', () => { + it('should normalize Red Hat Developer Hub plugins for deduplication', () => { + expect( + getBasePluginName( + '@red-hat-developer-hub/backstage-plugin-marketplace-dynamic', + ), + ).toBe('marketplace'); + expect( + getBasePluginName( + '@red-hat-developer-hub/backstage-plugin-marketplace-backend-dynamic', + ), + ).toBe('marketplace'); + expect( + getBasePluginName( + '@red-hat-developer-hub/backstage-plugin-global-header-dynamic', + ), + ).toBe('global-header'); + }); + + it('should normalize display names to base names', () => { + expect( + getBasePluginName( + '@Red Hat Developer Hub/Backstage Plugin Marketplace', + ), + ).toBe('marketplace'); + expect( + getBasePluginName( + '@Red Hat Developer Hub/Backstage Plugin Dynamic Home Page', + ), + ).toBe('dynamic-home-page'); + }); + + it('should handle community plugins', () => { + expect(getBasePluginName('@backstage-community/plugin-acr-dynamic')).toBe( + 'acr', + ); + expect( + getBasePluginName( + 'backstage-community-plugin-analytics-provider-segment', + ), + ).toBe('analytics-provider-segment'); + }); + + it('should remove frontend/backend suffixes for proper grouping', () => { + expect(getBasePluginName('some-plugin-frontend')).toBe('some-plugin'); + expect(getBasePluginName('some-plugin-backend')).toBe('some-plugin'); + expect(getBasePluginName('some-plugin-dynamic')).toBe('some-plugin'); + }); + }); + + describe('processPluginsForDisplay', () => { + const mockPlugins: DynamicPluginInfo[] = [ + { + name: '@red-hat-developer-hub/backstage-plugin-marketplace-dynamic', + version: '0.9.2', + role: 'frontend-plugin', + platform: 'web', + }, + { + name: '@red-hat-developer-hub/backstage-plugin-marketplace-backend-dynamic', + version: '0.8.0', + role: 'backend-plugin', + platform: 'node', + }, + { + name: '@red-hat-developer-hub/backstage-plugin-global-header-dynamic', + version: '1.15.1', + role: 'frontend-plugin', + platform: 'web', + }, + { + name: '@backstage-community/plugin-acr-dynamic', + version: '1.12.1', + role: 'frontend-plugin', + platform: 'web', + }, + ]; + + it('should deduplicate plugins with frontend/backend variants', () => { + const result = processPluginsForDisplay(mockPlugins); + + // Should have 3 unique plugins (marketplace deduped, global-header, acr) + expect(result).toHaveLength(3); + + // Check that marketplace is present (should prefer frontend) + const marketplacePlugin = result.find(p => p.name === 'Marketplace'); + expect(marketplacePlugin).toBeDefined(); + expect(marketplacePlugin?.role).toBe('frontend-plugin'); + expect(marketplacePlugin?.version).toBe('0.9.2'); // Frontend version + }); + + it('should prefer frontend plugins over backend when deduplicating', () => { + const result = processPluginsForDisplay(mockPlugins); + const marketplacePlugin = result.find(p => p.name === 'Marketplace'); + + expect(marketplacePlugin?.role).toBe('frontend-plugin'); + expect(marketplacePlugin?.platform).toBe('web'); + }); + + it('should preserve single plugins without duplicates', () => { + const result = processPluginsForDisplay(mockPlugins); + + const globalHeaderPlugin = result.find(p => p.name === 'Global Header'); + expect(globalHeaderPlugin).toBeDefined(); + expect(globalHeaderPlugin?.version).toBe('1.15.1'); + + const acrPlugin = result.find(p => p.name === 'Acr'); + expect(acrPlugin).toBeDefined(); + expect(acrPlugin?.version).toBe('1.12.1'); + }); + + it('should handle empty plugin list', () => { + const result = processPluginsForDisplay([]); + expect(result).toHaveLength(0); + }); + + it('should handle plugins with same base name but both frontend', () => { + const plugins: DynamicPluginInfo[] = [ + { + name: 'plugin-test-frontend', + version: '1.0.0', + role: 'frontend-plugin', + platform: 'web', + }, + { + name: 'plugin-test-frontend-v2', + version: '2.0.0', + role: 'frontend-plugin', + platform: 'web', + }, + ]; + + const result = processPluginsForDisplay(plugins); + // Should keep both since they have different base names + expect(result).toHaveLength(2); + }); + }); + + describe('getUniquePluginsCount', () => { + const mockPlugins: DynamicPluginInfo[] = [ + { + name: '@red-hat-developer-hub/backstage-plugin-marketplace-dynamic', + version: '0.9.2', + role: 'frontend-plugin', + platform: 'web', + }, + { + name: '@red-hat-developer-hub/backstage-plugin-marketplace-backend-dynamic', + version: '0.8.0', + role: 'backend-plugin', + platform: 'node', + }, + { + name: '@red-hat-developer-hub/backstage-plugin-global-header-dynamic', + version: '1.15.1', + role: 'frontend-plugin', + platform: 'web', + }, + ]; + + it('should return correct count after deduplication', () => { + const count = getUniquePluginsCount(mockPlugins); + expect(count).toBe(2); // marketplace (deduped) + global-header + }); + + it('should return 0 for empty plugin list', () => { + const count = getUniquePluginsCount([]); + expect(count).toBe(0); + }); + + it('should return correct count for plugins without duplicates', () => { + const uniquePlugins: DynamicPluginInfo[] = [ + { + name: 'plugin-a', + version: '1.0.0', + role: 'frontend-plugin', + platform: 'web', + }, + { + name: 'plugin-b', + version: '1.0.0', + role: 'frontend-plugin', + platform: 'web', + }, + ]; + + const count = getUniquePluginsCount(uniquePlugins); + expect(count).toBe(2); + }); + }); +}); diff --git a/workspaces/marketplace/plugins/marketplace/src/utils/pluginProcessing.ts b/workspaces/marketplace/plugins/marketplace/src/utils/pluginProcessing.ts new file mode 100644 index 0000000000..3811be7351 --- /dev/null +++ b/workspaces/marketplace/plugins/marketplace/src/utils/pluginProcessing.ts @@ -0,0 +1,90 @@ +/* + * 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 { DynamicPluginInfo } from '../api'; + +// Helper function to convert package name to readable display name +export const getReadableName = (packageName: string): string => { + // Remove common prefixes and convert to readable format + const readableName = packageName + .replace(/^@Red Hat Developer Hub\/Backstage Plugin\s*/, '') // Red Hat plugins (display names) + .replace(/^@red-hat-developer-hub\/backstage-plugin-/, '') // Red Hat plugins (package names) + .replace(/^red-hat-developer-hub-backstage-plugin-/, '') // Red Hat kebab-case + .replace(/^@backstage-community\/plugin-/, '') // Community plugins + .replace(/^backstage-community-plugin-/, '') // Community plugins alt + .replace(/^@backstage\/plugin-/, '') // Official Backstage plugins + .replace(/^backstage-plugin-/, '') // Generic backstage plugins + .replace(/-dynamic$/, '') // Remove -dynamic suffix + .replace(/-backend$/, '') // Remove -backend suffix + .replace(/-frontend$/, '') // Remove -frontend suffix + .replace(/^[\s-]+/, '') // Remove leading spaces or dashes + .replace(/-/g, ' ') // Convert remaining dashes to spaces + .replace(/\b\w/g, l => l.toUpperCase()); // Capitalize first letter of each word + + return readableName; +}; + +// Helper function to extract base plugin name (without frontend/backend suffix) +export const getBasePluginName = (packageName: string): string => { + const baseName = packageName + .replace(/^@Red Hat Developer Hub\/Backstage Plugin\s*/, '') // Red Hat plugins (display names) + .replace(/^@red-hat-developer-hub\/backstage-plugin-/, '') // Red Hat plugins (package names) + .replace(/^red-hat-developer-hub-backstage-plugin-/, '') // Red Hat kebab-case + .replace(/^@backstage-community\/plugin-/, '') // Community plugins + .replace(/^backstage-community-plugin-/, '') // Community plugins alt + .replace(/^@backstage\/plugin-/, '') // Official Backstage plugins + .replace(/^backstage-plugin-/, '') // Generic backstage plugins + .replace(/-dynamic$/, '') // Remove -dynamic suffix + .replace(/-frontend$/, '') // Remove -frontend suffix + .replace(/-backend$/, '') // Remove -backend suffix + .replace(/^[\s-]+/, '') // Remove leading spaces or dashes + .toLowerCase() // Normalize to lowercase for consistent comparison + .replace(/\s+/g, '-'); // Convert spaces back to dashes for base name + + return baseName; +}; + +// Process plugins to create single rows with readable names +export const processPluginsForDisplay = ( + plugins: DynamicPluginInfo[], +): DynamicPluginInfo[] => { + const pluginMap = new Map(); + + plugins.forEach(plugin => { + const basePluginName = getBasePluginName(plugin.name); + const existingPlugin = pluginMap.get(basePluginName); + + if ( + !existingPlugin || + (plugin.role === 'frontend-plugin' && + existingPlugin.role !== 'frontend-plugin') + ) { + // First occurrence of this plugin OR prefer frontend over backend + pluginMap.set(basePluginName, { + ...plugin, + name: getReadableName(plugin.name), + }); + } + // If both are frontend or both are backend, keep the first one + }); + + return Array.from(pluginMap.values()); +}; + +// Get the count of unique plugins (after deduplication) +export const getUniquePluginsCount = (plugins: DynamicPluginInfo[]): number => { + return processPluginsForDisplay(plugins).length; +}; diff --git a/workspaces/marketplace/yarn.lock b/workspaces/marketplace/yarn.lock index 8ad412d478..656b049af4 100644 --- a/workspaces/marketplace/yarn.lock +++ b/workspaces/marketplace/yarn.lock @@ -33,14 +33,14 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:^11.7.0": - version: 11.7.2 - resolution: "@apidevtools/json-schema-ref-parser@npm:11.7.2" +"@apidevtools/json-schema-ref-parser@npm:^14.0.3": + version: 14.2.0 + resolution: "@apidevtools/json-schema-ref-parser@npm:14.2.0" dependencies: - "@jsdevtools/ono": ^7.1.3 - "@types/json-schema": ^7.0.15 js-yaml: ^4.1.0 - checksum: 44096e5cd5a03b17ee5eb0a3b9e9a4db85d87da8ae2abda264eae615f2a43e3e6ba5ca208e1161d4d946755b121c10a9550e88792a725951f2c4cff6df0d8a19 + peerDependencies: + "@types/json-schema": ^7.0.15 + checksum: fff43815d0957718b0e66c066944396a9f30e312516d60a0ce3eb4d8f6f8833bdeca059565b0e1a35da89645e4123bd5357249effe04997dc40b4e8b3a0f6981 languageName: node linkType: hard @@ -2902,14 +2902,14 @@ __metadata: languageName: node linkType: hard -"@backstage/backend-app-api@npm:^1.2.2, @backstage/backend-app-api@npm:^1.2.5": - version: 1.2.5 - resolution: "@backstage/backend-app-api@npm:1.2.5" +"@backstage/backend-app-api@npm:^1.2.2, @backstage/backend-app-api@npm:^1.2.5, @backstage/backend-app-api@npm:^1.2.6": + version: 1.2.6 + resolution: "@backstage/backend-app-api@npm:1.2.6" dependencies: - "@backstage/backend-plugin-api": ^1.4.1 + "@backstage/backend-plugin-api": ^1.4.2 "@backstage/config": ^1.3.3 "@backstage/errors": ^1.2.7 - checksum: b0c43dbe6671f736ae5fd405262b3221e7a908b5c341ca816f7a828f9cb8a74e6275b2a77ef98e37d6a34b3d78a72af2a85b7c9e8a36b8fa96856fb515ecaac3 + checksum: 9e15ab6e7125663d24d2697091f1f70fb873d41549ed4b05c283ea1e86f38af8aa51939e4a366729204697effd4f0a10c9c6db586fa62e2b6d7ce163dcbd2d55 languageName: node linkType: hard @@ -3073,6 +3073,90 @@ __metadata: languageName: node linkType: hard +"@backstage/backend-defaults@npm:^0.12.0": + version: 0.12.0 + resolution: "@backstage/backend-defaults@npm:0.12.0" + dependencies: + "@aws-sdk/abort-controller": ^3.347.0 + "@aws-sdk/client-codecommit": ^3.350.0 + "@aws-sdk/client-s3": ^3.350.0 + "@aws-sdk/credential-providers": ^3.350.0 + "@aws-sdk/types": ^3.347.0 + "@azure/storage-blob": ^12.5.0 + "@backstage/backend-app-api": ^1.2.6 + "@backstage/backend-dev-utils": ^0.1.5 + "@backstage/backend-plugin-api": ^1.4.2 + "@backstage/cli-node": ^0.2.14 + "@backstage/config": ^1.3.3 + "@backstage/config-loader": ^1.10.2 + "@backstage/errors": ^1.2.7 + "@backstage/integration": ^1.17.1 + "@backstage/integration-aws-node": ^0.1.17 + "@backstage/plugin-auth-node": ^0.6.6 + "@backstage/plugin-events-node": ^0.4.14 + "@backstage/plugin-permission-node": ^0.10.3 + "@backstage/types": ^1.2.1 + "@google-cloud/storage": ^7.0.0 + "@keyv/memcache": ^2.0.1 + "@keyv/redis": ^4.0.1 + "@keyv/valkey": ^1.0.1 + "@manypkg/get-packages": ^1.1.3 + "@octokit/rest": ^19.0.3 + "@opentelemetry/api": ^1.9.0 + "@types/cors": ^2.8.6 + "@types/express": ^4.17.6 + archiver: ^7.0.0 + base64-stream: ^1.0.0 + better-sqlite3: ^12.0.0 + compression: ^1.7.4 + concat-stream: ^2.0.0 + cookie: ^0.7.0 + cors: ^2.8.5 + cron: ^3.0.0 + express: ^4.17.1 + express-promise-router: ^4.1.0 + express-rate-limit: ^7.5.0 + fs-extra: ^11.2.0 + git-url-parse: ^15.0.0 + helmet: ^6.0.0 + infinispan: ^0.12.0 + is-glob: ^4.0.3 + jose: ^5.0.0 + keyv: ^5.2.1 + knex: ^3.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^9.0.0 + mysql2: ^3.0.0 + node-fetch: ^2.7.0 + node-forge: ^1.3.1 + p-limit: ^3.1.0 + path-to-regexp: ^8.0.0 + pg: ^8.11.3 + pg-connection-string: ^2.3.0 + pg-format: ^1.0.4 + rate-limit-redis: ^4.2.0 + raw-body: ^2.4.1 + selfsigned: ^2.0.0 + tar: ^6.1.12 + triple-beam: ^1.4.1 + uuid: ^11.0.0 + winston: ^3.2.1 + winston-transport: ^4.5.0 + yauzl: ^3.0.0 + yn: ^4.0.0 + zod: ^3.22.4 + zod-to-json-schema: ^3.20.4 + peerDependencies: + "@google-cloud/cloud-sql-connector": ^1.4.0 + peerDependenciesMeta: + "@google-cloud/cloud-sql-connector": + optional: true + checksum: bacb5c88246eb9aa7731bb0f46ee43c3b90faf5eca24735a4d2fe3cd7575a0404441158667e77a7acaf9fcd3755722de9066792c3e718bee39c8c480dfb78209 + languageName: node + linkType: hard + "@backstage/backend-defaults@npm:^0.9.0": version: 0.9.0 resolution: "@backstage/backend-defaults@npm:0.9.0" @@ -3158,27 +3242,27 @@ __metadata: languageName: node linkType: hard -"@backstage/backend-dynamic-feature-service@npm:^0.7.2": - version: 0.7.2 - resolution: "@backstage/backend-dynamic-feature-service@npm:0.7.2" +"@backstage/backend-dynamic-feature-service@npm:^0.7.2, @backstage/backend-dynamic-feature-service@npm:^0.7.3": + version: 0.7.3 + resolution: "@backstage/backend-dynamic-feature-service@npm:0.7.3" dependencies: - "@backstage/backend-defaults": ^0.11.1 - "@backstage/backend-openapi-utils": ^0.5.5 - "@backstage/backend-plugin-api": ^1.4.1 + "@backstage/backend-defaults": ^0.12.0 + "@backstage/backend-openapi-utils": ^0.6.0 + "@backstage/backend-plugin-api": ^1.4.2 "@backstage/cli-common": ^0.1.15 - "@backstage/cli-node": ^0.2.13 + "@backstage/cli-node": ^0.2.14 "@backstage/config": ^1.3.3 "@backstage/config-loader": ^1.10.2 "@backstage/errors": ^1.2.7 - "@backstage/plugin-app-node": ^0.1.35 - "@backstage/plugin-auth-node": ^0.6.5 - "@backstage/plugin-catalog-backend": ^3.0.0 - "@backstage/plugin-events-backend": ^0.5.4 - "@backstage/plugin-events-node": ^0.4.13 + "@backstage/plugin-app-node": ^0.1.36 + "@backstage/plugin-auth-node": ^0.6.6 + "@backstage/plugin-catalog-backend": ^3.0.1 + "@backstage/plugin-events-backend": ^0.5.5 + "@backstage/plugin-events-node": ^0.4.14 "@backstage/plugin-permission-common": ^0.9.1 - "@backstage/plugin-permission-node": ^0.10.2 - "@backstage/plugin-scaffolder-node": ^0.10.0 - "@backstage/plugin-search-backend-node": ^1.3.13 + "@backstage/plugin-permission-node": ^0.10.3 + "@backstage/plugin-scaffolder-node": ^0.11.0 + "@backstage/plugin-search-backend-node": ^1.3.14 "@backstage/plugin-search-common": ^1.2.19 "@backstage/types": ^1.2.1 "@manypkg/get-packages": ^1.1.3 @@ -3189,7 +3273,7 @@ __metadata: fs-extra: ^11.2.0 lodash: ^4.17.21 winston: ^3.2.1 - checksum: f0e725c05d5de64fb0ca1371ba06d32abefc2cc430604b7a6b82d7e4e5f014c3b9234c4663862387cea8728cbf2c5e294d06e84d9823e23351bc7b606d6b6536 + checksum: 8c4ff098fd3510b905b4347ba2e38476edd2b8630032787dbaf42787f28e2133270bde6f88bc6ccc1d116410025633844d3b4b6b479c295861d27ba8d9fb5ae3 languageName: node linkType: hard @@ -3217,16 +3301,40 @@ __metadata: languageName: node linkType: hard -"@backstage/backend-plugin-api@npm:^1.0.0, @backstage/backend-plugin-api@npm:^1.1.1, @backstage/backend-plugin-api@npm:^1.3.0, @backstage/backend-plugin-api@npm:^1.4.1": - version: 1.4.1 - resolution: "@backstage/backend-plugin-api@npm:1.4.1" +"@backstage/backend-openapi-utils@npm:^0.6.0": + version: 0.6.0 + resolution: "@backstage/backend-openapi-utils@npm:0.6.0" + dependencies: + "@apidevtools/swagger-parser": ^10.1.0 + "@backstage/backend-plugin-api": ^1.4.2 + "@backstage/errors": ^1.2.7 + "@backstage/types": ^1.2.1 + "@types/express": ^4.17.6 + "@types/express-serve-static-core": ^4.17.5 + ajv: ^8.16.0 + express: ^4.17.1 + express-openapi-validator: ^5.5.8 + express-promise-router: ^4.1.0 + get-port: ^5.1.1 + json-schema-to-ts: ^3.0.0 + lodash: ^4.17.21 + mockttp: ^3.13.0 + openapi-merge: ^1.3.2 + openapi3-ts: ^3.1.2 + checksum: 81dddb23261cacb8b1a2e08d788dd96942610d4f2c094ffbd20de88933c142237c86776f73a0c30c040e0e5c58b882220af374cd9f662e17e966a97d8b7cc64b + languageName: node + linkType: hard + +"@backstage/backend-plugin-api@npm:^1.0.0, @backstage/backend-plugin-api@npm:^1.1.1, @backstage/backend-plugin-api@npm:^1.3.0, @backstage/backend-plugin-api@npm:^1.4.1, @backstage/backend-plugin-api@npm:^1.4.2": + version: 1.4.2 + resolution: "@backstage/backend-plugin-api@npm:1.4.2" dependencies: "@backstage/cli-common": ^0.1.15 "@backstage/config": ^1.3.3 "@backstage/errors": ^1.2.7 - "@backstage/plugin-auth-node": ^0.6.5 + "@backstage/plugin-auth-node": ^0.6.6 "@backstage/plugin-permission-common": ^0.9.1 - "@backstage/plugin-permission-node": ^0.10.2 + "@backstage/plugin-permission-node": ^0.10.3 "@backstage/types": ^1.2.1 "@types/express": ^4.17.6 "@types/json-schema": ^7.0.6 @@ -3235,7 +3343,7 @@ __metadata: knex: ^3.0.0 luxon: ^3.0.0 zod: ^3.22.4 - checksum: 5adb68945bbdc8323dff2e8bae6ecaba73c4ee02b1f1afbc3b5ad53374f3e2c4473ed79f0aac951205334aee56f2b98eb1bcc091b8d28c390a0288a25d72feb6 + checksum: 4aa4ff08a1166f21ff3abc58036ac240b4ca1ebb61132d724119e2f98a76d53a296bb786d3abfbc70511817de7cb6eb08f5060f13b8b0254b6464ce337812f5c languageName: node linkType: hard @@ -3278,15 +3386,15 @@ __metadata: languageName: node linkType: hard -"@backstage/catalog-client@npm:^1.10.2, @backstage/catalog-client@npm:^1.9.1": - version: 1.10.2 - resolution: "@backstage/catalog-client@npm:1.10.2" +"@backstage/catalog-client@npm:^1.10.2, @backstage/catalog-client@npm:^1.11.0, @backstage/catalog-client@npm:^1.9.1": + version: 1.11.0 + resolution: "@backstage/catalog-client@npm:1.11.0" dependencies: "@backstage/catalog-model": ^1.7.5 "@backstage/errors": ^1.2.7 cross-fetch: ^4.0.0 uri-template: ^2.0.0 - checksum: 43745b13c70b2fc0b8f82a4e0074a1af7efc32b1671b3fb8e7024e7aea016f156737a567efa5505d03117f71a618ad5243fa73005cf8bc1fe0aec11f19b4bf3b + checksum: 8288c6136992ba037b831b89683ac210157efc2241e877eebc0a37fc9a108cf0e1bcc629e48eeca79749a7f34b531fd1138a68c5b292c0a5e3f5b5490b2d9406 languageName: node linkType: hard @@ -3309,9 +3417,9 @@ __metadata: languageName: node linkType: hard -"@backstage/cli-node@npm:^0.2.13": - version: 0.2.13 - resolution: "@backstage/cli-node@npm:0.2.13" +"@backstage/cli-node@npm:^0.2.13, @backstage/cli-node@npm:^0.2.14": + version: 0.2.14 + resolution: "@backstage/cli-node@npm:0.2.14" dependencies: "@backstage/cli-common": ^0.1.15 "@backstage/errors": ^1.2.7 @@ -3321,7 +3429,7 @@ __metadata: fs-extra: ^11.2.0 semver: ^7.5.3 zod: ^3.22.4 - checksum: d1ce09a728b181db1eae0931ffee81795cd177d32f319edcab1eee8e624820a5bf23c28a9fc70e9883522fc7ded52be232718cc18e418d9febe34ef998737c72 + checksum: a5523f8cfe37a851bb29040d4421d76b21dc17a665584edb99fa483a5f19adfd18765cf5dd2bfce3363356749b67710daff933012f040fe2dd33e0045fc6d44b languageName: node linkType: hard @@ -3945,16 +4053,16 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-app-node@npm:^0.1.35": - version: 0.1.35 - resolution: "@backstage/plugin-app-node@npm:0.1.35" +"@backstage/plugin-app-node@npm:^0.1.35, @backstage/plugin-app-node@npm:^0.1.36": + version: 0.1.36 + resolution: "@backstage/plugin-app-node@npm:0.1.36" dependencies: - "@backstage/backend-plugin-api": ^1.4.1 + "@backstage/backend-plugin-api": ^1.4.2 "@backstage/config-loader": ^1.10.2 "@types/express": ^4.17.6 express: ^4.17.1 fs-extra: ^11.2.0 - checksum: b54d93835dcba6f7fc79f894c3de201b5b77a76eb5d9d25fc2f21ed6650dd0ae8d46ce57b90dbe8f86abd998af3788a66c0c1c4cab02b451ab9310a73a539a84 + checksum: ffb490e422ab6e371a15e4e9f00d92b71942dfe59cc9abc57bacfd479c6c4a493d4d7b7efa5f5b3b7fe375dc6deb1d47667b281e668b93c745a3586b617834d6 languageName: node linkType: hard @@ -4063,12 +4171,12 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-auth-node@npm:^0.6.2, @backstage/plugin-auth-node@npm:^0.6.5": - version: 0.6.5 - resolution: "@backstage/plugin-auth-node@npm:0.6.5" +"@backstage/plugin-auth-node@npm:^0.6.2, @backstage/plugin-auth-node@npm:^0.6.5, @backstage/plugin-auth-node@npm:^0.6.6": + version: 0.6.6 + resolution: "@backstage/plugin-auth-node@npm:0.6.6" dependencies: - "@backstage/backend-plugin-api": ^1.4.1 - "@backstage/catalog-client": ^1.10.2 + "@backstage/backend-plugin-api": ^1.4.2 + "@backstage/catalog-client": ^1.11.0 "@backstage/catalog-model": ^1.7.5 "@backstage/config": ^1.3.3 "@backstage/errors": ^1.2.7 @@ -4082,7 +4190,7 @@ __metadata: zod: ^3.22.4 zod-to-json-schema: ^3.21.4 zod-validation-error: ^3.4.0 - checksum: 04c3140ef4d40579eda27069a23516f13156614a2d71a15b66eb99c240df8b9e992dda8f026236becdbec366196bcd3a068527e70b5aa71c4d20a92d59f5b9b2 + checksum: cb3e69133b2d27bd0d10a5bd65a0a2f23caf4211de8137713d0fd398e93e808768d5e45150782b3f8c562cf0921f67ecb52e336bf3fb33bbf5b74bce03e25370 languageName: node linkType: hard @@ -4141,22 +4249,22 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-catalog-backend@npm:^3.0.0": - version: 3.0.0 - resolution: "@backstage/plugin-catalog-backend@npm:3.0.0" +"@backstage/plugin-catalog-backend@npm:^3.0.0, @backstage/plugin-catalog-backend@npm:^3.0.1": + version: 3.0.1 + resolution: "@backstage/plugin-catalog-backend@npm:3.0.1" dependencies: - "@backstage/backend-openapi-utils": ^0.5.5 - "@backstage/backend-plugin-api": ^1.4.1 - "@backstage/catalog-client": ^1.10.2 + "@backstage/backend-openapi-utils": ^0.6.0 + "@backstage/backend-plugin-api": ^1.4.2 + "@backstage/catalog-client": ^1.11.0 "@backstage/catalog-model": ^1.7.5 "@backstage/config": ^1.3.3 "@backstage/errors": ^1.2.7 "@backstage/integration": ^1.17.1 "@backstage/plugin-catalog-common": ^1.1.5 - "@backstage/plugin-catalog-node": ^1.17.2 - "@backstage/plugin-events-node": ^0.4.13 + "@backstage/plugin-catalog-node": ^1.18.0 + "@backstage/plugin-events-node": ^0.4.14 "@backstage/plugin-permission-common": ^0.9.1 - "@backstage/plugin-permission-node": ^0.10.2 + "@backstage/plugin-permission-node": ^0.10.3 "@backstage/types": ^1.2.1 "@opentelemetry/api": ^1.9.0 codeowners-utils: ^1.0.2 @@ -4176,7 +4284,7 @@ __metadata: yaml: ^2.0.0 yn: ^4.0.0 zod: ^3.22.4 - checksum: 263a66e85da2defa074e856a186ed679a0b20766d26581f6de96fe870e022b10d40ccc00da01dfb6abc04f1626cdbf1cf79d339f8b342ec358780d375690d709 + checksum: 5d8dcbbe8acccded9dfd1af0a453262b74c8be667e36ece55959121353f19cbf83fc7611f72cb7f726fd3eea5526e48e28a7b728a4b067f3e0d42f8c5cff66ff languageName: node linkType: hard @@ -4261,21 +4369,21 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-catalog-node@npm:^1.17.2": - version: 1.17.2 - resolution: "@backstage/plugin-catalog-node@npm:1.17.2" +"@backstage/plugin-catalog-node@npm:^1.17.2, @backstage/plugin-catalog-node@npm:^1.18.0": + version: 1.18.0 + resolution: "@backstage/plugin-catalog-node@npm:1.18.0" dependencies: - "@backstage/backend-plugin-api": ^1.4.1 - "@backstage/catalog-client": ^1.10.2 + "@backstage/backend-plugin-api": ^1.4.2 + "@backstage/catalog-client": ^1.11.0 "@backstage/catalog-model": ^1.7.5 "@backstage/errors": ^1.2.7 "@backstage/plugin-catalog-common": ^1.1.5 "@backstage/plugin-permission-common": ^0.9.1 - "@backstage/plugin-permission-node": ^0.10.2 + "@backstage/plugin-permission-node": ^0.10.3 "@backstage/types": ^1.2.1 lodash: ^4.17.21 yaml: ^2.0.0 - checksum: 09fafd3af78d92aeb1c698202ef3a29eb0d8c7a97f3ad449ab7fd05f8a2e5dc61425398c5f72f25f747e996929c41d96c3ad259bc52febc8e7ad49b94c6625e1 + checksum: 3688cf7b7e7b47a2f2cc02836bd6f41d35d9de88f3072d3993eb9364709485763bd35f4142b8a3fcb45b6fa36327a2b47b6bfd27eeeb61caf1c2cb517909ed50 languageName: node linkType: hard @@ -4366,30 +4474,30 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-events-backend@npm:^0.5.4": - version: 0.5.4 - resolution: "@backstage/plugin-events-backend@npm:0.5.4" +"@backstage/plugin-events-backend@npm:^0.5.5": + version: 0.5.5 + resolution: "@backstage/plugin-events-backend@npm:0.5.5" dependencies: - "@backstage/backend-openapi-utils": ^0.5.5 - "@backstage/backend-plugin-api": ^1.4.1 + "@backstage/backend-openapi-utils": ^0.6.0 + "@backstage/backend-plugin-api": ^1.4.2 "@backstage/config": ^1.3.3 "@backstage/errors": ^1.2.7 - "@backstage/plugin-events-node": ^0.4.13 + "@backstage/plugin-events-node": ^0.4.14 "@backstage/types": ^1.2.1 "@types/express": ^4.17.6 content-type: ^1.0.5 express: ^4.17.1 express-promise-router: ^4.1.0 knex: ^3.0.0 - checksum: 1c3cea46b2d1b50ba462cd9aba5b378f9f39589ddf002d1b673e77b2b355a3fc1f5e0351d7e9abbcd282920b3d55e4af36d452bc85fd8f2f073e5367e3b1c1a9 + checksum: 729062e47df8ec5f97cb604f87c7fe6bc5ac767fdf95904b6d917fe095c398c8cc7ce9e6723e2243dabcaff330cc75995cc8572ab181a7e088f643649f413d64 languageName: node linkType: hard -"@backstage/plugin-events-node@npm:^0.4.10, @backstage/plugin-events-node@npm:^0.4.13": - version: 0.4.13 - resolution: "@backstage/plugin-events-node@npm:0.4.13" +"@backstage/plugin-events-node@npm:^0.4.10, @backstage/plugin-events-node@npm:^0.4.13, @backstage/plugin-events-node@npm:^0.4.14": + version: 0.4.14 + resolution: "@backstage/plugin-events-node@npm:0.4.14" dependencies: - "@backstage/backend-plugin-api": ^1.4.1 + "@backstage/backend-plugin-api": ^1.4.2 "@backstage/errors": ^1.2.7 "@backstage/types": ^1.2.1 "@types/content-type": ^1.1.8 @@ -4398,7 +4506,7 @@ __metadata: cross-fetch: ^4.0.0 express: ^4.17.1 uri-template: ^2.0.0 - checksum: 842a18c1398302aa90e40bdfdb47577cc79133aa8b618518f0056a4108d22f26c9a868f2d4384728500cd8e66ed7ca58eb024fc0e3bbbaf45549999e1b5ddee9 + checksum: 618f9c286501b11824ed166fc9e39738c83d31bd31f70d31461bdd17a7268ff9807eb5ee4b7fcfeca7036a109763777e7ac64931bf7a8b2bfe3589efbc5ea7a5 languageName: node linkType: hard @@ -4667,21 +4775,21 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-permission-node@npm:^0.10.2": - version: 0.10.2 - resolution: "@backstage/plugin-permission-node@npm:0.10.2" +"@backstage/plugin-permission-node@npm:^0.10.2, @backstage/plugin-permission-node@npm:^0.10.3": + version: 0.10.3 + resolution: "@backstage/plugin-permission-node@npm:0.10.3" dependencies: - "@backstage/backend-plugin-api": ^1.4.1 + "@backstage/backend-plugin-api": ^1.4.2 "@backstage/config": ^1.3.3 "@backstage/errors": ^1.2.7 - "@backstage/plugin-auth-node": ^0.6.5 + "@backstage/plugin-auth-node": ^0.6.6 "@backstage/plugin-permission-common": ^0.9.1 "@types/express": ^4.17.6 express: ^4.17.1 express-promise-router: ^4.1.0 zod: ^3.22.4 zod-to-json-schema: ^3.20.4 - checksum: 70edf66456a0c1e798e8da6735c3d089dda61b8d1a7c73e1bd58517b2c2bd4ccb2734cbf36a0010f87dc5781bc0bd824c31a9ed1323f053c80b71ef966656d10 + checksum: 2697d47e9d973acb992fd8fb8288304de9234eb1b3963c2c735dbb7d70143997c051cde21f2ae9b4d9c506b5c2abad96fd9c0a11ae6127bbe92d38bff1ea6610 languageName: node linkType: hard @@ -4939,14 +5047,22 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-scaffolder-common@npm:^1.6.0": - version: 1.6.0 - resolution: "@backstage/plugin-scaffolder-common@npm:1.6.0" +"@backstage/plugin-scaffolder-common@npm:^1.6.0, @backstage/plugin-scaffolder-common@npm:^1.7.0": + version: 1.7.0 + resolution: "@backstage/plugin-scaffolder-common@npm:1.7.0" dependencies: "@backstage/catalog-model": ^1.7.5 + "@backstage/errors": ^1.2.7 + "@backstage/integration": ^1.17.1 "@backstage/plugin-permission-common": ^0.9.1 "@backstage/types": ^1.2.1 - checksum: 2d43d62df1398f305911287bc8c4d13abed39a799aa851871810f022830d54e8a171f8381310157868a4e942ce1706aff7991d57bfc4985a412f1a291db02815 + "@microsoft/fetch-event-source": ^2.0.1 + "@types/json-schema": ^7.0.9 + cross-fetch: ^4.0.0 + json-schema: ^0.4.0 + uri-template: ^2.0.0 + zen-observable: ^0.10.0 + checksum: 1f2feeee8ed53451a344968c24c25cf577ba129f4b7710cff4d958af91b96576ab3dc6646252e85532b4b89845f82b695496c8023a5c8ba35d4086e7bebf0c96 languageName: node linkType: hard @@ -4978,6 +5094,34 @@ __metadata: languageName: node linkType: hard +"@backstage/plugin-scaffolder-node@npm:^0.11.0": + version: 0.11.0 + resolution: "@backstage/plugin-scaffolder-node@npm:0.11.0" + dependencies: + "@backstage/backend-plugin-api": ^1.4.2 + "@backstage/catalog-model": ^1.7.5 + "@backstage/errors": ^1.2.7 + "@backstage/integration": ^1.17.1 + "@backstage/plugin-permission-common": ^0.9.1 + "@backstage/plugin-scaffolder-common": ^1.7.0 + "@backstage/types": ^1.2.1 + "@isomorphic-git/pgp-plugin": ^0.0.7 + concat-stream: ^2.0.0 + fs-extra: ^11.2.0 + globby: ^11.0.0 + isomorphic-git: ^1.23.0 + jsonschema: ^1.5.0 + lodash: ^4.17.21 + p-limit: ^3.1.0 + tar: ^6.1.12 + winston: ^3.2.1 + winston-transport: ^4.7.0 + zod: ^3.22.4 + zod-to-json-schema: ^3.20.4 + checksum: 55b56f41d764e226b42fb2b069b4c211c82f82695614ec1a44241d274d70f3ec3b6c39c665ff45583e0c181a394644029e49a277d1b6d9bbed98841e114941b3 + languageName: node + linkType: hard + "@backstage/plugin-scaffolder-react@npm:^1.18.0": version: 1.18.0 resolution: "@backstage/plugin-scaffolder-react@npm:1.18.0" @@ -5144,11 +5288,11 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-search-backend-node@npm:^1.3.13": - version: 1.3.13 - resolution: "@backstage/plugin-search-backend-node@npm:1.3.13" +"@backstage/plugin-search-backend-node@npm:^1.3.13, @backstage/plugin-search-backend-node@npm:^1.3.14": + version: 1.3.14 + resolution: "@backstage/plugin-search-backend-node@npm:1.3.14" dependencies: - "@backstage/backend-plugin-api": ^1.4.1 + "@backstage/backend-plugin-api": ^1.4.2 "@backstage/config": ^1.3.3 "@backstage/errors": ^1.2.7 "@backstage/plugin-permission-common": ^0.9.1 @@ -5158,7 +5302,7 @@ __metadata: lunr: ^2.3.9 ndjson: ^2.0.0 uuid: ^11.0.0 - checksum: faa9249d48b5697967f962cb47bf5211c47dd4dec4db9197b269c1777e3cccebd606e51e802198097b16662912f3e9e112ae5d15c6c0702d1f59d7a78046dcd1 + checksum: e52ad8141e8ddaec7f2e7185fc85049c14f6f22c55999c1f67e4407a8b176b9dab640fa2a7ef10754e2c1399f6fd2b13f0f6f12faa104f7a3d212b9c5c323993 languageName: node linkType: hard @@ -7518,6 +7662,7 @@ __metadata: node-gyp: ^9.0.0 prettier: ^3.4.2 typescript: ~5.3.0 + yaml: ^2.8.1 languageName: unknown linkType: soft @@ -10754,6 +10899,7 @@ __metadata: resolution: "@red-hat-developer-hub/backstage-plugin-marketplace-backend@workspace:plugins/marketplace-backend" dependencies: "@backstage/backend-defaults": ^0.11.1 + "@backstage/backend-dynamic-feature-service": ^0.7.3 "@backstage/backend-plugin-api": ^1.4.1 "@backstage/backend-test-utils": ^1.7.0 "@backstage/catalog-client": ^1.10.2 @@ -13828,7 +13974,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 @@ -15067,7 +15213,7 @@ __metadata: languageName: node linkType: hard -"ajv-formats@npm:~3.0.1": +"ajv-formats@npm:^3.0.1, ajv-formats@npm:~3.0.1": version: 3.0.1 resolution: "ajv-formats@npm:3.0.1" dependencies: @@ -16193,6 +16339,17 @@ __metadata: languageName: node linkType: hard +"better-sqlite3@npm:^12.0.0": + version: 12.2.0 + resolution: "better-sqlite3@npm:12.2.0" + dependencies: + bindings: ^1.5.0 + node-gyp: latest + prebuild-install: ^7.1.1 + checksum: b752119ea306c5a70d5bacdf5607fd69b39142b7c8c30d72f530047fedad0b651195b37e8a8067f106d3c56d6913dc0077f1d658916f07bcdad6841384d2bb1b + languageName: node + linkType: hard + "better-sqlite3@npm:^9.0.0": version: 9.6.0 resolution: "better-sqlite3@npm:9.6.0" @@ -16557,6 +16714,15 @@ __metadata: languageName: node linkType: hard +"buffer-xor@npm:^2.0.2": + version: 2.0.2 + resolution: "buffer-xor@npm:2.0.2" + dependencies: + safe-buffer: ^5.1.1 + checksum: 78226fcae9f4a0b4adec69dffc049f26f6bab240dfdd1b3f6fe07c4eb6b90da202ea5c363f98af676156ee39450a06405fddd9e8965f68a5327edcc89dcbe5d0 + languageName: node + linkType: hard + "buffer@npm:5.6.0": version: 5.6.0 resolution: "buffer@npm:5.6.0" @@ -16610,7 +16776,7 @@ __metadata: languageName: node linkType: hard -"busboy@npm:^1.0.0, busboy@npm:^1.6.0": +"busboy@npm:^1.6.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" dependencies: @@ -17452,18 +17618,6 @@ __metadata: languageName: node linkType: hard -"concat-stream@npm:^1.5.2": - version: 1.6.2 - resolution: "concat-stream@npm:1.6.2" - dependencies: - buffer-from: ^1.0.0 - inherits: ^2.0.3 - readable-stream: ^2.2.2 - typedarray: ^0.0.6 - checksum: 1ef77032cb4459dcd5187bd710d6fc962b067b64ec6a505810de3d2b8cc0605638551b42f8ec91edf6fcd26141b32ef19ad749239b58fae3aba99187adc32285 - languageName: node - linkType: hard - "concat-stream@npm:^2.0.0": version: 2.0.0 resolution: "concat-stream@npm:2.0.0" @@ -18611,6 +18765,15 @@ __metadata: languageName: node linkType: hard +"default-user-agent@npm:^1.0.0": + version: 1.0.0 + resolution: "default-user-agent@npm:1.0.0" + dependencies: + os-name: ~1.0.3 + checksum: b1ef07c8e7de846a66e1e120d7ba11969faa36c8db4af2317f9b64d30e7507d129e3f721c7cc3f531a1719c1ab463d830bf426fbcda87b11defe23689f4d2b60 + languageName: node + linkType: hard + "defaults@npm:^1.0.3": version: 1.0.4 resolution: "defaults@npm:1.0.4" @@ -18846,6 +19009,13 @@ __metadata: languageName: node linkType: hard +"digest-header@npm:^1.0.0": + version: 1.1.0 + resolution: "digest-header@npm:1.1.0" + checksum: fadbdda75e1cc650e460c8fe2064f74c43cc005d0eab66cc390dd1ae2678cfb41f69f151323fbd3e059e28c941f1b9adc6ea4dbd9c918cb246f34a5eb8e103f0 + languageName: node + linkType: hard + "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -20281,26 +20451,27 @@ __metadata: languageName: node linkType: hard -"express-openapi-validator@npm:^5.0.4": - version: 5.3.9 - resolution: "express-openapi-validator@npm:5.3.9" +"express-openapi-validator@npm:^5.0.4, express-openapi-validator@npm:^5.5.8": + version: 5.5.8 + resolution: "express-openapi-validator@npm:5.5.8" dependencies: - "@apidevtools/json-schema-ref-parser": ^11.7.0 + "@apidevtools/json-schema-ref-parser": ^14.0.3 "@types/multer": ^1.4.12 ajv: ^8.17.1 ajv-draft-04: ^1.0.0 - ajv-formats: ^2.1.1 + ajv-formats: ^3.0.1 content-type: ^1.0.5 json-schema-traverse: ^1.0.0 lodash.clonedeep: ^4.5.0 lodash.get: ^4.4.2 media-typer: ^1.1.0 - multer: ^1.4.5-lts.1 + multer: ^2.0.2 ono: ^7.1.3 - path-to-regexp: ^8.1.0 + path-to-regexp: ^8.2.0 + qs: ^6.14.0 peerDependencies: express: "*" - checksum: 0469b383b769f070dfad9c9148b18857c5aaa9aa14c596e1ada155dac63e1c355e455d42f3c81af62c4aefd3db4ad8b892d34a432e170e4eb7af73d7dbd2f644 + checksum: f76ef56748a28c4ed338ec509eb359bcf992505a565a1a59c0c6ef02c369b03f7d306a94d51b5de4a1d1c69618529e5a7a3aa3c5e3c1dfc0437e6f789a4ab381 languageName: node linkType: hard @@ -20907,6 +21078,13 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:^1.7.2": + version: 1.9.0 + resolution: "form-data-encoder@npm:1.9.0" + checksum: a73f617976f91b594dbd777ec5147abdb0c52d707475130f8cefc8ae9102ccf51be154b929f7c18323729c2763ac25b16055f5034bc188834e9febeb0d971d7f + languageName: node + linkType: hard + "form-data@npm:^2.3.2, form-data@npm:^2.5.0": version: 2.5.2 resolution: "form-data@npm:2.5.2" @@ -20948,6 +21126,16 @@ __metadata: languageName: node linkType: hard +"formdata-node@npm:^4.3.3": + version: 4.4.1 + resolution: "formdata-node@npm:4.4.1" + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + checksum: d91d4f667cfed74827fc281594102c0dabddd03c9f8b426fc97123eedbf73f5060ee43205d89284d6854e2fc5827e030cd352ef68b93beda8decc2d72128c576 + languageName: node + linkType: hard + "formidable@npm:^2.1.2": version: 2.1.2 resolution: "formidable@npm:2.1.2" @@ -20978,6 +21166,18 @@ __metadata: languageName: node linkType: hard +"formstream@npm:^1.1.1": + version: 1.5.2 + resolution: "formstream@npm:1.5.2" + dependencies: + destroy: ^1.0.4 + mime: ^2.5.2 + node-hex: ^1.0.1 + pause-stream: ~0.0.11 + checksum: ed04f7ee02633608237a5ed9000d8a7c19d7ed2a69254f5d47cf81d799ffbbecf52bf2440ffa940cb1150f9f80b7b83bf17aac914be11c0fb14bd480226424e7 + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -22567,6 +22767,19 @@ __metadata: languageName: node linkType: hard +"infinispan@npm:^0.12.0": + version: 0.12.0 + resolution: "infinispan@npm:0.12.0" + dependencies: + buffer-xor: ^2.0.2 + log4js: ^6.4.6 + protobufjs: ^7.0.0 + underscore: ^1.13.3 + urllib: ^3.23.0 + checksum: 28e490dcadd2325677bd231c9e4637a90bb54ce9056cdde9e592041ba6303cbde143b7724e32c9a6e44725e52bac546727e4ff49cdd5a565fba03090863b7137 + languageName: node + linkType: hard + "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -25283,7 +25496,7 @@ __metadata: languageName: node linkType: hard -"log4js@npm:6.9.1": +"log4js@npm:6.9.1, log4js@npm:^6.4.6": version: 6.9.1 resolution: "log4js@npm:6.9.1" dependencies: @@ -26288,7 +26501,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -26306,7 +26519,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:2.6.0": +"mime@npm:2.6.0, mime@npm:^2.5.2": version: 2.6.0 resolution: "mime@npm:2.6.0" bin: @@ -26443,7 +26656,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": +"minimist@npm:^1.1.0, minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0 @@ -26591,7 +26804,7 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.4, mkdirp@npm:^0.5.6": +"mkdirp@npm:^0.5.6": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" dependencies: @@ -26768,18 +26981,18 @@ __metadata: languageName: node linkType: hard -"multer@npm:^1.4.5-lts.1": - version: 1.4.5-lts.1 - resolution: "multer@npm:1.4.5-lts.1" +"multer@npm:^2.0.2": + version: 2.0.2 + resolution: "multer@npm:2.0.2" dependencies: append-field: ^1.0.0 - busboy: ^1.0.0 - concat-stream: ^1.5.2 - mkdirp: ^0.5.4 + busboy: ^1.6.0 + concat-stream: ^2.0.0 + mkdirp: ^0.5.6 object-assign: ^4.1.1 - type-is: ^1.6.4 - xtend: ^4.0.0 - checksum: d6dfa78a6ec592b74890412f8962da8a87a3dcfe20f612e039b735b8e0faa72c735516c447f7de694ee0d981eb0a1b892fb9e2402a0348dc6091d18c38d89ecc + type-is: ^1.6.18 + xtend: ^4.0.2 + checksum: 187f3539b3d5b7f80a289288fc21d3e4116ab9ed21ac9b9ceb25272c7344d215e9572f7e7ed01edb876afddf24661b06578f2c0fc67f36655ebb51a13ccf83dc languageName: node linkType: hard @@ -27023,7 +27236,7 @@ __metadata: languageName: node linkType: hard -"node-domexception@npm:^1.0.0": +"node-domexception@npm:1.0.0, node-domexception@npm:^1.0.0": version: 1.0.0 resolution: "node-domexception@npm:1.0.0" checksum: ee1d37dd2a4eb26a8a92cd6b64dfc29caec72bff5e1ed9aba80c294f57a31ba4895a60fd48347cf17dd6e766da0ae87d75657dfd1f384ebfa60462c2283f5c7f @@ -27116,6 +27329,13 @@ __metadata: languageName: node linkType: hard +"node-hex@npm:^1.0.1": + version: 1.0.1 + resolution: "node-hex@npm:1.0.1" + checksum: 9053d532859ee7e9653972af77ac7b73edc4f13b9b53d0b96e4045e3ac78ac4460571d4b72ad31e9095be5f7d01e6fd71f268f02ad6029091f8cabae1d4ce4df + languageName: node + linkType: hard + "node-int64@npm:^0.4.0": version: 0.4.0 resolution: "node-int64@npm:0.4.0" @@ -27726,6 +27946,18 @@ __metadata: languageName: node linkType: hard +"os-name@npm:~1.0.3": + version: 1.0.3 + resolution: "os-name@npm:1.0.3" + dependencies: + osx-release: ^1.0.0 + win-release: ^1.0.0 + bin: + os-name: cli.js + checksum: 2fc86cc199f8b4992bb00041401c5ab0407e3069e05981f3aa3e5a44cee9b7f22c2b0f5db2c0c1d55656c519884272b5e1e55517358c2e5f728b37dd38f5af78 + languageName: node + linkType: hard + "os-tmpdir@npm:~1.0.2": version: 1.0.2 resolution: "os-tmpdir@npm:1.0.2" @@ -27733,6 +27965,17 @@ __metadata: languageName: node linkType: hard +"osx-release@npm:^1.0.0": + version: 1.1.0 + resolution: "osx-release@npm:1.1.0" + dependencies: + minimist: ^1.1.0 + bin: + osx-release: cli.js + checksum: abd437ef21dbfb04f098acc90112cc92ef10c17213e3fd75f8eba45931bd85f6d564ecade0642fac51acff2015597194a76a11773009a90baeb35a03b1c36b06 + languageName: node + linkType: hard + "outdent@npm:^0.5.0": version: 0.5.0 resolution: "outdent@npm:0.5.0" @@ -28163,10 +28406,10 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.1.0": - version: 8.2.0 - resolution: "path-to-regexp@npm:8.2.0" - checksum: 56e13e45962e776e9e7cd72e87a441cfe41f33fd539d097237ceb16adc922281136ca12f5a742962e33d8dda9569f630ba594de56d8b7b6e49adf31803c5e771 +"path-to-regexp@npm:^8.0.0, path-to-regexp@npm:^8.2.0": + version: 8.3.0 + resolution: "path-to-regexp@npm:8.3.0" + checksum: 73e0d3db449f9899692b10be8480bbcfa294fd575be2d09bce3e63f2f708d1fccd3aaa8591709f8b82062c528df116e118ff9df8f5c52ccc4c2443a90be73e10 languageName: node linkType: hard @@ -28177,6 +28420,15 @@ __metadata: languageName: node linkType: hard +"pause-stream@npm:~0.0.11": + version: 0.0.11 + resolution: "pause-stream@npm:0.0.11" + dependencies: + through: ~2.3 + checksum: 3c4a14052a638b92e0c96eb00c0d7977df7f79ea28395250c525d197f1fc02d34ce1165d5362e2e6ebbb251524b94a76f3f0d4abc39ab8b016d97449fe15583c + languageName: node + linkType: hard + "pause@npm:0.0.1": version: 0.0.1 resolution: "pause@npm:0.0.1" @@ -29200,9 +29452,9 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^7.2.5, protobufjs@npm:^7.2.6, protobufjs@npm:^7.3.2, protobufjs@npm:^7.4.0": - version: 7.4.0 - resolution: "protobufjs@npm:7.4.0" +"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.5, protobufjs@npm:^7.2.6, protobufjs@npm:^7.3.2, protobufjs@npm:^7.4.0": + version: 7.5.4 + resolution: "protobufjs@npm:7.5.4" dependencies: "@protobufjs/aspromise": ^1.1.2 "@protobufjs/base64": ^1.1.2 @@ -29216,7 +29468,7 @@ __metadata: "@protobufjs/utf8": ^1.1.0 "@types/node": ">=13.7.0" long: ^5.0.0 - checksum: ba0e6b60541bbf818bb148e90f5eb68bd99004e29a6034ad9895a381cbd352be8dce5376e47ae21b2e05559f2505b4a5f4a3c8fa62402822c6ab4dcdfb89ffb3 + checksum: 53bf83b9a726b05d43da35bb990dba7536759787dccea9a67b8f31be9df470ba17f1f1b982ca19956cfc7726f3ec7e0e883ca4ad93b5ec753cc025a637fc704f languageName: node linkType: hard @@ -29335,7 +29587,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.9.4": +"qs@npm:^6.10.1, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.11.2, qs@npm:^6.12.2, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.9.4": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -30115,7 +30367,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.5, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -31224,6 +31476,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^5.0.1": + version: 5.7.2 + resolution: "semver@npm:5.7.2" + bin: + semver: bin/semver + checksum: fb4ab5e0dd1c22ce0c937ea390b4a822147a9c53dbd2a9a0132f12fe382902beef4fbf12cf51bb955248d8d15874ce8cd89532569756384f994309825f10b686 + languageName: node + linkType: hard + "semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -32958,7 +33219,7 @@ __metadata: languageName: node linkType: hard -"through@npm:^2.3.6": +"through@npm:^2.3.6, through@npm:~2.3": version: 2.3.8 resolution: "through@npm:2.3.8" checksum: a38c3e059853c494af95d50c072b83f8b676a9ba2818dcc5b108ef252230735c54e0185437618596c790bbba8fcdaef5b290405981ffa09dce67b1f1bf190cbd @@ -33496,7 +33757,14 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^1.6.16, type-is@npm:^1.6.4, type-is@npm:~1.6.18": +"type-fest@npm:^4.3.1": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: 7055c0e3eb188425d07403f1d5dc175ca4c4f093556f26871fe22041bc93d137d54bef5851afa320638ca1379106c594f5aa153caa654ac1a7f22c71588a4e80 + languageName: node + linkType: hard + +"type-is@npm:^1.6.16, type-is@npm:^1.6.18, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" dependencies: @@ -33838,7 +34106,7 @@ __metadata: languageName: node linkType: hard -"underscore@npm:^1.12.1": +"underscore@npm:^1.12.1, underscore@npm:^1.13.3": version: 1.13.7 resolution: "underscore@npm:1.13.7" checksum: 174b011af29e4fbe2c70eb2baa8bfab0d0336cf2f5654f364484967bc6264a86224d0134b9176e4235c8cceae00d11839f0fd4824268de04b11c78aca1241684 @@ -33866,12 +34134,12 @@ __metadata: languageName: node linkType: hard -"undici@npm:^5.28.4": - version: 5.28.4 - resolution: "undici@npm:5.28.4" +"undici@npm:^5.28.2, undici@npm:^5.28.4": + version: 5.29.0 + resolution: "undici@npm:5.29.0" dependencies: "@fastify/busboy": ^2.0.0 - checksum: a8193132d84540e4dc1895ecc8dbaa176e8a49d26084d6fbe48a292e28397cd19ec5d13bc13e604484e76f94f6e334b2bdc740d5f06a6e50c44072818d0c19f9 + checksum: a25b5462c1b6ffb974f5ffc492ffd64146a9983aad0cbda6fde65e2b22f6f1acd43f09beacc66cc47624a113bd0c684ffc60366102b6a21b038fbfafb7d75195 languageName: node linkType: hard @@ -34160,6 +34428,25 @@ __metadata: languageName: node linkType: hard +"urllib@npm:^3.23.0": + version: 3.27.3 + resolution: "urllib@npm:3.27.3" + dependencies: + default-user-agent: ^1.0.0 + digest-header: ^1.0.0 + form-data-encoder: ^1.7.2 + formdata-node: ^4.3.3 + formstream: ^1.1.1 + mime-types: ^2.1.35 + pump: ^3.0.0 + qs: ^6.11.2 + type-fest: ^4.3.1 + undici: ^5.28.2 + ylru: ^1.3.2 + checksum: b587fe23f9ec2b24a279a89dc4be0de7b81437edddc62d1e20c064d7c715f755af316ca6b22173e5fc2f4e76fe8c002734b087a725d90abd39e40a0730553299 + languageName: node + linkType: hard + "urlpattern-polyfill@npm:^10.0.0": version: 10.0.0 resolution: "urlpattern-polyfill@npm:10.0.0" @@ -34544,6 +34831,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:4.0.0-beta.3": + version: 4.0.0-beta.3 + resolution: "web-streams-polyfill@npm:4.0.0-beta.3" + checksum: dfec1fbf52b9140e4183a941e380487b6c3d5d3838dd1259be81506c1c9f2abfcf5aeb670aeeecfd9dff4271a6d8fef931b193c7bedfb42542a3b05ff36c0d16 + languageName: node + linkType: hard + "web-streams-polyfill@npm:^3.0.3": version: 3.3.3 resolution: "web-streams-polyfill@npm:3.3.3" @@ -34871,6 +35165,15 @@ __metadata: languageName: node linkType: hard +"win-release@npm:^1.0.0": + version: 1.1.1 + resolution: "win-release@npm:1.1.1" + dependencies: + semver: ^5.0.1 + checksum: 8943898cc4badaf8598342d63093e49ae9a64c140cf190e81472d3a8890f3387b8408181412e1b58658fe7777ce5d1e3f02eee4beeaee49909d1d17a72d52fc1 + languageName: node + linkType: hard + "winston-transport@npm:^4.5.0, winston-transport@npm:^4.7.0": version: 4.8.0 resolution: "winston-transport@npm:4.8.0" @@ -35095,12 +35398,12 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.7.0, yaml@npm:^2.7.1": - version: 2.7.1 - resolution: "yaml@npm:2.7.1" +"yaml@npm:^2.0.0, yaml@npm:^2.0.0-10, yaml@npm:^2.2.1, yaml@npm:^2.2.2, yaml@npm:^2.7.0, yaml@npm:^2.7.1, yaml@npm:^2.8.1": + version: 2.8.1 + resolution: "yaml@npm:2.8.1" bin: yaml: bin.mjs - checksum: 385f8115ddfafdf8e599813cca8b2bf4e3f6a01b919fff5ae7da277e164df684d7dfe558b4085172094792b5a04786d3c55fa8b74abb0ee029873f031150bb80 + checksum: 35b46150d48bc1da2fd5b1521a48a4fa36d68deaabe496f3c3fa9646d5796b6b974f3930a02c4b5aee6c85c860d7d7f79009416724465e835f40b87898c36de4 languageName: node linkType: hard @@ -35158,7 +35461,7 @@ __metadata: languageName: node linkType: hard -"ylru@npm:^1.2.0": +"ylru@npm:^1.2.0, ylru@npm:^1.3.2": version: 1.4.0 resolution: "ylru@npm:1.4.0" checksum: e0bf797476487e3d57a6e8790cbb749cff2089e2afc87e46bc84ce7605c329d578ff422c8e8c2ddf167681ddd218af0f58e099733ae1044cba9e9472ebedc01d From 405e045bbec164c519605d635a516f85ce2ff8db Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 5 Sep 2025 03:07:18 -0400 Subject: [PATCH 02/14] update package and yarn.lock Signed-off-by: Yi Cai --- .../plugins/marketplace/package.json | 1 + workspaces/marketplace/yarn.lock | 567 +++++++++++++++++- 2 files changed, 559 insertions(+), 9 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace/package.json b/workspaces/marketplace/plugins/marketplace/package.json index a8e0d580d0..fa0e44fdf8 100644 --- a/workspaces/marketplace/plugins/marketplace/package.json +++ b/workspaces/marketplace/plugins/marketplace/package.json @@ -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", diff --git a/workspaces/marketplace/yarn.lock b/workspaces/marketplace/yarn.lock index 656b049af4..a887235597 100644 --- a/workspaces/marketplace/yarn.lock +++ b/workspaces/marketplace/yarn.lock @@ -2762,6 +2762,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.19.0, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.6, @babel/runtime@npm:^7.28.3": + version: 7.28.3 + resolution: "@babel/runtime@npm:7.28.3" + checksum: dd22662b9e02b6e66cfb061d6f9730eb0aa3b3a390a7bd70fe9a64116d86a3704df6d54ab978cb4acc13b58dbf63a3d7dd4616b0b87030eb14a22835e0aa602d + languageName: node + linkType: hard + "@babel/template@npm:^7.25.9, @babel/template@npm:^7.3.3": version: 7.25.9 resolution: "@babel/template@npm:7.25.9" @@ -6239,6 +6246,13 @@ __metadata: languageName: node linkType: hard +"@date-io/core@npm:^3.0.0, @date-io/core@npm:^3.2.0": + version: 3.2.0 + resolution: "@date-io/core@npm:3.2.0" + checksum: 711e43b449bc29f5f1aaf49e34e722a9fe42238b1ee20e81e3a11692118002872d915c2a289f1ed24e53f88bd220ce3888d7ec0a1705c131e305478f534a3450 + languageName: node + linkType: hard + "@date-io/date-fns@npm:^1.3.13": version: 1.3.13 resolution: "@date-io/date-fns@npm:1.3.13" @@ -6250,6 +6264,20 @@ __metadata: languageName: node linkType: hard +"@date-io/date-fns@npm:^3.0.0": + version: 3.2.1 + resolution: "@date-io/date-fns@npm:3.2.1" + dependencies: + "@date-io/core": ^3.2.0 + peerDependencies: + date-fns: ^3.2.0 || ^4.1.0 + peerDependenciesMeta: + date-fns: + optional: true + checksum: 3c8ab25d604c36b47e8309d50bdb9c0052cb3697a391be030839605449eaff4527e0a9c1bc99e917f40268a6bbff8d8548daaa3bbc864cdcbe79702b53c649db + languageName: node + linkType: hard + "@electric-sql/pglite@npm:^0.2.14": version: 0.2.17 resolution: "@electric-sql/pglite@npm:0.2.17" @@ -6283,7 +6311,26 @@ __metadata: languageName: node linkType: hard -"@emotion/cache@npm:^11.13.0, @emotion/cache@npm:^11.13.5": +"@emotion/babel-plugin@npm:^11.13.5": + version: 11.13.5 + resolution: "@emotion/babel-plugin@npm:11.13.5" + dependencies: + "@babel/helper-module-imports": ^7.16.7 + "@babel/runtime": ^7.18.3 + "@emotion/hash": ^0.9.2 + "@emotion/memoize": ^0.9.0 + "@emotion/serialize": ^1.3.3 + babel-plugin-macros: ^3.1.0 + convert-source-map: ^1.5.0 + escape-string-regexp: ^4.0.0 + find-root: ^1.1.0 + source-map: ^0.5.7 + stylis: 4.2.0 + checksum: c41df7e6c19520e76d1939f884be878bf88b5ba00bd3de9d05c5b6c5baa5051686ab124d7317a0645de1b017b574d8139ae1d6390ec267fbe8e85a5252afb542 + languageName: node + linkType: hard + +"@emotion/cache@npm:^11.13.0, @emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.14.0": version: 11.14.0 resolution: "@emotion/cache@npm:11.14.0" dependencies: @@ -6296,6 +6343,13 @@ __metadata: languageName: node linkType: hard +"@emotion/core@npm:^11.0.0": + version: 11.0.0 + resolution: "@emotion/core@npm:11.0.0" + checksum: d9beeff0c120ec4d3cd89aa74e1ee3ff658976f27d14820e4e8f8e9981b4461b4a78c9e9c0853397ba09dff36b4e19357858ac8773ee5eb72bfd4b185926b5f3 + languageName: node + linkType: hard + "@emotion/hash@npm:^0.8.0": version: 0.8.0 resolution: "@emotion/hash@npm:0.8.0" @@ -6342,6 +6396,27 @@ __metadata: languageName: node linkType: hard +"@emotion/react@npm:^11.10.4": + version: 11.14.0 + resolution: "@emotion/react@npm:11.14.0" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.13.5 + "@emotion/cache": ^11.14.0 + "@emotion/serialize": ^1.3.3 + "@emotion/use-insertion-effect-with-fallbacks": ^1.2.0 + "@emotion/utils": ^1.4.2 + "@emotion/weak-memoize": ^0.4.0 + hoist-non-react-statics: ^3.3.1 + peerDependencies: + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 3cf023b11d132b56168713764d6fced8e5a1f0687dfe0caa2782dfd428c8f9e30f9826a919965a311d87b523cd196722aaf75919cd0f6bd0fd57f8a6a0281500 + languageName: node + linkType: hard + "@emotion/react@npm:^11.10.5": version: 11.13.3 resolution: "@emotion/react@npm:11.13.3" @@ -6376,6 +6451,19 @@ __metadata: languageName: node linkType: hard +"@emotion/serialize@npm:^1.3.3": + version: 1.3.3 + resolution: "@emotion/serialize@npm:1.3.3" + dependencies: + "@emotion/hash": ^0.9.2 + "@emotion/memoize": ^0.9.0 + "@emotion/unitless": ^0.10.0 + "@emotion/utils": ^1.4.2 + csstype: ^3.0.2 + checksum: 510331233767ae4e09e925287ca2c7269b320fa1d737ea86db5b3c861a734483ea832394c0c1fe5b21468fe335624a75e72818831d303ba38125f54f44ba02e7 + languageName: node + linkType: hard + "@emotion/sheet@npm:^1.4.0": version: 1.4.0 resolution: "@emotion/sheet@npm:1.4.0" @@ -6383,6 +6471,26 @@ __metadata: languageName: node linkType: hard +"@emotion/styled@npm:^11.10.4": + version: 11.14.1 + resolution: "@emotion/styled@npm:11.14.1" + dependencies: + "@babel/runtime": ^7.18.3 + "@emotion/babel-plugin": ^11.13.5 + "@emotion/is-prop-valid": ^1.3.0 + "@emotion/serialize": ^1.3.3 + "@emotion/use-insertion-effect-with-fallbacks": ^1.2.0 + "@emotion/utils": ^1.4.2 + peerDependencies: + "@emotion/react": ^11.0.0-rc.0 + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 86238d9f5c41232a73499e441fa9112e855545519d6772f489fa885634bb8f31b422a9ba9d1e8bc0b4ad66132f9d398b1c309d92c19c5ee21356b41671ec984a + languageName: node + linkType: hard + "@emotion/styled@npm:^11.10.5": version: 11.13.0 resolution: "@emotion/styled@npm:11.13.0" @@ -6419,6 +6527,15 @@ __metadata: languageName: node linkType: hard +"@emotion/use-insertion-effect-with-fallbacks@npm:^1.2.0": + version: 1.2.0 + resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.2.0" + peerDependencies: + react: ">=16.8.0" + checksum: 8ff6aec7f2924526ff8c8f8f93d4b8236376e2e12c435314a18c9a373016e24dfdf984e82bbc83712b8e90ff4783cd765eb39fc7050d1a43245e5728740ddd71 + languageName: node + linkType: hard + "@emotion/utils@npm:^1.4.0, @emotion/utils@npm:^1.4.1, @emotion/utils@npm:^1.4.2": version: 1.4.2 resolution: "@emotion/utils@npm:1.4.2" @@ -6827,6 +6944,15 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.7.3": + version: 1.7.3 + resolution: "@floating-ui/core@npm:1.7.3" + dependencies: + "@floating-ui/utils": ^0.2.10 + checksum: 5adfb28ddfa1776ec83516439256b9026e5d62b5413f62ae51e50a870cf0df4bea9abf72aacc0610ee84bc00e85883d0d32f2a0976ee7fa89728a717a7494f27 + languageName: node + linkType: hard + "@floating-ui/dom@npm:^1.0.0": version: 1.6.12 resolution: "@floating-ui/dom@npm:1.6.12" @@ -6837,6 +6963,16 @@ __metadata: languageName: node linkType: hard +"@floating-ui/dom@npm:^1.7.4": + version: 1.7.4 + resolution: "@floating-ui/dom@npm:1.7.4" + dependencies: + "@floating-ui/core": ^1.7.3 + "@floating-ui/utils": ^0.2.10 + checksum: 806923e6f5b09e024c366070f2115a4db6e8ad28462bac29cd075170a6f7d900497da3ee542439bd0770b8e2fff12b636cc30873d1c82e9ec4a487870b080643 + languageName: node + linkType: hard + "@floating-ui/react-dom@npm:^2.0.0": version: 2.1.2 resolution: "@floating-ui/react-dom@npm:2.1.2" @@ -6849,6 +6985,25 @@ __metadata: languageName: node linkType: hard +"@floating-ui/react-dom@npm:^2.0.8": + version: 2.1.6 + resolution: "@floating-ui/react-dom@npm:2.1.6" + dependencies: + "@floating-ui/dom": ^1.7.4 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 24ff266806cd4cba6ad066f0eda7b99583f68af877f41df0b2a8d10a392692e3a1c1d666ebb75571a060818ede940bae59d833aa517ed538f7dba9dddd9991ae + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.10": + version: 0.2.10 + resolution: "@floating-ui/utils@npm:0.2.10" + checksum: ffc4c24a46a665cfd0337e9aaf7de8415b572f8a0f323af39175e4b575582aed13d172e7f049eedeece9eaf022bad019c140a2d192580451984ae529bdf1285c + languageName: node + linkType: hard + "@floating-ui/utils@npm:^0.2.8": version: 0.2.8 resolution: "@floating-ui/utils@npm:0.2.8" @@ -7357,6 +7512,24 @@ __metadata: languageName: node linkType: hard +"@hello-pangea/dnd@npm:^16.0.0": + version: 16.6.0 + resolution: "@hello-pangea/dnd@npm:16.6.0" + dependencies: + "@babel/runtime": ^7.24.1 + css-box-model: ^1.2.1 + memoize-one: ^6.0.0 + raf-schd: ^4.0.3 + react-redux: ^8.1.3 + redux: ^4.2.1 + use-memo-one: ^1.1.3 + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + checksum: 00dcf9c2a7632e56c13e4a38c888081f65cec6bac0640b975b6cd17f2afa8995e8df02735db6f17de3a7ade3779386fc8375d7cedc884f8704e2e6c2977ebcec + languageName: node + linkType: hard + "@httptoolkit/httpolyglot@npm:^2.2.1": version: 2.2.2 resolution: "@httptoolkit/httpolyglot@npm:2.2.2" @@ -8467,6 +8640,36 @@ __metadata: languageName: node linkType: hard +"@material-table/core@npm:^6.4.4": + version: 6.4.4 + resolution: "@material-table/core@npm:6.4.4" + dependencies: + "@babel/runtime": ^7.19.0 + "@date-io/core": ^3.0.0 + "@date-io/date-fns": ^3.0.0 + "@emotion/core": ^11.0.0 + "@emotion/react": ^11.10.4 + "@emotion/styled": ^11.10.4 + "@hello-pangea/dnd": ^16.0.0 + "@mui/icons-material": ">=5.10.6" + "@mui/material": ">=5.11.12" + "@mui/x-date-pickers": ^6.19.0 + classnames: ^2.3.2 + date-fns: ^3.2.0 + debounce: ^1.2.1 + deep-eql: ^4.1.1 + deepmerge: ^4.2.2 + prop-types: ^15.8.1 + uuid: ^9.0.0 + zustand: ^4.3.0 + peerDependencies: + "@mui/system": ">=5.15.5" + react: ">=18.0.0" + react-dom: ">=18.0.0" + checksum: 1f4c38951981c9112e3223bd31e5a8b0f486e635f727cd269b6d8d7fec0522c4bf7f3fe02c72be3281ff98e633bd4cb0ec83692c925ef4663dae56863857563e + languageName: node + linkType: hard + "@material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4, @material-ui/core@npm:^4.9.13": version: 4.12.4 resolution: "@material-ui/core@npm:4.12.4" @@ -9039,6 +9242,28 @@ __metadata: languageName: node linkType: hard +"@mui/base@npm:^5.0.0-beta.22": + version: 5.0.0-dev.20240529-082515-213b5e33ab + resolution: "@mui/base@npm:5.0.0-dev.20240529-082515-213b5e33ab" + dependencies: + "@babel/runtime": ^7.24.6 + "@floating-ui/react-dom": ^2.0.8 + "@mui/types": ^7.2.14-dev.20240529-082515-213b5e33ab + "@mui/utils": ^6.0.0-dev.20240529-082515-213b5e33ab + "@popperjs/core": ^2.11.8 + clsx: ^2.1.1 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 7ca5d369c5f4dfa2bb36fe49d721f3dc28a6d067bd74492e8f4531671ff9debe33bade579d0cdcb7d5f899bfc5003c8f710b1f9422edd22f06d805dbc68c9f4e + languageName: node + linkType: hard + "@mui/core-downloads-tracker@npm:^5.17.1": version: 5.17.1 resolution: "@mui/core-downloads-tracker@npm:5.17.1" @@ -9046,6 +9271,13 @@ __metadata: languageName: node linkType: hard +"@mui/core-downloads-tracker@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/core-downloads-tracker@npm:7.3.2" + checksum: 01c0976e5fb6a8f0b59ebd58019af5452d7761e97daf0f2ef121bc3323508edf2fea1b13ea1a54b0098d6ee5eef70041c9722fe43291b237b989b6813a24b71e + languageName: node + linkType: hard + "@mui/icons-material@npm:5.15.17": version: 5.15.17 resolution: "@mui/icons-material@npm:5.15.17" @@ -9078,6 +9310,22 @@ __metadata: languageName: node linkType: hard +"@mui/icons-material@npm:>=5.10.6": + version: 7.3.2 + resolution: "@mui/icons-material@npm:7.3.2" + dependencies: + "@babel/runtime": ^7.28.3 + peerDependencies: + "@mui/material": ^7.3.2 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 30d31b05510c9da3bd5ba97960a0210298c101abf58efc77b2b6647b1edaf0d4b81aa671a9f5790f17977970e8cdca9139da8cfac4de31be618908754e9a3d7f + languageName: node + linkType: hard + "@mui/icons-material@npm:^5.16.7, @mui/icons-material@npm:^5.17.1": version: 5.17.1 resolution: "@mui/icons-material@npm:5.17.1" @@ -9094,6 +9342,42 @@ __metadata: languageName: node linkType: hard +"@mui/material@npm:>=5.11.12": + version: 7.3.2 + resolution: "@mui/material@npm:7.3.2" + dependencies: + "@babel/runtime": ^7.28.3 + "@mui/core-downloads-tracker": ^7.3.2 + "@mui/system": ^7.3.2 + "@mui/types": ^7.4.6 + "@mui/utils": ^7.3.2 + "@popperjs/core": ^2.11.8 + "@types/react-transition-group": ^4.4.12 + clsx: ^2.1.1 + csstype: ^3.1.3 + prop-types: ^15.8.1 + react-is: ^19.1.1 + react-transition-group: ^4.4.5 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@mui/material-pigment-css": ^7.3.2 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@mui/material-pigment-css": + optional: true + "@types/react": + optional: true + checksum: 993e0c33ed5d180cdac13fadfa067bd753ceb3a01940a777b6dcdbd347324ae0d0c000ef567f6cc358e065033e131ce0e9bc8790a29502146b805978938c5c0f + languageName: node + linkType: hard + "@mui/material@npm:^5.12.2, @mui/material@npm:^5.14.18, @mui/material@npm:^5.15.17": version: 5.17.1 resolution: "@mui/material@npm:5.17.1" @@ -9161,6 +9445,23 @@ __metadata: languageName: node linkType: hard +"@mui/private-theming@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/private-theming@npm:7.3.2" + dependencies: + "@babel/runtime": ^7.28.3 + "@mui/utils": ^7.3.2 + prop-types: ^15.8.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 443f898565cbe4a99742a84d68c6e18c2ab306a4279a00313d42a07a59cf75e8cde9356bfe9e1fd6e99d0eb08e0fa9797de34d2df12004ee3185c025feb2a7c5 + languageName: node + linkType: hard + "@mui/styled-engine@npm:^5.16.14": version: 5.16.14 resolution: "@mui/styled-engine@npm:5.16.14" @@ -9182,6 +9483,29 @@ __metadata: languageName: node linkType: hard +"@mui/styled-engine@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/styled-engine@npm:7.3.2" + dependencies: + "@babel/runtime": ^7.28.3 + "@emotion/cache": ^11.14.0 + "@emotion/serialize": ^1.3.3 + "@emotion/sheet": ^1.4.0 + csstype: ^3.1.3 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.4.1 + "@emotion/styled": ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + checksum: 452ab8bfa9a4e06b6b9a227f06465d356602459dac92fe3197eed6b5d278ec178b902405217071cc2490e515fe56b9993bb7610db75204a7eb58fd8ab840c167 + languageName: node + linkType: hard + "@mui/styles@npm:5.18.0": version: 5.18.0 resolution: "@mui/styles@npm:5.18.0" @@ -9272,6 +9596,48 @@ __metadata: languageName: node linkType: hard +"@mui/system@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/system@npm:7.3.2" + dependencies: + "@babel/runtime": ^7.28.3 + "@mui/private-theming": ^7.3.2 + "@mui/styled-engine": ^7.3.2 + "@mui/types": ^7.4.6 + "@mui/utils": ^7.3.2 + clsx: ^2.1.1 + csstype: ^3.1.3 + prop-types: ^15.8.1 + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 9a7b97e22991959dcdb60b4b0d60b359df4dce3735235db4fa51a214cf515f584ff84fd10f15849c91aeda7b8d39658eb9378253d3fac2a6db0b9baca693e3a7 + languageName: node + linkType: hard + +"@mui/types@npm:^7.2.14-dev.20240529-082515-213b5e33ab, @mui/types@npm:^7.4.6": + version: 7.4.6 + resolution: "@mui/types@npm:7.4.6" + dependencies: + "@babel/runtime": ^7.28.3 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 7e4b4ae06066468e8a8211376d0a08250325f4b92f6d2ea24576df37dfcd0f683602567debdf15889e0e4510eb48d677aa0d71a2ed9423c16dbd4b4ecfbccbb9 + languageName: node + linkType: hard + "@mui/types@npm:~7.2.15, @mui/types@npm:~7.2.24": version: 7.2.24 resolution: "@mui/types@npm:7.2.24" @@ -9284,7 +9650,7 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.17.1": +"@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.14.16, @mui/utils@npm:^5.17.1": version: 5.17.1 resolution: "@mui/utils@npm:5.17.1" dependencies: @@ -9304,7 +9670,7 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^6.4.9": +"@mui/utils@npm:^6.0.0-dev.20240529-082515-213b5e33ab, @mui/utils@npm:^6.4.9": version: 6.4.9 resolution: "@mui/utils@npm:6.4.9" dependencies: @@ -9324,6 +9690,74 @@ __metadata: languageName: node linkType: hard +"@mui/utils@npm:^7.3.2": + version: 7.3.2 + resolution: "@mui/utils@npm:7.3.2" + dependencies: + "@babel/runtime": ^7.28.3 + "@mui/types": ^7.4.6 + "@types/prop-types": ^15.7.15 + clsx: ^2.1.1 + prop-types: ^15.8.1 + react-is: ^19.1.1 + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 201fb1f49434c3ee12de3a177660af04cb6411a68f8c13dc62ea828c522758b53040046daf2341b38da3b0a8a455ef4454a49cca4fa4e7b4d345d05e3ca61ecc + languageName: node + linkType: hard + +"@mui/x-date-pickers@npm:^6.19.0": + version: 6.20.2 + resolution: "@mui/x-date-pickers@npm:6.20.2" + dependencies: + "@babel/runtime": ^7.23.2 + "@mui/base": ^5.0.0-beta.22 + "@mui/utils": ^5.14.16 + "@types/react-transition-group": ^4.4.8 + clsx: ^2.0.0 + prop-types: ^15.8.1 + react-transition-group: ^4.4.5 + peerDependencies: + "@emotion/react": ^11.9.0 + "@emotion/styled": ^11.8.1 + "@mui/material": ^5.8.6 + "@mui/system": ^5.8.0 + date-fns: ^2.25.0 || ^3.2.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + checksum: 0447b911ea0d78d4ee2080827bc075d8c1ed4764bd289d6bf65ee2ff870ac8ef72daef8a1858ccf27aad6c296cfece5455f6834a2d18a2c8e719518cd5464a0b + languageName: node + linkType: hard + "@n1ru4l/push-pull-async-iterable-iterator@npm:^3.1.0": version: 3.2.0 resolution: "@n1ru4l/push-pull-async-iterable-iterator@npm:3.2.0" @@ -10955,6 +11389,7 @@ __metadata: "@backstage/test-utils": ^1.7.10 "@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 @@ -14172,6 +14607,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:^15.7.15": + version: 15.7.15 + resolution: "@types/prop-types@npm:15.7.15" + checksum: 31aa2f59b28f24da6fb4f1d70807dae2aedfce090ec63eaf9ea01727a9533ef6eaf017de5bff99fbccad7d1c9e644f52c6c2ba30869465dd22b1a7221c29f356 + languageName: node + linkType: hard + "@types/protocol-buffers-schema@npm:^3.4.3": version: 3.4.3 resolution: "@types/protocol-buffers-schema@npm:3.4.3" @@ -14243,6 +14685,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^4.4.12, @types/react-transition-group@npm:^4.4.8": + version: 4.4.12 + resolution: "@types/react-transition-group@npm:4.4.12" + peerDependencies: + "@types/react": "*" + checksum: 13d36396cae4d3c316b03d4a0ba299f0d039c59368ba65e04b0c3dc06fd0a16f59d2c669c3e32d6d525a95423f156b84e550d26bff0bdd8df285f305f8f3a0ed + languageName: node + linkType: hard + "@types/react@npm:^18": version: 18.3.12 resolution: "@types/react@npm:18.3.12" @@ -17248,7 +17699,7 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^2.1.0, clsx@npm:^2.1.1": +"clsx@npm:^2.0.0, clsx@npm:^2.1.0, clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57 @@ -18144,7 +18595,7 @@ __metadata: languageName: node linkType: hard -"css-box-model@npm:^1.2.0": +"css-box-model@npm:^1.2.0, css-box-model@npm:^1.2.1": version: 1.2.1 resolution: "css-box-model@npm:1.2.1" dependencies: @@ -18573,6 +19024,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^3.2.0": + version: 3.6.0 + resolution: "date-fns@npm:3.6.0" + checksum: 0daa1e9a436cf99f9f2ae9232b55e11f3dd46132bee10987164f3eebd29f245b2e066d7d7db40782627411ecf18551d8f4c9fcdf2226e48bb66545407d448ab7 + languageName: node + linkType: hard + "date-format@npm:^4.0.14": version: 4.0.14 resolution: "date-format@npm:4.0.14" @@ -18601,7 +19059,7 @@ __metadata: languageName: node linkType: hard -"debounce@npm:^1.2.0": +"debounce@npm:^1.2.0, debounce@npm:^1.2.1": version: 1.2.1 resolution: "debounce@npm:1.2.1" checksum: 682a89506d9e54fb109526f4da255c5546102fbb8e3ae75eef3b04effaf5d4853756aee97475cd4650641869794e44f410eeb20ace2b18ea592287ab2038519e @@ -18687,6 +19145,15 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^4.1.1": + version: 4.1.4 + resolution: "deep-eql@npm:4.1.4" + dependencies: + type-detect: ^4.0.0 + checksum: 01c3ca78ff40d79003621b157054871411f94228ceb9b2cab78da913c606631c46e8aa79efc4aa0faf3ace3092acd5221255aab3ef0e8e7b438834f0ca9a16c7 + languageName: node + linkType: hard + "deep-equal@npm:^2.0.5": version: 2.2.3 resolution: "deep-equal@npm:2.2.3" @@ -26096,6 +26563,13 @@ __metadata: languageName: node linkType: hard +"memoize-one@npm:^6.0.0": + version: 6.0.0 + resolution: "memoize-one@npm:6.0.0" + checksum: f185ea69f7cceae5d1cb596266dcffccf545e8e7b4106ec6aa93b71ab9d16460dd118ac8b12982c55f6d6322fcc1485de139df07eacffaae94888b9b3ad7675f + languageName: node + linkType: hard + "merge-descriptors@npm:1.0.3": version: 1.0.3 resolution: "merge-descriptors@npm:1.0.3" @@ -29645,7 +30119,7 @@ __metadata: languageName: node linkType: hard -"raf-schd@npm:^4.0.2": +"raf-schd@npm:^4.0.2, raf-schd@npm:^4.0.3": version: 4.0.3 resolution: "raf-schd@npm:4.0.3" checksum: 45514041c5ad31fa96aef3bb3c572a843b92da2f2cd1cb4a47c9ad58e48761d3a4126e18daa32b2bfa0bc2551a42d8f324a0e40e536cb656969929602b4e8b58 @@ -30055,6 +30529,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^19.1.1": + version: 19.1.1 + resolution: "react-is@npm:19.1.1" + checksum: e60ed01c27fe4d22b08f8a31f18831d144a801d08a909ca31fb1d02721b4f4cde0759148d6341f660a4d6ce54a78e22b8b39520b67e2e76254e583885868ab43 + languageName: node + linkType: hard + "react-markdown@npm:^8.0.0": version: 8.0.7 resolution: "react-markdown@npm:8.0.7" @@ -30102,6 +30583,38 @@ __metadata: languageName: node linkType: hard +"react-redux@npm:^8.1.3": + version: 8.1.3 + resolution: "react-redux@npm:8.1.3" + dependencies: + "@babel/runtime": ^7.12.1 + "@types/hoist-non-react-statics": ^3.3.1 + "@types/use-sync-external-store": ^0.0.3 + hoist-non-react-statics: ^3.3.2 + react-is: ^18.0.0 + use-sync-external-store: ^1.0.0 + peerDependencies: + "@types/react": ^16.8 || ^17.0 || ^18.0 + "@types/react-dom": ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: ">=0.59" + redux: ^4 || ^5.0.0-beta.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + react-dom: + optional: true + react-native: + optional: true + redux: + optional: true + checksum: 192ea6f6053148ec80a4148ec607bc259403b937e515f616a1104ca5ab357e97e98b8245ed505a17afee67a72341d4a559eaca9607968b4a422aa9b44ba7eb89 + languageName: node + linkType: hard + "react-redux@npm:^9.1.2": version: 9.1.2 resolution: "react-redux@npm:9.1.2" @@ -30489,7 +31002,7 @@ __metadata: languageName: node linkType: hard -"redux@npm:^4.0.0, redux@npm:^4.0.4": +"redux@npm:^4.0.0, redux@npm:^4.0.4, redux@npm:^4.2.1": version: 4.2.1 resolution: "redux@npm:4.2.1" dependencies: @@ -33729,6 +34242,13 @@ __metadata: languageName: node linkType: hard +"type-detect@npm:^4.0.0": + version: 4.1.0 + resolution: "type-detect@npm:4.1.0" + checksum: 3b32f873cd02bc7001b00a61502b7ddc4b49278aabe68d652f732e1b5d768c072de0bc734b427abf59d0520a5f19a2e07309ab921ef02018fa1cb4af155cdb37 + languageName: node + linkType: hard + "type-fest@npm:^0.13.1": version: 0.13.1 resolution: "type-fest@npm:0.13.1" @@ -34486,7 +35006,7 @@ __metadata: languageName: node linkType: hard -"use-memo-one@npm:^1.1.1": +"use-memo-one@npm:^1.1.1, use-memo-one@npm:^1.1.3": version: 1.1.3 resolution: "use-memo-one@npm:1.1.3" peerDependencies: @@ -34532,6 +35052,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.2": + version: 1.5.0 + resolution: "use-sync-external-store@npm:1.5.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 5e639c9273200adb6985b512c96a3a02c458bc8ca1a72e91da9cdc6426144fc6538dca434b0f99b28fb1baabc82e1c383ba7900b25ccdcb43758fb058dc66c34 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -35590,6 +36119,26 @@ __metadata: languageName: node linkType: hard +"zustand@npm:^4.3.0": + version: 4.5.7 + resolution: "zustand@npm:4.5.7" + dependencies: + use-sync-external-store: ^1.2.2 + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0.6" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + checksum: 103ab43456bbc3be6afe79b18a93c7fa46ffaa1aa35c45b213f13f4cd0868fee78b43c6805c6d80a822297df2e455fd021c28be94b80529ec4806b2724f20219 + languageName: node + linkType: hard + "zwitch@npm:^2.0.0": version: 2.0.4 resolution: "zwitch@npm:2.0.4" From 2b7a4c663064026536c916a4f231c2367662e928 Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 5 Sep 2025 03:12:39 -0400 Subject: [PATCH 03/14] added changeset Signed-off-by: Yi Cai --- workspaces/marketplace/.changeset/unlucky-hornets-cross.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 workspaces/marketplace/.changeset/unlucky-hornets-cross.md diff --git a/workspaces/marketplace/.changeset/unlucky-hornets-cross.md b/workspaces/marketplace/.changeset/unlucky-hornets-cross.md new file mode 100644 index 0000000000..5ba5457c62 --- /dev/null +++ b/workspaces/marketplace/.changeset/unlucky-hornets-cross.md @@ -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 From d1f56b6a204ec8780aa95ed177d0dc94c7062219 Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 5 Sep 2025 03:15:42 -0400 Subject: [PATCH 04/14] yarn dedupe Signed-off-by: Yi Cai --- workspaces/marketplace/yarn.lock | 179 ++----------------------------- 1 file changed, 10 insertions(+), 169 deletions(-) diff --git a/workspaces/marketplace/yarn.lock b/workspaces/marketplace/yarn.lock index a887235597..5d6cdbb00c 100644 --- a/workspaces/marketplace/yarn.lock +++ b/workspaces/marketplace/yarn.lock @@ -2755,14 +2755,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.27.1 - resolution: "@babel/runtime@npm:7.27.1" - checksum: 11339838a54783e5b14e04d94d7a4d032e9965c5823f3f687e41556fa40344ae7aeb57c535720b7a74ab3e8217def7834a6f1a665ee55bbb3befede141419913 - languageName: node - linkType: hard - -"@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.19.0, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.6, @babel/runtime@npm:^7.28.3": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.19.0, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.6, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.28.3, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.28.3 resolution: "@babel/runtime@npm:7.28.3" checksum: dd22662b9e02b6e66cfb061d6f9730eb0aa3b3a390a7bd70fe9a64116d86a3704df6d54ab978cb4acc13b58dbf63a3d7dd4616b0b87030eb14a22835e0aa602d @@ -6292,25 +6285,6 @@ __metadata: languageName: node linkType: hard -"@emotion/babel-plugin@npm:^11.12.0": - version: 11.12.0 - resolution: "@emotion/babel-plugin@npm:11.12.0" - dependencies: - "@babel/helper-module-imports": ^7.16.7 - "@babel/runtime": ^7.18.3 - "@emotion/hash": ^0.9.2 - "@emotion/memoize": ^0.9.0 - "@emotion/serialize": ^1.2.0 - babel-plugin-macros: ^3.1.0 - convert-source-map: ^1.5.0 - escape-string-regexp: ^4.0.0 - find-root: ^1.1.0 - source-map: ^0.5.7 - stylis: 4.2.0 - checksum: b5d4b3dfe97e6763794a42b5c3a027a560caa1aa6dcaf05c18e5969691368dd08245c077bad7397dcc720b53d29caeaaec1888121e68cfd9ab02ff52f6fef662 - languageName: node - linkType: hard - "@emotion/babel-plugin@npm:^11.13.5": version: 11.13.5 resolution: "@emotion/babel-plugin@npm:11.13.5" @@ -6330,7 +6304,7 @@ __metadata: languageName: node linkType: hard -"@emotion/cache@npm:^11.13.0, @emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.14.0": +"@emotion/cache@npm:^11.13.5, @emotion/cache@npm:^11.14.0": version: 11.14.0 resolution: "@emotion/cache@npm:11.14.0" dependencies: @@ -6396,7 +6370,7 @@ __metadata: languageName: node linkType: hard -"@emotion/react@npm:^11.10.4": +"@emotion/react@npm:^11.10.4, @emotion/react@npm:^11.10.5": version: 11.14.0 resolution: "@emotion/react@npm:11.14.0" dependencies: @@ -6417,40 +6391,6 @@ __metadata: languageName: node linkType: hard -"@emotion/react@npm:^11.10.5": - version: 11.13.3 - resolution: "@emotion/react@npm:11.13.3" - dependencies: - "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.12.0 - "@emotion/cache": ^11.13.0 - "@emotion/serialize": ^1.3.1 - "@emotion/use-insertion-effect-with-fallbacks": ^1.1.0 - "@emotion/utils": ^1.4.0 - "@emotion/weak-memoize": ^0.4.0 - hoist-non-react-statics: ^3.3.1 - peerDependencies: - react: ">=16.8.0" - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 0b58374bf28de914b49881f0060acfb908989869ebab63a2287773fc5e91a39f15552632b03d376c3e9835c5b4f23a5ebac8b0963b29af164d46c0a77ac928f0 - languageName: node - linkType: hard - -"@emotion/serialize@npm:^1.2.0, @emotion/serialize@npm:^1.3.0, @emotion/serialize@npm:^1.3.1": - version: 1.3.2 - resolution: "@emotion/serialize@npm:1.3.2" - dependencies: - "@emotion/hash": ^0.9.2 - "@emotion/memoize": ^0.9.0 - "@emotion/unitless": ^0.10.0 - "@emotion/utils": ^1.4.1 - csstype: ^3.0.2 - checksum: 8051bafe32459e1aecf716cdb66a22b090060806104cca89d4e664893b56878d3e9bb94a4657df9b7b3fd183700a9be72f7144c959ddcbd3cf7b330206919237 - languageName: node - linkType: hard - "@emotion/serialize@npm:^1.3.3": version: 1.3.3 resolution: "@emotion/serialize@npm:1.3.3" @@ -6471,7 +6411,7 @@ __metadata: languageName: node linkType: hard -"@emotion/styled@npm:^11.10.4": +"@emotion/styled@npm:^11.10.4, @emotion/styled@npm:^11.10.5": version: 11.14.1 resolution: "@emotion/styled@npm:11.14.1" dependencies: @@ -6491,26 +6431,6 @@ __metadata: languageName: node linkType: hard -"@emotion/styled@npm:^11.10.5": - version: 11.13.0 - resolution: "@emotion/styled@npm:11.13.0" - dependencies: - "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.12.0 - "@emotion/is-prop-valid": ^1.3.0 - "@emotion/serialize": ^1.3.0 - "@emotion/use-insertion-effect-with-fallbacks": ^1.1.0 - "@emotion/utils": ^1.4.0 - peerDependencies: - "@emotion/react": ^11.0.0-rc.0 - react: ">=16.8.0" - peerDependenciesMeta: - "@types/react": - optional: true - checksum: f5b951059418f57bc8ea32b238afb25965ece3314f2ffd1b14ce049ba3c066a424990dfbfabbf57bb88e044eaa80bf19f620ac988adda3d2fc483177be6da05e - languageName: node - linkType: hard - "@emotion/unitless@npm:^0.10.0": version: 0.10.0 resolution: "@emotion/unitless@npm:0.10.0" @@ -6518,15 +6438,6 @@ __metadata: languageName: node linkType: hard -"@emotion/use-insertion-effect-with-fallbacks@npm:^1.1.0": - version: 1.1.0 - resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.1.0" - peerDependencies: - react: ">=16.8.0" - checksum: 63665191773b27de66807c53b90091ef0d10d5161381f62726cfceecfe1d8c944f18594b8021805fc81575b64246fd5ab9c75d60efabec92f940c1c410530949 - languageName: node - linkType: hard - "@emotion/use-insertion-effect-with-fallbacks@npm:^1.2.0": version: 1.2.0 resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.2.0" @@ -6536,7 +6447,7 @@ __metadata: languageName: node linkType: hard -"@emotion/utils@npm:^1.4.0, @emotion/utils@npm:^1.4.1, @emotion/utils@npm:^1.4.2": +"@emotion/utils@npm:^1.4.2": version: 1.4.2 resolution: "@emotion/utils@npm:1.4.2" checksum: 04cf76849c6401205c058b82689fd0ec5bf501aed6974880fe9681a1d61543efb97e848f4c38664ac4a9068c7ad2d1cb84f73bde6cf95f1208aa3c28e0190321 @@ -6935,15 +6846,6 @@ __metadata: languageName: node linkType: hard -"@floating-ui/core@npm:^1.6.0": - version: 1.6.8 - resolution: "@floating-ui/core@npm:1.6.8" - dependencies: - "@floating-ui/utils": ^0.2.8 - checksum: 82faa6ea9d57e466779324e51308d6d49c098fb9d184a08d9bb7f4fad83f08cc070fc491f8d56f0cad44a16215fb43f9f829524288413e6c33afcb17303698de - languageName: node - linkType: hard - "@floating-ui/core@npm:^1.7.3": version: 1.7.3 resolution: "@floating-ui/core@npm:1.7.3" @@ -6953,16 +6855,6 @@ __metadata: languageName: node linkType: hard -"@floating-ui/dom@npm:^1.0.0": - version: 1.6.12 - resolution: "@floating-ui/dom@npm:1.6.12" - dependencies: - "@floating-ui/core": ^1.6.0 - "@floating-ui/utils": ^0.2.8 - checksum: 956514ed100c0c853e73ace9e3c877b7e535444d7c31326f687a7690d49cb1e59ef457e9c93b76141aea0d280e83ed5a983bb852718b62eea581f755454660f6 - languageName: node - linkType: hard - "@floating-ui/dom@npm:^1.7.4": version: 1.7.4 resolution: "@floating-ui/dom@npm:1.7.4" @@ -6973,19 +6865,7 @@ __metadata: languageName: node linkType: hard -"@floating-ui/react-dom@npm:^2.0.0": - version: 2.1.2 - resolution: "@floating-ui/react-dom@npm:2.1.2" - dependencies: - "@floating-ui/dom": ^1.0.0 - peerDependencies: - react: ">=16.8.0" - react-dom: ">=16.8.0" - checksum: 25bb031686e23062ed4222a8946e76b3f9021d40a48437bd747233c4964a766204b8a55f34fa8b259839af96e60db7c6e3714d81f1de06914294f90e86ffbc48 - languageName: node - linkType: hard - -"@floating-ui/react-dom@npm:^2.0.8": +"@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.0.8": version: 2.1.6 resolution: "@floating-ui/react-dom@npm:2.1.6" dependencies: @@ -7004,13 +6884,6 @@ __metadata: languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.8": - version: 0.2.8 - resolution: "@floating-ui/utils@npm:0.2.8" - checksum: deb98bba017c4e073c7ad5740d4dec33a4d3e0942d412e677ac0504f3dade15a68fc6fd164d43c93c0bb0bcc5dc5015c1f4080dfb1a6161140fe660624f7c875 - languageName: node - linkType: hard - "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -14600,14 +14473,7 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.14, @types/prop-types@npm:^15.7.3": - version: 15.7.14 - resolution: "@types/prop-types@npm:15.7.14" - checksum: d0c5407b9ccc3dd5fae0ccf9b1007e7622ba5e6f1c18399b4f24dff33619d469da4b9fa918a374f19dc0d9fe6a013362aab0b844b606cfc10676efba3f5f736d - languageName: node - linkType: hard - -"@types/prop-types@npm:^15.7.15": +"@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.12, @types/prop-types@npm:^15.7.14, @types/prop-types@npm:^15.7.15, @types/prop-types@npm:^15.7.3": version: 15.7.15 resolution: "@types/prop-types@npm:15.7.15" checksum: 31aa2f59b28f24da6fb4f1d70807dae2aedfce090ec63eaf9ea01727a9533ef6eaf017de5bff99fbccad7d1c9e644f52c6c2ba30869465dd22b1a7221c29f356 @@ -14676,16 +14542,7 @@ __metadata: languageName: node linkType: hard -"@types/react-transition-group@npm:^4.2.0, @types/react-transition-group@npm:^4.4.10": - version: 4.4.11 - resolution: "@types/react-transition-group@npm:4.4.11" - dependencies: - "@types/react": "*" - checksum: a6e3b2e4363cb019e256ae4f19dadf9d7eb199da1a5e4109bbbf6a132821884044d332e9c74b520b1e5321a7f545502443fd1ce0b18649c8b510fa4220b0e5c2 - languageName: node - linkType: hard - -"@types/react-transition-group@npm:^4.4.12, @types/react-transition-group@npm:^4.4.8": +"@types/react-transition-group@npm:^4.2.0, @types/react-transition-group@npm:^4.4.10, @types/react-transition-group@npm:^4.4.12, @types/react-transition-group@npm:^4.4.8": version: 4.4.12 resolution: "@types/react-transition-group@npm:4.4.12" peerDependencies: @@ -30522,14 +30379,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^19.0.0": - version: 19.1.0 - resolution: "react-is@npm:19.1.0" - checksum: 3eb4eac7f09bf178bdc6fa98d384f5f243b85de7c99679a88b0154ead4d818ad94386ccb00ea31ec52409ffd13299057f5ec6ca2eaec06f9f7eddc1ad4832332 - languageName: node - linkType: hard - -"react-is@npm:^19.1.1": +"react-is@npm:^19.0.0, react-is@npm:^19.1.1": version: 19.1.1 resolution: "react-is@npm:19.1.1" checksum: e60ed01c27fe4d22b08f8a31f18831d144a801d08a909ca31fb1d02721b4f4cde0759148d6341f660a4d6ce54a78e22b8b39520b67e2e76254e583885868ab43 @@ -35043,16 +34893,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.0": - version: 1.2.2 - resolution: "use-sync-external-store@npm:1.2.2" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: fe07c071c4da3645f112c38c0e57beb479a8838616ff4e92598256ecce527f2888c08febc7f9b2f0ce2f0e18540ba3cde41eb2035e4fafcb4f52955037098a81 - languageName: node - linkType: hard - -"use-sync-external-store@npm:^1.2.2": +"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:^1.2.2": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" peerDependencies: From 8d464eaf41a81b71e0475deacf559a5beab6ae07 Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 5 Sep 2025 03:28:51 -0400 Subject: [PATCH 05/14] fix tsc issue Signed-off-by: Yi Cai --- .../InstalledPluginsTable.tsx | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx index 74b6a3a4a5..c1146b329e 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx @@ -116,32 +116,26 @@ export const InstalledPluginsTable = () => { const processedPlugins = processPluginsForDisplay(installedPlugins); data = [...processedPlugins] - .sort( - ( - a: Record, - b: Record, - ) => { - const field = orderBy.field!; - const orderMultiplier = orderDirection === 'desc' ? -1 : 1; + .sort((a: Record, b: Record) => { + const field = Array.isArray(orderBy.field) + ? orderBy.field[0] + : orderBy.field; + const orderMultiplier = orderDirection === 'desc' ? -1 : 1; - if (a[field] === null || b[field] === null) { - return 0; - } + if (!field || 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; - } + // 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 - ); - }, - ) + return ( + (a[field] as string).localeCompare(b[field] as string) * + orderMultiplier + ); + }) .filter(plugin => plugin.name .toLowerCase() @@ -184,7 +178,7 @@ export const InstalledPluginsTable = () => { debounceInterval: 500, }} columns={columns} - data={fetchData} + data={fetchData as any} localization={{ body: { emptyDataSourceMessage: emptyMessage, From a414de935cc901a3d87cc61e3d6a3ff230b91bec Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Mon, 8 Sep 2025 21:15:47 -0400 Subject: [PATCH 06/14] addressed review comments Signed-off-by: Yi Cai --- .../marketplace/packages/app/src/App.tsx | 14 +------ .../plugins/marketplace/knip-report.md | 1 - .../plugins/marketplace/report.api.md | 9 ----- .../InstalledPluginsTable.tsx | 32 ++++++++++++++-- .../pages/DynamicMarketplacePluginRouter.tsx | 37 ++++++++++--------- .../plugins/marketplace/src/plugin.ts | 15 -------- workspaces/marketplace/rbac-policy.csv | 14 +++++++ 7 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 workspaces/marketplace/rbac-policy.csv diff --git a/workspaces/marketplace/packages/app/src/App.tsx b/workspaces/marketplace/packages/app/src/App.tsx index ac136baa77..289cbafdda 100644 --- a/workspaces/marketplace/packages/app/src/App.tsx +++ b/workspaces/marketplace/packages/app/src/App.tsx @@ -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'; @@ -136,14 +133,7 @@ const routes = ( } /> } /> - - - - } - /> + } /> ); diff --git a/workspaces/marketplace/plugins/marketplace/knip-report.md b/workspaces/marketplace/plugins/marketplace/knip-report.md index c2ad928652..0c88afc9c3 100644 --- a/workspaces/marketplace/plugins/marketplace/knip-report.md +++ b/workspaces/marketplace/plugins/marketplace/knip-report.md @@ -6,4 +6,3 @@ | :-------------------------- | :---------------- | :------- | | @testing-library/user-event | package.json:64:6 | error | | msw | package.json:65:6 | error | - diff --git a/workspaces/marketplace/plugins/marketplace/report.api.md b/workspaces/marketplace/plugins/marketplace/report.api.md index 8f66203193..efd543683a 100644 --- a/workspaces/marketplace/plugins/marketplace/report.api.md +++ b/workspaces/marketplace/plugins/marketplace/report.api.md @@ -4,14 +4,10 @@ ```ts -/// - 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'; @@ -21,11 +17,6 @@ export const DynamicMarketplacePluginContent: () => JSX_2.Element; // @public export const DynamicMarketplacePluginRouter: () => JSX_2.Element; -// @public (undocumented) -export const InstallationContextProvider: ({ children, }: { - children: ReactElement>; -}) => JSX_2.Element; - // @public export const MarketplaceFullPageRouter: () => JSX_2.Element; diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx index c1146b329e..1b6a2be931 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx @@ -46,19 +46,42 @@ export const InstalledPluginsTable = () => { { title: 'Name', field: 'name', + align: 'left', + width: '30ch', defaultSort: 'asc', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, }, { title: 'Version', field: 'version', - width: '15%', + width: '54ch', + align: 'left', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, }, { title: 'Actions', + align: 'right', + width: '78px', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, render: (_rowData: DynamicPluginInfo) => { return ( - + { @@ -68,7 +91,7 @@ export const InstalledPluginsTable = () => { - + { @@ -78,7 +101,7 @@ export const InstalledPluginsTable = () => { - + { @@ -176,6 +199,7 @@ export const InstalledPluginsTable = () => { paging: true, thirdSortClick: false, debounceInterval: 500, + emptyRowsWhenPaging: false, }} columns={columns} data={fetchData as any} diff --git a/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx b/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx index d54f10b3ee..21924d87be 100644 --- a/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx @@ -35,6 +35,7 @@ import { MarketplacePluginDrawer } from '../components/MarketplacePluginDrawer'; import { MarketplacePluginInstallPage } from './MarketplacePluginInstallPage'; import { MarketplacePackageDrawer } from '../components/MarketplacePackageDrawer'; import { MarketplacePackageInstallPage } from './MarketplacePackageInstallPage'; +import { InstallationContextProvider } from '../components/InstallationContext'; // Constants for consistent styling const TAB_ICON_STYLE = { @@ -123,23 +124,25 @@ const MarketplacePage = () => { }; export const DynamicMarketplacePluginRouter = () => ( - - - - - - - - + + + + + + + + + + ); export const DynamicMarketplacePluginContent = () => ( diff --git a/workspaces/marketplace/plugins/marketplace/src/plugin.ts b/workspaces/marketplace/plugins/marketplace/src/plugin.ts index b5b1d903a4..807ad4ff9e 100644 --- a/workspaces/marketplace/plugins/marketplace/src/plugin.ts +++ b/workspaces/marketplace/plugins/marketplace/src/plugin.ts @@ -136,21 +136,6 @@ export const DynamicMarketplacePluginContent = marketplacePlugin.provide( }), ); -/** - * @public - */ -export const InstallationContextProvider = marketplacePlugin.provide( - createComponentExtension({ - name: 'InstallationContextProvider', - component: { - lazy: () => - import('./components/InstallationContext').then( - m => m.InstallationContextProvider, - ), - }, - }), -); - /** * @public */ diff --git a/workspaces/marketplace/rbac-policy.csv b/workspaces/marketplace/rbac-policy.csv new file mode 100644 index 0000000000..4ee475637a --- /dev/null +++ b/workspaces/marketplace/rbac-policy.csv @@ -0,0 +1,14 @@ +g, user:default/ciiay, role:default/admins +p, role:default/admins, catalog-entity, read, allow +p, role:default/admins, catalog.entity.create, create, allow +p, role:default/admins, catalog.location.read , read, allow +p, role:default/admins, catalog.location.create , create, allow +p, role:default/admins, policy.entity.read, read, allow +p, role:default/admins, policy.entity.delete, delete, allow +p, role:default/admins, policy.entity.update, update, allow +p, role:default/admins, policy.entity.create, create, allow +p, role:default/admins, kubernetes.clusters.read, read, allow +p, role:default/admins, kubernetes.resources.read, read, allow + +p, role:default/admins, extensions-plugin, read, allow +p, role:default/admins, extensions-plugin, create, allow From 8610897efbda804e08a306247efdfa62e735f3bf Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 19 Sep 2025 02:06:03 -0400 Subject: [PATCH 07/14] addressed feedback Signed-off-by: Yi Cai --- .../.changeset/unlucky-hornets-cross.md | 13 +- .../InstalledPackagesTable.tsx | 341 ++++++++++++++++++ .../InstalledPluginsTable.tsx | 76 +++- .../src/hooks/useInstalledPluginsCount.ts | 4 +- .../pages/DynamicMarketplacePluginRouter.tsx | 10 +- .../plugins/marketplace/src/plugin.ts | 15 +- .../src/shared-components/SearchTextField.tsx | 43 ++- 7 files changed, 472 insertions(+), 30 deletions(-) create mode 100644 workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx diff --git a/workspaces/marketplace/.changeset/unlucky-hornets-cross.md b/workspaces/marketplace/.changeset/unlucky-hornets-cross.md index 5ba5457c62..08acd302dc 100644 --- a/workspaces/marketplace/.changeset/unlucky-hornets-cross.md +++ b/workspaces/marketplace/.changeset/unlucky-hornets-cross.md @@ -1,6 +1,15 @@ --- '@red-hat-developer-hub/backstage-plugin-marketplace-backend': minor -'@red-hat-developer-hub/backstage-plugin-marketplace': minor +'@red-hat-developer-hub/backstage-plugin-marketplace': major --- -Integrate plugins-info plugin and add `Installed Plugins` tab with enhanced UI +Integrate plugins-info plugin and add `Installed packages` tab with enhanced UI. + +BREAKING: The deprecated `InstallationContextProvider` export behavior changed. + +- 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. +- Migration: There is no replacement API; this was internal-only. Please upgrade to the latest RHDH where features no longer rely on this provider. + +Also: + +- New `Installed packages` tab with dual-source mapping and client-side filtering/pagination. diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx new file mode 100644 index 0000000000..ff6bbb7cde --- /dev/null +++ b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx @@ -0,0 +1,341 @@ +/* + * 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 { useMemo, 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 { useQuery } from '@tanstack/react-query'; + +import { dynamicPluginsInfoApiRef } from '../../api'; +import { useInstalledPluginsCount } from '../../hooks/useInstalledPluginsCount'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; + +import { useMarketplaceApi } from '../../hooks/useMarketplaceApi'; +import { useQueryFullTextSearch } from '../../hooks/useQueryFullTextSearch'; +import { SearchTextField } from '../../shared-components/SearchTextField'; + +type InstalledPackageRow = { + displayName: string; + packageName: string; + version?: string; + hasEntity: boolean; +}; + +export const InstalledPackagesTable = () => { + const [error, setError] = useState(undefined); + const { count } = useInstalledPluginsCount(); + const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef); + const marketplaceApi = useMarketplaceApi(); + const fullTextSearch = useQueryFullTextSearch(); + + // Fetch once and cache + const installedQuery = useQuery({ + queryKey: ['dynamic-plugins-info', 'installed'], + queryFn: () => dynamicPluginInfo.listLoadedPlugins(), + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + }); + + const packagesQuery = useQuery({ + queryKey: ['marketplace', 'packages'], + queryFn: () => marketplaceApi.getPackages({}), + staleTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + }); + const columns: TableColumn[] = useMemo( + () => [ + { + title: 'Name', + field: 'displayName', + align: 'left', + width: '30ch', + defaultSort: 'asc', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, + }, + { + title: 'Package', + field: 'packageName', + width: '54ch', + align: 'left', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, + }, + { + title: 'Version', + field: 'version', + width: '24ch', + align: 'left', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, + }, + { + title: 'Actions', + align: 'right', + width: '78px', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, + render: (row: InstalledPackageRow) => { + const disabled = !row.hasEntity; + const tooltipTitle = + 'To enable actions, add a catalog entity for this package'; + return ( + + {disabled ? ( + + + theme.palette.action.disabled }} + > + + + + + ) : ( + theme.palette.text.primary }} + onClick={() => { + // TODO: Implement edit functionality + }} + > + + + )} + {disabled ? ( + + + theme.palette.action.disabled }} + > + + + + + ) : ( + theme.palette.text.primary }} + onClick={() => { + // TODO: Implement delete functionality + }} + > + + + )} + {disabled ? ( + + + theme.palette.action.disabled }} + > + + + + + ) : ( + theme.palette.text.primary }} + onClick={() => { + // TODO: Implement download functionality + }} + > + + + )} + + ); + }, + sorting: false, + }, + ], + [], + ); + const fetchData = async ( + query: Query, + ): Promise> => { + const { page = 0, pageSize = 5 } = query || {}; + + try { + // Ensure data available; avoid re-fetching on search or pagination + const installed = installedQuery.data ?? []; + const packagesResponse = packagesQuery.data ?? { items: [] as any[] }; + + // Debug: sizes + // eslint-disable-next-line no-console + console.log( + '[InstalledPackagesTable] dynamic-plugins-info count', + installed.length, + ); + // eslint-disable-next-line no-console + console.log( + '[InstalledPackagesTable] catalog packages count', + packagesResponse.items?.length ?? 0, + ); + + const entitiesByName = new Map( + (packagesResponse.items ?? []).map(entity => [ + (entity.metadata?.name ?? '').toLowerCase(), + entity, + ]), + ); + + // Build rows for ALL installed items; if no matching entity, mark missing + const rows: InstalledPackageRow[] = installed.map(p => { + const normalized = p.name + .replace(/[@/]/g, '-') + .replace(/-dynamic$/, '') + .replace(/^-+/, '') + .toLowerCase(); + const entity = entitiesByName.get(normalized) as any | undefined; + return { + displayName: entity + ? (entity.metadata?.title as string) || + (entity.metadata?.name as string) + : 'Missing catalog entity', + // Show the npm package name directly from dynamic-plugins-info record + packageName: p.name, + // Prefer dynamic-plugins-info version, then fallback to entity spec.version + version: + (p.version as string | undefined) ?? + (entity?.spec?.version as string | undefined) ?? + undefined, + hasEntity: !!entity, + } as InstalledPackageRow; + }); + // Debug: rows and missing entity stats + // eslint-disable-next-line no-console + console.log('[InstalledPackagesTable] rows count', rows.length); + // eslint-disable-next-line no-console + console.log( + '[InstalledPackagesTable] missing entity rows', + rows.filter(r => r.displayName === 'Missing catalog entity').length, + ); + + const sortField = + ((query as any)?.orderBy?.field as string) || 'displayName'; + const sortDirection = (((query as any)?.orderDirection as + | 'asc' + | 'desc') || 'asc') as 'asc' | 'desc'; + + const filteredRows = [...rows] + .filter(row => { + const term = fullTextSearch.current?.toLowerCase().trim() ?? ''; + return row.displayName.toLowerCase().trim().includes(term); + }) + .sort((a, b) => { + const aVal = ((a as any)[sortField] ?? '').toString(); + const bVal = ((b as any)[sortField] ?? '').toString(); + const cmp = aVal.localeCompare(bVal); + return sortDirection === 'desc' ? -cmp : cmp; + }); + // Debug: filtered rows count + // eslint-disable-next-line no-console + console.log( + '[InstalledPackagesTable] filtered rows count', + filteredRows.length, + ); + const totalCount = filteredRows.length; + const start = Math.max(0, page * pageSize); + const end = Math.min(totalCount, start + pageSize); + return { data: filteredRows.slice(start, end), page, totalCount }; + } catch (loadingError) { + // eslint-disable-next-line no-console + console.error('Failed to load plugins', loadingError); + setError(loadingError as Error); + return { data: [], totalCount: 0, page: 0 }; + } + }; + if (error) { + return ; + } + + // Conditional empty message based on search state + const emptyMessage = (fullTextSearch.current || '').trim() + ? 'No results found. Try a different search term.' + : 'No records to display'; + + return ( + <> +
+ +
+
+ + ); +}; diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx index 1b6a2be931..5299cfb072 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx @@ -25,7 +25,7 @@ import { useApi } from '@backstage/core-plugin-api'; import { Query, QueryResult } from '@material-table/core'; -import { DynamicPluginInfo, dynamicPluginsInfoApiRef } from '../../api'; +import { dynamicPluginsInfoApiRef } from '../../api'; import { useInstalledPluginsCount } from '../../hooks/useInstalledPluginsCount'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -34,18 +34,25 @@ import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; -import { processPluginsForDisplay } from '../../utils/pluginProcessing'; +import { useMarketplaceApi } from '../../hooks/useMarketplaceApi'; + +type InstalledPackageRow = { + displayName: string; + packageName: string; + version?: string; +}; export const InstalledPluginsTable = () => { const [error, setError] = useState(undefined); const [currentSearchTerm, setCurrentSearchTerm] = useState(''); const { count } = useInstalledPluginsCount(); const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef); - let data: DynamicPluginInfo[] = []; - const columns: TableColumn[] = [ + const marketplaceApi = useMarketplaceApi(); + let data: InstalledPackageRow[] = []; + const columns: TableColumn[] = [ { title: 'Name', - field: 'name', + field: 'displayName', align: 'left', width: '30ch', defaultSort: 'asc', @@ -56,10 +63,22 @@ export const InstalledPluginsTable = () => { textAlign: 'left', }, }, + { + title: 'Package', + field: 'packageName', + width: '54ch', + align: 'left', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, + }, { title: 'Version', field: 'version', - width: '54ch', + width: '24ch', align: 'left', headerStyle: { textAlign: 'left', @@ -78,7 +97,7 @@ export const InstalledPluginsTable = () => { cellStyle: { textAlign: 'left', }, - render: (_rowData: DynamicPluginInfo) => { + render: (_rowData: InstalledPackageRow) => { return ( @@ -118,10 +137,10 @@ export const InstalledPluginsTable = () => { }, ]; const fetchData = async ( - query: Query, - ): Promise> => { + query: Query, + ): Promise> => { const { - orderBy = { field: 'name' }, + orderBy = { field: 'displayName' }, orderDirection = 'asc', page = 0, pageSize = 5, @@ -133,12 +152,35 @@ export const InstalledPluginsTable = () => { try { // for now sorting/searching/pagination is handled client-side - const installedPlugins = await dynamicPluginInfo.listLoadedPlugins(); + const installed = await dynamicPluginInfo.listLoadedPlugins(); + + // Normalize installed names to entity names: replace @ and / with - + const installedEntityNames = Array.from( + new Set(installed.map(p => p.name.replace(/[@/]/g, '-').toLowerCase())), + ); + + // Fetch marketplace package entities + const packagesResponse = await marketplaceApi.getPackages({}); + const entitiesByName = new Map( + packagesResponse.items.map(entity => [ + (entity.metadata?.name ?? '').toLowerCase(), + entity, + ]), + ); - // Process plugins to create single rows with readable names - const processedPlugins = processPluginsForDisplay(installedPlugins); + // Map into rows for installed packages only + const rows: InstalledPackageRow[] = installedEntityNames + .map(name => entitiesByName.get(name)) + .filter(Boolean) + .map(entity => ({ + displayName: + (entity!.metadata as any)?.title || + (entity!.metadata?.name as string), + packageName: (entity as any)!.spec?.packageName as string, + version: ((entity as any)!.spec?.version as string) ?? undefined, + })); - data = [...processedPlugins] + data = [...rows] .sort((a: Record, b: Record) => { const field = Array.isArray(orderBy.field) ? orderBy.field[0] @@ -159,8 +201,8 @@ export const InstalledPluginsTable = () => { orderMultiplier ); }) - .filter(plugin => - plugin.name + .filter(row => + row.displayName .toLowerCase() .trim() .includes(search.toLowerCase().trim()), @@ -191,7 +233,7 @@ export const InstalledPluginsTable = () => { return (
{ const [count, setCount] = useState(0); @@ -31,7 +31,7 @@ export const useInstalledPluginsCount = () => { setLoading(true); setError(undefined); const plugins = await dynamicPluginInfo.listLoadedPlugins(); - setCount(getUniquePluginsCount(plugins)); + setCount(plugins.length); } catch (err) { setError(err as Error); setCount(0); diff --git a/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx b/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx index 21924d87be..b4d5d18adf 100644 --- a/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx @@ -28,7 +28,7 @@ import Typography from '@mui/material/Typography'; import { themeId } from '../consts'; import { ReactQueryProvider } from '../components/ReactQueryProvider'; import { MarketplaceCatalogContent } from '../components/MarketplaceCatalogContent'; -import { InstalledPluginsTable } from '../components/InstalledPlugins/InstalledPluginsTable'; +import { InstalledPackagesTable } from '../components/InstalledPackages/InstalledPackagesTable'; import { useInstalledPluginsCount } from '../hooks/useInstalledPluginsCount'; import { MarketplaceCollectionPage } from './MarketplaceCollectionPage'; import { MarketplacePluginDrawer } from '../components/MarketplacePluginDrawer'; @@ -67,8 +67,8 @@ const MarketplacePage = () => { const { count: installedPluginsCount, loading } = useInstalledPluginsCount(); const installedPluginsTitle = loading - ? 'Installed Plugins' - : `Installed Plugins (${installedPluginsCount})`; + ? 'Installed packages' + : `Installed packages (${installedPluginsCount})`; return ( <> @@ -92,7 +92,7 @@ const MarketplacePage = () => { { }} > - + diff --git a/workspaces/marketplace/plugins/marketplace/src/plugin.ts b/workspaces/marketplace/plugins/marketplace/src/plugin.ts index 807ad4ff9e..283c972cc3 100644 --- a/workspaces/marketplace/plugins/marketplace/src/plugin.ts +++ b/workspaces/marketplace/plugins/marketplace/src/plugin.ts @@ -112,7 +112,7 @@ export const MarketplaceTabbedPageRouter = marketplacePlugin.provide( */ export const DynamicMarketplacePluginRouter = marketplacePlugin.provide( createRoutableExtension({ - name: 'MarketplaceRouter', + name: 'DynamicMarketplacePluginRouter', component: () => import('./pages/DynamicMarketplacePluginRouter').then( m => m.DynamicMarketplacePluginRouter, @@ -145,3 +145,16 @@ export const MarketplaceIcon: IconComponent = MUIMarketplaceIcon; * @public */ export const PluginsIcon: IconComponent = MUIPluginsIcon; + +/** + * @public + * @deprecated + */ +export const InstallationContextProvider = marketplacePlugin.provide( + createComponentExtension({ + name: 'InstallationContextProvider', + component: { + sync: () => null, + }, + }), +); diff --git a/workspaces/marketplace/plugins/marketplace/src/shared-components/SearchTextField.tsx b/workspaces/marketplace/plugins/marketplace/src/shared-components/SearchTextField.tsx index 161d0f2cf4..acc137a642 100644 --- a/workspaces/marketplace/plugins/marketplace/src/shared-components/SearchTextField.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/shared-components/SearchTextField.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import { useCallback, useEffect, useRef, useState } from 'react'; import FormControl from '@mui/material/FormControl'; import TextField from '@mui/material/TextField'; import InputAdornment from '@mui/material/InputAdornment'; @@ -32,6 +33,24 @@ export interface SearchTextFieldProps { export const SearchTextField = (props: SearchTextFieldProps) => { const fullTextSearch = useQueryFullTextSearch(); + const timerRef = useRef(undefined); + const [inputValue, setInputValue] = useState( + fullTextSearch.current || '', + ); + + // Keep local input in sync if URL param changes externally (e.g., back/forward) + useEffect(() => { + setInputValue(fullTextSearch.current || ''); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fullTextSearch]); + + useEffect(() => { + return () => { + if (timerRef.current) { + window.clearTimeout(timerRef.current); + } + }; + }, []); const options = props.variant === 'search' @@ -52,8 +71,20 @@ export const SearchTextField = (props: SearchTextFieldProps) => { variant="standard" hiddenLabel placeholder={options.placeholder} - value={fullTextSearch.current} - onChange={fullTextSearch.onChange} + value={inputValue} + onChange={useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setInputValue(value); + if (timerRef.current) { + window.clearTimeout(timerRef.current); + } + timerRef.current = window.setTimeout(() => { + fullTextSearch.onChange({ target: { value } } as any); + }, 300) as unknown as number; + }, + [fullTextSearch], + )} InputProps={{ startAdornment: ( @@ -66,7 +97,13 @@ export const SearchTextField = (props: SearchTextFieldProps) => { { + if (timerRef.current) { + window.clearTimeout(timerRef.current); + } + setInputValue(''); + fullTextSearch.clear(); + }} aria-label={options.clear} > From 69f3930693cacbe7b13dc69b7a177a3773505501 Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 19 Sep 2025 02:14:12 -0400 Subject: [PATCH 08/14] code improvement Signed-off-by: Yi Cai --- .../src/api/DynamicPluginsInfoClient.ts | 14 ++------------ .../marketplace/plugins/marketplace/src/plugin.ts | 4 +--- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace/src/api/DynamicPluginsInfoClient.ts b/workspaces/marketplace/plugins/marketplace/src/api/DynamicPluginsInfoClient.ts index b56c0e91b4..671e086c7a 100644 --- a/workspaces/marketplace/plugins/marketplace/src/api/DynamicPluginsInfoClient.ts +++ b/workspaces/marketplace/plugins/marketplace/src/api/DynamicPluginsInfoClient.ts @@ -14,17 +14,12 @@ * limitations under the License. */ -import { - DiscoveryApi, - FetchApi, - IdentityApi, -} from '@backstage/core-plugin-api'; +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; import { DynamicPluginInfo, DynamicPluginsInfoApi } from '.'; export interface DynamicPluginsInfoClientOptions { discoveryApi: DiscoveryApi; fetchApi: FetchApi; - identityApi: IdentityApi; } const loadedPluginsEndpoint = '/loaded-plugins'; @@ -32,20 +27,15 @@ 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 { 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 response = await this.fetchApi.fetch(targetUrl); const data = await response.json(); if (!response.ok) { const message = data.error?.message || data.message || data.toString?.(); diff --git a/workspaces/marketplace/plugins/marketplace/src/plugin.ts b/workspaces/marketplace/plugins/marketplace/src/plugin.ts index 283c972cc3..b058c658cc 100644 --- a/workspaces/marketplace/plugins/marketplace/src/plugin.ts +++ b/workspaces/marketplace/plugins/marketplace/src/plugin.ts @@ -64,13 +64,11 @@ export const marketplacePlugin = createPlugin({ deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef, - identityApi: identityApiRef, }, - factory: ({ discoveryApi, fetchApi, identityApi }) => + factory: ({ discoveryApi, fetchApi }) => new DynamicPluginsInfoClient({ discoveryApi, fetchApi, - identityApi, }), }), ], From 0660152cc6ee61d7167afbcecba3bcd2cd76b40d Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 19 Sep 2025 02:24:42 -0400 Subject: [PATCH 09/14] updated report.api.md Signed-off-by: Yi Cai --- workspaces/marketplace/plugins/marketplace/report.api.md | 3 +++ workspaces/marketplace/plugins/marketplace/src/plugin.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/workspaces/marketplace/plugins/marketplace/report.api.md b/workspaces/marketplace/plugins/marketplace/report.api.md index efd543683a..216cfee04e 100644 --- a/workspaces/marketplace/plugins/marketplace/report.api.md +++ b/workspaces/marketplace/plugins/marketplace/report.api.md @@ -17,6 +17,9 @@ export const DynamicMarketplacePluginContent: () => JSX_2.Element; // @public export const DynamicMarketplacePluginRouter: () => JSX_2.Element; +// @public @deprecated (undocumented) +export const InstallationContextProvider: () => null; + // @public export const MarketplaceFullPageRouter: () => JSX_2.Element; diff --git a/workspaces/marketplace/plugins/marketplace/src/plugin.ts b/workspaces/marketplace/plugins/marketplace/src/plugin.ts index b058c658cc..764e5ab859 100644 --- a/workspaces/marketplace/plugins/marketplace/src/plugin.ts +++ b/workspaces/marketplace/plugins/marketplace/src/plugin.ts @@ -146,7 +146,7 @@ export const PluginsIcon: IconComponent = MUIPluginsIcon; /** * @public - * @deprecated + * @deprecated Use the latest RHDH. This no-op export remains only for backward compatibility and will be removed in a future major release. */ export const InstallationContextProvider = marketplacePlugin.provide( createComponentExtension({ From 2aaaa38c4d13f28786a8e8f61e0960f245c77728 Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Fri, 19 Sep 2025 23:01:53 -0400 Subject: [PATCH 10/14] added link to name column Signed-off-by: Yi Cai --- .../InstalledPackagesTable.tsx | 71 ++++++++++++++++--- .../components/MarketplacePackageContent.tsx | 33 +++++---- .../components/MarketplacePackageDrawer.tsx | 20 +++--- .../MarketplacePackageInstallContent.tsx | 39 ++++------ .../marketplace/src/hooks/usePackage.ts | 5 +- .../pages/DynamicMarketplacePluginRouter.tsx | 21 +++++- .../pages/MarketplacePackageInstallPage.tsx | 6 +- 7 files changed, 133 insertions(+), 62 deletions(-) diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx index ff6bbb7cde..8ee0f1a9ae 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx @@ -22,12 +22,13 @@ import { TableColumn, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +import { Link } from '@backstage/core-components'; +import { useLocation } from 'react-router-dom'; import { Query, QueryResult } from '@material-table/core'; import { useQuery } from '@tanstack/react-query'; import { dynamicPluginsInfoApiRef } from '../../api'; -import { useInstalledPluginsCount } from '../../hooks/useInstalledPluginsCount'; import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; @@ -36,22 +37,27 @@ import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; import { useMarketplaceApi } from '../../hooks/useMarketplaceApi'; +import { getReadableName } from '../../utils/pluginProcessing'; import { useQueryFullTextSearch } from '../../hooks/useQueryFullTextSearch'; import { SearchTextField } from '../../shared-components/SearchTextField'; type InstalledPackageRow = { displayName: string; packageName: string; + role?: string; version?: string; hasEntity: boolean; + namespace?: string; + name?: string; }; export const InstalledPackagesTable = () => { const [error, setError] = useState(undefined); - const { count } = useInstalledPluginsCount(); + const [filteredCount, setFilteredCount] = useState(0); const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef); const marketplaceApi = useMarketplaceApi(); const fullTextSearch = useQueryFullTextSearch(); + const location = useLocation(); // Fetch once and cache const installedQuery = useQuery({ @@ -81,9 +87,22 @@ export const InstalledPackagesTable = () => { cellStyle: { textAlign: 'left', }, + render: (row: InstalledPackageRow) => { + if (row.hasEntity && row.namespace && row.name) { + const params = new URLSearchParams(location.search); + params.set('package', `${row.namespace}/${row.name}`); + const to = `${location.pathname}?${params.toString()}`; + return ( + e.stopPropagation()}> + {row.displayName} + + ); + } + return row.displayName; + }, }, { - title: 'Package', + title: 'npm package name', field: 'packageName', width: '54ch', align: 'left', @@ -94,6 +113,18 @@ export const InstalledPackagesTable = () => { textAlign: 'left', }, }, + { + title: 'Role', + field: 'role', + width: '24ch', + align: 'left', + headerStyle: { + textAlign: 'left', + }, + cellStyle: { + textAlign: 'left', + }, + }, { title: 'Version', field: 'version', @@ -197,7 +228,7 @@ export const InstalledPackagesTable = () => { sorting: false, }, ], - [], + [location.pathname, location.search], ); const fetchData = async ( query: Query, @@ -236,19 +267,30 @@ export const InstalledPackagesTable = () => { .replace(/^-+/, '') .toLowerCase(); const entity = entitiesByName.get(normalized) as any | undefined; + const rawName = entity + ? (entity.metadata?.title as string) || + (entity.metadata?.name as string) + : getReadableName(p.name); + const cleanedName = rawName.replace(/\s+(frontend|backend)$/i, ''); return { - displayName: entity - ? (entity.metadata?.title as string) || - (entity.metadata?.name as string) - : 'Missing catalog entity', + displayName: cleanedName, // Show the npm package name directly from dynamic-plugins-info record packageName: p.name, + // Humanized role from dynamic-plugins-info + role: (p as any).role + ? (() => { + const raw = ((p as any).role as string).replace(/-/g, ' '); + return raw.charAt(0).toUpperCase() + raw.slice(1).toLowerCase(); + })() + : undefined, // Prefer dynamic-plugins-info version, then fallback to entity spec.version version: (p.version as string | undefined) ?? (entity?.spec?.version as string | undefined) ?? undefined, hasEntity: !!entity, + namespace: entity?.metadata?.namespace ?? 'default', + name: entity?.metadata?.name, } as InstalledPackageRow; }); // Debug: rows and missing entity stats @@ -284,9 +326,16 @@ export const InstalledPackagesTable = () => { filteredRows.length, ); const totalCount = filteredRows.length; - const start = Math.max(0, page * pageSize); + setFilteredCount(totalCount); + const lastPage = Math.max(0, Math.ceil(totalCount / pageSize) - 1); + const effectivePage = Math.min(page, lastPage); + const start = Math.max(0, effectivePage * pageSize); const end = Math.min(totalCount, start + pageSize); - return { data: filteredRows.slice(start, end), page, totalCount }; + return { + data: filteredRows.slice(start, end), + page: effectivePage, + totalCount, + }; } catch (loadingError) { // eslint-disable-next-line no-console console.error('Failed to load plugins', loadingError); @@ -312,7 +361,7 @@ export const InstalledPackagesTable = () => {
{ const MarketplacePackageContent = ({ pkg }: { pkg: MarketplacePackage }) => { const getInstallPath = useRouteRef(packageInstallRouteRef); + const location = useLocation(); + const installBase = getInstallPath({ + namespace: pkg.metadata.namespace!, + name: pkg.metadata.name, + }); + const preservedParams = new URLSearchParams(location.search); + preservedParams.delete('package'); + const installTo = preservedParams.size + ? `${installBase}?${preservedParams.toString()}` + : installBase; return ( @@ -110,14 +122,7 @@ const MarketplacePackageContent = ({ pkg }: { pkg: MarketplacePackage }) => { - + { mapPackageInstallStatusToButton[ pkg.spec?.installStatus ?? @@ -157,7 +162,13 @@ const MarketplacePackageContent = ({ pkg }: { pkg: MarketplacePackage }) => { export const MarketplacePackageContentLoader = () => { const params = useRouteRefParams(packageInstallRouteRef); - const pkg = usePackage(params.namespace, params.name); + const [searchParams] = useSearchParams(); + const qp = searchParams.get('package'); + const qpNs = qp?.split('/')[0]; + const qpName = qp?.split('/')[1]; + const namespace = qpNs || params.namespace; + const name = qpName || params.name; + const pkg = usePackage(namespace, name); if (pkg.isLoading) { return ; @@ -167,8 +178,6 @@ export const MarketplacePackageContentLoader = () => { return ; } return ( - + ); }; diff --git a/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageDrawer.tsx b/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageDrawer.tsx index fc6bf9bc46..7ee3fc3363 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageDrawer.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageDrawer.tsx @@ -15,9 +15,8 @@ */ import { useState, useLayoutEffect } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams, useLocation } from 'react-router-dom'; -import { useRouteRef } from '@backstage/core-plugin-api'; import { ErrorBoundary } from '@backstage/core-components'; import Box from '@mui/material/Box'; @@ -26,25 +25,26 @@ import IconButton from '@mui/material/IconButton'; import CloseIcon from '@mui/icons-material/Close'; import { useTheme } from '@mui/material/styles'; -import { packagesRouteRef } from '../routes'; - import { MarketplacePackageContentLoader } from './MarketplacePackageContent'; export const MarketplacePackageDrawer = () => { const [open, setOpen] = useState(false); + const [searchParams] = useSearchParams(); + const location = useLocation(); useLayoutEffect(() => { - setOpen(true); - }, []); + setOpen(!!searchParams.get('package')); + }, [searchParams]); const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const packagesPath = `${useRouteRef(packagesRouteRef)()}${searchParams.size > 0 ? '?' : ''}${searchParams}`; const theme = useTheme(); const handleClose = () => { - // TODO analytics? setOpen(false); + const params = new URLSearchParams(location.search); + params.delete('package'); + const qs = params.toString(); + const target = qs ? `${location.pathname}?${qs}` : location.pathname; setTimeout( - () => navigate(packagesPath), + () => navigate(target), theme.transitions.duration.leavingScreen, ); }; diff --git a/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageInstallContent.tsx b/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageInstallContent.tsx index 35ad36ee2f..a7db3d79ab 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageInstallContent.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageInstallContent.tsx @@ -19,10 +19,9 @@ import type { SetStateAction } from 'react'; import { useCallback, useEffect, useState } from 'react'; import { ErrorPage, Progress } from '@backstage/core-components'; -import { useRouteRef, useRouteRefParams } from '@backstage/core-plugin-api'; +import { useRouteRefParams } from '@backstage/core-plugin-api'; -import yaml from 'yaml'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { MarketplacePackage, @@ -40,11 +39,7 @@ import Tab from '@mui/material/Tab'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; -import { - packageInstallRouteRef, - pluginInstallRouteRef, - pluginRouteRef, -} from '../routes'; +import { packageInstallRouteRef } from '../routes'; import { CodeEditorContextProvider, useCodeEditor } from './CodeEditor'; import { usePackage } from '../hooks/usePackage'; @@ -75,26 +70,16 @@ export const MarketplacePackageInstallContent = ({ : 'calc(100vh - 160px)'; const codeEditor = useCodeEditor(); - const params = useRouteRefParams(pluginInstallRouteRef); - - const pluginLink = useRouteRef(pluginRouteRef)({ - namespace: params.namespace, - name: params.name, - }); + const params = useRouteRefParams(packageInstallRouteRef); const onLoaded = useCallback(() => { - const dynamicPluginYaml = { - plugins: [ - { - package: pkg.spec?.dynamicArtifact ?? './dynamic-plugins/dist/....', - disabled: false, - }, - ], - }; - codeEditor.setValue(yaml.stringify(dynamicPluginYaml)); + const path = pkg.spec?.dynamicArtifact ?? './dynamic-plugins/dist/....'; + const yamlContent = `plugins:\n - package: ${JSON.stringify(path)}\n disabled: false\n`; + codeEditor.setValue(yamlContent); }, [codeEditor, pkg]); const navigate = useNavigate(); + const location = useLocation(); const examples = [ { [`${pkg.metadata.name}`]: pkg.spec?.appConfigExamples, @@ -242,7 +227,13 @@ export const MarketplacePackageInstallContent = ({ variant="outlined" color="primary" sx={{ ml: 2 }} - onClick={() => navigate(pluginLink)} + onClick={() => { + const ns = pkg.metadata.namespace ?? params.namespace; + const name = pkg.metadata.name; + const preserved = new URLSearchParams(location.search); + preserved.set('package', `${ns}/${name}`); + navigate(`/extensions/installed-packages?${preserved.toString()}`); + }} > Cancel diff --git a/workspaces/marketplace/plugins/marketplace/src/hooks/usePackage.ts b/workspaces/marketplace/plugins/marketplace/src/hooks/usePackage.ts index a936a99f8d..11929c9cb5 100644 --- a/workspaces/marketplace/plugins/marketplace/src/hooks/usePackage.ts +++ b/workspaces/marketplace/plugins/marketplace/src/hooks/usePackage.ts @@ -18,10 +18,11 @@ import { useQuery } from '@tanstack/react-query'; import { useMarketplaceApi } from './useMarketplaceApi'; -export const usePackage = (namespace: string, name: string) => { +export const usePackage = (namespace?: string, name?: string) => { const marketplaceApi = useMarketplaceApi(); return useQuery({ queryKey: ['marketplaceApi', 'getPackageByName', namespace, name], - queryFn: () => marketplaceApi.getPackageByName(namespace, name), + queryFn: () => marketplaceApi.getPackageByName(namespace!, name!), + enabled: Boolean(namespace && name), }); }; diff --git a/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx b/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx index b4d5d18adf..2f361096ba 100644 --- a/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/pages/DynamicMarketplacePluginRouter.tsx @@ -14,7 +14,13 @@ * limitations under the License. */ -import { Routes, Route } from 'react-router-dom'; +import { + Routes, + Route, + useParams, + useLocation, + Navigate, +} from 'react-router-dom'; import { Page, Header, @@ -63,6 +69,16 @@ const TabLabel = ({ ); +const PackageDeepLinkRedirect = () => { + const { namespace, name } = useParams(); + const location = useLocation(); + const params = new URLSearchParams(location.search); + if (namespace && name) { + params.set('package', `${namespace}/${name}`); + } + return ; +}; + const MarketplacePage = () => { const { count: installedPluginsCount, loading } = useInstalledPluginsCount(); @@ -104,6 +120,7 @@ const MarketplacePage = () => { > + @@ -116,7 +133,7 @@ const MarketplacePage = () => { /> diff --git a/workspaces/marketplace/plugins/marketplace/src/pages/MarketplacePackageInstallPage.tsx b/workspaces/marketplace/plugins/marketplace/src/pages/MarketplacePackageInstallPage.tsx index 33450d2f49..db8e6bbb7b 100644 --- a/workspaces/marketplace/plugins/marketplace/src/pages/MarketplacePackageInstallPage.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/pages/MarketplacePackageInstallPage.tsx @@ -15,6 +15,7 @@ */ import { useRouteRef, useRouteRefParams } from '@backstage/core-plugin-api'; +import { useLocation } from 'react-router-dom'; import { Page, Header, @@ -30,15 +31,18 @@ import { MarketplacePackageInstallContentLoader } from '../components/Marketplac const PackageInstallHeader = () => { const params = useRouteRefParams(packageInstallRouteRef); + const location = useLocation(); const pkg = usePackage(params.namespace, params.name); const displayName = pkg.data?.metadata.title ?? params.name; const title = `Install ${displayName}`; - const packageLink = useRouteRef(packageRouteRef)({ + const baseLink = useRouteRef(packageRouteRef)({ namespace: params.namespace, name: params.name, }); + const preserved = new URLSearchParams(location.search); + const packageLink = preserved.size ? `${baseLink}?${preserved}` : baseLink; return
; }; From c290b15eab6e99d71bff3ad6c92e12635b25e776 Mon Sep 17 00:00:00 2001 From: Yi Cai Date: Tue, 23 Sep 2025 20:12:17 -0400 Subject: [PATCH 11/14] addressed review comments Signed-off-by: Yi Cai --- .../InstalledPackagesTable.test.tsx | 194 +++++++++++++ .../InstalledPackagesTable.tsx | 27 +- .../InstalledPluginsTable.tsx | 258 ------------------ .../marketplace/src/components/Markdown.tsx | 1 - ....tsx => MarketplacePackageEditContent.tsx} | 93 ++++++- .../src/hooks/useInstallPackage.ts | 34 +++ .../hooks/useInstalledPackagesCount.test.tsx | 67 +++++ ...sCount.ts => useInstalledPackagesCount.ts} | 2 +- .../pages/DynamicMarketplacePluginRouter.tsx | 8 +- .../src/pages/MarketplaceFullPageRouter.tsx | 4 +- ...age.tsx => MarketplacePackageEditPage.tsx} | 14 +- .../pages/MarketplacePluginInstallPage.tsx | 2 +- .../src/pages/MarketplaceTabbedPageRouter.tsx | 4 +- 13 files changed, 396 insertions(+), 312 deletions(-) create mode 100644 workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.test.tsx delete mode 100644 workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx rename workspaces/marketplace/plugins/marketplace/src/components/{MarketplacePackageInstallContent.tsx => MarketplacePackageEditContent.tsx} (73%) create mode 100644 workspaces/marketplace/plugins/marketplace/src/hooks/useInstallPackage.ts create mode 100644 workspaces/marketplace/plugins/marketplace/src/hooks/useInstalledPackagesCount.test.tsx rename workspaces/marketplace/plugins/marketplace/src/hooks/{useInstalledPluginsCount.ts => useInstalledPackagesCount.ts} (96%) rename workspaces/marketplace/plugins/marketplace/src/pages/{MarketplacePackageInstallPage.tsx => MarketplacePackageEditPage.tsx} (81%) diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.test.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.test.tsx new file mode 100644 index 0000000000..946a9fff15 --- /dev/null +++ b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.test.tsx @@ -0,0 +1,194 @@ +/* + * 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 { BrowserRouter } from 'react-router-dom'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { mockApis, MockErrorApi, TestApiProvider } from '@backstage/test-utils'; +import { QueryClientProvider } from '@tanstack/react-query'; + +import { queryClient } from '../../queryclient'; +import { marketplaceApiRef } from '../../api'; +import { dynamicPluginsInfoApiRef } from '../../api'; +import { InstalledPackagesTable } from './InstalledPackagesTable'; +import { errorApiRef } from '@backstage/core-plugin-api'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; + +describe('InstalledPackagesTable', () => { + const renderWithProviders = (apis: any) => + render( + + + + + + + , + ); + + beforeEach(() => { + queryClient.clear(); + queryClient.setDefaultOptions({ queries: { retry: false } }); + }); + + it('renders rows for all dynamic-plugins-info entries and maps names when entity exists', async () => { + const dynamicPlugins = [ + { + name: '@scope/pkg-a-dynamic', + version: '1.0.0', + role: 'frontend-plugin', + platform: 'fe', + }, + { + name: 'pkg-b-dynamic', + version: '2.0.0', + role: 'backend-plugin', + platform: 'be', + }, + ]; + + const entities = { + items: [ + { + apiVersion: 'extensions.backstage.io/v1alpha1', + kind: 'Package', + metadata: { + namespace: 'rhdh', + name: 'scope-pkg-a', + title: 'Package A', + }, + spec: { packageName: '@scope/pkg-a', version: '1.0.0' }, + }, + ], + totalItems: 1, + pageInfo: {}, + }; + + const apis = [ + [ + dynamicPluginsInfoApiRef, + { listLoadedPlugins: jest.fn().mockResolvedValue(dynamicPlugins) }, + ], + [ + marketplaceApiRef, + { getPackages: jest.fn().mockResolvedValue(entities) }, + ], + ] as const; + + renderWithProviders(apis); + + await waitFor(() => + expect(screen.getByText('Installed packages (2)')).toBeInTheDocument(), + ); + + // Name column shows mapped entity title for first row + expect(screen.getByText('Package A')).toBeInTheDocument(); + // Second row has no entity -> uses readable fallback (should include raw package name minus suffixes) + expect(screen.getByText(/pkg-b/i)).toBeInTheDocument(); + + // npm package name column shows the dynamic plugin record name directly + expect(screen.getByText('@scope/pkg-a-dynamic')).toBeInTheDocument(); + expect(screen.getByText('pkg-b-dynamic')).toBeInTheDocument(); + + // Role and Version columns + expect( + screen.getByText('Frontend plugin'.replace('plugin', 'plugin')), + ).toBeInTheDocument(); + expect(screen.getByText('1.0.0')).toBeInTheDocument(); + expect(screen.getByText('2.0.0')).toBeInTheDocument(); + }); + + it('disables actions and shows tooltip when entity is missing', async () => { + const dynamicPlugins = [ + { + name: 'no-entity-dynamic', + version: '1.0.0', + role: 'frontend-plugin', + platform: 'fe', + }, + ]; + + const entities = { items: [], totalItems: 0, pageInfo: {} }; + + const apis = [ + [ + dynamicPluginsInfoApiRef, + { listLoadedPlugins: jest.fn().mockResolvedValue(dynamicPlugins) }, + ], + [ + marketplaceApiRef, + { getPackages: jest.fn().mockResolvedValue(entities) }, + ], + ] as const; + + renderWithProviders(apis); + + await waitFor(() => + expect(screen.getByText('Installed packages (1)')).toBeInTheDocument(), + ); + + const disabledButtons = screen.getAllByRole('button', { hidden: true }); + // There are three disabled action buttons rendered wrapped in span + expect(disabledButtons.length).toBeGreaterThanOrEqual(3); + }); + + it('filters by search query', async () => { + const dynamicPlugins = [ + { + name: 'alpha-dynamic', + version: '1.0.0', + role: 'frontend-plugin', + platform: 'fe', + }, + { + name: 'beta-dynamic', + version: '1.0.0', + role: 'backend-plugin', + platform: 'be', + }, + ]; + const entities = { items: [], totalItems: 0, pageInfo: {} }; + const apis = [ + [ + dynamicPluginsInfoApiRef, + { listLoadedPlugins: jest.fn().mockResolvedValue(dynamicPlugins) }, + ], + [ + marketplaceApiRef, + { getPackages: jest.fn().mockResolvedValue(entities) }, + ], + ] as const; + + renderWithProviders(apis); + + await waitFor(() => + expect(screen.getByText('Installed packages (2)')).toBeInTheDocument(), + ); + + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'alpha' } }); + + await waitFor(() => + expect(screen.getByText('Installed packages (1)')).toBeInTheDocument(), + ); + expect(screen.queryByText('beta')).not.toBeInTheDocument(); + }); +}); diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx index 8ee0f1a9ae..42db2eeb0b 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPackages/InstalledPackagesTable.tsx @@ -240,18 +240,6 @@ export const InstalledPackagesTable = () => { const installed = installedQuery.data ?? []; const packagesResponse = packagesQuery.data ?? { items: [] as any[] }; - // Debug: sizes - // eslint-disable-next-line no-console - console.log( - '[InstalledPackagesTable] dynamic-plugins-info count', - installed.length, - ); - // eslint-disable-next-line no-console - console.log( - '[InstalledPackagesTable] catalog packages count', - packagesResponse.items?.length ?? 0, - ); - const entitiesByName = new Map( (packagesResponse.items ?? []).map(entity => [ (entity.metadata?.name ?? '').toLowerCase(), @@ -293,14 +281,6 @@ export const InstalledPackagesTable = () => { name: entity?.metadata?.name, } as InstalledPackageRow; }); - // Debug: rows and missing entity stats - // eslint-disable-next-line no-console - console.log('[InstalledPackagesTable] rows count', rows.length); - // eslint-disable-next-line no-console - console.log( - '[InstalledPackagesTable] missing entity rows', - rows.filter(r => r.displayName === 'Missing catalog entity').length, - ); const sortField = ((query as any)?.orderBy?.field as string) || 'displayName'; @@ -319,12 +299,7 @@ export const InstalledPackagesTable = () => { const cmp = aVal.localeCompare(bVal); return sortDirection === 'desc' ? -cmp : cmp; }); - // Debug: filtered rows count - // eslint-disable-next-line no-console - console.log( - '[InstalledPackagesTable] filtered rows count', - filteredRows.length, - ); + const totalCount = filteredRows.length; setFilteredCount(totalCount); const lastPage = Math.max(0, Math.ceil(totalCount / pageSize) - 1); diff --git a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx b/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx deleted file mode 100644 index 5299cfb072..0000000000 --- a/workspaces/marketplace/plugins/marketplace/src/components/InstalledPlugins/InstalledPluginsTable.tsx +++ /dev/null @@ -1,258 +0,0 @@ -/* - * 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 { 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 { dynamicPluginsInfoApiRef } from '../../api'; -import { useInstalledPluginsCount } from '../../hooks/useInstalledPluginsCount'; -import EditIcon from '@mui/icons-material/Edit'; -import DeleteIcon from '@mui/icons-material/Delete'; -import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; -import Box from '@mui/material/Box'; -import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; - -import { useMarketplaceApi } from '../../hooks/useMarketplaceApi'; - -type InstalledPackageRow = { - displayName: string; - packageName: string; - version?: string; -}; - -export const InstalledPluginsTable = () => { - const [error, setError] = useState(undefined); - const [currentSearchTerm, setCurrentSearchTerm] = useState(''); - const { count } = useInstalledPluginsCount(); - const dynamicPluginInfo = useApi(dynamicPluginsInfoApiRef); - const marketplaceApi = useMarketplaceApi(); - let data: InstalledPackageRow[] = []; - const columns: TableColumn[] = [ - { - title: 'Name', - field: 'displayName', - align: 'left', - width: '30ch', - defaultSort: 'asc', - headerStyle: { - textAlign: 'left', - }, - cellStyle: { - textAlign: 'left', - }, - }, - { - title: 'Package', - field: 'packageName', - width: '54ch', - align: 'left', - headerStyle: { - textAlign: 'left', - }, - cellStyle: { - textAlign: 'left', - }, - }, - { - title: 'Version', - field: 'version', - width: '24ch', - align: 'left', - headerStyle: { - textAlign: 'left', - }, - cellStyle: { - textAlign: 'left', - }, - }, - { - title: 'Actions', - align: 'right', - width: '78px', - headerStyle: { - textAlign: 'left', - }, - cellStyle: { - textAlign: 'left', - }, - render: (_rowData: InstalledPackageRow) => { - return ( - - - { - // TODO: Implement edit functionality - }} - > - - - - - { - // TODO: Implement delete functionality - }} - > - - - - - { - // TODO: Implement download functionality - }} - > - - - - - ); - }, - sorting: false, - }, - ]; - const fetchData = async ( - query: Query, - ): Promise> => { - const { - orderBy = { field: 'displayName' }, - orderDirection = 'asc', - page = 0, - pageSize = 5, - search = '', - } = query || {}; - - // Track current search term for conditional empty message - setCurrentSearchTerm(search); - - try { - // for now sorting/searching/pagination is handled client-side - const installed = await dynamicPluginInfo.listLoadedPlugins(); - - // Normalize installed names to entity names: replace @ and / with - - const installedEntityNames = Array.from( - new Set(installed.map(p => p.name.replace(/[@/]/g, '-').toLowerCase())), - ); - - // Fetch marketplace package entities - const packagesResponse = await marketplaceApi.getPackages({}); - const entitiesByName = new Map( - packagesResponse.items.map(entity => [ - (entity.metadata?.name ?? '').toLowerCase(), - entity, - ]), - ); - - // Map into rows for installed packages only - const rows: InstalledPackageRow[] = installedEntityNames - .map(name => entitiesByName.get(name)) - .filter(Boolean) - .map(entity => ({ - displayName: - (entity!.metadata as any)?.title || - (entity!.metadata?.name as string), - packageName: (entity as any)!.spec?.packageName as string, - version: ((entity as any)!.spec?.version as string) ?? undefined, - })); - - data = [...rows] - .sort((a: Record, b: Record) => { - const field = Array.isArray(orderBy.field) - ? orderBy.field[0] - : orderBy.field; - const orderMultiplier = orderDirection === 'desc' ? -1 : 1; - - if (!field || 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(row => - row.displayName - .toLowerCase() - .trim() - .includes(search.toLowerCase().trim()), - ); - const totalCount = data.length; - let start = 0; - let end = totalCount; - if (totalCount > pageSize) { - start = page * pageSize; - end = start + pageSize; - } - return { data: data.slice(start, end), page, totalCount }; - } catch (loadingError) { - // eslint-disable-next-line no-console - console.error('Failed to load plugins', loadingError); - setError(loadingError as Error); - return { data: [], totalCount: 0, page: 0 }; - } - }; - if (error) { - return ; - } - - // Conditional empty message based on search state - const emptyMessage = currentSearchTerm.trim() - ? 'No results found. Try a different search term.' - : 'No records to display'; - - return ( -
- ); -}; diff --git a/workspaces/marketplace/plugins/marketplace/src/components/Markdown.tsx b/workspaces/marketplace/plugins/marketplace/src/components/Markdown.tsx index e4ffb76b6a..c2fc9aa038 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/Markdown.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/Markdown.tsx @@ -56,7 +56,6 @@ export const Markdown = (props: MarkdownProps) => { // TODO load images from marketplace assets endpoint ??? const transformImageUri = (href: string): string => { - // console.log('Markdown transformImageUri href', href); return href; }; diff --git a/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageInstallContent.tsx b/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageEditContent.tsx similarity index 73% rename from workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageInstallContent.tsx rename to workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageEditContent.tsx index a7db3d79ab..d5957c2cb2 100644 --- a/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageInstallContent.tsx +++ b/workspaces/marketplace/plugins/marketplace/src/components/MarketplacePackageEditContent.tsx @@ -38,10 +38,13 @@ import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; +import Alert from '@mui/material/Alert'; +import CircularProgress from '@mui/material/CircularProgress'; import { packageInstallRouteRef } from '../routes'; import { CodeEditorContextProvider, useCodeEditor } from './CodeEditor'; +import { useInstallPackage } from '../hooks/useInstallPackage'; import { usePackage } from '../hooks/usePackage'; import { CodeEditorCard } from './CodeEditorCard'; import { TabPanel } from './TabPanel'; @@ -53,12 +56,15 @@ interface TabItem { others?: { [key: string]: any }; } -export const MarketplacePackageInstallContent = ({ +export const MarketplacePackageEditContent = ({ pkg, }: { pkg: MarketplacePackage; }) => { + const { mutateAsync: installPackage } = useInstallPackage(); const [hasGlobalHeader, setHasGlobalHeader] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [saveError, setSaveError] = useState(null); useEffect(() => { const header = document.querySelector('nav#global-header'); @@ -104,6 +110,61 @@ export const MarketplacePackageInstallContent = ({ setTabIndex(newValue); }; + const handleSave = async () => { + try { + setSaveError(null); + setIsSubmitting(true); + const raw = codeEditor.getValue() ?? ''; + const lines = raw.split('\n'); + const idx = lines.findIndex(l => /^\s*plugins\s*:/i.test(l)); + if (idx === -1) { + setIsSubmitting(false); + return; + } + const bodyLines = lines.slice(idx + 1); + // Detect common indentation among non-empty lines + const nonEmpty = bodyLines.filter(l => l.trim().length > 0); + const commonIndent = nonEmpty.reduce( + (acc, l) => { + const m = l.match(/^(\s*)/); + const indent = m ? m[1].length : 0; + return acc === null ? indent : Math.min(acc, indent); + }, + null as number | null, + ); + const pluginsYamlString = + (commonIndent ?? 0) > 0 + ? bodyLines + .map(l => + l.startsWith(' '.repeat(commonIndent!)) + ? l.slice(commonIndent!) + : l, + ) + .join('\n') + : bodyLines.join('\n'); + + const res = await installPackage({ + namespace: pkg.metadata.namespace ?? params.namespace, + name: pkg.metadata.name, + configYaml: pluginsYamlString.trim(), + }); + + if ((res as any)?.status === 'OK') { + const ns = pkg.metadata.namespace ?? params.namespace; + const name = pkg.metadata.name; + const preserved = new URLSearchParams(location.search); + preserved.set('package', `${ns}/${name}`); + navigate(`/extensions/installed-packages?${preserved.toString()}`); + } else { + setSaveError((res as any)?.error?.message ?? 'Failed to save'); + setIsSubmitting(false); + } + } catch (e: any) { + setSaveError(e?.error?.message ?? 'Failed to save'); + setIsSubmitting(false); + } + }; + return ( - Installation instructions - - } + title={Edit instructions} action={ + {saveError && ( + + {saveError} + + )} + -