From e944a1acbd0c1a6143837bb69b72d6cee8490402 Mon Sep 17 00:00:00 2001 From: Debsmita Santra Date: Tue, 15 Sep 2026 11:17:13 +0530 Subject: [PATCH 1/6] chore(quickstart): defer NFS UI to async chunks and slim legacy exports --- .../quickstart-nfs-sync-optimization.md | 9 + .../e2e-tests/quick-start-admin-guest.spec.ts | 18 +- .../e2e-tests/quick-start-developer.spec.ts | 8 +- .../quickstart/e2e-tests/utils/helper.ts | 10 +- .../e2e-tests/utils/translations.ts | 7 + .../packages/app-legacy/package.json | 2 +- .../quickstart/packages/app/package.json | 2 +- .../quickstart/plugins/quickstart/README.md | 4 +- .../plugins/quickstart/package.json | 5 +- .../plugins/quickstart/report-legacy.api.md | 78 ++++++- .../src/QuickstartHelpMenuItem.test.tsx | 14 -- .../quickstart/src/QuickstartHelpMenuItem.tsx | 65 +++++- .../plugins/quickstart/src/QuickstartInit.tsx | 4 +- .../src/components/Quickstart.test.tsx | 8 +- .../QuickstartContent/QuickstartCtaLink.tsx | 66 +++--- .../QuickstartContent/QuickstartIcon.test.tsx | 126 +++++++++++ .../QuickstartContent/QuickstartIcon.tsx | 107 +++++++++ .../QuickstartContent/QuickstartItem.test.tsx | 6 +- .../QuickstartContent/QuickstartItem.tsx | 39 ++-- .../QuickstartItemIcon.test.tsx | 91 +++++--- .../QuickstartContent/QuickstartItemIcon.tsx | 79 +++---- .../QuickstartContent/quickstartIconIds.ts | 48 ++++ .../src/icons/ShapesOutlinedIcon.tsx | 24 ++ .../plugins/quickstart/src/index.tsx | 18 +- .../quickstart/src/lazyQuickstartUi.tsx | 47 ++++ .../plugins/quickstart/src/legacyExports.ts | 10 +- .../quickstart/src/nfsExports.test.tsx | 7 + workspaces/quickstart/yarn.lock | 205 +----------------- 28 files changed, 697 insertions(+), 410 deletions(-) create mode 100644 workspaces/quickstart/.changeset/quickstart-nfs-sync-optimization.md create mode 100644 workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.test.tsx create mode 100644 workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx create mode 100644 workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/quickstartIconIds.ts create mode 100644 workspaces/quickstart/plugins/quickstart/src/icons/ShapesOutlinedIcon.tsx create mode 100644 workspaces/quickstart/plugins/quickstart/src/lazyQuickstartUi.tsx diff --git a/workspaces/quickstart/.changeset/quickstart-nfs-sync-optimization.md b/workspaces/quickstart/.changeset/quickstart-nfs-sync-optimization.md new file mode 100644 index 00000000000..4fd166696c9 --- /dev/null +++ b/workspaces/quickstart/.changeset/quickstart-nfs-sync-optimization.md @@ -0,0 +1,9 @@ +--- +'@red-hat-developer-hub/backstage-plugin-quickstart': minor +--- + +Defer Quickstart NFS drawer content, init snackbar, and help menu item to async chunks so the Module Federation sync graph stays thin. The help menu uses a blueprint `loader` and a local MUI `MenuItem` instead of `global-header/components` to avoid pulling `@backstage/core-components` into the quickstart MF graph. + +Bumps `@red-hat-developer-hub/backstage-plugin-global-header` to ^3.0.0 for the loader blueprint API. + +Adds public legacy exports `quickstartTranslationRef` and `quickstartTranslations`. Legacy `QuickstartButton` and `QuickstartDrawerProvider` exported types widen from concrete function signatures to `React.ComponentType` because they are now re-exported from lazy component extensions rather than concrete component modules. diff --git a/workspaces/quickstart/e2e-tests/quick-start-admin-guest.spec.ts b/workspaces/quickstart/e2e-tests/quick-start-admin-guest.spec.ts index 93dfbbe9c99..bc432ce5a9a 100644 --- a/workspaces/quickstart/e2e-tests/quick-start-admin-guest.spec.ts +++ b/workspaces/quickstart/e2e-tests/quick-start-admin-guest.spec.ts @@ -15,7 +15,11 @@ */ import { expect, test, TestInfo } from '@playwright/test'; import { UIhelper, switchToLocale } from './utils/helper'; -import { getTranslations, QuickstartMessages } from './utils/translations'; +import { + getProgressPattern, + getTranslations, + QuickstartMessages, +} from './utils/translations'; import { runAccessibilityTests } from './utils/accessibility'; test.describe('Test Quick Start plugin', () => { @@ -103,7 +107,7 @@ test.describe('Test Quick Start plugin', () => { .locator('..'); // List // Find the CTA button within this specific List container to avoid matching the setupAuthentication button const configureGitCta = parentList - .getByRole('button', { name: translations.steps.configureGit.ctaTitle }) + .getByRole('link', { name: translations.steps.configureGit.ctaTitle }) .first(); await configureGitCta.waitFor({ state: 'visible' }); const href = await configureGitCta.getAttribute('href'); @@ -133,7 +137,7 @@ test.describe('Test Quick Start plugin', () => { .locator('..') .locator('..'); const setupLightspeedCta = setupLightspeedParentList - .getByRole('button', { + .getByRole('link', { name: translations.steps.setupLightspeed.ctaTitle, }) .first(); @@ -147,13 +151,11 @@ test.describe('Test Quick Start plugin', () => { translations.steps.managePlugins.ctaTitle, '/extensions', ); - await uiHelper.clickButtonByText(translations.steps.managePlugins.ctaTitle); + await uiHelper.clickLinkByText(translations.steps.managePlugins.ctaTitle); await expect(page).toHaveURL(/extensions/); - const progressPattern = new RegExp( - translations.footer.progress.replace('{{progress}}', '\\d+'), - ); - await uiHelper.verifyText(progressPattern); + await expect(page.getByRole('progressbar')).toBeVisible(); + await uiHelper.verifyText(getProgressPattern(translations.footer.progress)); await uiHelper.clickButtonByText(translations.footer.hide); await expect( page.getByRole('button', { name: translations.footer.hide }), diff --git a/workspaces/quickstart/e2e-tests/quick-start-developer.spec.ts b/workspaces/quickstart/e2e-tests/quick-start-developer.spec.ts index bea1d1c88f6..2ff017e98d4 100644 --- a/workspaces/quickstart/e2e-tests/quick-start-developer.spec.ts +++ b/workspaces/quickstart/e2e-tests/quick-start-developer.spec.ts @@ -120,7 +120,7 @@ test.describe('Test Quick Start plugin', () => { translations.steps.importApplication.ctaTitle, '/bulk-import', ); - await uiHelper.clickButtonByText( + await uiHelper.clickLinkByText( translations.steps.importApplication.ctaTitle, { exact: true, @@ -131,7 +131,7 @@ test.describe('Test Quick Start plugin', () => { translations.steps.learnAboutCatalog.ctaTitle, '/catalog', ); - await uiHelper.clickButtonByText( + await uiHelper.clickLinkByText( translations.steps.learnAboutCatalog.ctaTitle, ); await uiHelper.verifyHeading('My Company Catalog'); @@ -142,7 +142,7 @@ test.describe('Test Quick Start plugin', () => { translations.steps.exploreSelfServiceTemplates.ctaTitle, '/create', ); - await uiHelper.clickButtonByText( + await uiHelper.clickLinkByText( translations.steps.exploreSelfServiceTemplates.ctaTitle, ); await expect(page).toHaveURL(/\/create/); @@ -151,7 +151,7 @@ test.describe('Test Quick Start plugin', () => { translations.steps.findAllLearningPaths.ctaTitle, '/learning-paths', ); - await uiHelper.clickButtonByText( + await uiHelper.clickLinkByText( translations.steps.findAllLearningPaths.ctaTitle, ); const progress100Pattern = new RegExp( diff --git a/workspaces/quickstart/e2e-tests/utils/helper.ts b/workspaces/quickstart/e2e-tests/utils/helper.ts index ff6308a73a5..36cc1f97d72 100644 --- a/workspaces/quickstart/e2e-tests/utils/helper.ts +++ b/workspaces/quickstart/e2e-tests/utils/helper.ts @@ -51,8 +51,16 @@ export class UIhelper { .first() .click(); } + async clickLinkByText( + text: string | RegExp, + options: { exact: boolean } = { exact: false }, + ) { + await this.page + .getByRole('link', { name: text, exact: options?.exact }) + .first() + .click(); + } async verifyButtonURL(label: string | RegExp, url: string) { - // Use locator('a') to find the actual anchor tag, not getByRole which may see button role const anchor = this.page.locator('a').filter({ hasText: label }); expect(await anchor.first().getAttribute('href')).toContain(url); } diff --git a/workspaces/quickstart/e2e-tests/utils/translations.ts b/workspaces/quickstart/e2e-tests/utils/translations.ts index 0acb071107b..ccfb448fd24 100644 --- a/workspaces/quickstart/e2e-tests/utils/translations.ts +++ b/workspaces/quickstart/e2e-tests/utils/translations.ts @@ -42,6 +42,13 @@ function transformFlatMessagesIntoTree( return messages as QuickstartMessages; } +/** Matches localized footer progress text, including decimal percentages. */ +export function getProgressPattern(footerProgressTemplate: string): RegExp { + return new RegExp( + footerProgressTemplate.replace('{{progress}}', '\\d+(?:\\.\\d+)?'), + ); +} + export function getTranslations(locale: string) { const languageCode = locale.split('-')[0].toLowerCase(); diff --git a/workspaces/quickstart/packages/app-legacy/package.json b/workspaces/quickstart/packages/app-legacy/package.json index 9deb1d871a6..ccb3933d673 100644 --- a/workspaces/quickstart/packages/app-legacy/package.json +++ b/workspaces/quickstart/packages/app-legacy/package.json @@ -48,7 +48,7 @@ "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", "@mui/styles": "5.18.0", - "@red-hat-developer-hub/backstage-plugin-global-header": "^2.0.1", + "@red-hat-developer-hub/backstage-plugin-global-header": "^3.0.0", "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^", "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11", "react": "^18.0.2", diff --git a/workspaces/quickstart/packages/app/package.json b/workspaces/quickstart/packages/app/package.json index 43cc999ea44..126372f2e9c 100644 --- a/workspaces/quickstart/packages/app/package.json +++ b/workspaces/quickstart/packages/app/package.json @@ -44,7 +44,7 @@ "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", "@red-hat-developer-hub/backstage-plugin-app-react": "^1.0.0", - "@red-hat-developer-hub/backstage-plugin-global-header": "^2.0.1", + "@red-hat-developer-hub/backstage-plugin-global-header": "^3.0.0", "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^", "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11", "material-icons": "^1.13.14", diff --git a/workspaces/quickstart/plugins/quickstart/README.md b/workspaces/quickstart/plugins/quickstart/README.md index 6ad1aa2ec2b..1d521557716 100644 --- a/workspaces/quickstart/plugins/quickstart/README.md +++ b/workspaces/quickstart/plugins/quickstart/README.md @@ -129,10 +129,10 @@ Each quickstart item supports the following properties: - `title` (required): The display title for the quickstart step - `description` (required): A brief description of what the step covers -- `icon` (optional): Icon identifier (supports Material UI icons) +- `icon` (optional): Step icon. Supported values: built-in ids (`Admin`, `Rbac`, `Git`, `Plugins`, `Import`, `Catalog`, `SelfService`, `Learning`, `Lightspeed`), registered system icon ids, Material Icons ligature names (e.g. `home`, `school`), an image URL, or an inline SVG string - `roles` (optional): Array of user roles that should see this quickstart item. Supported values: `['admin', 'developer']`. If not specified, defaults to `['admin']` - `cta` (optional): Call-to-action object with: - - `text`: Button text + - `text`: CTA label text - `link`: Target URL or route ## Role-Based Access Control (RBAC) diff --git a/workspaces/quickstart/plugins/quickstart/package.json b/workspaces/quickstart/plugins/quickstart/package.json index b2d8c0870cc..c9da219469c 100644 --- a/workspaces/quickstart/plugins/quickstart/package.json +++ b/workspaces/quickstart/plugins/quickstart/package.json @@ -42,11 +42,12 @@ "@backstage/plugin-app-react": "^0.2.6", "@backstage/plugin-permission-react": "^0.5.4", "@backstage/theme": "^0.7.3", + "@backstage/ui": "^0.17.1", "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", "@patternfly/react-icons": "^6.5.0", "@red-hat-developer-hub/backstage-plugin-app-react": "^1.0.0", - "@red-hat-developer-hub/backstage-plugin-global-header": "^2.0.1", + "@red-hat-developer-hub/backstage-plugin-global-header": "^3.0.0", "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11", "react-use": "^17.6.0" }, @@ -57,7 +58,6 @@ }, "devDependencies": { "@backstage/cli": "^0.36.5", - "@backstage/core-app-api": "^1.20.4", "@backstage/dev-utils": "^1.1.26", "@backstage/frontend-defaults": "^0.5.5", "@backstage/plugin-permission-common": "^0.9.10", @@ -65,7 +65,6 @@ "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.0.0", - "msw": "^1.0.0", "react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-dom": "16.13.1 || ^17.0.0 || ^18.0.0", "react-router-dom": "^6.0.0" diff --git a/workspaces/quickstart/plugins/quickstart/report-legacy.api.md b/workspaces/quickstart/plugins/quickstart/report-legacy.api.md index 798265cd267..584e1084fb6 100644 --- a/workspaces/quickstart/plugins/quickstart/report-legacy.api.md +++ b/workspaces/quickstart/plugins/quickstart/report-legacy.api.md @@ -7,6 +7,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CSSProperties } from 'react'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { PropsWithChildren } from 'react'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; +import { TranslationResource } from '@backstage/frontend-plugin-api'; // @public export type DrawerPartialState = { @@ -29,9 +31,7 @@ export const filterQuickstartItemsByRole: ( ) => QuickstartItemData[]; // @public -export const QuickstartButton: ( - input: QuickstartButtonProps, -) => JSX_2.Element | null; +export const QuickstartButton: React.ComponentType; // @public export interface QuickstartButtonProps { @@ -56,9 +56,7 @@ export interface QuickstartDrawerContextType { } // @public -export const QuickstartDrawerProvider: ( - input: PropsWithChildren, -) => JSX_2.Element; +export const QuickstartDrawerProvider: React.ComponentType; // @public export const QuickstartDrawerStateExposer: ( @@ -86,6 +84,74 @@ export interface QuickstartItemData { // @public export const quickstartPlugin: BackstagePlugin<{}, {}, {}>; +// @public +export const quickstartTranslationRef: TranslationRef< + 'plugin.quickstart', + { + readonly 'header.title': string; + readonly 'header.subtitle': string; + readonly 'steps.setupAuthentication.title': string; + readonly 'steps.setupAuthentication.description': string; + readonly 'steps.setupAuthentication.ctaTitle': string; + readonly 'steps.configureRbac.title': string; + readonly 'steps.configureRbac.description': string; + readonly 'steps.configureRbac.ctaTitle': string; + readonly 'steps.configureGit.title': string; + readonly 'steps.configureGit.description': string; + readonly 'steps.configureGit.ctaTitle': string; + readonly 'steps.managePlugins.title': string; + readonly 'steps.managePlugins.description': string; + readonly 'steps.managePlugins.ctaTitle': string; + readonly 'steps.importApplication.title': string; + readonly 'steps.importApplication.description': string; + readonly 'steps.importApplication.ctaTitle': string; + readonly 'steps.learnAboutCatalog.title': string; + readonly 'steps.learnAboutCatalog.description': string; + readonly 'steps.learnAboutCatalog.ctaTitle': string; + readonly 'steps.exploreSelfServiceTemplates.title': string; + readonly 'steps.exploreSelfServiceTemplates.description': string; + readonly 'steps.exploreSelfServiceTemplates.ctaTitle': string; + readonly 'steps.findAllLearningPaths.title': string; + readonly 'steps.findAllLearningPaths.description': string; + readonly 'steps.findAllLearningPaths.ctaTitle': string; + readonly 'steps.setupLightspeed.title': string; + readonly 'steps.setupLightspeed.description': string; + readonly 'steps.setupLightspeed.ctaTitle': string; + readonly 'steps.getStartedWithLightspeed.title': string; + readonly 'steps.getStartedWithLightspeed.description': string; + readonly 'steps.getStartedWithLightspeed.ctaTitle': string; + readonly 'button.quickstart': string; + readonly 'button.openQuickstartGuide': string; + readonly 'button.closeDrawer': string; + readonly 'button.gotIt': string; + readonly 'snackbar.helpPrompt': string; + readonly 'footer.progress': string; + readonly 'footer.notStarted': string; + readonly 'footer.hide': string; + readonly 'content.loading': string; + readonly 'content.emptyState.title': string; + readonly 'item.expandAriaLabel': string; + readonly 'item.collapseAriaLabel': string; + readonly 'item.expandButtonAriaLabel': string; + readonly 'item.collapseButtonAriaLabel': string; + readonly 'dev.pageTitle': string; + readonly 'dev.pageDescription': string; + readonly 'dev.drawerControls': string; + readonly 'dev.currentState': string; + readonly 'dev.stateOpen': string; + readonly 'dev.stateClosed': string; + readonly 'dev.instructions': string; + readonly 'dev.step1': string; + readonly 'dev.step2': string; + readonly 'dev.step3': string; + readonly 'dev.step4': string; + readonly 'dev.step5': string; + } +>; + +// @public +export const quickstartTranslations: TranslationResource<'plugin.quickstart'>; + // @public export const useQuickstartDrawerContext: () => QuickstartDrawerContextType; diff --git a/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.test.tsx b/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.test.tsx index 154d4575060..ff6fbe956c3 100644 --- a/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.test.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.test.tsx @@ -32,20 +32,6 @@ jest.mock('@red-hat-developer-hub/backstage-plugin-app-react', () => ({ }), })); -jest.mock('@red-hat-developer-hub/backstage-plugin-global-header', () => ({ - GlobalHeaderMenuItem: ({ - title, - onClick, - }: { - title?: string; - onClick?: () => void; - }) => ( - - ), -})); - describe('QuickstartHelpMenuItem', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.tsx b/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.tsx index 4c6e114cd30..26a52dba9b9 100644 --- a/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.tsx @@ -15,22 +15,29 @@ */ import { useAppDrawer } from '@red-hat-developer-hub/backstage-plugin-app-react'; -import { GlobalHeaderMenuItem } from '@red-hat-developer-hub/backstage-plugin-global-header'; +import Box from '@mui/material/Box'; +import MenuItem from '@mui/material/MenuItem'; +import Typography from '@mui/material/Typography'; +import { useTheme } from '@mui/material/styles'; +import { QuickstartIcon } from './components/QuickstartContent/QuickstartIcon'; import { QUICKSTART_DRAWER_ID } from './const'; /** * Help-dropdown menu item that toggles the Quick start drawer. * - * Relies on `GlobalHeaderMenuItem` from global-header >= 1.21.1, which no - * longer uses `component={Fragment}` when `to` is absent (that dropped - * onClick / menuitem role in 1.21.0). + * Uses a local MUI `MenuItem` styled like global-header dropdown entries so + * this module does not bundle `global-header/components` (and its + * `@backstage/core-components` transitive deps) into the quickstart MF graph. + * The help menu blueprint `loader` still defers fetching until the dropdown + * opens. */ export const QuickstartHelpMenuItem = ({ handleClose, }: { handleClose?: () => void; }) => { + const theme = useTheme(); const { toggleDrawer } = useAppDrawer(); const handleClick = () => { @@ -39,10 +46,52 @@ export const QuickstartHelpMenuItem = ({ }; return ( - + sx={{ + py: 0.5, + px: 0, + width: '100%', + boxSizing: 'border-box', + display: 'flex', + color: 'inherit', + textDecoration: 'none', + }} + > + + + + + Quick start + + + + ); }; diff --git a/workspaces/quickstart/plugins/quickstart/src/QuickstartInit.tsx b/workspaces/quickstart/plugins/quickstart/src/QuickstartInit.tsx index 91e5675c6c3..595b506583e 100644 --- a/workspaces/quickstart/plugins/quickstart/src/QuickstartInit.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/QuickstartInit.tsx @@ -22,9 +22,9 @@ import { } from '@backstage/core-plugin-api'; import { useAppDrawer } from '@red-hat-developer-hub/backstage-plugin-app-react'; import Snackbar from '@mui/material/Snackbar'; -import CloseIcon from '@mui/icons-material/Close'; import IconButton from '@mui/material/IconButton'; +import { QuickstartIcon } from './components/QuickstartContent/QuickstartIcon'; import { useQuickstartRole } from './hooks/useQuickstartRole'; import { useTranslation } from './hooks/useTranslation'; import { QuickstartItemData } from './types'; @@ -150,7 +150,7 @@ export const QuickstartInit = () => { color="inherit" onClick={handleNotificationClose} > - + } /> diff --git a/workspaces/quickstart/plugins/quickstart/src/components/Quickstart.test.tsx b/workspaces/quickstart/plugins/quickstart/src/components/Quickstart.test.tsx index 190c39508eb..82b37d7933d 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/Quickstart.test.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/Quickstart.test.tsx @@ -107,7 +107,7 @@ describe('Quickstart', () => { }); fireEvent.click(expandButtons[0]); - const cta = await screen.findByRole('button', { name: 'Start Now' }); + const cta = await screen.findByRole('link', { name: 'Start Now' }); fireEvent.click(cta); await waitFor(() => { @@ -116,7 +116,7 @@ describe('Quickstart', () => { // Complete second admin item fireEvent.click(expandButtons[1]); - const cta2 = await screen.findByRole('button', { name: 'Continue' }); + const cta2 = await screen.findByRole('link', { name: 'Continue' }); fireEvent.click(cta2); await waitFor(() => { @@ -135,7 +135,7 @@ describe('Quickstart', () => { }); fireEvent.click(expandButtons[0]); - const cta = await screen.findByRole('button', { name: 'Start Now' }); + const cta = await screen.findByRole('link', { name: 'Start Now' }); fireEvent.click(cta); await waitFor(() => { @@ -144,7 +144,7 @@ describe('Quickstart', () => { // Complete second developer item fireEvent.click(expandButtons[1]); - const cta2 = await screen.findByRole('button', { name: 'Continue' }); + const cta2 = await screen.findByRole('link', { name: 'Continue' }); fireEvent.click(cta2); await waitFor(() => { diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartCtaLink.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartCtaLink.tsx index ce85269f514..ca0a3886670 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartCtaLink.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartCtaLink.tsx @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import { QuickstartItemCtaData } from '../../types'; -import { LinkButton } from '@backstage/core-components'; +import { Button, ButtonLink } from '@backstage/ui'; + import { useTranslation } from '../../hooks/useTranslation'; import { getTranslatedTextWithFallback } from '../../utils'; +import { QuickstartItemCtaData } from '../../types'; +import { QuickstartIcon } from './QuickstartIcon'; export type QuickstartCtaLinkProps = { cta?: QuickstartItemCtaData; @@ -26,54 +27,37 @@ export type QuickstartCtaLinkProps = { export const QuickstartCtaLink = ({ cta, onClick }: QuickstartCtaLinkProps) => { const { t } = useTranslation(); - // If no CTA is provided, show a default "Got it!" button + if (!cta) { return ( - { - e.preventDefault(); - onClick(); - }} - > + ); } const finalText = getTranslatedTextWithFallback(t, cta.textKey, cta.text); - const isExternalLink = cta.link.startsWith('http://') || cta.link.startsWith('https://'); - return isExternalLink ? ( - - {finalText} -   - - - ) : ( - + if (isExternalLink) { + return ( + } + > + {finalText} + + ); + } + + return ( + {finalText} - + ); }; diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.test.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.test.tsx new file mode 100644 index 00000000000..4db13ec6316 --- /dev/null +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { render, screen } from '@testing-library/react'; +import HomeIcon from '@mui/icons-material/Home'; + +import { QuickstartIcon } from './QuickstartIcon'; + +const mockGetSystemIcon = jest.fn(); + +jest.mock('@backstage/core-plugin-api', () => ({ + useApp: jest.fn(() => ({ + getSystemIcon: mockGetSystemIcon, + })), +})); + +describe('QuickstartIcon', () => { + beforeEach(() => { + mockGetSystemIcon.mockReset(); + mockGetSystemIcon.mockReturnValue(null); + }); + + it('returns null when icon is not provided', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders system icon when available', () => { + mockGetSystemIcon.mockReturnValue(() => ( + + )); + + render(); + expect(screen.getByTestId('system-home-icon')).toBeInTheDocument(); + expect(mockGetSystemIcon).toHaveBeenCalledWith('home'); + }); + + it('renders inline svg icons', () => { + const svgString = ''; + const { container } = render(); + expect(container.querySelector('img')).toBeInTheDocument(); + }); + + it('renders remote image urls', () => { + const { container } = render( + , + ); + expect(container.querySelector('img')).toHaveAttribute( + 'src', + 'https://example.com/icon.png', + ); + }); + + it('renders HTTP image urls', () => { + const { container } = render( + , + ); + expect(container.querySelector('img')).toHaveAttribute( + 'src', + 'http://example.com/icon.png', + ); + }); + + it('renders relative image paths', () => { + const { container } = render(); + expect(container.querySelector('img')).toHaveAttribute( + 'src', + '/assets/icon.png', + ); + }); + + it('renders data:image URIs', () => { + const dataUri = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; + const { container } = render(); + expect(container.querySelector('img')).toHaveAttribute('src', dataUri); + }); + + it('falls back to material ligature for legacy Import icon', () => { + render(); + expect(screen.getByText('login')).toHaveClass('material-icons-outlined'); + }); + + it('falls back to material ligature for legacy SelfService icon', () => { + render(); + expect(screen.getByText('control_point')).toHaveClass( + 'material-icons-outlined', + ); + }); + + it('falls back to material ligature for legacy Rbac icon', () => { + render(); + expect(screen.getByText('security')).toHaveClass('material-icons-outlined'); + }); + + it('falls back to material ligature for lowercase config ids', () => { + render(); + expect(screen.getByText('settings')).toHaveClass('material-icons-outlined'); + }); + + it('renders Shapes fallback for invalid icon ids', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + render(); + + expect(screen.getByTestId('ShapesOutlinedIcon')).toBeInTheDocument(); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('NotAValidIcon'), + ); + + warnSpy.mockRestore(); + }); +}); diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx new file mode 100644 index 00000000000..53d5b85f3ae --- /dev/null +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { useApp } from '@backstage/core-plugin-api'; +import Box from '@mui/material/Box'; +import MuiIcon from '@mui/material/Icon'; +import { SxProps, Theme } from '@mui/material/styles'; +import { SvgIconProps } from '@mui/material/SvgIcon'; + +import { ShapesOutlinedIcon } from '../../icons/ShapesOutlinedIcon'; +import { + resolveQuickstartIconId, + shouldUseMaterialLigature, + toMaterialLigature, +} from './quickstartIconIds'; + +export interface QuickstartIconProps { + icon: string; + size?: SvgIconProps['fontSize']; + sx?: SxProps; +} + +const flexSx = (sx?: SxProps): SxProps => + (sx + ? [{ display: 'flex', alignItems: 'center' }, sx] + : { display: 'flex', alignItems: 'center' }) as SxProps; + +/** + * Renders a quickstart icon using the same resolution order as global-header + * `HeaderIcon`, with an extra Material-ligature step for legacy quickstart + * config ids and lowercase icon names. + */ +export const QuickstartIcon = ({ + icon, + size = 'medium', + sx, +}: QuickstartIconProps) => { + const app = useApp(); + + if (!icon) { + return null; + } + + const resolvedIconId = resolveQuickstartIconId(icon); + const SystemIcon = app.getSystemIcon(resolvedIconId); + if (SystemIcon) { + return ( + + + + ); + } + + if (icon.startsWith(' + + + ); + } + + if ( + icon.startsWith('https://') || + icon.startsWith('http://') || + icon.startsWith('/') || + icon.startsWith('data:image/') + ) { + return ( + + + + ); + } + + if (shouldUseMaterialLigature(icon)) { + return ( + + {toMaterialLigature(icon)} + + ); + } + + // eslint-disable-next-line no-console + console.warn( + `QuickstartIcon: unregistered icon id "${icon}". Register via IconBundleBlueprint, app.getSystemIcon(), or use an image/SVG URL.`, + ); + + return ( + + + + ); +}; diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.test.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.test.tsx index 43efbb15841..7df18bd26eb 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.test.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.test.tsx @@ -72,9 +72,7 @@ describe('QuickstartItem)', () => { expect( screen.getByText('This is the test description'), ).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Start Now' }), - ).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Start Now' })).toBeInTheDocument(); }); it('calls handleOpen when item is clicked', async () => { @@ -89,7 +87,7 @@ describe('QuickstartItem)', () => { it('calls setProgress and sets stepCompleted in localStorage on CTA click', async () => { await renderItem(true); - const cta = screen.getByRole('button', { name: 'Start Now' }); + const cta = screen.getByRole('link', { name: 'Start Now' }); fireEvent.click(cta); expect(mockSetProgress).toHaveBeenCalled(); diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.tsx index cbeee8c12f3..34d297ae6d5 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ -import ExpandLess from '@mui/icons-material/ExpandLess'; -import ExpandMore from '@mui/icons-material/ExpandMore'; import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; import List from '@mui/material/List'; @@ -25,6 +23,7 @@ import ListItemText from '@mui/material/ListItemText'; import { useEffect, useState } from 'react'; import { QuickstartItemIcon } from './QuickstartItemIcon'; import { QuickstartCtaLink } from './QuickstartCtaLink'; +import { QuickstartIcon } from './QuickstartIcon'; import { QuickstartItemData } from '../../types'; import { useTranslation } from '../../hooks/useTranslation'; import { getTranslatedTextWithFallback } from '../../utils'; @@ -45,25 +44,21 @@ export const QuickstartItem = ({ handleOpen, }: QuickstartItemProps) => { const { t } = useTranslation(); - const [stepCompleted, setStepCompleted] = useState(false); const itemKey = `${item.title}-${index}`; - useEffect(() => { - const stepState = localStorage.getItem(itemKey); - if (stepState === 'true') { - setStepCompleted(true); - } - }, [itemKey]); + const [stepCompleted, setStepCompleted] = useState( + () => localStorage.getItem(itemKey) === 'true', + ); - useEffect(() => { - localStorage.setItem(itemKey, stepCompleted.toString()); - }, [itemKey, stepCompleted]); + const markStepCompleted = () => { + localStorage.setItem(itemKey, 'true'); + setStepCompleted(true); + setProgress(); + }; useEffect(() => { - if (stepCompleted) { - setProgress(); - } - }, [stepCompleted, setProgress]); + setStepCompleted(localStorage.getItem(itemKey) === 'true'); + }, [itemKey]); return ( `${theme.spacing(0.2)}` }}> @@ -132,7 +127,10 @@ export const QuickstartItem = ({ : { color: theme => theme.palette.text.secondary }), }} > - {open ? : } + @@ -159,12 +157,7 @@ export const QuickstartItem = ({ /> - { - setStepCompleted(true); - }} - /> + diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.test.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.test.tsx index c518fa16fdf..d1824e45ef5 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.test.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.test.tsx @@ -98,46 +98,60 @@ describe('QuickstartItemIcon', () => { expect(img.getAttribute('src')).toBe(dataUri); }); - it('should render Admin common icon', () => { - render(); + it('should render legacy Rbac icon as VpnKeyOutlined', () => { + const { container } = render(); expect( - screen.getByTestId('AdminPanelSettingsOutlinedIcon'), + container.querySelector('[data-testid="VpnKeyOutlinedIcon"]'), ).toBeInTheDocument(); }); - it('should render Rbac common icon', () => { - render(); - expect(screen.getByTestId('VpnKeyOutlinedIcon')).toBeInTheDocument(); + it('should render legacy Git icon as FileCopyOutlined', () => { + const { container } = render(); + expect( + container.querySelector('[data-testid="FileCopyOutlinedIcon"]'), + ).toBeInTheDocument(); }); - it('should render Git common icon', () => { - render(); - expect(screen.getByTestId('FileCopyOutlinedIcon')).toBeInTheDocument(); + it('should render legacy Plugins icon as PowerOutlined', () => { + const { container } = render(); + expect( + container.querySelector('[data-testid="PowerOutlinedIcon"]'), + ).toBeInTheDocument(); }); - it('should render Plugins common icon', () => { - render(); - expect(screen.getByTestId('PowerOutlinedIcon')).toBeInTheDocument(); + it('should render legacy Import icon as Login', () => { + const { container } = render(); + expect( + container.querySelector('[data-testid="LoginIcon"]'), + ).toBeInTheDocument(); }); - it('should render Import common icon', () => { - render(); - expect(screen.getByTestId('LoginIcon')).toBeInTheDocument(); + it('should render legacy Catalog icon as CategoryOutlined', () => { + const { container } = render(); + expect( + container.querySelector('[data-testid="CategoryOutlinedIcon"]'), + ).toBeInTheDocument(); }); - it('should render Catalog common icon', () => { - render(); - expect(screen.getByTestId('CategoryOutlinedIcon')).toBeInTheDocument(); + it('should render legacy SelfService icon as ControlPointOutlined', () => { + const { container } = render(); + expect( + container.querySelector('[data-testid="ControlPointOutlinedIcon"]'), + ).toBeInTheDocument(); }); - it('should render SelfService common icon', () => { - render(); - expect(screen.getByTestId('ControlPointOutlinedIcon')).toBeInTheDocument(); + it('should render legacy Learning icon as SchoolOutlined', () => { + const { container } = render(); + expect( + container.querySelector('[data-testid="SchoolOutlinedIcon"]'), + ).toBeInTheDocument(); }); - it('should render Learning common icon', () => { - render(); - expect(screen.getByTestId('SchoolOutlinedIcon')).toBeInTheDocument(); + it('should render legacy Admin icon as AdminPanelSettingsOutlined', () => { + const { container } = render(); + expect( + container.querySelector('[data-testid="AdminPanelSettingsOutlinedIcon"]'), + ).toBeInTheDocument(); }); it('should render Lightspeed common icon', () => { @@ -146,15 +160,12 @@ describe('QuickstartItemIcon', () => { expect(svg).toBeInTheDocument(); }); - it('should render material icon for unknown string', () => { + it('should fall back to material ligature for lowercase config ids', () => { render(); - - const icon = screen.getByText('settings'); - expect(icon).toBeInTheDocument(); - expect(icon).toHaveClass('material-icons-outlined'); + expect(screen.getByText('settings')).toHaveClass('material-icons-outlined'); }); - it('should apply custom sx prop', () => { + it('should apply custom sx prop to the icon container', () => { mockGetSystemIcon.mockReturnValue(() => ( )); @@ -163,7 +174,23 @@ describe('QuickstartItemIcon', () => { , ); - const box = container.firstChild; - expect(box).toBeInTheDocument(); + expect(container.firstChild).toBeInTheDocument(); + }); + + it('should apply custom sx prop only to the common icon container', () => { + const { container } = render( + , + ); + + const icon = container.querySelector( + '[data-testid="AdminPanelSettingsOutlinedIcon"]', + ); + expect(icon).toBeInTheDocument(); + expect(icon).not.toHaveStyle({ marginRight: '8px' }); + }); + + it('should render Shapes fallback for invalid icon ids', () => { + render(); + expect(screen.getByTestId('ShapesOutlinedIcon')).toBeInTheDocument(); }); }); diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.tsx index c57f84c8734..b64aaab3620 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.tsx @@ -14,18 +14,20 @@ * limitations under the License. */ -import MuiIcon from '@mui/material/Icon'; +import type { ComponentType } from 'react'; import AdminPanelSettingsOutlinedIcon from '@mui/icons-material/AdminPanelSettingsOutlined'; -import VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined'; -import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined'; -import PowerOutlinedIcon from '@mui/icons-material/PowerOutlined'; -import LoginIcon from '@mui/icons-material/Login'; import CategoryOutlinedIcon from '@mui/icons-material/CategoryOutlined'; import ControlPointOutlinedIcon from '@mui/icons-material/ControlPointOutlined'; +import FileCopyOutlinedIcon from '@mui/icons-material/FileCopyOutlined'; +import LoginIcon from '@mui/icons-material/Login'; +import PowerOutlinedIcon from '@mui/icons-material/PowerOutlined'; import SchoolOutlinedIcon from '@mui/icons-material/SchoolOutlined'; -import { SxProps, Theme } from '@mui/material/styles'; -import { useApp } from '@backstage/core-plugin-api'; +import VpnKeyOutlinedIcon from '@mui/icons-material/VpnKeyOutlined'; import Box from '@mui/material/Box'; +import { SxProps, Theme } from '@mui/material/styles'; +import { SvgIconProps } from '@mui/material/SvgIcon'; + +import { QuickstartIcon } from './QuickstartIcon'; import { LightspeedIcon } from './LightspeedIcon'; export interface QuickstartItemIconProps { @@ -33,64 +35,41 @@ export interface QuickstartItemIconProps { sx?: SxProps; } -const commonIcons: { - [k: string]: React.ReactNode; -} = { - Admin: , - Rbac: , - Git: , - Plugins: , - Import: , - Catalog: , - SelfService: , - Learning: , - Lightspeed: , +const commonIcons: Record< + string, + ComponentType> +> = { + Admin: AdminPanelSettingsOutlinedIcon, + Rbac: VpnKeyOutlinedIcon, + Git: FileCopyOutlinedIcon, + Plugins: PowerOutlinedIcon, + Import: LoginIcon, + Catalog: CategoryOutlinedIcon, + SelfService: ControlPointOutlinedIcon, + Learning: SchoolOutlinedIcon, }; export const QuickstartItemIcon = ({ icon, sx }: QuickstartItemIconProps) => { - const app = useApp(); if (!icon) { return null; } - const SystemIcon = app.getSystemIcon(icon); - if (SystemIcon) { + if (icon === 'Lightspeed') { return ( - + ); } - if (icon.startsWith(' - - - ); - } - - if ( - icon.startsWith('https://') || - icon.startsWith('http://') || - icon.startsWith('/') || - icon.startsWith('data:image/') - ) { + const CommonIcon = commonIcons[icon]; + if (CommonIcon) { return ( - - - + + + ); } - return ( - - {commonIcons[icon] || icon} - - ); + return ; }; diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/quickstartIconIds.ts b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/quickstartIconIds.ts new file mode 100644 index 00000000000..83cb1351dd5 --- /dev/null +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/quickstartIconIds.ts @@ -0,0 +1,48 @@ +/* + * Copyright Red Hat, Inc. + * + * 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. + */ + +/** + * Legacy PascalCase quickstart config icon ids mapped to RHDH common system / + * Material icon ligature ids. + * + * @see `@red-hat-developer-hub/backstage-plugin-app-defaults` `commonIcons` + */ +export const quickstartLegacyIconAliases: Record = { + Admin: 'manageAccounts', + Rbac: 'security', + Git: 'folder', + Plugins: 'extension', + Import: 'login', + Catalog: 'category', + SelfService: 'control_point', + Learning: 'school', +}; + +export const resolveQuickstartIconId = (icon: string): string => + quickstartLegacyIconAliases[icon] ?? icon; + +/** + * Material Icons ligature for legacy quickstart ids and lowercase config ids. + * camelCase ids become snake_case (e.g. manageAccounts → manage_accounts). + */ +export const toMaterialLigature = (icon: string): string => { + const resolved = resolveQuickstartIconId(icon); + return resolved.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase(); +}; + +/** Whether the config value can be rendered as a Material ligature. */ +export const shouldUseMaterialLigature = (icon: string): boolean => + icon in quickstartLegacyIconAliases || /^[a-z][a-z0-9_]*$/.test(icon); diff --git a/workspaces/quickstart/plugins/quickstart/src/icons/ShapesOutlinedIcon.tsx b/workspaces/quickstart/plugins/quickstart/src/icons/ShapesOutlinedIcon.tsx new file mode 100644 index 00000000000..56a524a4f85 --- /dev/null +++ b/workspaces/quickstart/plugins/quickstart/src/icons/ShapesOutlinedIcon.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 SvgIcon from '@mui/material/SvgIcon'; +import type { SvgIconProps } from '@mui/material/SvgIcon'; + +export const ShapesOutlinedIcon = (props: SvgIconProps) => ( + + + +); diff --git a/workspaces/quickstart/plugins/quickstart/src/index.tsx b/workspaces/quickstart/plugins/quickstart/src/index.tsx index 41f74702ad1..878e3870af1 100644 --- a/workspaces/quickstart/plugins/quickstart/src/index.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/index.tsx @@ -25,15 +25,16 @@ import { GlobalHeaderMenuItemBlueprint } from '@red-hat-developer-hub/backstage- import { quickstartTranslations } from './translations'; import { QUICKSTART_DRAWER_ID } from './const'; -import { QuickstartDrawerContent } from './QuickstartDrawerContent'; -import { QuickstartHelpMenuItem } from './QuickstartHelpMenuItem'; -import { QuickstartInit } from './QuickstartInit'; +import { + QuickstartDrawerContentElement, + QuickstartInitElement, +} from './lazyQuickstartUi'; const quickstartDrawer = AppDrawerContentBlueprint.make({ name: 'quickstart', params: { id: QUICKSTART_DRAWER_ID, - element: , + element: QuickstartDrawerContentElement, resizable: true, defaultWidth: 500, }, @@ -43,15 +44,20 @@ const quickstartHelpMenuItem = GlobalHeaderMenuItemBlueprint.make({ name: 'quickstart', params: { target: 'help', - component: QuickstartHelpMenuItem, priority: 50, + loader: async () => { + const { QuickstartHelpMenuItem } = await import( + './QuickstartHelpMenuItem' + ); + return QuickstartHelpMenuItem; + }, }, }); const quickstartInitElement = AppRootElementBlueprint.make({ name: 'quickstart-init', params: { - element: , + element: QuickstartInitElement, }, }); diff --git a/workspaces/quickstart/plugins/quickstart/src/lazyQuickstartUi.tsx b/workspaces/quickstart/plugins/quickstart/src/lazyQuickstartUi.tsx new file mode 100644 index 00000000000..f30a712de2e --- /dev/null +++ b/workspaces/quickstart/plugins/quickstart/src/lazyQuickstartUi.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { lazy, Suspense } from 'react'; + +const LazyQuickstartDrawerContent = lazy(() => + import('./QuickstartDrawerContent').then(m => ({ + default: m.QuickstartDrawerContent, + })), +); + +const LazyQuickstartInit = lazy(() => + import('./QuickstartInit').then(m => ({ + default: m.QuickstartInit, + })), +); + +/** + * Thin sync shells that defer Quickstart UI to async chunks. + * Keeps drawer content and snackbar init off the NFS sync graph. + * + * The help menu item uses a blueprint `loader` in `index.tsx` instead. + */ +export const QuickstartDrawerContentElement = ( + + + +); + +export const QuickstartInitElement = ( + + + +); diff --git a/workspaces/quickstart/plugins/quickstart/src/legacyExports.ts b/workspaces/quickstart/plugins/quickstart/src/legacyExports.ts index 131c0577b76..89fb311129b 100644 --- a/workspaces/quickstart/plugins/quickstart/src/legacyExports.ts +++ b/workspaces/quickstart/plugins/quickstart/src/legacyExports.ts @@ -26,7 +26,6 @@ export * from './plugin'; export { useQuickstartDrawerContext } from './hooks/useQuickstartDrawerContext'; export type { QuickstartDrawerContextType } from './components/QuickstartDrawerContext'; -export { QuickstartDrawerStateExposer } from './components/QuickstartDrawerStateExposer'; export type { DrawerStateExposerProps, DrawerPartialState, @@ -39,11 +38,10 @@ export type { QuickstartItemData, QuickstartItemCtaData, } from './types'; -export { QuickstartButton } from './components/QuickstartButton/QuickstartButton'; export type { QuickstartButtonProps } from './components/QuickstartButton/QuickstartButton'; -/** - * @public - */ -export { QuickstartDrawerProvider } from './components/QuickstartDrawerProvider'; +export { + quickstartTranslations, + quickstartTranslationRef, +} from './translations'; export { filterQuickstartItemsByRole } from './utils'; diff --git a/workspaces/quickstart/plugins/quickstart/src/nfsExports.test.tsx b/workspaces/quickstart/plugins/quickstart/src/nfsExports.test.tsx index 2d19cf3f49f..80790be6021 100644 --- a/workspaces/quickstart/plugins/quickstart/src/nfsExports.test.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/nfsExports.test.tsx @@ -31,4 +31,11 @@ describe('quickstart NFS exports', () => { it('should export quickstartTranslationsModule', () => { expect(quickstartTranslationsModule).toBeDefined(); }); + + it('should not re-export legacy OFS components from the NFS entry', () => { + expect( + (quickstartPlugin as { QuickstartDrawerProvider?: unknown }) + .QuickstartDrawerProvider, + ).toBeUndefined(); + }); }); diff --git a/workspaces/quickstart/yarn.lock b/workspaces/quickstart/yarn.lock index 79ff4d42fbb..db1fb4833d4 100644 --- a/workspaces/quickstart/yarn.lock +++ b/workspaces/quickstart/yarn.lock @@ -7150,32 +7150,6 @@ __metadata: languageName: node linkType: hard -"@mswjs/cookies@npm:^0.2.2": - version: 0.2.2 - resolution: "@mswjs/cookies@npm:0.2.2" - dependencies: - "@types/set-cookie-parser": "npm:^2.4.0" - set-cookie-parser: "npm:^2.4.6" - checksum: 10c0/f950062538d431674d581309cf19884fc4d3f57e2a276164cac0c9a3250071d42464ba7825d13be14c703ca5a912d62a62626f4a068d8f36d1629dbb63bde740 - languageName: node - linkType: hard - -"@mswjs/interceptors@npm:^0.17.10": - version: 0.17.10 - resolution: "@mswjs/interceptors@npm:0.17.10" - dependencies: - "@open-draft/until": "npm:^1.0.3" - "@types/debug": "npm:^4.1.7" - "@xmldom/xmldom": "npm:^0.8.3" - debug: "npm:^4.3.3" - headers-polyfill: "npm:3.2.5" - outvariant: "npm:^1.2.1" - strict-event-emitter: "npm:^0.2.4" - web-encoding: "npm:^1.1.5" - checksum: 10c0/0343a93711b60c321c40733d6bf2720a736d8e0730f5d0d9916ee4a24abfcfca4a83d1e4b2e21c3affef4fc61f04588104be002fbc8258dc4b0d202c384ade33 - languageName: node - linkType: hard - "@mui/core-downloads-tracker@npm:^5.18.0": version: 5.18.0 resolution: "@mui/core-downloads-tracker@npm:5.18.0" @@ -8225,13 +8199,6 @@ __metadata: languageName: node linkType: hard -"@open-draft/until@npm:^1.0.3": - version: 1.0.3 - resolution: "@open-draft/until@npm:1.0.3" - checksum: 10c0/f88bcd774b55359d14a4fa80f7bfe7d9d6d26a5995e94e823e43b211656daae3663e983f0a996937da286d22f6f5da2087b661845302f236ba27f8529dcd14fb - languageName: node - linkType: hard - "@openapi-contrib/openapi-schema-to-json-schema@npm:~3.2.0": version: 3.2.0 resolution: "@openapi-contrib/openapi-schema-to-json-schema@npm:3.2.0" @@ -9276,9 +9243,9 @@ __metadata: languageName: node linkType: hard -"@red-hat-developer-hub/backstage-plugin-global-header@npm:^2.0.1": - version: 2.0.1 - resolution: "@red-hat-developer-hub/backstage-plugin-global-header@npm:2.0.1" +"@red-hat-developer-hub/backstage-plugin-global-header@npm:^3.0.0": + version: 3.0.0 + resolution: "@red-hat-developer-hub/backstage-plugin-global-header@npm:3.0.0" dependencies: "@backstage/catalog-model": "npm:^1.10.0" "@backstage/core-components": "npm:^0.18.13" @@ -9308,7 +9275,7 @@ __metadata: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 react-router-dom: ^6.0.0 - checksum: 10c0/7eecbcb84b1dac8087193edf33ab46c4ccf01e3a80d8bc58a4c110eed31f61c011ea21ffd98764a485ace95a9995b44255749b1400cbaef8e2b1d0681d435470 + checksum: 10c0/29f3d6ee62425c889781cacb7601c119b46fe2c85eeefc6327182ce274a06ba8996f2bd39a92f19e90260ef5792aa48282a9cb94045702a0ce92e4ac8a0e9a36 languageName: node linkType: hard @@ -9318,7 +9285,6 @@ __metadata: dependencies: "@backstage-community/plugin-rbac-common": "npm:^1.19.0" "@backstage/cli": "npm:^0.36.5" - "@backstage/core-app-api": "npm:^1.20.4" "@backstage/core-components": "npm:^0.18.13" "@backstage/core-plugin-api": "npm:^1.12.9" "@backstage/dev-utils": "npm:^1.1.26" @@ -9329,16 +9295,16 @@ __metadata: "@backstage/plugin-permission-react": "npm:^0.5.4" "@backstage/test-utils": "npm:^1.7.21" "@backstage/theme": "npm:^0.7.3" + "@backstage/ui": "npm:^0.17.1" "@mui/icons-material": "npm:5.18.0" "@mui/material": "npm:5.18.0" "@patternfly/react-icons": "npm:^6.5.0" "@red-hat-developer-hub/backstage-plugin-app-react": "npm:^1.0.0" - "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^2.0.1" + "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.0.0" "@red-hat-developer-hub/backstage-plugin-theme": "npm:^0.14.11" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^14.0.0" "@testing-library/user-event": "npm:^14.0.0" - msw: "npm:^1.0.0" react: "npm:^16.13.1 || ^17.0.0 || ^18.0.0" react-dom: "npm:16.13.1 || ^17.0.0 || ^18.0.0" react-router-dom: "npm:^6.0.0" @@ -11878,13 +11844,6 @@ __metadata: languageName: node linkType: hard -"@types/cookie@npm:^0.4.1": - version: 0.4.1 - resolution: "@types/cookie@npm:0.4.1" - checksum: 10c0/f96afe12bd51be1ec61410b0641243d93fa3a494702407c787a4c872b5c8bcd39b224471452055e44a9ce42af1a636e87d161994226eaf4c2be9c30f60418409 - languageName: node - linkType: hard - "@types/cors@npm:^2.8.6": version: 2.8.18 resolution: "@types/cors@npm:2.8.18" @@ -11894,7 +11853,7 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.7": +"@types/debug@npm:^4.0.0": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" dependencies: @@ -12099,13 +12058,6 @@ __metadata: languageName: node linkType: hard -"@types/js-levenshtein@npm:^1.1.1": - version: 1.1.3 - resolution: "@types/js-levenshtein@npm:1.1.3" - checksum: 10c0/025f2bd8d865cfa7a996799a1a2f2a77fa2fc74a28971aa035a103de35d7c1e3d949721a88f57fdb532815bbcb2bf7019196a608ed0a8bbd1023d64c52bb251b - languageName: node - linkType: hard - "@types/js-yaml@npm:^4.0.1": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" @@ -12480,15 +12432,6 @@ __metadata: languageName: node linkType: hard -"@types/set-cookie-parser@npm:^2.4.0": - version: 2.4.10 - resolution: "@types/set-cookie-parser@npm:2.4.10" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/010b0c582ea70a2088618b4725808e80c30cce296c19ec58e51d94e0fd1038201b7b99238bf3ea74e1894163c8037d10a4f1729de62b2801ce240ff070f43e76 - languageName: node - linkType: hard - "@types/sockjs@npm:^0.3.36": version: 0.3.36 resolution: "@types/sockjs@npm:0.3.36" @@ -13141,13 +13084,6 @@ __metadata: languageName: node linkType: hard -"@xmldom/xmldom@npm:^0.8.3": - version: 0.8.15 - resolution: "@xmldom/xmldom@npm:0.8.15" - checksum: 10c0/59d09b85824b684fba5a0a224d5b43abea1a0d745e8d8b8136a7013e996eb6da122fdbb3deb32ed1621923a10e1937b1f7917cea43481d9d5b976e8c1ff836c1 - languageName: node - linkType: hard - "@xobotyi/scrollbar-width@npm:^1.9.5": version: 1.9.5 resolution: "@xobotyi/scrollbar-width@npm:1.9.5" @@ -13207,13 +13143,6 @@ __metadata: languageName: node linkType: hard -"@zxing/text-encoding@npm:0.9.0": - version: 0.9.0 - resolution: "@zxing/text-encoding@npm:0.9.0" - checksum: 10c0/d15bff181d46c2ab709e7242801a8d40408aa8c19b44462e5f60e766bf59105b44957914ab6baab60d10d466a5e965f21fe890c67dfdb7d5c7f940df457b4d0d - languageName: node - linkType: hard - "abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -13614,7 +13543,7 @@ __metadata: "@mui/material": "npm:5.18.0" "@mui/styles": "npm:5.18.0" "@playwright/test": "npm:1.61.1" - "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^2.0.1" + "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.0.0" "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^" "@red-hat-developer-hub/backstage-plugin-theme": "npm:^0.14.11" "@testing-library/dom": "npm:^9.0.0" @@ -13674,7 +13603,7 @@ __metadata: "@mui/material": "npm:5.18.0" "@playwright/test": "npm:1.61.1" "@red-hat-developer-hub/backstage-plugin-app-react": "npm:^1.0.0" - "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^2.0.1" + "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.0.0" "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^" "@red-hat-developer-hub/backstage-plugin-theme": "npm:^0.14.11" "@testing-library/dom": "npm:^9.0.0" @@ -15993,13 +15922,6 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.4.2": - version: 0.4.2 - resolution: "cookie@npm:0.4.2" - checksum: 10c0/beab41fbd7c20175e3a2799ba948c1dcc71ef69f23fe14eeeff59fc09f50c517b0f77098db87dbb4c55da802f9d86ee86cdc1cd3efd87760341551838d53fca2 - languageName: node - linkType: hard - "copy-to-clipboard@npm:^3.2.0, copy-to-clipboard@npm:^3.3.1": version: 3.3.3 resolution: "copy-to-clipboard@npm:3.3.3" @@ -19921,7 +19843,7 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^16.0.0, graphql@npm:^16.8.1": +"graphql@npm:^16.0.0": version: 16.11.0 resolution: "graphql@npm:16.11.0" checksum: 10c0/124da7860a2292e9acf2fed0c71fc0f6a9b9ca865d390d112bdd563c1f474357141501c12891f4164fe984315764736ad67f705219c62f7580681d431a85db88 @@ -20219,13 +20141,6 @@ __metadata: languageName: node linkType: hard -"headers-polyfill@npm:3.2.5": - version: 3.2.5 - resolution: "headers-polyfill@npm:3.2.5" - checksum: 10c0/10202f4ebfaecd6aa31305f29664f876ac01d9174a3fb8fcc5a0df3eaf9c1767fb0d6cf6f961484f2bfd2101b6768090976f146bd88aeedd07af4e741cb2dcb7 - languageName: node - linkType: hard - "helmet@npm:^6.0.0": version: 6.2.0 resolution: "helmet@npm:6.2.0" @@ -21305,13 +21220,6 @@ __metadata: languageName: node linkType: hard -"is-node-process@npm:^1.2.0": - version: 1.2.0 - resolution: "is-node-process@npm:1.2.0" - checksum: 10c0/5b24fda6776d00e42431d7bcd86bce81cb0b6cabeb944142fe7b077a54ada2e155066ad06dbe790abdb397884bdc3151e04a9707b8cd185099efbc79780573ed - languageName: node - linkType: hard - "is-number-object@npm:^1.1.1": version: 1.1.1 resolution: "is-number-object@npm:1.1.1" @@ -22300,13 +22208,6 @@ __metadata: languageName: node linkType: hard -"js-levenshtein@npm:^1.1.6": - version: 1.1.6 - resolution: "js-levenshtein@npm:1.1.6" - checksum: 10c0/14045735325ea1fd87f434a74b11d8a14380f090f154747e613529c7cff68b5ee607f5230fa40665d5fb6125a3791f4c223f73b9feca754f989b059f5c05864f - languageName: node - linkType: hard - "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -24894,40 +24795,6 @@ __metadata: languageName: node linkType: hard -"msw@npm:^1.0.0": - version: 1.3.5 - resolution: "msw@npm:1.3.5" - dependencies: - "@mswjs/cookies": "npm:^0.2.2" - "@mswjs/interceptors": "npm:^0.17.10" - "@open-draft/until": "npm:^1.0.3" - "@types/cookie": "npm:^0.4.1" - "@types/js-levenshtein": "npm:^1.1.1" - chalk: "npm:^4.1.1" - chokidar: "npm:^3.4.2" - cookie: "npm:^0.4.2" - graphql: "npm:^16.8.1" - headers-polyfill: "npm:3.2.5" - inquirer: "npm:^8.2.0" - is-node-process: "npm:^1.2.0" - js-levenshtein: "npm:^1.1.6" - node-fetch: "npm:^2.6.7" - outvariant: "npm:^1.4.0" - path-to-regexp: "npm:^6.3.0" - strict-event-emitter: "npm:^0.4.3" - type-fest: "npm:^2.19.0" - yargs: "npm:^17.3.1" - peerDependencies: - typescript: ">= 4.4.x" - peerDependenciesMeta: - typescript: - optional: true - bin: - msw: cli/index.js - checksum: 10c0/bb0b3625b68f1750bfe90ade6e9e98c64f509138ce7b09c8a53af19f8f662ac79881dad64fbc74c9426247b725e3ec5e6d45eea2f6b71ddc02184e0ddf743e4d - languageName: node - linkType: hard - "multer@npm:^2.0.2": version: 2.4.0 resolution: "multer@npm:2.4.0" @@ -25942,13 +25809,6 @@ __metadata: languageName: node linkType: hard -"outvariant@npm:^1.2.1, outvariant@npm:^1.4.0": - version: 1.4.3 - resolution: "outvariant@npm:1.4.3" - checksum: 10c0/5976ca7740349cb8c71bd3382e2a762b1aeca6f33dc984d9d896acdf3c61f78c3afcf1bfe9cc633a7b3c4b295ec94d292048f83ea2b2594fae4496656eba992c - languageName: node - linkType: hard - "own-keys@npm:^1.0.1": version: 1.0.1 resolution: "own-keys@npm:1.0.1" @@ -26465,13 +26325,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^6.3.0": - version: 6.3.0 - resolution: "path-to-regexp@npm:6.3.0" - checksum: 10c0/73b67f4638b41cde56254e6354e46ae3a2ebc08279583f6af3d96fe4664fc75788f74ed0d18ca44fa4a98491b69434f9eee73b97bb5314bd1b5adb700f5c18d6 - languageName: node - linkType: hard - "path-to-regexp@npm:~0.1.12": version: 0.1.12 resolution: "path-to-regexp@npm:0.1.12" @@ -29696,13 +29549,6 @@ __metadata: languageName: node linkType: hard -"set-cookie-parser@npm:^2.4.6": - version: 2.7.1 - resolution: "set-cookie-parser@npm:2.7.1" - checksum: 10c0/060c198c4c92547ac15988256f445eae523f57f2ceefeccf52d30d75dedf6bff22b9c26f756bd44e8e560d44ff4ab2130b178bd2e52ef5571bf7be3bd7632d9a - languageName: node - linkType: hard - "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -30410,22 +30256,6 @@ __metadata: languageName: node linkType: hard -"strict-event-emitter@npm:^0.2.4": - version: 0.2.8 - resolution: "strict-event-emitter@npm:0.2.8" - dependencies: - events: "npm:^3.3.0" - checksum: 10c0/6891e19fea4f0289e4da2fe7050d85906eaca7f774aa38fe674f0e58fdece1b63b868614fa23974c4cb862aa99358caa987523b705fdfff4639231c62e384394 - languageName: node - linkType: hard - -"strict-event-emitter@npm:^0.4.3": - version: 0.4.6 - resolution: "strict-event-emitter@npm:0.4.6" - checksum: 10c0/d0231ef081cb1937b1445da59a1ec202d1c097d825c504f398600532490a4104e200b0dce4137467a8eaac5f8f9718d01c99869687afad78cad3b14c4b2e6a39 - languageName: node - linkType: hard - "string-argv@npm:~0.3.1": version: 0.3.2 resolution: "string-argv@npm:0.3.2" @@ -32778,19 +32608,6 @@ __metadata: languageName: node linkType: hard -"web-encoding@npm:^1.1.5": - version: 1.1.5 - resolution: "web-encoding@npm:1.1.5" - dependencies: - "@zxing/text-encoding": "npm:0.9.0" - util: "npm:^0.12.3" - dependenciesMeta: - "@zxing/text-encoding": - optional: true - checksum: 10c0/59d5413338ec0894c690006f5d8508b0c88cae1d8c78606c3f326e351c672196461ed808b849fe08d0900fa56a61fcacb9ff576499068d2ead0a7bc04afa7d34 - languageName: node - linkType: hard - "web-namespaces@npm:^2.0.0": version: 2.0.1 resolution: "web-namespaces@npm:2.0.1" @@ -33401,7 +33218,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.7.2": +"yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 23a89b08391c16e059c0268ed802651f680b12a9 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Mon, 21 Sep 2026 14:42:09 +0530 Subject: [PATCH 2/6] fix(quickstart): drop @backstage/ui CTA and use thin header blueprints Address review feedback on the NFS sync optimization: use MUI Button + router Link for CTAs, import GlobalHeaderMenuItemBlueprint from global-header/blueprints to keep markdown/highlight out of the MF async graph, and replace the copied ShapesOutlinedIcon with MUI WidgetsOutlined. Co-authored-by: Cursor --- .../quickstart-nfs-sync-optimization.md | 4 ++- .../plugins/quickstart/package.json | 1 - .../QuickstartContent/QuickstartCtaLink.tsx | 30 ++++++++++++------- .../QuickstartContent/QuickstartIcon.test.tsx | 4 +-- .../QuickstartContent/QuickstartIcon.tsx | 12 ++++---- .../QuickstartItemIcon.test.tsx | 4 +-- .../src/icons/ShapesOutlinedIcon.tsx | 24 --------------- .../plugins/quickstart/src/index.tsx | 2 +- workspaces/quickstart/yarn.lock | 1 - 9 files changed, 35 insertions(+), 47 deletions(-) delete mode 100644 workspaces/quickstart/plugins/quickstart/src/icons/ShapesOutlinedIcon.tsx diff --git a/workspaces/quickstart/.changeset/quickstart-nfs-sync-optimization.md b/workspaces/quickstart/.changeset/quickstart-nfs-sync-optimization.md index 4fd166696c9..61bb5d9d839 100644 --- a/workspaces/quickstart/.changeset/quickstart-nfs-sync-optimization.md +++ b/workspaces/quickstart/.changeset/quickstart-nfs-sync-optimization.md @@ -4,6 +4,8 @@ Defer Quickstart NFS drawer content, init snackbar, and help menu item to async chunks so the Module Federation sync graph stays thin. The help menu uses a blueprint `loader` and a local MUI `MenuItem` instead of `global-header/components` to avoid pulling `@backstage/core-components` into the quickstart MF graph. -Bumps `@red-hat-developer-hub/backstage-plugin-global-header` to ^3.0.0 for the loader blueprint API. +CTA links use MUI `Button` with `react-router-dom` `Link` (and native anchors for external URLs) instead of `@backstage/ui`. Invalid icon fallbacks use MUI `WidgetsOutlined` instead of a copied `ShapesOutlinedIcon`. Menu-item registration imports `GlobalHeaderMenuItemBlueprint` from `global-header/blueprints` so the critical header / markdown highlight stack stays out of the quickstart async graph. + +Bumps `@red-hat-developer-hub/backstage-plugin-global-header` for the `/blueprints` export and loader blueprint API. Adds public legacy exports `quickstartTranslationRef` and `quickstartTranslations`. Legacy `QuickstartButton` and `QuickstartDrawerProvider` exported types widen from concrete function signatures to `React.ComponentType` because they are now re-exported from lazy component extensions rather than concrete component modules. diff --git a/workspaces/quickstart/plugins/quickstart/package.json b/workspaces/quickstart/plugins/quickstart/package.json index c9da219469c..aab5ccf1d3a 100644 --- a/workspaces/quickstart/plugins/quickstart/package.json +++ b/workspaces/quickstart/plugins/quickstart/package.json @@ -42,7 +42,6 @@ "@backstage/plugin-app-react": "^0.2.6", "@backstage/plugin-permission-react": "^0.5.4", "@backstage/theme": "^0.7.3", - "@backstage/ui": "^0.17.1", "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", "@patternfly/react-icons": "^6.5.0", diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartCtaLink.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartCtaLink.tsx index ca0a3886670..54ef5da0369 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartCtaLink.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartCtaLink.tsx @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button, ButtonLink } from '@backstage/ui'; +import OpenInNewIcon from '@mui/icons-material/OpenInNew'; +import Button from '@mui/material/Button'; +import { Link } from 'react-router-dom'; import { useTranslation } from '../../hooks/useTranslation'; import { getTranslatedTextWithFallback } from '../../utils'; import { QuickstartItemCtaData } from '../../types'; -import { QuickstartIcon } from './QuickstartIcon'; export type QuickstartCtaLinkProps = { cta?: QuickstartItemCtaData; @@ -30,7 +31,7 @@ export const QuickstartCtaLink = ({ cta, onClick }: QuickstartCtaLinkProps) => { if (!cta) { return ( - ); @@ -42,22 +43,31 @@ export const QuickstartCtaLink = ({ cta, onClick }: QuickstartCtaLinkProps) => { if (isExternalLink) { return ( - } + onClick={onClick} + endIcon={} + sx={{ gap: '5px' }} > {finalText} - + ); } return ( - + ); }; diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.test.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.test.tsx index 4db13ec6316..14e3cf2c854 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.test.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.test.tsx @@ -111,12 +111,12 @@ describe('QuickstartIcon', () => { expect(screen.getByText('settings')).toHaveClass('material-icons-outlined'); }); - it('renders Shapes fallback for invalid icon ids', () => { + it('renders MUI fallback for invalid icon ids', () => { const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); render(); - expect(screen.getByTestId('ShapesOutlinedIcon')).toBeInTheDocument(); + expect(screen.getByTestId('QuickstartIconFallback')).toBeInTheDocument(); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining('NotAValidIcon'), ); diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx index 53d5b85f3ae..16d9d7ca254 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx @@ -15,12 +15,12 @@ */ import { useApp } from '@backstage/core-plugin-api'; +import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined'; import Box from '@mui/material/Box'; import MuiIcon from '@mui/material/Icon'; import { SxProps, Theme } from '@mui/material/styles'; import { SvgIconProps } from '@mui/material/SvgIcon'; -import { ShapesOutlinedIcon } from '../../icons/ShapesOutlinedIcon'; import { resolveQuickstartIconId, shouldUseMaterialLigature, @@ -39,9 +39,8 @@ const flexSx = (sx?: SxProps): SxProps => : { display: 'flex', alignItems: 'center' }) as SxProps; /** - * Renders a quickstart icon using the same resolution order as global-header - * `HeaderIcon`, with an extra Material-ligature step for legacy quickstart - * config ids and lowercase icon names. + * Renders quickstart icons from system icons, image/SVG URLs, or Material + * ligatures for legacy config ids. Falls back to a MUI outlined widget icon. */ export const QuickstartIcon = ({ icon, @@ -101,7 +100,10 @@ export const QuickstartIcon = ({ return ( - + ); }; diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.test.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.test.tsx index d1824e45ef5..2ed90783847 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.test.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.test.tsx @@ -189,8 +189,8 @@ describe('QuickstartItemIcon', () => { expect(icon).not.toHaveStyle({ marginRight: '8px' }); }); - it('should render Shapes fallback for invalid icon ids', () => { + it('should render MUI fallback for invalid icon ids', () => { render(); - expect(screen.getByTestId('ShapesOutlinedIcon')).toBeInTheDocument(); + expect(screen.getByTestId('QuickstartIconFallback')).toBeInTheDocument(); }); }); diff --git a/workspaces/quickstart/plugins/quickstart/src/icons/ShapesOutlinedIcon.tsx b/workspaces/quickstart/plugins/quickstart/src/icons/ShapesOutlinedIcon.tsx deleted file mode 100644 index 56a524a4f85..00000000000 --- a/workspaces/quickstart/plugins/quickstart/src/icons/ShapesOutlinedIcon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * 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 SvgIcon from '@mui/material/SvgIcon'; -import type { SvgIconProps } from '@mui/material/SvgIcon'; - -export const ShapesOutlinedIcon = (props: SvgIconProps) => ( - - - -); diff --git a/workspaces/quickstart/plugins/quickstart/src/index.tsx b/workspaces/quickstart/plugins/quickstart/src/index.tsx index 878e3870af1..0525b98c581 100644 --- a/workspaces/quickstart/plugins/quickstart/src/index.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/index.tsx @@ -21,7 +21,7 @@ import { } from '@backstage/frontend-plugin-api'; import { TranslationBlueprint } from '@backstage/plugin-app-react'; import { AppDrawerContentBlueprint } from '@red-hat-developer-hub/backstage-plugin-app-react'; -import { GlobalHeaderMenuItemBlueprint } from '@red-hat-developer-hub/backstage-plugin-global-header'; +import { GlobalHeaderMenuItemBlueprint } from '@red-hat-developer-hub/backstage-plugin-global-header/blueprints'; import { quickstartTranslations } from './translations'; import { QUICKSTART_DRAWER_ID } from './const'; diff --git a/workspaces/quickstart/yarn.lock b/workspaces/quickstart/yarn.lock index db1fb4833d4..382956a94c2 100644 --- a/workspaces/quickstart/yarn.lock +++ b/workspaces/quickstart/yarn.lock @@ -9295,7 +9295,6 @@ __metadata: "@backstage/plugin-permission-react": "npm:^0.5.4" "@backstage/test-utils": "npm:^1.7.21" "@backstage/theme": "npm:^0.7.3" - "@backstage/ui": "npm:^0.17.1" "@mui/icons-material": "npm:5.18.0" "@mui/material": "npm:5.18.0" "@patternfly/react-icons": "npm:^6.5.0" From 6c5cbc54522e977ada64d2c2e84d6788ebaeb1ca Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Mon, 21 Sep 2026 14:48:33 +0530 Subject: [PATCH 3/6] fix(quickstart): refactor QuickstartIcon to clear Sonar duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure SVG/URL image rendering so CPD no longer matches ScorecardIcon (4.4% → under the 3% new-code duplication gate). Co-authored-by: Cursor --- .../QuickstartContent/QuickstartIcon.tsx | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx index 16d9d7ca254..ef2c66bb562 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartIcon.tsx @@ -38,6 +38,40 @@ const flexSx = (sx?: SxProps): SxProps => ? [{ display: 'flex', alignItems: 'center' }, sx] : { display: 'flex', alignItems: 'center' }) as SxProps; +const toInlineSvgDataUri = (rawSvg: string): string => + `data:image/svg+xml;base64,${btoa(rawSvg)}`; + +const isRemoteOrPathImage = (value: string): boolean => + /^(https?:\/\/|\/|data:image\/)/.test(value); + +type QuickstartImageIconProps = { + src: string; + size: SvgIconProps['fontSize']; + sx?: SxProps; + outlined?: boolean; +}; + +/** Renders an icon from an image `src` (URL, path, or data URI). */ +const QuickstartImageIcon = ({ + src, + size, + sx, + outlined = true, +}: QuickstartImageIconProps) => ( + + + +); + /** * Renders quickstart icons from system icons, image/SVG URLs, or Material * ligatures for legacy config ids. Falls back to a MUI outlined widget icon. @@ -64,25 +98,18 @@ export const QuickstartIcon = ({ } if (icon.startsWith(' - - + ); } - if ( - icon.startsWith('https://') || - icon.startsWith('http://') || - icon.startsWith('/') || - icon.startsWith('data:image/') - ) { - return ( - - - - ); + if (isRemoteOrPathImage(icon)) { + return ; } if (shouldUseMaterialLigature(icon)) { From a1377850747fca2537ab83e2a380604b8db639e5 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Tue, 22 Sep 2026 10:30:55 +0530 Subject: [PATCH 4/6] chore(quickstart): bump global-header to ^3.1.0 for /blueprints Use the published package that exports the thin blueprints entry so CI resolves the import without a local link. Co-authored-by: Cursor --- .../quickstart/packages/app-legacy/package.json | 2 +- workspaces/quickstart/packages/app/package.json | 2 +- .../quickstart/plugins/quickstart/package.json | 2 +- workspaces/quickstart/yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/workspaces/quickstart/packages/app-legacy/package.json b/workspaces/quickstart/packages/app-legacy/package.json index ccb3933d673..eb2e1a8eb0f 100644 --- a/workspaces/quickstart/packages/app-legacy/package.json +++ b/workspaces/quickstart/packages/app-legacy/package.json @@ -48,7 +48,7 @@ "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", "@mui/styles": "5.18.0", - "@red-hat-developer-hub/backstage-plugin-global-header": "^3.0.0", + "@red-hat-developer-hub/backstage-plugin-global-header": "^3.1.0", "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^", "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11", "react": "^18.0.2", diff --git a/workspaces/quickstart/packages/app/package.json b/workspaces/quickstart/packages/app/package.json index 126372f2e9c..f263711a102 100644 --- a/workspaces/quickstart/packages/app/package.json +++ b/workspaces/quickstart/packages/app/package.json @@ -44,7 +44,7 @@ "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", "@red-hat-developer-hub/backstage-plugin-app-react": "^1.0.0", - "@red-hat-developer-hub/backstage-plugin-global-header": "^3.0.0", + "@red-hat-developer-hub/backstage-plugin-global-header": "^3.1.0", "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^", "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11", "material-icons": "^1.13.14", diff --git a/workspaces/quickstart/plugins/quickstart/package.json b/workspaces/quickstart/plugins/quickstart/package.json index aab5ccf1d3a..e48ead8bad3 100644 --- a/workspaces/quickstart/plugins/quickstart/package.json +++ b/workspaces/quickstart/plugins/quickstart/package.json @@ -46,7 +46,7 @@ "@mui/material": "5.18.0", "@patternfly/react-icons": "^6.5.0", "@red-hat-developer-hub/backstage-plugin-app-react": "^1.0.0", - "@red-hat-developer-hub/backstage-plugin-global-header": "^3.0.0", + "@red-hat-developer-hub/backstage-plugin-global-header": "^3.1.0", "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11", "react-use": "^17.6.0" }, diff --git a/workspaces/quickstart/yarn.lock b/workspaces/quickstart/yarn.lock index 382956a94c2..29845717d36 100644 --- a/workspaces/quickstart/yarn.lock +++ b/workspaces/quickstart/yarn.lock @@ -9243,9 +9243,9 @@ __metadata: languageName: node linkType: hard -"@red-hat-developer-hub/backstage-plugin-global-header@npm:^3.0.0": - version: 3.0.0 - resolution: "@red-hat-developer-hub/backstage-plugin-global-header@npm:3.0.0" +"@red-hat-developer-hub/backstage-plugin-global-header@npm:^3.1.0": + version: 3.1.0 + resolution: "@red-hat-developer-hub/backstage-plugin-global-header@npm:3.1.0" dependencies: "@backstage/catalog-model": "npm:^1.10.0" "@backstage/core-components": "npm:^0.18.13" @@ -9275,7 +9275,7 @@ __metadata: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 react-router-dom: ^6.0.0 - checksum: 10c0/29f3d6ee62425c889781cacb7601c119b46fe2c85eeefc6327182ce274a06ba8996f2bd39a92f19e90260ef5792aa48282a9cb94045702a0ce92e4ac8a0e9a36 + checksum: 10c0/49f607d144051fac3e2c8ed4e15bfd46c7c996427358cb9013c606b974ea6807702a78b235da44ff6d4ed75bfcdd9e5297478876173d0f21d16edcbb5254bc26 languageName: node linkType: hard @@ -9299,7 +9299,7 @@ __metadata: "@mui/material": "npm:5.18.0" "@patternfly/react-icons": "npm:^6.5.0" "@red-hat-developer-hub/backstage-plugin-app-react": "npm:^1.0.0" - "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.0.0" + "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.1.0" "@red-hat-developer-hub/backstage-plugin-theme": "npm:^0.14.11" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^14.0.0" @@ -13542,7 +13542,7 @@ __metadata: "@mui/material": "npm:5.18.0" "@mui/styles": "npm:5.18.0" "@playwright/test": "npm:1.61.1" - "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.0.0" + "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.1.0" "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^" "@red-hat-developer-hub/backstage-plugin-theme": "npm:^0.14.11" "@testing-library/dom": "npm:^9.0.0" @@ -13602,7 +13602,7 @@ __metadata: "@mui/material": "npm:5.18.0" "@playwright/test": "npm:1.61.1" "@red-hat-developer-hub/backstage-plugin-app-react": "npm:^1.0.0" - "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.0.0" + "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^3.1.0" "@red-hat-developer-hub/backstage-plugin-quickstart": "workspace:^" "@red-hat-developer-hub/backstage-plugin-theme": "npm:^0.14.11" "@testing-library/dom": "npm:^9.0.0" From bad2477c078fb01e4c01e2e3e6135a0817642833 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Tue, 22 Sep 2026 17:17:00 +0530 Subject: [PATCH 5/6] fix(quickstart): i18n help menu Quick start label Use t('button.quickstart') so the help-dropdown item matches other Quickstart UI strings. Co-authored-by: Cursor --- .../plugins/quickstart/src/QuickstartHelpMenuItem.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.tsx b/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.tsx index 26a52dba9b9..75cb74e0a05 100644 --- a/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/QuickstartHelpMenuItem.tsx @@ -22,6 +22,7 @@ import { useTheme } from '@mui/material/styles'; import { QuickstartIcon } from './components/QuickstartContent/QuickstartIcon'; import { QUICKSTART_DRAWER_ID } from './const'; +import { useTranslation } from './hooks/useTranslation'; /** * Help-dropdown menu item that toggles the Quick start drawer. @@ -38,6 +39,7 @@ export const QuickstartHelpMenuItem = ({ handleClose?: () => void; }) => { const theme = useTheme(); + const { t } = useTranslation(); const { toggleDrawer } = useAppDrawer(); const handleClick = () => { @@ -88,7 +90,7 @@ export const QuickstartHelpMenuItem = ({ }} /> - Quick start + {t('button.quickstart')} From 7b0ef48225d75928d4fb8726b562e58d2c5ba857 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Tue, 22 Sep 2026 19:13:30 +0530 Subject: [PATCH 6/6] docs(quickstart): clarify dual legacy icon map invariant Document that drawer commonIcons and QuickstartIcon aliases intentionally diverge (MUI vs ligature/system ids) so future edits keep both in sync. Co-authored-by: Cursor --- .../components/QuickstartContent/QuickstartItem.tsx | 1 + .../QuickstartContent/QuickstartItemIcon.tsx | 10 ++++++++++ .../components/QuickstartContent/quickstartIconIds.ts | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.tsx index 34d297ae6d5..d53bbd66eab 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItem.tsx @@ -21,6 +21,7 @@ import ListItem from '@mui/material/ListItem'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import { useEffect, useState } from 'react'; + import { QuickstartItemIcon } from './QuickstartItemIcon'; import { QuickstartCtaLink } from './QuickstartCtaLink'; import { QuickstartIcon } from './QuickstartIcon'; diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.tsx b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.tsx index b64aaab3620..94021ae6077 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.tsx +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/QuickstartItemIcon.tsx @@ -35,6 +35,15 @@ export interface QuickstartItemIconProps { sx?: SxProps; } +/** + * Legacy PascalCase step-type ids → MUI outlined components for the drawer. + * + * Checked before `QuickstartIcon` so step rows keep the historical OFS glyphs. + * These are intentionally separate from `quickstartLegacyIconAliases`, which + * maps the same keys to system / Material ligature ids for the generic + * `QuickstartIcon` path (help menu, CTAs, etc.). Updating one map does not + * update the other — keep both in sync when adding a new legacy id. + */ const commonIcons: Record< string, ComponentType> @@ -62,6 +71,7 @@ export const QuickstartItemIcon = ({ icon, sx }: QuickstartItemIconProps) => { ); } + // Prefer commonIcons over system icons so legacy step ids keep OFS MUI glyphs. const CommonIcon = commonIcons[icon]; if (CommonIcon) { return ( diff --git a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/quickstartIconIds.ts b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/quickstartIconIds.ts index 83cb1351dd5..e8c0c001ed6 100644 --- a/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/quickstartIconIds.ts +++ b/workspaces/quickstart/plugins/quickstart/src/components/QuickstartContent/quickstartIconIds.ts @@ -16,7 +16,11 @@ /** * Legacy PascalCase quickstart config icon ids mapped to RHDH common system / - * Material icon ligature ids. + * Material icon ligature ids for `QuickstartIcon`. + * + * Drawer step rows use a separate MUI map in `QuickstartItemIcon` (`commonIcons`) + * and short-circuit before this path — same keys, different glyph targets. + * Keep both maps aligned when adding a legacy id. * * @see `@red-hat-developer-hub/backstage-plugin-app-defaults` `commonIcons` */