From 00f7d2e6f012110a7442ce7579d4f24b50b43769 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Tue, 28 Jul 2026 14:27:26 +0530 Subject: [PATCH 1/3] feat(orchestrator): reduce NFS Module Federation sync size Lazy-load heavy dependencies to reduce sync bundle size: - Replace OrchestratorIcon (MUI SvgIcon) with inline SVG in alpha.tsx - Extract widget rendering into FormDecoratorContent and dynamically import it in FormWidgetsApi, moving the ~860 KB RJSF widget graph from sync to async Co-Authored-By: Claude Opus 4.6 --- .../.changeset/lazy-load-nfs-sync.md | 6 ++ .../src/FormDecoratorContent.tsx | 67 +++++++++++++++++++ .../src/FormWidgetsApi.test.tsx | 11 ++- .../src/FormWidgetsApi.tsx | 62 +++++++---------- .../plugins/orchestrator/src/alpha.tsx | 19 +++++- 5 files changed, 125 insertions(+), 40 deletions(-) create mode 100644 workspaces/orchestrator/.changeset/lazy-load-nfs-sync.md create mode 100644 workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx diff --git a/workspaces/orchestrator/.changeset/lazy-load-nfs-sync.md b/workspaces/orchestrator/.changeset/lazy-load-nfs-sync.md new file mode 100644 index 00000000000..47aebd37af7 --- /dev/null +++ b/workspaces/orchestrator/.changeset/lazy-load-nfs-sync.md @@ -0,0 +1,6 @@ +--- +'@red-hat-developer-hub/backstage-plugin-orchestrator': patch +'@red-hat-developer-hub/backstage-plugin-orchestrator-form-widgets': patch +--- + +Reduce NFS Module Federation sync size by lazy-loading heavy dependencies. diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx new file mode 100644 index 00000000000..5624ac16400 --- /dev/null +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx @@ -0,0 +1,67 @@ +/* + * 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 React from 'react'; +import { + FormDecoratorProps, + OrchestratorFormContextProps, +} from '@red-hat-developer-hub/backstage-plugin-orchestrator-form-api'; +import { FormValidation } from '@rjsf/utils'; +import { JsonObject } from '@backstage/types'; + +import { + SchemaUpdater, + ActiveTextInput, + ActiveText, + ActiveDropdown, + ActiveMultiSelect, +} from './widgets'; +import { useGetExtraErrors } from './utils'; + +const customValidate = ( + _formData: JsonObject | undefined, + errors: FormValidation, +): FormValidation => { + return errors; +}; + +const widgets = { + SchemaUpdater, + ActiveTextInput, + ActiveText, + ActiveDropdown, + ActiveMultiSelect, +}; + +const FormDecoratorContent = ({ + FormComponent, + ...props +}: { + FormComponent: React.ComponentType; +} & OrchestratorFormContextProps) => { + const getExtraErrors = useGetExtraErrors(); + + return ( + + ); +}; + +export default FormDecoratorContent; diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.test.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.test.tsx index 68052a80ee2..571bbcdb73f 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.test.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { ComponentType } from 'react'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import { FormWidgetsApi } from './FormWidgetsApi'; import * as utils from './utils'; @@ -39,7 +39,10 @@ describe('FormWidgetsApi', () => { expect(api.getReviewComponent?.()).toBeUndefined(); }); - it('decorates form component with widgets and context props', () => { + it('decorates form component with widgets and context props', async () => { + // Pre-load the lazy module so import() resolves from cache + await import('./FormDecoratorContent'); + const api = new FormWidgetsApi(); const receivedProps: Record[] = []; @@ -61,6 +64,10 @@ describe('FormWidgetsApi', () => { />, ); + // Flush the microtask queue so the dynamic import resolves + // and the state update re-renders the component + await act(async () => {}); + expect(receivedProps).toHaveLength(1); expect(receivedProps[0].widgets).toEqual( expect.objectContaining({ diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.tsx index 8a409170a3c..0c56d69e714 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.tsx @@ -20,51 +20,41 @@ import { OrchestratorFormApi, OrchestratorFormContextProps, } from '@red-hat-developer-hub/backstage-plugin-orchestrator-form-api'; -import { FormValidation } from '@rjsf/utils'; -import { JsonObject } from '@backstage/types'; - -import { - SchemaUpdater, - ActiveTextInput, - ActiveText, - ActiveDropdown, - ActiveMultiSelect, -} from './widgets'; -import { useGetExtraErrors } from './utils'; - -const customValidate = ( - _formData: JsonObject | undefined, - errors: FormValidation, -): FormValidation => { - // Trigger synchronous field validation - return errors; -}; - -const widgets = { - SchemaUpdater, - ActiveTextInput, - ActiveText, - ActiveDropdown, - ActiveMultiSelect, -}; export class FormWidgetsApi implements OrchestratorFormApi { + private contentPromise: Promise< + typeof import('./FormDecoratorContent') + > | null = null; + getFormDecorator: OrchestratorFormApi['getFormDecorator'] = () => { // eslint-disable-next-line no-console console.log('Using FormWidgetsApi by RHDH orchestrator-form-widgets.'); + if (!this.contentPromise) { + this.contentPromise = import('./FormDecoratorContent'); + } + const contentPromise = this.contentPromise; + return (FormComponent: React.ComponentType) => { return (props: OrchestratorFormContextProps) => { - const getExtraErrors = useGetExtraErrors(); + const [DecoratorContent, setDecoratorContent] = + React.useState | null>(null); + + React.useEffect(() => { + let mounted = true; + contentPromise.then(m => { + if (mounted) setDecoratorContent(() => m.default); + }); + return () => { + mounted = false; + }; + }, []); + + if (!DecoratorContent) { + return null; + } - return ( - - ); + return ; }; }; }; diff --git a/workspaces/orchestrator/plugins/orchestrator/src/alpha.tsx b/workspaces/orchestrator/plugins/orchestrator/src/alpha.tsx index 0c54fdc5527..363ed7b7e0d 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/alpha.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/alpha.tsx @@ -35,7 +35,6 @@ import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className'; import { orchestratorApiRef, OrchestratorClient } from './api'; -import OrchestratorIcon from './components/OrchestratorIcon'; import { entityInstanceRouteRef, entityWorkflowRouteRef, @@ -58,7 +57,23 @@ const orchestratorPage = PageBlueprint.make({ path: '/orchestrator', routeRef: orchestratorRootRouteRef, title: 'Orchestrator', - icon: , + icon: ( + + + + + + + + + + ), noHeader: true, loader: () => import('./components/Router').then(m => ), }, From ba3b956fb14fdd31be4f5c93fe2cd917e470c717 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Tue, 28 Jul 2026 14:36:09 +0530 Subject: [PATCH 2/3] chore: update API reports Co-Authored-By: Claude Opus 4.6 --- .../plugins/orchestrator/report-alpha.api.md | 10 +++++----- .../orchestrator/plugins/orchestrator/report.api.md | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator/report-alpha.api.md b/workspaces/orchestrator/plugins/orchestrator/report-alpha.api.md index f6da73896d3..3256ed1be90 100644 --- a/workspaces/orchestrator/plugins/orchestrator/report-alpha.api.md +++ b/workspaces/orchestrator/plugins/orchestrator/report-alpha.api.md @@ -228,10 +228,10 @@ export const orchestratorTranslationRef: TranslationRef< readonly 'table.headers.description': string; readonly 'table.headers.version': string; readonly 'table.headers.duration': string; - readonly 'table.headers.status': string; readonly 'table.headers.entity': string; readonly 'table.headers.runStatus': string; readonly 'table.headers.started': string; + readonly 'table.headers.status': string; readonly 'table.headers.workflowStatus': string; readonly 'table.headers.lastRun': string; readonly 'table.headers.lastRunStatus': string; @@ -244,11 +244,11 @@ export const orchestratorTranslationRef: TranslationRef< readonly 'table.actions.viewRuns': string; readonly 'table.actions.viewInputSchema': string; readonly 'table.actions.viewRunVariables': string; - readonly 'table.filters.placeholder': string; - readonly 'table.filters.status': string; readonly 'table.filters.entity': string; readonly 'table.filters.started': string; + readonly 'table.filters.status': string; readonly 'table.filters.runBy': string; + readonly 'table.filters.placeholder': string; readonly 'table.filters.clearAll': string; readonly 'table.filters.startedOptions.today': string; readonly 'table.filters.startedOptions.yesterday': string; @@ -313,9 +313,9 @@ export const orchestratorTranslationRef: TranslationRef< readonly 'run.logs.noLogsAvailable': string; readonly 'run.abort.button': string; readonly 'run.abort.title': string; - readonly 'run.abort.warning': string; readonly 'run.abort.completed.title': string; readonly 'run.abort.completed.message': string; + readonly 'run.abort.warning': string; readonly 'run.retrigger': string; readonly 'run.viewVariables': string; readonly 'run.suggestedNextWorkflow': string; @@ -326,10 +326,10 @@ export const orchestratorTranslationRef: TranslationRef< readonly 'workflow.errors.abortFailed': string; readonly 'workflow.errors.abortFailedWithReason': string; readonly 'workflow.errors.failedToLoadDetails': string; - readonly 'workflow.definition': string; readonly 'workflow.status.available': string; readonly 'workflow.status.unavailable': string; readonly 'workflow.successRatio': string; + readonly 'workflow.definition': string; readonly 'workflow.inputSchema': string; readonly 'workflow.inputSchemaDescription': string; readonly 'workflow.successRatioDescription': string; diff --git a/workspaces/orchestrator/plugins/orchestrator/report.api.md b/workspaces/orchestrator/plugins/orchestrator/report.api.md index e3053aef832..44cc2b5bed4 100644 --- a/workspaces/orchestrator/plugins/orchestrator/report.api.md +++ b/workspaces/orchestrator/plugins/orchestrator/report.api.md @@ -45,10 +45,10 @@ export const orchestratorTranslationRef: TranslationRef< readonly 'table.headers.description': string; readonly 'table.headers.version': string; readonly 'table.headers.duration': string; - readonly 'table.headers.status': string; readonly 'table.headers.entity': string; readonly 'table.headers.runStatus': string; readonly 'table.headers.started': string; + readonly 'table.headers.status': string; readonly 'table.headers.workflowStatus': string; readonly 'table.headers.lastRun': string; readonly 'table.headers.lastRunStatus': string; @@ -61,11 +61,11 @@ export const orchestratorTranslationRef: TranslationRef< readonly 'table.actions.viewRuns': string; readonly 'table.actions.viewInputSchema': string; readonly 'table.actions.viewRunVariables': string; - readonly 'table.filters.placeholder': string; - readonly 'table.filters.status': string; readonly 'table.filters.entity': string; readonly 'table.filters.started': string; + readonly 'table.filters.status': string; readonly 'table.filters.runBy': string; + readonly 'table.filters.placeholder': string; readonly 'table.filters.clearAll': string; readonly 'table.filters.startedOptions.today': string; readonly 'table.filters.startedOptions.yesterday': string; @@ -130,9 +130,9 @@ export const orchestratorTranslationRef: TranslationRef< readonly 'run.logs.noLogsAvailable': string; readonly 'run.abort.button': string; readonly 'run.abort.title': string; - readonly 'run.abort.warning': string; readonly 'run.abort.completed.title': string; readonly 'run.abort.completed.message': string; + readonly 'run.abort.warning': string; readonly 'run.retrigger': string; readonly 'run.viewVariables': string; readonly 'run.suggestedNextWorkflow': string; @@ -143,10 +143,10 @@ export const orchestratorTranslationRef: TranslationRef< readonly 'workflow.errors.abortFailed': string; readonly 'workflow.errors.abortFailedWithReason': string; readonly 'workflow.errors.failedToLoadDetails': string; - readonly 'workflow.definition': string; readonly 'workflow.status.available': string; readonly 'workflow.status.unavailable': string; readonly 'workflow.successRatio': string; + readonly 'workflow.definition': string; readonly 'workflow.inputSchema': string; readonly 'workflow.inputSchemaDescription': string; readonly 'workflow.successRatioDescription': string; From 9bf2d58f28cac93758cc27529b362a41ae03e8fe Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Wed, 29 Jul 2026 11:30:10 +0530 Subject: [PATCH 3/3] fix: address PR review feedback - Remove unnecessary `import React from 'react'` in FormDecoratorContent and FormWidgetsApi, use named imports instead - Move inline SVG from alpha.tsx back to OrchestratorIcon component, replacing MUI SvgIcon with plain element - Update API reports Co-Authored-By: Claude Opus 4.6 --- .../report.api.md | 2 - .../orchestrator-backend/report.api.md | 2 - .../src/FormDecoratorContent.tsx | 4 +- .../src/FormWidgetsApi.tsx | 9 ++-- .../plugins/orchestrator/report.api.md | 6 ++- .../plugins/orchestrator/src/alpha.tsx | 19 +------ .../src/components/OrchestratorIcon.tsx | 50 +++++++------------ .../report.api.md | 2 - 8 files changed, 31 insertions(+), 63 deletions(-) diff --git a/workspaces/orchestrator/plugins/orchestrator-backend-module-loki/report.api.md b/workspaces/orchestrator/plugins/orchestrator-backend-module-loki/report.api.md index 4a68f9763d2..b1921d2b965 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend-module-loki/report.api.md +++ b/workspaces/orchestrator/plugins/orchestrator-backend-module-loki/report.api.md @@ -3,11 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts - import { BackendFeature } from '@backstage/backend-plugin-api'; // @public const orchestratorModuleLoki: BackendFeature; export default orchestratorModuleLoki; - ``` diff --git a/workspaces/orchestrator/plugins/orchestrator-backend/report.api.md b/workspaces/orchestrator/plugins/orchestrator-backend/report.api.md index d7788cf2af2..8e5572e0bcc 100644 --- a/workspaces/orchestrator/plugins/orchestrator-backend/report.api.md +++ b/workspaces/orchestrator/plugins/orchestrator-backend/report.api.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts - import { BackendFeature } from '@backstage/backend-plugin-api'; // @public @@ -11,5 +10,4 @@ const orchestratorPlugin: BackendFeature; export default orchestratorPlugin; // (No @packageDocumentation comment for this package) - ``` diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx index 5624ac16400..ce8a6b1c13e 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormDecoratorContent.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import type { ComponentType } from 'react'; import { FormDecoratorProps, OrchestratorFormContextProps, @@ -50,7 +50,7 @@ const FormDecoratorContent = ({ FormComponent, ...props }: { - FormComponent: React.ComponentType; + FormComponent: ComponentType; } & OrchestratorFormContextProps) => { const getExtraErrors = useGetExtraErrors(); diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.tsx index 0c56d69e714..2029e2b5868 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/FormWidgetsApi.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import React from 'react'; +import { useEffect, useState } from 'react'; +import type { ComponentType } from 'react'; import { FormDecoratorProps, OrchestratorFormApi, @@ -35,12 +36,12 @@ export class FormWidgetsApi implements OrchestratorFormApi { } const contentPromise = this.contentPromise; - return (FormComponent: React.ComponentType) => { + return (FormComponent: ComponentType) => { return (props: OrchestratorFormContextProps) => { const [DecoratorContent, setDecoratorContent] = - React.useState | null>(null); + useState | null>(null); - React.useEffect(() => { + useEffect(() => { let mounted = true; contentPromise.then(m => { if (mounted) setDecoratorContent(() => m.default); diff --git a/workspaces/orchestrator/plugins/orchestrator/report.api.md b/workspaces/orchestrator/plugins/orchestrator/report.api.md index 44cc2b5bed4..064494da8a4 100644 --- a/workspaces/orchestrator/plugins/orchestrator/report.api.md +++ b/workspaces/orchestrator/plugins/orchestrator/report.api.md @@ -7,7 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { RouteRef } from '@backstage/core-plugin-api'; -import { SvgIconProps } from '@mui/material/SvgIcon'; +import type { SVGProps } from 'react'; import { TranslationRef } from '@backstage/frontend-plugin-api'; import { TranslationResource } from '@backstage/frontend-plugin-api'; @@ -20,7 +20,9 @@ export const IsOrchestratorCatalogTabAvailable: (entity: Entity) => boolean; export const OrchestratorCatalogTab: () => JSX_2.Element; // @public -export const OrchestratorIcon: (props: SvgIconProps) => JSX_2.Element; +export const OrchestratorIcon: ( + props: SVGProps, +) => JSX_2.Element; // @public export const OrchestratorPage: () => JSX_2.Element; diff --git a/workspaces/orchestrator/plugins/orchestrator/src/alpha.tsx b/workspaces/orchestrator/plugins/orchestrator/src/alpha.tsx index 363ed7b7e0d..0c54fdc5527 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/alpha.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/alpha.tsx @@ -35,6 +35,7 @@ import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className'; import { orchestratorApiRef, OrchestratorClient } from './api'; +import OrchestratorIcon from './components/OrchestratorIcon'; import { entityInstanceRouteRef, entityWorkflowRouteRef, @@ -57,23 +58,7 @@ const orchestratorPage = PageBlueprint.make({ path: '/orchestrator', routeRef: orchestratorRootRouteRef, title: 'Orchestrator', - icon: ( - - - - - - - - - - ), + icon: , noHeader: true, loader: () => import('./components/Router').then(m => ), }, diff --git a/workspaces/orchestrator/plugins/orchestrator/src/components/OrchestratorIcon.tsx b/workspaces/orchestrator/plugins/orchestrator/src/components/OrchestratorIcon.tsx index 4351cd39512..803d3f7bd49 100644 --- a/workspaces/orchestrator/plugins/orchestrator/src/components/OrchestratorIcon.tsx +++ b/workspaces/orchestrator/plugins/orchestrator/src/components/OrchestratorIcon.tsx @@ -14,43 +14,29 @@ * limitations under the License. */ -import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; +import type { SVGProps } from 'react'; /** * @public * Orchestrator icon */ -const OrchestratorIcon = (props: SvgIconProps) => ( - - - - - - - - - +const OrchestratorIcon = (props: SVGProps) => ( + + + + + + + + + ); export default OrchestratorIcon; diff --git a/workspaces/orchestrator/plugins/scaffolder-backend-module-orchestrator/report.api.md b/workspaces/orchestrator/plugins/scaffolder-backend-module-orchestrator/report.api.md index e94e938da72..9ed375dd302 100644 --- a/workspaces/orchestrator/plugins/scaffolder-backend-module-orchestrator/report.api.md +++ b/workspaces/orchestrator/plugins/scaffolder-backend-module-orchestrator/report.api.md @@ -3,7 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts - import { BackendFeature } from '@backstage/backend-plugin-api'; // Warning: (ae-missing-release-tag) "scaffolderModule" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -15,5 +14,4 @@ export default scaffolderModule; // Warnings were encountered during analysis: // // src/index.d.ts:2:32 - (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" - ```