diff --git a/workspaces/cost-management/.changeset/nfs-frontend-alpha.md b/workspaces/cost-management/.changeset/nfs-frontend-alpha.md
new file mode 100644
index 00000000000..1d6c8d718dd
--- /dev/null
+++ b/workspaces/cost-management/.changeset/nfs-frontend-alpha.md
@@ -0,0 +1,5 @@
+---
+'@red-hat-developer-hub/plugin-cost-management': minor
+---
+
+Add a New Frontend System `/alpha` export so the plugin loads in RHDH 2.1, with Optimizations and Cost Management sidebar pages. Legacy PluginRoot exports are unchanged.
diff --git a/workspaces/cost-management/AGENTS.md b/workspaces/cost-management/AGENTS.md
index 36f50cd35c3..0f16935bec2 100644
--- a/workspaces/cost-management/AGENTS.md
+++ b/workspaces/cost-management/AGENTS.md
@@ -5,9 +5,11 @@
Run all commands from `workspaces/cost-management/` (the workspace root).
- Install: `yarn install`
-- Dev (full app + backend, slow): `yarn start`
+- Dev (NFS app + full backend, RBAC): `yarn start`
+- Dev (legacy app + full backend): `yarn start:legacy`
- Dev (plugin hot-reload, fast): `yarn start:dev`
-- Dev (frontend plugin only): `yarn start:fe-plugin`
+- Dev (NFS frontend plugin only): `yarn start:fe-plugin`
+- Dev (legacy frontend plugin only): `yarn start:fe-plugin:legacy`
- Dev (backend plugin only): `yarn start:be-plugin`
- Build all: `yarn build:all`
- Test all (with coverage): `yarn test:all`
@@ -33,6 +35,9 @@ workspaces/cost-management/
│ └── cost-management-common/ # Shared types, API clients, permissions
│ └── src/generated/ # Auto-generated OpenAPI types — do not hand-edit
├── packages/ # Dev environment shell only (app + backend wiring)
+│ ├── app/ # NFS frontend (`yarn start`)
+│ ├── app-legacy/ # Legacy frontend (`yarn start:legacy`)
+│ └── backend/ # Full backend with RBAC
├── docs/
│ ├── rbac.md # Full RBAC permission reference
│ └── dynamic-plugin.md # Dynamic plugin installation guide
diff --git a/workspaces/cost-management/docs/dynamic-plugin.md b/workspaces/cost-management/docs/dynamic-plugin.md
index 41833aebb51..c0c84af7b38 100644
--- a/workspaces/cost-management/docs/dynamic-plugin.md
+++ b/workspaces/cost-management/docs/dynamic-plugin.md
@@ -80,3 +80,20 @@ The procedure involves the following steps:
> proxy. SSO tokens are obtained internally via OAuth2 `client_credentials`
> grant and never exposed to the browser. RBAC filtering is enforced
> server-side before data is returned. See [rbac.md](./rbac.md) for details.
+
+5. **RHDH 2.1 (New Frontend System):** enable the same OCI packages. Do **not** require `dynamicRoutes` / `importName`. Pages are discovered from the plugin `/alpha` export. Optional operator overrides:
+
+ ```yaml
+ app:
+ extensions:
+ - page:cost-management:
+ config:
+ title: Optimizations
+ path: /cost-management/optimizations
+ - page:cost-management/openshift:
+ config:
+ title: Cost Management
+ path: /cost-management/openshift
+ ```
+
+ The sidebar shows two items (**Optimizations** and **Cost Management**). Nested `menuItems.parent` is not available on NFS ([RHDH-specific gaps](https://github.com/redhat-developer/rhdh/blob/main/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md#rhdh-specific-gaps)). Enable NFS on the backend with `APP_CONFIG_app_packageName=app-next` and `ENABLE_STANDARD_MODULE_FEDERATION=true` ([config migration](https://github.com/redhat-developer/rhdh/blob/main/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md)).
diff --git a/workspaces/cost-management/package.json b/workspaces/cost-management/package.json
index 73b59283998..8a37e756227 100644
--- a/workspaces/cost-management/package.json
+++ b/workspaces/cost-management/package.json
@@ -6,10 +6,12 @@
"node": "22 || 24"
},
"scripts": {
- "start": "concurrently -c auto -n \"fe,be\" -p \"{name}:{pid}\" \"yarn start-app\" \"yarn start-backend\"",
+ "start": "backstage-cli repo start",
+ "start:legacy": "backstage-cli repo start packages/app-legacy packages/backend",
"start-app": "yarn workspace app start",
"start-backend": "yarn workspace backend start",
"start:fe-plugin": "yarn workspace @red-hat-developer-hub/plugin-cost-management start",
+ "start:fe-plugin:legacy": "yarn workspace @red-hat-developer-hub/plugin-cost-management start:legacy",
"start:be-plugin": "yarn workspace @red-hat-developer-hub/plugin-cost-management-backend start",
"start:dev": "concurrently -c auto -n \"fe,be\" -p \"{name}:{pid}\" \"yarn start:fe-plugin\" \"yarn start:be-plugin\"",
"tsc": "tsc",
diff --git a/workspaces/cost-management/packages/app-legacy/.eslintignore b/workspaces/cost-management/packages/app-legacy/.eslintignore
new file mode 100644
index 00000000000..a48cf0de7af
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/.eslintignore
@@ -0,0 +1 @@
+public
diff --git a/workspaces/cost-management/packages/app-legacy/.eslintrc.js b/workspaces/cost-management/packages/app-legacy/.eslintrc.js
new file mode 100644
index 00000000000..e2a53a6ad28
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/workspaces/cost-management/packages/app/e2e-tests/README.md b/workspaces/cost-management/packages/app-legacy/e2e-tests/README.md
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/README.md
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/README.md
diff --git a/workspaces/cost-management/packages/app/e2e-tests/TEST-COVERAGE.md b/workspaces/cost-management/packages/app-legacy/e2e-tests/TEST-COVERAGE.md
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/TEST-COVERAGE.md
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/TEST-COVERAGE.md
diff --git a/workspaces/cost-management/packages/app/e2e-tests/app.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/app.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/app.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/app.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/apply-recommendation.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/apply-recommendation.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/apply-recommendation.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/apply-recommendation.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/dark-theme.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/dark-theme.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/dark-theme.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/dark-theme.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/fixtures/auth.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/fixtures/auth.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/fixtures/auth.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/fixtures/auth.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/fixtures/optimizationResponses.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/fixtures/optimizationResponses.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/fixtures/optimizationResponses.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/fixtures/optimizationResponses.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/global-setup.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/global-setup.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/global-setup.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/global-setup.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/live-cluster.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/live-cluster.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/live-cluster.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/live-cluster.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/marketplace.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/marketplace.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/marketplace.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/marketplace.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/navigation.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/navigation.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/navigation.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/navigation.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/openshift-cost-management.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/openshift-cost-management.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/openshift-cost-management.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/openshift-cost-management.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/optimization.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/optimization.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/optimization.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/optimization.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/pages/ResourceOptimizationPage.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/pages/ResourceOptimizationPage.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/pages/ResourceOptimizationPage.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/pages/ResourceOptimizationPage.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/rbac-dynamic-permissions.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/rbac-dynamic-permissions.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/rbac-dynamic-permissions.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/rbac-dynamic-permissions.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/rbac.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/rbac.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/rbac.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/rbac.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/secure-proxy.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/secure-proxy.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/secure-proxy.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/secure-proxy.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/table-and-pagination.test.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/table-and-pagination.test.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/table-and-pagination.test.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/table-and-pagination.test.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/utils/apiUtils.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/utils/apiUtils.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/utils/apiUtils.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/utils/apiUtils.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/utils/devMode.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/utils/devMode.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/utils/devMode.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/utils/devMode.ts
diff --git a/workspaces/cost-management/packages/app/e2e-tests/utils/routes.ts b/workspaces/cost-management/packages/app-legacy/e2e-tests/utils/routes.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/e2e-tests/utils/routes.ts
rename to workspaces/cost-management/packages/app-legacy/e2e-tests/utils/routes.ts
diff --git a/workspaces/cost-management/packages/app-legacy/package.json b/workspaces/cost-management/packages/app-legacy/package.json
new file mode 100644
index 00000000000..9cae7ea8b2c
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/package.json
@@ -0,0 +1,89 @@
+{
+ "name": "app-legacy",
+ "version": "0.0.5",
+ "private": true,
+ "bundled": true,
+ "backstage": {
+ "role": "frontend"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/redhat-developer/rhdh-plugins",
+ "directory": "workspaces/cost-management/packages/app-legacy"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "clean": "backstage-cli package clean",
+ "test": "backstage-cli package test",
+ "lint": "backstage-cli package lint"
+ },
+ "dependencies": {
+ "@backstage-community/plugin-rbac": "1.55.2",
+ "@backstage-community/plugin-tech-radar": "^0.7.4",
+ "@backstage/app-defaults": "^1.7.11",
+ "@backstage/catalog-model": "^1.10.0",
+ "@backstage/cli": "^0.36.5",
+ "@backstage/core-app-api": "^1.20.4",
+ "@backstage/core-components": "^0.18.13",
+ "@backstage/core-plugin-api": "^1.12.9",
+ "@backstage/integration-react": "^1.2.21",
+ "@backstage/plugin-api-docs": "^0.14.4",
+ "@backstage/plugin-catalog": "^2.0.8",
+ "@backstage/plugin-catalog-common": "^1.1.10",
+ "@backstage/plugin-catalog-graph": "^0.6.7",
+ "@backstage/plugin-catalog-import": "^0.13.17",
+ "@backstage/plugin-catalog-react": "^3.2.2",
+ "@backstage/plugin-org": "^0.7.8",
+ "@backstage/plugin-permission-react": "^0.5.4",
+ "@backstage/plugin-scaffolder": "^1.38.2",
+ "@backstage/plugin-search": "^1.7.7",
+ "@backstage/plugin-search-react": "^1.11.7",
+ "@backstage/plugin-techdocs": "^1.18.0",
+ "@backstage/plugin-techdocs-module-addons-contrib": "^1.1.39",
+ "@backstage/plugin-techdocs-react": "^1.3.14",
+ "@backstage/plugin-user-settings": "^0.9.6",
+ "@backstage/theme": "^0.7.3",
+ "@material-ui/core": "^4.12.2",
+ "@material-ui/icons": "^4.9.1",
+ "@patternfly/patternfly": "^6.3.0",
+ "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.0",
+ "@red-hat-developer-hub/plugin-cost-management": "workspace:^",
+ "history": "^5.0.0",
+ "react": "^18.0.2",
+ "react-dom": "^18.0.2",
+ "react-router": "^6.30.2",
+ "react-router-dom": "^6.30.2",
+ "react-use": "^17.2.4"
+ },
+ "devDependencies": {
+ "@backstage/test-utils": "^1.7.21",
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "@mui/icons-material": "^5.16.1",
+ "@mui/material": "^5.16.1",
+ "@playwright/test": "1.61.1",
+ "@testing-library/dom": "^10.0.0",
+ "@testing-library/jest-dom": "^6.0.0",
+ "@testing-library/react": "^16.0.0",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/react": "^18",
+ "@types/react-dom": "*",
+ "cross-env": "^7.0.0"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/workspaces/cost-management/packages/app-legacy/public/android-chrome-192x192.png b/workspaces/cost-management/packages/app-legacy/public/android-chrome-192x192.png
new file mode 100644
index 00000000000..eec0ae25b97
Binary files /dev/null and b/workspaces/cost-management/packages/app-legacy/public/android-chrome-192x192.png differ
diff --git a/workspaces/cost-management/packages/app-legacy/public/apple-touch-icon.png b/workspaces/cost-management/packages/app-legacy/public/apple-touch-icon.png
new file mode 100644
index 00000000000..3158830ac77
Binary files /dev/null and b/workspaces/cost-management/packages/app-legacy/public/apple-touch-icon.png differ
diff --git a/workspaces/cost-management/packages/app-legacy/public/favicon-16x16.png b/workspaces/cost-management/packages/app-legacy/public/favicon-16x16.png
new file mode 100644
index 00000000000..58cf61a35eb
Binary files /dev/null and b/workspaces/cost-management/packages/app-legacy/public/favicon-16x16.png differ
diff --git a/workspaces/cost-management/packages/app-legacy/public/favicon-32x32.png b/workspaces/cost-management/packages/app-legacy/public/favicon-32x32.png
new file mode 100644
index 00000000000..c0915ece759
Binary files /dev/null and b/workspaces/cost-management/packages/app-legacy/public/favicon-32x32.png differ
diff --git a/workspaces/cost-management/packages/app-legacy/public/favicon.ico b/workspaces/cost-management/packages/app-legacy/public/favicon.ico
new file mode 100644
index 00000000000..5e45e5dfbde
Binary files /dev/null and b/workspaces/cost-management/packages/app-legacy/public/favicon.ico differ
diff --git a/workspaces/cost-management/packages/app-legacy/public/index.html b/workspaces/cost-management/packages/app-legacy/public/index.html
new file mode 100644
index 00000000000..18da7c47733
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/public/index.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= config.getOptionalString('app.title') ?? 'Backstage' %>
+
+
+
+
+
+
+
diff --git a/workspaces/cost-management/packages/app-legacy/public/manifest.json b/workspaces/cost-management/packages/app-legacy/public/manifest.json
new file mode 100644
index 00000000000..4a7c1b4ec42
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Backstage",
+ "name": "Backstage",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "48x48",
+ "type": "image/png"
+ }
+ ],
+ "start_url": "./index.html",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/workspaces/cost-management/packages/app-legacy/public/robots.txt b/workspaces/cost-management/packages/app-legacy/public/robots.txt
new file mode 100644
index 00000000000..01b0f9a1073
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/public/robots.txt
@@ -0,0 +1,2 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
diff --git a/workspaces/cost-management/packages/app-legacy/public/safari-pinned-tab.svg b/workspaces/cost-management/packages/app-legacy/public/safari-pinned-tab.svg
new file mode 100644
index 00000000000..0f500b30023
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/public/safari-pinned-tab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/workspaces/cost-management/packages/app-legacy/src/App.test.tsx b/workspaces/cost-management/packages/app-legacy/src/App.test.tsx
new file mode 100644
index 00000000000..452cbf840ac
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/src/App.test.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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, waitFor } from '@testing-library/react';
+import App from './App';
+
+describe('App', () => {
+ it('should render', async () => {
+ process.env = {
+ NODE_ENV: 'test',
+ APP_CONFIG: [
+ {
+ data: {
+ app: { title: 'Test' },
+ backend: { baseUrl: 'http://localhost:7007' },
+ techdocs: {
+ storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
+ },
+ },
+ context: 'test',
+ },
+ ] as any,
+ };
+
+ const rendered = render();
+
+ await waitFor(() => {
+ expect(rendered.baseElement).toBeInTheDocument();
+ });
+ });
+});
diff --git a/workspaces/cost-management/packages/app-legacy/src/App.tsx b/workspaces/cost-management/packages/app-legacy/src/App.tsx
new file mode 100644
index 00000000000..cabb5c96a1a
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/src/App.tsx
@@ -0,0 +1,157 @@
+/*
+ * 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 { Navigate, Route } from 'react-router-dom';
+import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
+import {
+ CatalogEntityPage,
+ CatalogIndexPage,
+ catalogPlugin,
+} from '@backstage/plugin-catalog';
+import {
+ CatalogImportPage,
+ catalogImportPlugin,
+} from '@backstage/plugin-catalog-import';
+import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
+import { orgPlugin } from '@backstage/plugin-org';
+import { SearchPage } from '@backstage/plugin-search';
+import {
+ TechDocsIndexPage,
+ techdocsPlugin,
+ TechDocsReaderPage,
+} from '@backstage/plugin-techdocs';
+import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
+import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
+import { UserSettingsPage } from '@backstage/plugin-user-settings';
+import { apis } from './apis';
+import { entityPage } from './components/catalog/EntityPage';
+import { searchPage } from './components/search/SearchPage';
+import { Root } from './components/Root';
+import {
+ AlertDisplay,
+ OAuthRequestDialog,
+ SignInPage,
+} from '@backstage/core-components';
+import { createApp } from '@backstage/app-defaults';
+import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
+import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
+import { RequirePermission } from '@backstage/plugin-permission-react';
+import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
+import {
+ ResourceOptimizationPage,
+ OpenShiftPage,
+} from '@red-hat-developer-hub/plugin-cost-management';
+import { useRhdhTheme } from './hooks/useRhdhTheme';
+import { githubAuthApiRef } from '@backstage/core-plugin-api';
+import { RbacPage } from '@backstage-community/plugin-rbac';
+import '@patternfly/patternfly/patternfly.css';
+import '@patternfly/patternfly/patternfly-charts.css';
+
+const options: Parameters[0] = {
+ apis,
+ bindRoutes({ bind }) {
+ bind(catalogPlugin.externalRoutes, {
+ createComponent: scaffolderPlugin.routes.root,
+ viewTechDoc: techdocsPlugin.routes.docRoot,
+ createFromTemplate: scaffolderPlugin.routes.selectedTemplate,
+ });
+ bind(apiDocsPlugin.externalRoutes, {
+ registerApi: catalogImportPlugin.routes.importPage,
+ });
+ bind(scaffolderPlugin.externalRoutes, {
+ registerComponent: catalogImportPlugin.routes.importPage,
+ viewTechDoc: techdocsPlugin.routes.docRoot,
+ });
+ bind(orgPlugin.externalRoutes, {
+ catalogIndex: catalogPlugin.routes.catalogIndex,
+ });
+ },
+ components: {
+ SignInPage: props => (
+
+ ),
+ },
+};
+
+// eslint-disable-next-line react-hooks/rules-of-hooks
+const rhdhTheme = useRhdhTheme();
+if (rhdhTheme !== null) {
+ options.themes = rhdhTheme.themes;
+}
+
+const app = createApp(options);
+
+const routes = (
+
+ } />
+ } />
+ }
+ >
+ {entityPage}
+
+ } />
+ }
+ >
+
+
+
+
+ } />
+ } />
+
+
+
+ }
+ />
+ }>
+ {searchPage}
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+);
+
+export default app.createRoot(
+ <>
+
+
+
+ {routes}
+
+ >,
+);
diff --git a/workspaces/cost-management/packages/app/src/apis.ts b/workspaces/cost-management/packages/app-legacy/src/apis.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/src/apis.ts
rename to workspaces/cost-management/packages/app-legacy/src/apis.ts
diff --git a/workspaces/cost-management/packages/app/src/components/Root/LogoFull.tsx b/workspaces/cost-management/packages/app-legacy/src/components/Root/LogoFull.tsx
similarity index 100%
rename from workspaces/cost-management/packages/app/src/components/Root/LogoFull.tsx
rename to workspaces/cost-management/packages/app-legacy/src/components/Root/LogoFull.tsx
diff --git a/workspaces/cost-management/packages/app/src/components/Root/LogoIcon.tsx b/workspaces/cost-management/packages/app-legacy/src/components/Root/LogoIcon.tsx
similarity index 100%
rename from workspaces/cost-management/packages/app/src/components/Root/LogoIcon.tsx
rename to workspaces/cost-management/packages/app-legacy/src/components/Root/LogoIcon.tsx
diff --git a/workspaces/cost-management/packages/app/src/components/Root/Root.tsx b/workspaces/cost-management/packages/app-legacy/src/components/Root/Root.tsx
similarity index 100%
rename from workspaces/cost-management/packages/app/src/components/Root/Root.tsx
rename to workspaces/cost-management/packages/app-legacy/src/components/Root/Root.tsx
diff --git a/workspaces/cost-management/packages/app/src/components/Root/index.ts b/workspaces/cost-management/packages/app-legacy/src/components/Root/index.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/src/components/Root/index.ts
rename to workspaces/cost-management/packages/app-legacy/src/components/Root/index.ts
diff --git a/workspaces/cost-management/packages/app/src/components/catalog/EntityPage.tsx b/workspaces/cost-management/packages/app-legacy/src/components/catalog/EntityPage.tsx
similarity index 100%
rename from workspaces/cost-management/packages/app/src/components/catalog/EntityPage.tsx
rename to workspaces/cost-management/packages/app-legacy/src/components/catalog/EntityPage.tsx
diff --git a/workspaces/cost-management/packages/app/src/components/rhdh-logo/RhdhLogoFull.tsx b/workspaces/cost-management/packages/app-legacy/src/components/rhdh-logo/RhdhLogoFull.tsx
similarity index 100%
rename from workspaces/cost-management/packages/app/src/components/rhdh-logo/RhdhLogoFull.tsx
rename to workspaces/cost-management/packages/app-legacy/src/components/rhdh-logo/RhdhLogoFull.tsx
diff --git a/workspaces/cost-management/packages/app/src/components/rhdh-logo/RhdhLogoIcon.tsx b/workspaces/cost-management/packages/app-legacy/src/components/rhdh-logo/RhdhLogoIcon.tsx
similarity index 100%
rename from workspaces/cost-management/packages/app/src/components/rhdh-logo/RhdhLogoIcon.tsx
rename to workspaces/cost-management/packages/app-legacy/src/components/rhdh-logo/RhdhLogoIcon.tsx
diff --git a/workspaces/cost-management/packages/app/src/components/search/SearchPage.tsx b/workspaces/cost-management/packages/app-legacy/src/components/search/SearchPage.tsx
similarity index 100%
rename from workspaces/cost-management/packages/app/src/components/search/SearchPage.tsx
rename to workspaces/cost-management/packages/app-legacy/src/components/search/SearchPage.tsx
diff --git a/workspaces/cost-management/packages/app/src/hooks/useRhdhTheme.ts b/workspaces/cost-management/packages/app-legacy/src/hooks/useRhdhTheme.ts
similarity index 100%
rename from workspaces/cost-management/packages/app/src/hooks/useRhdhTheme.ts
rename to workspaces/cost-management/packages/app-legacy/src/hooks/useRhdhTheme.ts
diff --git a/workspaces/cost-management/packages/app-legacy/src/index.tsx b/workspaces/cost-management/packages/app-legacy/src/index.tsx
new file mode 100644
index 00000000000..703b06bae48
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/src/index.tsx
@@ -0,0 +1,22 @@
+/*
+ * 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 '@backstage/cli/asset-types';
+import ReactDOM from 'react-dom/client';
+
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render();
diff --git a/workspaces/cost-management/packages/app-legacy/src/setupTests.ts b/workspaces/cost-management/packages/app-legacy/src/setupTests.ts
new file mode 100644
index 00000000000..e6c3ea992e8
--- /dev/null
+++ b/workspaces/cost-management/packages/app-legacy/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 '@testing-library/jest-dom';
diff --git a/workspaces/cost-management/packages/app/package.json b/workspaces/cost-management/packages/app/package.json
index d3b71b293ff..aaa9d1f32e1 100644
--- a/workspaces/cost-management/packages/app/package.json
+++ b/workspaces/cost-management/packages/app/package.json
@@ -20,54 +20,33 @@
},
"dependencies": {
"@backstage-community/plugin-rbac": "1.55.2",
- "@backstage-community/plugin-tech-radar": "^0.7.4",
- "@backstage/app-defaults": "^1.7.11",
- "@backstage/catalog-model": "^1.10.0",
"@backstage/cli": "^0.36.5",
- "@backstage/core-app-api": "^1.20.4",
"@backstage/core-components": "^0.18.13",
"@backstage/core-plugin-api": "^1.12.9",
+ "@backstage/frontend-defaults": "^0.5.5",
+ "@backstage/frontend-plugin-api": "^0.18.0",
"@backstage/integration-react": "^1.2.21",
- "@backstage/plugin-api-docs": "^0.14.4",
+ "@backstage/plugin-app-react": "^0.2.6",
"@backstage/plugin-catalog": "^2.0.8",
- "@backstage/plugin-catalog-common": "^1.1.10",
- "@backstage/plugin-catalog-graph": "^0.6.7",
- "@backstage/plugin-catalog-import": "^0.13.17",
- "@backstage/plugin-catalog-react": "^3.2.2",
- "@backstage/plugin-org": "^0.7.8",
- "@backstage/plugin-permission-react": "^0.5.4",
- "@backstage/plugin-scaffolder": "^1.38.2",
- "@backstage/plugin-search": "^1.7.7",
- "@backstage/plugin-search-react": "^1.11.7",
- "@backstage/plugin-techdocs": "^1.18.0",
- "@backstage/plugin-techdocs-module-addons-contrib": "^1.1.39",
- "@backstage/plugin-techdocs-react": "^1.3.14",
"@backstage/plugin-user-settings": "^0.9.6",
"@backstage/theme": "^0.7.3",
- "@material-ui/core": "^4.12.2",
- "@material-ui/icons": "^4.9.1",
- "@patternfly/patternfly": "^6.3.0",
- "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.0",
+ "@backstage/ui": "^0.17.1",
+ "@mui/icons-material": "^5.17.1",
+ "@mui/material": "^5.17.1",
+ "@red-hat-developer-hub/backstage-plugin-theme": "^0.14.11",
"@red-hat-developer-hub/plugin-cost-management": "workspace:^",
- "history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router": "^6.30.2",
- "react-router-dom": "^6.30.2",
- "react-use": "^17.2.4"
+ "react-router-dom": "^6.30.2"
},
"devDependencies": {
"@backstage/test-utils": "^1.7.21",
- "@emotion/react": "^11.11.4",
- "@emotion/styled": "^11.11.5",
- "@mui/icons-material": "^5.16.1",
- "@mui/material": "^5.16.1",
"@playwright/test": "1.61.1",
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.0.0",
- "@types/react": "^18",
"@types/react-dom": "*",
"cross-env": "^7.0.0"
},
diff --git a/workspaces/cost-management/packages/app/src/App.test.tsx b/workspaces/cost-management/packages/app/src/App.test.tsx
index 452cbf840ac..176b12a1517 100644
--- a/workspaces/cost-management/packages/app/src/App.test.tsx
+++ b/workspaces/cost-management/packages/app/src/App.test.tsx
@@ -35,7 +35,7 @@ describe('App', () => {
] as any,
};
- const rendered = render();
+ const rendered = render(App.createRoot());
await waitFor(() => {
expect(rendered.baseElement).toBeInTheDocument();
diff --git a/workspaces/cost-management/packages/app/src/App.tsx b/workspaces/cost-management/packages/app/src/App.tsx
index cabb5c96a1a..c14d8f010f9 100644
--- a/workspaces/cost-management/packages/app/src/App.tsx
+++ b/workspaces/cost-management/packages/app/src/App.tsx
@@ -14,144 +14,26 @@
* limitations under the License.
*/
-import { Navigate, Route } from 'react-router-dom';
-import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
-import {
- CatalogEntityPage,
- CatalogIndexPage,
- catalogPlugin,
-} from '@backstage/plugin-catalog';
-import {
- CatalogImportPage,
- catalogImportPlugin,
-} from '@backstage/plugin-catalog-import';
-import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
-import { orgPlugin } from '@backstage/plugin-org';
-import { SearchPage } from '@backstage/plugin-search';
-import {
- TechDocsIndexPage,
- techdocsPlugin,
- TechDocsReaderPage,
-} from '@backstage/plugin-techdocs';
-import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
-import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
-import { UserSettingsPage } from '@backstage/plugin-user-settings';
-import { apis } from './apis';
-import { entityPage } from './components/catalog/EntityPage';
-import { searchPage } from './components/search/SearchPage';
-import { Root } from './components/Root';
-import {
- AlertDisplay,
- OAuthRequestDialog,
- SignInPage,
-} from '@backstage/core-components';
-import { createApp } from '@backstage/app-defaults';
-import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
-import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
-import { RequirePermission } from '@backstage/plugin-permission-react';
-import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
-import {
- ResourceOptimizationPage,
- OpenShiftPage,
-} from '@red-hat-developer-hub/plugin-cost-management';
-import { useRhdhTheme } from './hooks/useRhdhTheme';
-import { githubAuthApiRef } from '@backstage/core-plugin-api';
-import { RbacPage } from '@backstage-community/plugin-rbac';
-import '@patternfly/patternfly/patternfly.css';
-import '@patternfly/patternfly/patternfly-charts.css';
+import rbacPlugin, {
+ rbacTranslationsModule,
+} from '@backstage-community/plugin-rbac/alpha';
+import { createApp } from '@backstage/frontend-defaults';
+import catalogPlugin from '@backstage/plugin-catalog/alpha';
+import userSettingsPlugin from '@backstage/plugin-user-settings/alpha';
+import { rhdhThemeModule } from '@red-hat-developer-hub/backstage-plugin-theme/alpha';
+import costManagementPlugin from '@red-hat-developer-hub/plugin-cost-management/alpha';
+import { navModule } from './modules/nav';
+import { signInModule } from './modules/signIn';
-const options: Parameters[0] = {
- apis,
- bindRoutes({ bind }) {
- bind(catalogPlugin.externalRoutes, {
- createComponent: scaffolderPlugin.routes.root,
- viewTechDoc: techdocsPlugin.routes.docRoot,
- createFromTemplate: scaffolderPlugin.routes.selectedTemplate,
- });
- bind(apiDocsPlugin.externalRoutes, {
- registerApi: catalogImportPlugin.routes.importPage,
- });
- bind(scaffolderPlugin.externalRoutes, {
- registerComponent: catalogImportPlugin.routes.importPage,
- viewTechDoc: techdocsPlugin.routes.docRoot,
- });
- bind(orgPlugin.externalRoutes, {
- catalogIndex: catalogPlugin.routes.catalogIndex,
- });
- },
- components: {
- SignInPage: props => (
-
- ),
- },
-};
-
-// eslint-disable-next-line react-hooks/rules-of-hooks
-const rhdhTheme = useRhdhTheme();
-if (rhdhTheme !== null) {
- options.themes = rhdhTheme.themes;
-}
-
-const app = createApp(options);
-
-const routes = (
-
- } />
- } />
- }
- >
- {entityPage}
-
- } />
- }
- >
-
-
-
-
- } />
- } />
-
-
-
- }
- />
- }>
- {searchPage}
-
- } />
- } />
- } />
- } />
- } />
-
-);
-
-export default app.createRoot(
- <>
-
-
-
- {routes}
-
- >,
-);
+export default createApp({
+ features: [
+ rhdhThemeModule,
+ navModule,
+ catalogPlugin,
+ userSettingsPlugin,
+ costManagementPlugin,
+ rbacPlugin,
+ rbacTranslationsModule,
+ signInModule,
+ ],
+});
diff --git a/workspaces/cost-management/packages/app/src/index.tsx b/workspaces/cost-management/packages/app/src/index.tsx
index 703b06bae48..e2d1ca3d371 100644
--- a/workspaces/cost-management/packages/app/src/index.tsx
+++ b/workspaces/cost-management/packages/app/src/index.tsx
@@ -16,7 +16,8 @@
import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
-
import App from './App';
+// eslint-disable-next-line @backstage/no-ui-css-imports-in-non-frontend
+import '@backstage/ui/css/styles.css';
-ReactDOM.createRoot(document.getElementById('root')!).render();
+ReactDOM.createRoot(document.getElementById('root')!).render(App.createRoot());
diff --git a/workspaces/cost-management/packages/app/src/modules/nav/Sidebar.tsx b/workspaces/cost-management/packages/app/src/modules/nav/Sidebar.tsx
new file mode 100644
index 00000000000..9702191ba57
--- /dev/null
+++ b/workspaces/cost-management/packages/app/src/modules/nav/Sidebar.tsx
@@ -0,0 +1,71 @@
+/*
+ * 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 { Administration } from '@backstage-community/plugin-rbac';
+import {
+ Sidebar,
+ SidebarDivider,
+ SidebarGroup,
+ SidebarItem,
+ SidebarScrollWrapper,
+ SidebarSpace,
+} from '@backstage/core-components';
+import { NavContentBlueprint } from '@backstage/plugin-app-react';
+import { UserSettingsSignInAvatar } from '@backstage/plugin-user-settings';
+import MenuIcon from '@mui/icons-material/Menu';
+
+import { SidebarLogo } from './SidebarLogo';
+
+export const SidebarContent = NavContentBlueprint.make({
+ params: {
+ component: ({ navItems }) => {
+ const nav = navItems.withComponent(item => (
+ item.icon} to={item.href} text={item.title} />
+ ));
+
+ // Consume so the `/` → `/catalog` redirect is not listed in the sidebar.
+ nav.take('page:app/home-redirect');
+ // Consume page:rbac so it is not always shown. Administration hides
+ // itself unless getUserAuthorization() returns Authorized (legacy match).
+ nav.take('page:rbac');
+
+ return (
+
+
+
+ }>
+ {nav.take('page:catalog')}
+ {nav.take('page:cost-management')}
+ {nav.take('page:cost-management/openshift')}
+
+ {nav.rest({ sortBy: 'title' })}
+
+
+
+
+
+ }
+ to="/settings"
+ >
+ {nav.take('page:user-settings')}
+
+
+ );
+ },
+ },
+});
diff --git a/workspaces/cost-management/packages/app/src/modules/nav/SidebarLogo.tsx b/workspaces/cost-management/packages/app/src/modules/nav/SidebarLogo.tsx
new file mode 100644
index 00000000000..e6c992c0669
--- /dev/null
+++ b/workspaces/cost-management/packages/app/src/modules/nav/SidebarLogo.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 {
+ Link,
+ sidebarConfig,
+ useSidebarOpenState,
+} from '@backstage/core-components';
+import Box from '@mui/material/Box';
+import {
+ LogoFull,
+ LogoIcon,
+} from '@red-hat-developer-hub/backstage-plugin-theme';
+
+export const SidebarLogo = () => {
+ const { isOpen } = useSidebarOpenState();
+
+ return (
+
+
+ {isOpen ? : }
+
+
+ );
+};
diff --git a/workspaces/cost-management/packages/app/src/modules/nav/homeRedirect.tsx b/workspaces/cost-management/packages/app/src/modules/nav/homeRedirect.tsx
new file mode 100644
index 00000000000..bc42d55d195
--- /dev/null
+++ b/workspaces/cost-management/packages/app/src/modules/nav/homeRedirect.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 { PageBlueprint } from '@backstage/frontend-plugin-api';
+import { Navigate } from 'react-router-dom';
+
+/**
+ * NFS has no default `/` page. Match the legacy app, which redirects `/` to catalog.
+ */
+export const homeRedirectPage = PageBlueprint.make({
+ name: 'home-redirect',
+ params: {
+ path: '/',
+ noHeader: true,
+ loader: async () => ,
+ },
+});
diff --git a/workspaces/cost-management/packages/app/src/modules/nav/index.ts b/workspaces/cost-management/packages/app/src/modules/nav/index.ts
new file mode 100644
index 00000000000..a3e1f522c22
--- /dev/null
+++ b/workspaces/cost-management/packages/app/src/modules/nav/index.ts
@@ -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 { createFrontendModule } from '@backstage/frontend-plugin-api';
+import { homeRedirectPage } from './homeRedirect';
+import { SidebarContent } from './Sidebar';
+
+export const navModule = createFrontendModule({
+ pluginId: 'app',
+ extensions: [homeRedirectPage, SidebarContent],
+});
diff --git a/workspaces/cost-management/packages/app/src/modules/signIn/SignInPageComponent.tsx b/workspaces/cost-management/packages/app/src/modules/signIn/SignInPageComponent.tsx
new file mode 100644
index 00000000000..6699b30861a
--- /dev/null
+++ b/workspaces/cost-management/packages/app/src/modules/signIn/SignInPageComponent.tsx
@@ -0,0 +1,30 @@
+/*
+ * 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 { SignInPage } from '@backstage/core-components';
+import { githubAuthApiRef } from '@backstage/core-plugin-api';
+import type { SignInPageProps } from '@backstage/plugin-app-react';
+
+const githubProvider = {
+ id: 'github-auth-provider',
+ title: 'GitHub',
+ message: 'Sign in using GitHub',
+ apiRef: githubAuthApiRef,
+};
+
+export function SignInPageComponent(props: SignInPageProps) {
+ return ;
+}
diff --git a/workspaces/cost-management/packages/app/src/modules/signIn/index.ts b/workspaces/cost-management/packages/app/src/modules/signIn/index.ts
new file mode 100644
index 00000000000..eb252724498
--- /dev/null
+++ b/workspaces/cost-management/packages/app/src/modules/signIn/index.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { createFrontendModule } from '@backstage/frontend-plugin-api';
+import { SignInPageBlueprint } from '@backstage/plugin-app-react';
+
+export const signInModule = createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ SignInPageBlueprint.make({
+ params: {
+ loader: () =>
+ import('./SignInPageComponent').then(m => m.SignInPageComponent),
+ },
+ }),
+ ],
+});
diff --git a/workspaces/cost-management/playwright.config.ts b/workspaces/cost-management/playwright.config.ts
index 9b36e7ca6a8..90edf37f129 100644
--- a/workspaces/cost-management/playwright.config.ts
+++ b/workspaces/cost-management/playwright.config.ts
@@ -24,7 +24,9 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({
...(process.env.PLAYWRIGHT_URL
? {
- globalSetup: require.resolve('./packages/app/e2e-tests/global-setup'),
+ globalSetup: require.resolve(
+ './packages/app-legacy/e2e-tests/global-setup',
+ ),
}
: {}),
@@ -38,7 +40,7 @@ export default defineConfig({
? []
: [
{
- command: 'yarn start-app',
+ command: 'yarn workspace app-legacy start',
port: 3000,
reuseExistingServer: true,
timeout: 120_000,
@@ -77,7 +79,7 @@ export default defineConfig({
// Merge-gate safe: runs against the local dev server in CI
{
name: 'chromium',
- testDir: 'packages/app/e2e-tests',
+ testDir: 'packages/app-legacy/e2e-tests',
testMatch: /app\.test\.ts$/,
use: {
channel: 'chrome',
@@ -90,7 +92,7 @@ export default defineConfig({
? [
{
name: 'live',
- testDir: 'packages/app/e2e-tests',
+ testDir: 'packages/app-legacy/e2e-tests',
testMatch: /^(?!app\.test\.ts$).*\.test\.ts$/,
use: {
channel: 'chrome' as const,
diff --git a/workspaces/cost-management/plugins/cost-management/README.md b/workspaces/cost-management/plugins/cost-management/README.md
index 147b387a6e3..0afaec0abfb 100644
--- a/workspaces/cost-management/plugins/cost-management/README.md
+++ b/workspaces/cost-management/plugins/cost-management/README.md
@@ -44,7 +44,34 @@ You can follow one of these options depending on your environment and how you ch
backend.add(import('@red-hat-developer-hub/plugin-cost-management-backend'));
```
-1. Add the `ResourceOptimizationPage` extension to your `App.tsx` routes
+1. Add the frontend plugin to your app.
+
+ **New Frontend System** (RHDH 2.1 / `app-next`):
+
+ ```ts
+ import costManagementPlugin from '@red-hat-developer-hub/plugin-cost-management/alpha';
+
+ const app = createApp({
+ features: [costManagementPlugin],
+ });
+ ```
+
+ Sidebar items come from the plugin: **Optimizations** (`/cost-management/optimizations`) and **Cost Management** (`/cost-management/openshift`). Optional overrides:
+
+ ```yaml
+ app:
+ extensions:
+ - page:cost-management:
+ config:
+ title: Optimizations
+ path: /cost-management/optimizations
+ - page:cost-management/openshift:
+ config:
+ title: Cost Management
+ path: /cost-management/openshift
+ ```
+
+ **Legacy frontend** (`FlatRoutes`):
```ts
// packages/app/src/App.tsx
diff --git a/workspaces/cost-management/plugins/cost-management/dev/index.tsx b/workspaces/cost-management/plugins/cost-management/dev/index.tsx
index 82cf9f92304..234a3185d9f 100644
--- a/workspaces/cost-management/plugins/cost-management/dev/index.tsx
+++ b/workspaces/cost-management/plugins/cost-management/dev/index.tsx
@@ -14,26 +14,26 @@
* limitations under the License.
*/
-import { createDevApp } from '@backstage/dev-utils';
-import {
- resourceOptimizationPlugin,
- ResourceOptimizationPage,
- OpenShiftPage,
-} from '../src/plugin';
-import { CostManagementIconOutlined } from '../src/components/icon/CostManagementIconOutlined';
-
-createDevApp()
- .registerPlugin(resourceOptimizationPlugin)
- .addPage({
- title: 'Optimizations',
- path: '/cost-management/optimizations',
- element: ,
- icon: CostManagementIconOutlined,
- })
- .addPage({
- title: 'OpenShift',
- path: '/cost-management/openshift',
- element: ,
- icon: CostManagementIconOutlined,
- })
- .render();
+/**
+ * New Frontend System plugin standalone (yarn start / yarn start:fe-plugin).
+ * Legacy createDevApp: yarn start:legacy
+ */
+
+import '@backstage/cli/asset-types';
+
+import ReactDOM from 'react-dom/client';
+import { createApp } from '@backstage/frontend-defaults';
+
+import costManagementPlugin from '../src/alpha';
+
+const DEFAULT_PATH = '/cost-management/optimizations';
+
+const app = createApp({
+ features: [costManagementPlugin],
+});
+
+if (typeof window !== 'undefined' && window.location.pathname === '/') {
+ window.location.pathname = DEFAULT_PATH;
+}
+
+ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot());
diff --git a/workspaces/cost-management/plugins/cost-management/dev/legacy.tsx b/workspaces/cost-management/plugins/cost-management/dev/legacy.tsx
new file mode 100644
index 00000000000..82cf9f92304
--- /dev/null
+++ b/workspaces/cost-management/plugins/cost-management/dev/legacy.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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 { createDevApp } from '@backstage/dev-utils';
+import {
+ resourceOptimizationPlugin,
+ ResourceOptimizationPage,
+ OpenShiftPage,
+} from '../src/plugin';
+import { CostManagementIconOutlined } from '../src/components/icon/CostManagementIconOutlined';
+
+createDevApp()
+ .registerPlugin(resourceOptimizationPlugin)
+ .addPage({
+ title: 'Optimizations',
+ path: '/cost-management/optimizations',
+ element: ,
+ icon: CostManagementIconOutlined,
+ })
+ .addPage({
+ title: 'OpenShift',
+ path: '/cost-management/openshift',
+ element: ,
+ icon: CostManagementIconOutlined,
+ })
+ .render();
diff --git a/workspaces/cost-management/plugins/cost-management/package.json b/workspaces/cost-management/plugins/cost-management/package.json
index e95e974bbc1..3ee8b20edf3 100644
--- a/workspaces/cost-management/plugins/cost-management/package.json
+++ b/workspaces/cost-management/plugins/cost-management/package.json
@@ -14,6 +14,8 @@
"dependencies": {
"@backstage/core-components": "^0.18.13",
"@backstage/core-plugin-api": "^1.12.9",
+ "@backstage/frontend-plugin-api": "^0.18.0",
+ "@backstage/plugin-app-react": "^0.2.6",
"@backstage/plugin-permission-common": "^0.9.10",
"@backstage/plugin-permission-react": "^0.5.4",
"@backstage/theme": "^0.7.3",
@@ -49,6 +51,8 @@
"@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/frontend-test-utils": "^0.6.3",
"@backstage/plugin-search-react": "^1.11.7",
"@backstage/test-utils": "^1.7.21",
"@testing-library/dom": "^10.0.0",
@@ -62,6 +66,7 @@
},
"exports": {
".": "./src/index.ts",
+ "./alpha": "./src/alpha.tsx",
"./package.json": "./package.json"
},
"files": [
@@ -90,12 +95,16 @@
"postpack": "backstage-cli package postpack",
"prepack": "backstage-cli package prepack",
"start": "backstage-cli package start",
+ "start:legacy": "backstage-cli package start --entrypoint=dev/legacy.tsx",
"test": "backstage-cli package test"
},
"sideEffects": false,
"types": "src/index.ts",
"typesVersions": {
"*": {
+ "alpha": [
+ "src/alpha.tsx"
+ ],
"package.json": [
"package.json"
]
diff --git a/workspaces/cost-management/plugins/cost-management/report-alpha.api.md b/workspaces/cost-management/plugins/cost-management/report-alpha.api.md
new file mode 100644
index 00000000000..36f88dfe1d4
--- /dev/null
+++ b/workspaces/cost-management/plugins/cost-management/report-alpha.api.md
@@ -0,0 +1,251 @@
+## API Report File for "@red-hat-developer-hub/plugin-cost-management"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { AnyApiFactory } from '@backstage/frontend-plugin-api';
+import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
+import { ApiFactory } from '@backstage/frontend-plugin-api';
+import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
+import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api';
+import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
+import { ExtensionInput } from '@backstage/frontend-plugin-api';
+import { IconComponent } from '@backstage/frontend-plugin-api';
+import { IconElement } from '@backstage/frontend-plugin-api';
+import { JSX as JSX_2 } from 'react';
+import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api';
+import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api';
+import { PathParams } from '@backstage/core-plugin-api';
+import { RouteRef } from '@backstage/core-plugin-api';
+import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api';
+import { SubRouteRef } from '@backstage/core-plugin-api';
+
+// @alpha
+const _default: OverridableFrontendPlugin<
+ {
+ root: RouteRef;
+ openShift: RouteRef;
+ breakdown: SubRouteRef>;
+ },
+ {},
+ {
+ 'api:cost-management/cost-management-slim': OverridableExtensionDefinition<{
+ kind: 'api';
+ name: 'cost-management-slim';
+ config: {};
+ configInput: {};
+ output: ExtensionDataRef;
+ inputs: {};
+ params: <
+ TApi,
+ TImpl extends TApi,
+ TDeps extends { [name in string]: unknown },
+ >(
+ params: ApiFactory,
+ ) => ExtensionBlueprintParams;
+ }>;
+ 'api:cost-management/optimizations': OverridableExtensionDefinition<{
+ kind: 'api';
+ name: 'optimizations';
+ config: {};
+ configInput: {};
+ output: ExtensionDataRef;
+ inputs: {};
+ params: <
+ TApi,
+ TImpl extends TApi,
+ TDeps extends { [name in string]: unknown },
+ >(
+ params: ApiFactory,
+ ) => ExtensionBlueprintParams;
+ }>;
+ 'api:cost-management/orchestrator-slim': OverridableExtensionDefinition<{
+ kind: 'api';
+ name: 'orchestrator-slim';
+ config: {};
+ configInput: {};
+ output: ExtensionDataRef;
+ inputs: {};
+ params: <
+ TApi,
+ TImpl extends TApi,
+ TDeps extends { [name in string]: unknown },
+ >(
+ params: ApiFactory,
+ ) => ExtensionBlueprintParams;
+ }>;
+ 'icon-bundle:cost-management/cost-management-icons': OverridableExtensionDefinition<{
+ kind: 'icon-bundle';
+ name: 'cost-management-icons';
+ config: {};
+ configInput: {};
+ output: ExtensionDataRef<
+ {
+ [x: string]: IconComponent | IconElement;
+ },
+ 'core.icons',
+ {}
+ >;
+ inputs: {};
+ params: {
+ icons: { [key in string]: IconComponent | IconElement };
+ };
+ }>;
+ 'page:cost-management': OverridableExtensionDefinition<{
+ kind: 'page';
+ name: undefined;
+ config: {
+ path: string | undefined;
+ title: string | undefined;
+ };
+ configInput: {
+ path?: string | undefined;
+ title?: string | undefined;
+ };
+ output:
+ | ExtensionDataRef
+ | ExtensionDataRef<
+ RouteRef_2,
+ 'core.routing.ref',
+ {
+ optional: true;
+ }
+ >
+ | ExtensionDataRef
+ | ExtensionDataRef<
+ string,
+ 'core.title',
+ {
+ optional: true;
+ }
+ >
+ | ExtensionDataRef<
+ IconElement,
+ 'core.icon',
+ {
+ optional: true;
+ }
+ >;
+ inputs: {
+ pages: ExtensionInput<
+ | ConfigurableExtensionDataRef
+ | ConfigurableExtensionDataRef
+ | ConfigurableExtensionDataRef<
+ RouteRef_2,
+ 'core.routing.ref',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'core.title',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ IconElement,
+ 'core.icon',
+ {
+ optional: true;
+ }
+ >,
+ {
+ singleton: false;
+ optional: false;
+ internal: false;
+ }
+ >;
+ };
+ params: {
+ path: string;
+ title?: string;
+ icon?: IconElement;
+ loader?: () => Promise;
+ routeRef?: RouteRef_2;
+ noHeader?: boolean;
+ };
+ }>;
+ 'page:cost-management/openshift': OverridableExtensionDefinition<{
+ kind: 'page';
+ name: 'openshift';
+ config: {
+ path: string | undefined;
+ title: string | undefined;
+ };
+ configInput: {
+ path?: string | undefined;
+ title?: string | undefined;
+ };
+ output:
+ | ExtensionDataRef
+ | ExtensionDataRef<
+ RouteRef_2,
+ 'core.routing.ref',
+ {
+ optional: true;
+ }
+ >
+ | ExtensionDataRef
+ | ExtensionDataRef<
+ string,
+ 'core.title',
+ {
+ optional: true;
+ }
+ >
+ | ExtensionDataRef<
+ IconElement,
+ 'core.icon',
+ {
+ optional: true;
+ }
+ >;
+ inputs: {
+ pages: ExtensionInput<
+ | ConfigurableExtensionDataRef
+ | ConfigurableExtensionDataRef
+ | ConfigurableExtensionDataRef<
+ RouteRef_2,
+ 'core.routing.ref',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'core.title',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ IconElement,
+ 'core.icon',
+ {
+ optional: true;
+ }
+ >,
+ {
+ singleton: false;
+ optional: false;
+ internal: false;
+ }
+ >;
+ };
+ params: {
+ path: string;
+ title?: string;
+ icon?: IconElement;
+ loader?: () => Promise;
+ routeRef?: RouteRef_2;
+ noHeader?: boolean;
+ };
+ }>;
+ }
+>;
+export default _default;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/workspaces/cost-management/plugins/cost-management/report.api.md b/workspaces/cost-management/plugins/cost-management/report.api.md
index 208035109e5..87eb5d67c41 100644
--- a/workspaces/cost-management/plugins/cost-management/report.api.md
+++ b/workspaces/cost-management/plugins/cost-management/report.api.md
@@ -55,7 +55,7 @@ export const resourceOptimizationPlugin: BackstagePlugin<
{}
>;
-// @public (undocumented)
+// @public
export function Router(): JSX_2.Element;
// (No @packageDocumentation comment for this package)
diff --git a/workspaces/cost-management/plugins/cost-management/src/Router.tsx b/workspaces/cost-management/plugins/cost-management/src/Router.tsx
index 5360f7dd949..a94b2dcc955 100644
--- a/workspaces/cost-management/plugins/cost-management/src/Router.tsx
+++ b/workspaces/cost-management/plugins/cost-management/src/Router.tsx
@@ -18,23 +18,35 @@ import { ErrorPage } from '@backstage/core-components';
import { Routes, Route } from 'react-router-dom';
import { OptimizationsPage } from './pages/optimizations/OptimizationsPage';
import { OptimizationsBreakdownPage } from './pages/optimizations-breakdown/OptimizationsBreakdownPage';
-import { OpenShiftPage } from './pages/openshift/OpenShiftPage';
import { usePatternFlyTheme } from './hooks/usePatternFlyTheme';
-/** @public */
+const notFound = ;
+
+/**
+ * NFS Optimizations page loader: list + breakdown only.
+ * OpenShift is a separate PageBlueprint, not a child of this mount.
+ */
+export function OptimizationsRouter() {
+ usePatternFlyTheme();
+
+ return (
+
+ } />
+ } />
+
+
+ );
+}
+
+/** @public Legacy ResourceOptimizationPage mount (list + breakdown). */
export function Router() {
usePatternFlyTheme();
return (
} />
- } />
- } />
} />
- }
- />
+
);
}
diff --git a/workspaces/cost-management/plugins/cost-management/src/alpha.test.tsx b/workspaces/cost-management/plugins/cost-management/src/alpha.test.tsx
new file mode 100644
index 00000000000..c302edcf937
--- /dev/null
+++ b/workspaces/cost-management/plugins/cost-management/src/alpha.test.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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 { coreExtensionData } from '@backstage/frontend-plugin-api';
+import { createExtensionTester } from '@backstage/frontend-test-utils';
+
+import plugin from './alpha';
+import { openShiftRouteRef, rootRouteRef } from './routes';
+
+describe('cost-management alpha', () => {
+ it('exports a valid frontend plugin', () => {
+ expect(plugin).toBeDefined();
+ expect(plugin.$$type).toBe('@backstage/FrontendPlugin');
+ });
+
+ it('has plugin id cost-management', () => {
+ expect(plugin.id).toBe('cost-management');
+ });
+
+ it('exposes root, openShift and breakdown routes', () => {
+ expect(plugin.routes).toHaveProperty('root');
+ expect(plugin.routes).toHaveProperty('openShift');
+ expect(plugin.routes).toHaveProperty('breakdown');
+ });
+
+ it('registers pages, APIs and the icon bundle', () => {
+ expect(plugin.getExtension('page:cost-management')).toBeDefined();
+ expect(plugin.getExtension('page:cost-management/openshift')).toBeDefined();
+ expect(
+ plugin.getExtension('api:cost-management/optimizations'),
+ ).toBeDefined();
+ expect(
+ plugin.getExtension('api:cost-management/orchestrator-slim'),
+ ).toBeDefined();
+ expect(
+ plugin.getExtension('api:cost-management/cost-management-slim'),
+ ).toBeDefined();
+ expect(
+ plugin.getExtension('icon-bundle:cost-management/cost-management-icons'),
+ ).toBeDefined();
+ });
+});
+
+describe('cost-management NFS wiring', () => {
+ it('declares Optimizations page path, title and route ref', () => {
+ const tester = createExtensionTester(
+ plugin.getExtension('page:cost-management'),
+ );
+
+ expect(tester.get(coreExtensionData.routePath)).toBe(
+ '/cost-management/optimizations',
+ );
+ expect(tester.get(coreExtensionData.title)).toBe('Optimizations');
+ expect(tester.get(coreExtensionData.routeRef)).toBe(rootRouteRef);
+ });
+
+ it('declares OpenShift page with Cost Management nav title', () => {
+ const tester = createExtensionTester(
+ plugin.getExtension('page:cost-management/openshift'),
+ );
+
+ expect(tester.get(coreExtensionData.routePath)).toBe(
+ '/cost-management/openshift',
+ );
+ expect(tester.get(coreExtensionData.title)).toBe('Cost Management');
+ expect(tester.get(coreExtensionData.routeRef)).toBe(openShiftRouteRef);
+ });
+});
diff --git a/workspaces/cost-management/plugins/cost-management/src/alpha.tsx b/workspaces/cost-management/plugins/cost-management/src/alpha.tsx
new file mode 100644
index 00000000000..9599e3019d5
--- /dev/null
+++ b/workspaces/cost-management/plugins/cost-management/src/alpha.tsx
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+
+/**
+ * New Frontend System entry (`./alpha`).
+ *
+ * @packageDocumentation
+ */
+
+import {
+ discoveryApiRef,
+ fetchApiRef,
+ identityApiRef,
+} from '@backstage/core-plugin-api';
+import {
+ ApiBlueprint,
+ createApiFactory,
+ createFrontendPlugin,
+ PageBlueprint,
+} from '@backstage/frontend-plugin-api';
+import { IconBundleBlueprint } from '@backstage/plugin-app-react';
+import {
+ CostManagementSlimClient,
+ OptimizationsClient,
+ OrchestratorSlimClient,
+} from '@red-hat-developer-hub/plugin-cost-management-common/clients';
+import {
+ costManagementSlimApiRef,
+ optimizationsApiRef,
+ orchestratorSlimApiRef,
+} from './apis';
+import { CostManagementIconOutlined } from './components/icon';
+import {
+ optimizationsBreakdownRouteRef,
+ openShiftRouteRef,
+ rootRouteRef,
+} from './routes';
+
+const optimizationsApi = ApiBlueprint.make({
+ name: 'optimizations',
+ params: defineParams =>
+ defineParams(
+ createApiFactory({
+ api: optimizationsApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ fetchApi: fetchApiRef,
+ },
+ factory: ({ discoveryApi, fetchApi }) =>
+ new OptimizationsClient({
+ discoveryApi,
+ fetchApi,
+ }),
+ }),
+ ),
+});
+
+const orchestratorSlimApi = ApiBlueprint.make({
+ name: 'orchestrator-slim',
+ params: defineParams =>
+ defineParams(
+ createApiFactory({
+ api: orchestratorSlimApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ fetchApi: fetchApiRef,
+ identityApi: identityApiRef,
+ },
+ factory: ({ discoveryApi, fetchApi, identityApi }) =>
+ new OrchestratorSlimClient({
+ discoveryApi,
+ fetchApi,
+ identityApi,
+ }),
+ }),
+ ),
+});
+
+const costManagementSlimApi = ApiBlueprint.make({
+ name: 'cost-management-slim',
+ params: defineParams =>
+ defineParams(
+ createApiFactory({
+ api: costManagementSlimApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ fetchApi: fetchApiRef,
+ },
+ factory: ({ discoveryApi, fetchApi }) =>
+ new CostManagementSlimClient({
+ discoveryApi,
+ fetchApi,
+ }),
+ }),
+ ),
+});
+
+const costManagementIcons = IconBundleBlueprint.make({
+ name: 'cost-management-icons',
+ params: {
+ icons: {
+ costManagementIconOutlined: CostManagementIconOutlined,
+ },
+ },
+});
+
+const optimizationsPage = PageBlueprint.make({
+ params: {
+ path: '/cost-management/optimizations',
+ routeRef: rootRouteRef,
+ title: 'Optimizations',
+ icon: ,
+ noHeader: true,
+ loader: () => import('./Router').then(m => ),
+ },
+});
+
+const openShiftPage = PageBlueprint.make({
+ name: 'openshift',
+ params: {
+ path: '/cost-management/openshift',
+ routeRef: openShiftRouteRef,
+ title: 'Cost Management',
+ icon: ,
+ noHeader: true,
+ loader: () =>
+ import('./pages/openshift/OpenShiftPage').then(m => ),
+ },
+});
+
+/**
+ * Cost Management plugin for the Backstage new frontend system.
+ * @alpha
+ */
+export default createFrontendPlugin({
+ pluginId: 'cost-management',
+ extensions: [
+ optimizationsApi,
+ orchestratorSlimApi,
+ costManagementSlimApi,
+ costManagementIcons,
+ optimizationsPage,
+ openShiftPage,
+ ],
+ routes: {
+ root: rootRouteRef,
+ openShift: openShiftRouteRef,
+ breakdown: optimizationsBreakdownRouteRef,
+ },
+});
diff --git a/workspaces/cost-management/plugins/cost-management/src/pages/openshift/OpenShiftPage.tsx b/workspaces/cost-management/plugins/cost-management/src/pages/openshift/OpenShiftPage.tsx
index 780f454b238..f452aa9bfa0 100644
--- a/workspaces/cost-management/plugins/cost-management/src/pages/openshift/OpenShiftPage.tsx
+++ b/workspaces/cost-management/plugins/cost-management/src/pages/openshift/OpenShiftPage.tsx
@@ -31,6 +31,7 @@ import { PageHeader } from './components/PageHeader';
import { TableToolbar } from './components/TableToolbar';
import { useApi } from '@backstage/core-plugin-api';
import { costManagementSlimApiRef } from '../../apis';
+import { usePatternFlyTheme } from '../../hooks/usePatternFlyTheme';
import useAsync from 'react-use/lib/useAsync';
import { CURRENCY_SYMBOLS } from '../../constants/currencies';
import { DownloadIconButton } from './components/DownloadIconButton';
@@ -220,6 +221,7 @@ function buildCostManagementQueryParams(
/** @public */
export function OpenShiftPage() {
+ usePatternFlyTheme();
const api = useApi(costManagementSlimApiRef);
const [groupBy, setGroupBy] = useState('project');
const [overheadDistribution, setOverheadDistribution] =
diff --git a/workspaces/cost-management/yarn.lock b/workspaces/cost-management/yarn.lock
index 0ab3f41d523..e52d02f9fa9 100644
--- a/workspaces/cost-management/yarn.lock
+++ b/workspaces/cost-management/yarn.lock
@@ -3914,6 +3914,55 @@ __metadata:
languageName: node
linkType: hard
+"@backstage/frontend-app-api@npm:^0.16.7":
+ version: 0.16.7
+ resolution: "@backstage/frontend-app-api@npm:0.16.7"
+ dependencies:
+ "@backstage/config": "npm:^1.3.8"
+ "@backstage/core-app-api": "npm:^1.20.4"
+ "@backstage/core-plugin-api": "npm:^1.12.9"
+ "@backstage/errors": "npm:^1.3.1"
+ "@backstage/filter-predicates": "npm:^0.1.4"
+ "@backstage/frontend-defaults": "npm:^0.5.5"
+ "@backstage/frontend-plugin-api": "npm:^0.18.0"
+ "@backstage/types": "npm:^1.2.2"
+ "@backstage/version-bridge": "npm:^1.0.12"
+ lodash: "npm:^4.17.21"
+ peerDependencies:
+ "@types/react": ^17.0.0 || ^18.0.0
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ react-router-dom: ^6.30.2
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/061d4e42d0ca5d55e368111bbdf0d1fea9d44d5e55b16c8ad00605d7a8eee363cca68396e98d0ba1aa2760c2d8bf269712922b147a68580ddb2ccd37a4ef12b2
+ languageName: node
+ linkType: hard
+
+"@backstage/frontend-defaults@npm:^0.5.5":
+ version: 0.5.5
+ resolution: "@backstage/frontend-defaults@npm:0.5.5"
+ dependencies:
+ "@backstage/config": "npm:^1.3.8"
+ "@backstage/core-components": "npm:^0.18.13"
+ "@backstage/errors": "npm:^1.3.1"
+ "@backstage/frontend-app-api": "npm:^0.16.7"
+ "@backstage/frontend-plugin-api": "npm:^0.18.0"
+ "@backstage/plugin-app": "npm:^0.5.2"
+ "@react-hookz/web": "npm:^24.0.0"
+ peerDependencies:
+ "@types/react": ^17.0.0 || ^18.0.0
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ react-router-dom: ^6.30.2
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/ee19823794719c430c263c2f2482f053366bf212f3b51de7cca13712e2786bd016f3b780c8d1ebdbe3b010c1559a3689a55290ae66acd71fbc18b9ecd34acacc
+ languageName: node
+ linkType: hard
+
"@backstage/frontend-plugin-api@npm:^0.15.1":
version: 0.15.1
resolution: "@backstage/frontend-plugin-api@npm:0.15.1"
@@ -4006,6 +4055,42 @@ __metadata:
languageName: node
linkType: hard
+"@backstage/frontend-test-utils@npm:^0.6.3":
+ version: 0.6.3
+ resolution: "@backstage/frontend-test-utils@npm:0.6.3"
+ dependencies:
+ "@backstage/config": "npm:^1.3.8"
+ "@backstage/core-app-api": "npm:^1.20.4"
+ "@backstage/core-plugin-api": "npm:^1.12.9"
+ "@backstage/filter-predicates": "npm:^0.1.4"
+ "@backstage/frontend-app-api": "npm:^0.16.7"
+ "@backstage/frontend-plugin-api": "npm:^0.18.0"
+ "@backstage/plugin-app": "npm:^0.5.2"
+ "@backstage/plugin-app-react": "npm:^0.2.6"
+ "@backstage/plugin-permission-common": "npm:^0.9.10"
+ "@backstage/plugin-permission-react": "npm:^0.5.4"
+ "@backstage/test-utils": "npm:^1.7.21"
+ "@backstage/types": "npm:^1.2.2"
+ "@backstage/version-bridge": "npm:^1.0.12"
+ i18next: "npm:^22.4.15"
+ zen-observable: "npm:^0.10.0"
+ zod: "npm:^3.25.76 || ^4.0.0"
+ peerDependencies:
+ "@testing-library/react": ^16.0.0
+ "@types/jest": "*"
+ "@types/react": ^17.0.0 || ^18.0.0
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ react-router-dom: ^6.30.2
+ peerDependenciesMeta:
+ "@types/jest":
+ optional: true
+ "@types/react":
+ optional: true
+ checksum: 10c0/276546fc9b2d43e2cac397b8c81c9148db8e2d5fad6d981559dac3013298b3e7d3ad76f9cfe059cb7626a6c6d1745477bbb6fdc619633d38c95e824f610f7edb
+ languageName: node
+ linkType: hard
+
"@backstage/integration-aws-node@npm:^0.2.1":
version: 0.2.1
resolution: "@backstage/integration-aws-node@npm:0.2.1"
@@ -4179,6 +4264,44 @@ __metadata:
languageName: node
linkType: hard
+"@backstage/plugin-app@npm:^0.5.2":
+ version: 0.5.2
+ resolution: "@backstage/plugin-app@npm:0.5.2"
+ dependencies:
+ "@backstage/core-components": "npm:^0.18.13"
+ "@backstage/core-plugin-api": "npm:^1.12.9"
+ "@backstage/filter-predicates": "npm:^0.1.4"
+ "@backstage/frontend-plugin-api": "npm:^0.18.0"
+ "@backstage/integration-react": "npm:^1.2.21"
+ "@backstage/plugin-app-react": "npm:^0.2.6"
+ "@backstage/plugin-permission-react": "npm:^0.5.4"
+ "@backstage/theme": "npm:^0.7.3"
+ "@backstage/types": "npm:^1.2.2"
+ "@backstage/ui": "npm:^0.17.1"
+ "@backstage/version-bridge": "npm:^1.0.12"
+ "@material-ui/core": "npm:^4.9.13"
+ "@material-ui/icons": "npm:^4.9.1"
+ "@material-ui/lab": "npm:^4.0.0-alpha.61"
+ "@react-hookz/web": "npm:^24.0.0"
+ "@remixicon/react": "npm:>=4.6.0 <4.9.0"
+ motion: "npm:^12.0.0"
+ react-aria: "npm:~3.48.0"
+ react-stately: "npm:~3.46.0"
+ react-use: "npm:^17.2.4"
+ zen-observable: "npm:^0.10.0"
+ zod: "npm:^4.0.0"
+ peerDependencies:
+ "@types/react": ^17.0.0 || ^18.0.0
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ react-router-dom: ^6.30.2
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10c0/622637256285c0d32e844fa43c25893e39c50fa8d22aa6cda9ee331db13366ddb8cb490a0e6691ed842d65de5d34c21b67299afb6e18df1cbcafce92028b9a76
+ languageName: node
+ linkType: hard
+
"@backstage/plugin-auth-backend-module-github-provider@npm:^0.5.6":
version: 0.5.6
resolution: "@backstage/plugin-auth-backend-module-github-provider@npm:0.5.6"
@@ -8036,7 +8159,7 @@ __metadata:
languageName: node
linkType: hard
-"@material-ui/lab@npm:4.0.0-alpha.61":
+"@material-ui/lab@npm:4.0.0-alpha.61, @material-ui/lab@npm:^4.0.0-alpha.61":
version: 4.0.0-alpha.61
resolution: "@material-ui/lab@npm:4.0.0-alpha.61"
dependencies:
@@ -8566,10 +8689,10 @@ __metadata:
languageName: node
linkType: hard
-"@mui/core-downloads-tracker@npm:^5.17.1":
- version: 5.17.1
- resolution: "@mui/core-downloads-tracker@npm:5.17.1"
- checksum: 10c0/c36641e274a27cdef8a75218021a5ebef0ce588cd0aec004802d56e8d59ac9d8603b8b4840df480de40ce2c3af8898d49929a8564ed5a784ff2b3100dce31366
+"@mui/core-downloads-tracker@npm:^5.18.0":
+ version: 5.18.0
+ resolution: "@mui/core-downloads-tracker@npm:5.18.0"
+ checksum: 10c0/d82962a1b69878cf6a6785b6600302a943d474af00bba50169e207ce0bb5d072f1ed65783c7d5ca940cbe4228ab86bc95bb57545cf2cd039d588f2571bbefe0c
languageName: node
linkType: hard
@@ -8589,13 +8712,13 @@ __metadata:
languageName: node
linkType: hard
-"@mui/material@npm:^5.12.2, @mui/material@npm:^5.14.18, @mui/material@npm:^5.16.1":
- version: 5.17.1
- resolution: "@mui/material@npm:5.17.1"
+"@mui/material@npm:^5.12.2, @mui/material@npm:^5.14.18, @mui/material@npm:^5.16.1, @mui/material@npm:^5.17.1":
+ version: 5.18.0
+ resolution: "@mui/material@npm:5.18.0"
dependencies:
"@babel/runtime": "npm:^7.23.9"
- "@mui/core-downloads-tracker": "npm:^5.17.1"
- "@mui/system": "npm:^5.17.1"
+ "@mui/core-downloads-tracker": "npm:^5.18.0"
+ "@mui/system": "npm:^5.18.0"
"@mui/types": "npm:~7.2.15"
"@mui/utils": "npm:^5.17.1"
"@popperjs/core": "npm:^2.11.8"
@@ -8618,7 +8741,7 @@ __metadata:
optional: true
"@types/react":
optional: true
- checksum: 10c0/55c120879e65833dda16ae1100eb1e2b43d0a7c80a9d1209121c71953c5e7646a1e76d8bd0204382a401af2d8d11b59383e92969525a6cc3f0149a138b25cad7
+ checksum: 10c0/a743c9105d08b636d4d9cf0aa276283b9095113771c188c7f5ac6953606dd77a5eb082dbbd889446a9a8573b8676d7249140c29eec55ae8320592e5f02d0f2cb
languageName: node
linkType: hard
@@ -8639,12 +8762,13 @@ __metadata:
languageName: node
linkType: hard
-"@mui/styled-engine@npm:^5.16.14":
- version: 5.16.14
- resolution: "@mui/styled-engine@npm:5.16.14"
+"@mui/styled-engine@npm:^5.18.0":
+ version: 5.18.0
+ resolution: "@mui/styled-engine@npm:5.18.0"
dependencies:
"@babel/runtime": "npm:^7.23.9"
"@emotion/cache": "npm:^11.13.5"
+ "@emotion/serialize": "npm:^1.3.3"
csstype: "npm:^3.1.3"
prop-types: "npm:^15.8.1"
peerDependencies:
@@ -8656,17 +8780,17 @@ __metadata:
optional: true
"@emotion/styled":
optional: true
- checksum: 10c0/cd512faea4ad3ff5a9b315e136a518223ea3e4e34462fe70c56d1f166c46bee0a885ed982773d75c1d56ead62b95989cc5907601e8d65bfa75494b3f3288c2ad
+ checksum: 10c0/68dad75142eea160fc51abf14915d07afd0e7e7791823f6ea6845b2037fde9de6c17b84247a1f283a1437d130857cb97c1a8474c25c161a934671bc48f205418
languageName: node
linkType: hard
-"@mui/system@npm:^5.17.1":
- version: 5.17.1
- resolution: "@mui/system@npm:5.17.1"
+"@mui/system@npm:^5.18.0":
+ version: 5.18.0
+ resolution: "@mui/system@npm:5.18.0"
dependencies:
"@babel/runtime": "npm:^7.23.9"
"@mui/private-theming": "npm:^5.17.1"
- "@mui/styled-engine": "npm:^5.16.14"
+ "@mui/styled-engine": "npm:^5.18.0"
"@mui/types": "npm:~7.2.15"
"@mui/utils": "npm:^5.17.1"
clsx: "npm:^2.1.0"
@@ -8684,7 +8808,7 @@ __metadata:
optional: true
"@types/react":
optional: true
- checksum: 10c0/ab74424e536164b720126ddd31ff0ceea4fb51d72f8d18f9be5621b33f8bbdf7fa8c96f8d1d2c4544ddacbaa84df1a197667f10cbe8915e00df103930e40f56e
+ checksum: 10c0/9f5ad15f08c71560e9723b1f136214a0871079a976285f8b813041081850e1f9e2e9fb00766c15814217852694a521a9a91cde3bed95b8062defa8052f69eabf
languageName: node
linkType: hard
@@ -10347,7 +10471,7 @@ __metadata:
languageName: node
linkType: hard
-"@red-hat-developer-hub/backstage-plugin-theme@npm:^0.14.0":
+"@red-hat-developer-hub/backstage-plugin-theme@npm:^0.14.0, @red-hat-developer-hub/backstage-plugin-theme@npm:^0.14.11":
version: 0.14.11
resolution: "@red-hat-developer-hub/backstage-plugin-theme@npm:0.14.11"
dependencies:
@@ -10494,6 +10618,10 @@ __metadata:
"@backstage/core-components": "npm:^0.18.13"
"@backstage/core-plugin-api": "npm:^1.12.9"
"@backstage/dev-utils": "npm:^1.1.26"
+ "@backstage/frontend-defaults": "npm:^0.5.5"
+ "@backstage/frontend-plugin-api": "npm:^0.18.0"
+ "@backstage/frontend-test-utils": "npm:^0.6.3"
+ "@backstage/plugin-app-react": "npm:^0.2.6"
"@backstage/plugin-permission-common": "npm:^0.9.10"
"@backstage/plugin-permission-react": "npm:^0.5.4"
"@backstage/plugin-search-react": "npm:^1.11.7"
@@ -15247,16 +15375,9 @@ __metadata:
languageName: node
linkType: hard
-"app-root-path@npm:^3.1.0":
- version: 3.1.0
- resolution: "app-root-path@npm:3.1.0"
- checksum: 10c0/4a0fd976de1bffcdb18a5e1f8050091f15d0780e0582bca99aaa9d52de71f0e08e5185355fcffc781180bfb898499e787a2f5ed79b9c448b942b31dc947acaa9
- languageName: node
- linkType: hard
-
-"app@workspace:packages/app":
+"app-legacy@workspace:packages/app-legacy":
version: 0.0.0-use.local
- resolution: "app@workspace:packages/app"
+ resolution: "app-legacy@workspace:packages/app-legacy"
dependencies:
"@backstage-community/plugin-rbac": "npm:1.55.2"
"@backstage-community/plugin-tech-radar": "npm:^0.7.4"
@@ -15310,6 +15431,48 @@ __metadata:
languageName: unknown
linkType: soft
+"app-root-path@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "app-root-path@npm:3.1.0"
+ checksum: 10c0/4a0fd976de1bffcdb18a5e1f8050091f15d0780e0582bca99aaa9d52de71f0e08e5185355fcffc781180bfb898499e787a2f5ed79b9c448b942b31dc947acaa9
+ languageName: node
+ linkType: hard
+
+"app@workspace:packages/app":
+ version: 0.0.0-use.local
+ resolution: "app@workspace:packages/app"
+ dependencies:
+ "@backstage-community/plugin-rbac": "npm:1.55.2"
+ "@backstage/cli": "npm:^0.36.5"
+ "@backstage/core-components": "npm:^0.18.13"
+ "@backstage/core-plugin-api": "npm:^1.12.9"
+ "@backstage/frontend-defaults": "npm:^0.5.5"
+ "@backstage/frontend-plugin-api": "npm:^0.18.0"
+ "@backstage/integration-react": "npm:^1.2.21"
+ "@backstage/plugin-app-react": "npm:^0.2.6"
+ "@backstage/plugin-catalog": "npm:^2.0.8"
+ "@backstage/plugin-user-settings": "npm:^0.9.6"
+ "@backstage/test-utils": "npm:^1.7.21"
+ "@backstage/theme": "npm:^0.7.3"
+ "@backstage/ui": "npm:^0.17.1"
+ "@mui/icons-material": "npm:^5.17.1"
+ "@mui/material": "npm:^5.17.1"
+ "@playwright/test": "npm:1.61.1"
+ "@red-hat-developer-hub/backstage-plugin-theme": "npm:^0.14.11"
+ "@red-hat-developer-hub/plugin-cost-management": "workspace:^"
+ "@testing-library/dom": "npm:^10.0.0"
+ "@testing-library/jest-dom": "npm:^6.0.0"
+ "@testing-library/react": "npm:^16.0.0"
+ "@testing-library/user-event": "npm:^14.0.0"
+ "@types/react-dom": "npm:*"
+ cross-env: "npm:^7.0.0"
+ react: "npm:^18.0.2"
+ react-dom: "npm:^18.0.2"
+ react-router: "npm:^6.30.2"
+ react-router-dom: "npm:^6.30.2"
+ languageName: unknown
+ linkType: soft
+
"append-field@npm:^1.0.0":
version: 1.0.0
resolution: "append-field@npm:1.0.0"
@@ -20970,6 +21133,28 @@ __metadata:
languageName: node
linkType: hard
+"framer-motion@npm:^12.43.0":
+ version: 12.43.0
+ resolution: "framer-motion@npm:12.43.0"
+ dependencies:
+ motion-dom: "npm:^12.43.0"
+ motion-utils: "npm:^12.39.0"
+ tslib: "npm:^2.4.0"
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ checksum: 10c0/cce3c1daf8f53da4d9299e6bdd2a1e9f08a30e13d84ce1e468cf25285f9ec2a900c812e0aa3dce79e7453a2a3cf328a717451beef9bf0a6175a3d3794116e36a
+ languageName: node
+ linkType: hard
+
"framer-motion@npm:^6.5.1":
version: 6.5.1
resolution: "framer-motion@npm:6.5.1"
@@ -26569,6 +26754,43 @@ __metadata:
languageName: node
linkType: hard
+"motion-dom@npm:^12.43.0":
+ version: 12.43.0
+ resolution: "motion-dom@npm:12.43.0"
+ dependencies:
+ motion-utils: "npm:^12.39.0"
+ checksum: 10c0/ef7d3f6eaa6cfeb1a939847a8b2581d119f2efc054948921ece0e2b1091f0969c15ca7707b87a46dddac7f156a714878f168e7b9f6ea189eeb7d5d9fbbb99d26
+ languageName: node
+ linkType: hard
+
+"motion-utils@npm:^12.39.0":
+ version: 12.39.0
+ resolution: "motion-utils@npm:12.39.0"
+ checksum: 10c0/6d7a2a2cc0797b72410a666a9cc1c201c8e39bf9669670464e433fe1e72af5f0217154c869867b34fbadf3664cf222c0d022bbc4eed7927f201ae971918e7440
+ languageName: node
+ linkType: hard
+
+"motion@npm:^12.0.0":
+ version: 12.43.0
+ resolution: "motion@npm:12.43.0"
+ dependencies:
+ framer-motion: "npm:^12.43.0"
+ tslib: "npm:^2.4.0"
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ checksum: 10c0/15544ecc71d356661a959c78c4f84b3ca91728f5d17265b68912d2bbcf86f20409ebf5acf2f6c82e0c0425d1024267b04abd5b786177a3694bf706f7a3a529d6
+ languageName: node
+ linkType: hard
+
"mri@npm:1.1.4":
version: 1.1.4
resolution: "mri@npm:1.1.4"