From 3749c7ea2a36ee56fda3dd8522dd43b2ec5586ce Mon Sep 17 00:00:00 2001 From: Ashraf Masarwa Date: Mon, 16 Feb 2026 11:53:48 +0200 Subject: [PATCH 1/2] FLPATH-3235 | [Bug] Cost management sidebar nav visible to users without cost management RBAC permissions --- .../packages/app/src/components/Root/Root.tsx | 61 +++++++++------ .../report.api.md | 29 +++++++ .../src/Router.tsx | 76 +++++++++++++++++-- .../redhat-resource-optimization/src/apis.ts | 21 +++++ .../hooks/useResourceOptimizationAccess.ts | 58 ++++++++++++++ .../redhat-resource-optimization/src/index.ts | 4 + .../src/plugin.ts | 33 ++++++++ 7 files changed, 255 insertions(+), 27 deletions(-) create mode 100644 workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/hooks/useResourceOptimizationAccess.ts diff --git a/workspaces/redhat-resource-optimization/packages/app/src/components/Root/Root.tsx b/workspaces/redhat-resource-optimization/packages/app/src/components/Root/Root.tsx index df38280a71..a93447bba2 100644 --- a/workspaces/redhat-resource-optimization/packages/app/src/components/Root/Root.tsx +++ b/workspaces/redhat-resource-optimization/packages/app/src/components/Root/Root.tsx @@ -43,7 +43,10 @@ import MenuIcon from '@material-ui/icons/Menu'; import SearchIcon from '@material-ui/icons/Search'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; -import { AnalyticsIconOutlined } from '@red-hat-developer-hub/plugin-redhat-resource-optimization'; +import { + AnalyticsIconOutlined, + useResourceOptimizationAccess, +} from '@red-hat-developer-hub/plugin-redhat-resource-optimization'; import { OrchestratorIcon } from '@red-hat-developer-hub/backstage-plugin-orchestrator'; import { useRhdhTheme } from '../../hooks/useRhdhTheme'; import { Administration } from '@backstage-community/plugin-rbac'; @@ -157,6 +160,11 @@ const SidebarLogo = () => { export const Root = ({ children }: PropsWithChildren<{}>) => { const classes = useSidebarItemStyles(); const location = useLocation(); + const { + optimizationsAllowed, + costManagementAllowed, + loading: accessLoading, + } = useResourceOptimizationAccess(); const isOpenShiftActive = useMemo(() => { const pathname = location.pathname; @@ -178,6 +186,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { return false; }, [location.pathname]); + const showCostManagementSubmenu = + !accessLoading && (optimizationsAllowed || costManagementAllowed); + return ( @@ -199,27 +210,33 @@ export const Root = ({ children }: PropsWithChildren<{}>) => { {/* End global nav */} - } - text="Cost management" - > - - - + {showCostManagementSubmenu && ( + } + text="Cost management" + > + {costManagementAllowed && ( + + )} + {optimizationsAllowed && ( + + )} + + )} Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { PathParams } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; @@ -26,6 +27,17 @@ export const AnalyticsIconOutlined: ( props: SvgIconProps, ) => React_2.JSX.Element; +// @public +export interface ResourceOptimizationAccessApi { + // (undocumented) + getCostManagementAccess(): Promise; + // (undocumented) + getOptimizationsAccess(): Promise; +} + +// @public (undocumented) +export const resourceOptimizationAccessApiRef: ApiRef; + // @public export const ResourceOptimizationIcon: ( props: SvgIconProps & { @@ -59,5 +71,22 @@ export const resourceOptimizationPlugin: BackstagePlugin< // @public (undocumented) export function Router(): React_2.JSX.Element; +// @public +export function useResourceOptimizationAccess(): UseResourceOptimizationAccessResult; + +// Warning: (ae-missing-release-tag) "UseResourceOptimizationAccessResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface UseResourceOptimizationAccessResult { + // (undocumented) + costManagementAllowed: boolean; + // (undocumented) + error?: Error; + // (undocumented) + loading: boolean; + // (undocumented) + optimizationsAllowed: boolean; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/Router.tsx b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/Router.tsx index 845d8eef9c..e53aed4e45 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/Router.tsx +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/Router.tsx @@ -14,13 +14,58 @@ * limitations under the License. */ -import { ErrorPage } from '@backstage/core-components'; +import { ErrorPage, Progress } from '@backstage/core-components'; import React from 'react'; -import { Routes, Route } from 'react-router-dom'; +import { Navigate, 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'; +import { useResourceOptimizationAccess } from './hooks/useResourceOptimizationAccess'; + +function RequireOptimizationsAccess({ + children, +}: Readonly<{ children: React.ReactNode }>) { + const { optimizationsAllowed, costManagementAllowed, loading } = + useResourceOptimizationAccess(); + + if (loading) { + return ; + } + if (!optimizationsAllowed) { + return ( + + ); + } + return <>{children}; +} + +function RequireCostManagementAccess({ + children, +}: Readonly<{ children: React.ReactNode }>) { + const { optimizationsAllowed, costManagementAllowed, loading } = + useResourceOptimizationAccess(); + + if (loading) { + return ; + } + if (!costManagementAllowed) { + return ( + + ); + } + return <>{children}; +} /** @public */ export function Router() { @@ -29,9 +74,30 @@ export function Router() { return ( - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> } diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/apis.ts b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/apis.ts index eafb3bc70b..f3c126209f 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/apis.ts +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/apis.ts @@ -35,3 +35,24 @@ export const orchestratorSlimApiRef = createApiRef({ export const costManagementSlimApiRef = createApiRef({ id: 'plugin.redhat-cost-management-slim.api', }); + +/** + * API for checking RBAC access to Optimizations and OpenShift (Cost Management) sections. + * Used by the sidebar to show/hide items and by the Router to guard routes. + * @public + */ +export interface ResourceOptimizationAccessApi { + /** Returns true if the user has access to the Optimizations section. */ + getOptimizationsAccess(): Promise; + /** Returns true if the user has access to the OpenShift (Cost Management) section. */ + getCostManagementAccess(): Promise; +} + +/** + * API ref for {@link ResourceOptimizationAccessApi}. + * @public + */ +export const resourceOptimizationAccessApiRef = + createApiRef({ + id: 'plugin.redhat-resource-optimization.access', + }); diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/hooks/useResourceOptimizationAccess.ts b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/hooks/useResourceOptimizationAccess.ts new file mode 100644 index 0000000000..645af3be63 --- /dev/null +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/hooks/useResourceOptimizationAccess.ts @@ -0,0 +1,58 @@ +/* + * 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 { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { resourceOptimizationAccessApiRef } from '../apis'; + +/** + * Return type of {@link useResourceOptimizationAccess}. + * @public + */ +export interface UseResourceOptimizationAccessResult { + /** Whether the user has access to the Optimizations section. */ + optimizationsAllowed: boolean; + /** Whether the user has access to the OpenShift (Cost Management) section. */ + costManagementAllowed: boolean; + /** True while access is being fetched. */ + loading: boolean; + /** Set if fetching access failed. */ + error?: Error; +} + +/** + * Hook to fetch RBAC access for Optimizations and OpenShift (Cost Management). + * Use in sidebar to show/hide items and in Router to guard routes. + * @public + */ +export function useResourceOptimizationAccess(): UseResourceOptimizationAccessResult { + const accessApi = useApi(resourceOptimizationAccessApiRef); + + const { value, loading, error } = useAsync(async () => { + const [optimizationsAllowed, costManagementAllowed] = await Promise.all([ + accessApi.getOptimizationsAccess(), + accessApi.getCostManagementAccess(), + ]); + return { optimizationsAllowed, costManagementAllowed }; + }, [accessApi]); + + return { + optimizationsAllowed: value?.optimizationsAllowed ?? false, + costManagementAllowed: value?.costManagementAllowed ?? false, + loading, + error, + }; +} diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/index.ts b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/index.ts index 1b540b453a..b7df27c9f4 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/index.ts +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/index.ts @@ -17,3 +17,7 @@ export * from './components/icon'; export { Router } from './Router'; export { resourceOptimizationPlugin, ResourceOptimizationPage } from './plugin'; +export { useResourceOptimizationAccess } from './hooks/useResourceOptimizationAccess'; +export type { UseResourceOptimizationAccessResult } from './hooks/useResourceOptimizationAccess'; +export { resourceOptimizationAccessApiRef } from './apis'; +export type { ResourceOptimizationAccessApi } from './apis'; diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/plugin.ts b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/plugin.ts index f21b739abe..cf20194aa2 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/plugin.ts +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/plugin.ts @@ -31,7 +31,11 @@ import { optimizationsApiRef, orchestratorSlimApiRef, costManagementSlimApiRef, + resourceOptimizationAccessApiRef, + type ResourceOptimizationAccessApi, } from './apis'; + +const resourceOptimizationPluginId = 'redhat-resource-optimization'; import { optimizationsBreakdownRouteRef, rootRouteRef } from './routes'; /** @public */ @@ -79,6 +83,35 @@ export const resourceOptimizationPlugin = createPlugin({ }); }, }), + createApiFactory({ + api: resourceOptimizationAccessApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory({ discoveryApi, fetchApi }): ResourceOptimizationAccessApi { + return { + async getOptimizationsAccess() { + const baseUrl = await discoveryApi.getBaseUrl( + resourceOptimizationPluginId, + ); + const res = await fetchApi.fetch(`${baseUrl}/access`); + const data = (await res.json()) as { decision: string }; + return data.decision === 'ALLOW'; + }, + async getCostManagementAccess() { + const baseUrl = await discoveryApi.getBaseUrl( + resourceOptimizationPluginId, + ); + const res = await fetchApi.fetch( + `${baseUrl}/access/cost-management`, + ); + const data = (await res.json()) as { decision: string }; + return data.decision === 'ALLOW'; + }, + }; + }, + }), ], routes: { root: rootRouteRef, From ffcad129ce56956e5c2446a00635b5cf01431ca6 Mon Sep 17 00:00:00 2001 From: Ashraf Masarwa Date: Mon, 16 Feb 2026 12:51:21 +0200 Subject: [PATCH 2/2] Fix API.md --- .../redhat-resource-optimization/report.api.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/report.api.md b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/report.api.md index bb7a4b0d64..39eb7797e3 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/report.api.md +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/report.api.md @@ -29,13 +29,11 @@ export const AnalyticsIconOutlined: ( // @public export interface ResourceOptimizationAccessApi { - // (undocumented) getCostManagementAccess(): Promise; - // (undocumented) getOptimizationsAccess(): Promise; } -// @public (undocumented) +// @public export const resourceOptimizationAccessApiRef: ApiRef; // @public @@ -74,17 +72,11 @@ export function Router(): React_2.JSX.Element; // @public export function useResourceOptimizationAccess(): UseResourceOptimizationAccessResult; -// Warning: (ae-missing-release-tag) "UseResourceOptimizationAccessResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface UseResourceOptimizationAccessResult { - // (undocumented) costManagementAllowed: boolean; - // (undocumented) error?: Error; - // (undocumented) loading: boolean; - // (undocumented) optimizationsAllowed: boolean; }