Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -178,6 +186,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => {
return false;
}, [location.pathname]);

const showCostManagementSubmenu =
!accessLoading && (optimizationsAllowed || costManagementAllowed);

return (
<SidebarPage>
<Sidebar>
Expand All @@ -199,27 +210,33 @@ export const Root = ({ children }: PropsWithChildren<{}>) => {
{/* End global nav */}
<SidebarDivider />

<CollapsibleSubmenu
icon={<AnalyticsIconOutlined />}
text="Cost management"
>
<SidebarItem
icon={EmptyIcon}
to="/redhat-resource-optimization/ocp"
text="OpenShift"
className={`${classes.costManagementItem} ${
isOpenShiftActive ? '' : classes.inactiveItem
}`}
/>
<SidebarItem
icon={EmptyIcon}
to="/redhat-resource-optimization"
text="Optimizations"
className={`${classes.costManagementItem} ${
isOptimizationsActive ? '' : classes.inactiveItem
}`}
/>
</CollapsibleSubmenu>
{showCostManagementSubmenu && (
<CollapsibleSubmenu
icon={<AnalyticsIconOutlined />}
text="Cost management"
>
{costManagementAllowed && (
<SidebarItem
icon={EmptyIcon}
to="/redhat-resource-optimization/ocp"
text="OpenShift"
className={`${classes.costManagementItem} ${
isOpenShiftActive ? '' : classes.inactiveItem
}`}
/>
)}
{optimizationsAllowed && (
<SidebarItem
icon={EmptyIcon}
to="/redhat-resource-optimization"
text="Optimizations"
className={`${classes.costManagementItem} ${
isOptimizationsActive ? '' : classes.inactiveItem
}`}
/>
)}
</CollapsibleSubmenu>
)}

<SidebarItem
icon={OrchestratorIcon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
> 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';
Expand All @@ -26,6 +27,15 @@ export const AnalyticsIconOutlined: (
props: SvgIconProps,
) => React_2.JSX.Element;

// @public
export interface ResourceOptimizationAccessApi {
getCostManagementAccess(): Promise<boolean>;
getOptimizationsAccess(): Promise<boolean>;
}

// @public
export const resourceOptimizationAccessApiRef: ApiRef<ResourceOptimizationAccessApi>;

// @public
export const ResourceOptimizationIcon: (
props: SvgIconProps & {
Expand Down Expand Up @@ -59,5 +69,16 @@ export const resourceOptimizationPlugin: BackstagePlugin<
// @public (undocumented)
export function Router(): React_2.JSX.Element;

// @public
export function useResourceOptimizationAccess(): UseResourceOptimizationAccessResult;

// @public
export interface UseResourceOptimizationAccessResult {
costManagementAllowed: boolean;
error?: Error;
loading: boolean;
optimizationsAllowed: boolean;
}

// (No @packageDocumentation comment for this package)
```
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Progress />;
}
if (!optimizationsAllowed) {
return (
<Navigate
to={
costManagementAllowed
? '/redhat-resource-optimization/ocp'
: '/catalog'
}
replace
/>
);
}
return <>{children}</>;
}

function RequireCostManagementAccess({
children,
}: Readonly<{ children: React.ReactNode }>) {
const { optimizationsAllowed, costManagementAllowed, loading } =
useResourceOptimizationAccess();

if (loading) {
return <Progress />;
}
if (!costManagementAllowed) {
return (
<Navigate
to={optimizationsAllowed ? '/redhat-resource-optimization' : '/catalog'}
replace
/>
);
}
return <>{children}</>;
}

/** @public */
export function Router() {
Expand All @@ -29,9 +74,30 @@ export function Router() {

return (
<Routes>
<Route path="/" element={<OptimizationsPage />} />
<Route path="/ocp" element={<OpenShiftPage />} />
<Route path="/:id/*" element={<OptimizationsBreakdownPage />} />
<Route
path="/"
element={
<RequireOptimizationsAccess>
<OptimizationsPage />
</RequireOptimizationsAccess>
}
/>
<Route
path="/ocp"
element={
<RequireCostManagementAccess>
<OpenShiftPage />
</RequireCostManagementAccess>
}
/>
<Route
path="/:id/*"
element={
<RequireOptimizationsAccess>
<OptimizationsBreakdownPage />
</RequireOptimizationsAccess>
}
/>
<Route
path="*"
element={<ErrorPage status="404" statusMessage="Page not found" />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,24 @@ export const orchestratorSlimApiRef = createApiRef<OrchestratorSlimApi>({
export const costManagementSlimApiRef = createApiRef<CostManagementSlimApi>({
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<boolean>;
/** Returns true if the user has access to the OpenShift (Cost Management) section. */
getCostManagementAccess(): Promise<boolean>;
}

/**
* API ref for {@link ResourceOptimizationAccessApi}.
* @public
*/
export const resourceOptimizationAccessApiRef =
createApiRef<ResourceOptimizationAccessApi>({
id: 'plugin.redhat-resource-optimization.access',
});
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ import {
optimizationsApiRef,
orchestratorSlimApiRef,
costManagementSlimApiRef,
resourceOptimizationAccessApiRef,
type ResourceOptimizationAccessApi,
} from './apis';

const resourceOptimizationPluginId = 'redhat-resource-optimization';
import { optimizationsBreakdownRouteRef, rootRouteRef } from './routes';

/** @public */
Expand Down Expand Up @@ -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,
Expand Down