From 9b16a40099decb39a8754cd701502109fb9e1f6e Mon Sep 17 00:00:00 2001 From: Matthew Short Date: Wed, 13 Aug 2025 16:27:33 -0700 Subject: [PATCH 1/5] ACM-22876 - RBAC UI Implementation - Roles - Initial page creation with mock data Signed-off-by: Matthew Short --- frontend/public/locales/en/translation.json | 2 + .../src/routes/UserManagement/Roles/Roles.tsx | 18 --- .../UserManagement/Roles/RolesManagement.tsx | 8 +- .../{Roles.test.tsx => RolesPage.test.tsx} | 4 +- .../routes/UserManagement/Roles/RolesPage.tsx | 28 +++++ .../UserManagement/Roles/RolesTable.tsx | 62 ++++++++++ .../UserManagement/Roles/RolesTableHelper.tsx | 114 ++++++++++++++++++ 7 files changed, 212 insertions(+), 24 deletions(-) delete mode 100644 frontend/src/routes/UserManagement/Roles/Roles.tsx rename frontend/src/routes/UserManagement/Roles/{Roles.test.tsx => RolesPage.test.tsx} (91%) create mode 100644 frontend/src/routes/UserManagement/Roles/RolesPage.tsx create mode 100644 frontend/src/routes/UserManagement/Roles/RolesTable.tsx create mode 100644 frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 94503de173a..731c2f3dfe5 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -2039,6 +2039,7 @@ "No risks found": "No risks found", "No role assignments": "No role assignments", "No role assignments have been created for this user yet. Create a role assignment to grant specific permissions.": "No role assignments have been created for this user yet. Create a role assignment to grant specific permissions.", + "No roles": "No roles", "No status": "No status", "No users": "No users", "No users in group": "No users in group", @@ -2197,6 +2198,7 @@ "Permanently destroy clusters?": "Permanently destroy clusters?", "Permanently remove node pool {{name}}?": "Permanently remove node pool {{name}}?", "Permission created": "Permission created", + "Permissions": "Permissions", "Persistent volume": "Persistent volume", "Persistent volume storage class for etcd data volumes": "Persistent volume storage class for etcd data volumes", "Personalize this view": "Personalize this view", diff --git a/frontend/src/routes/UserManagement/Roles/Roles.tsx b/frontend/src/routes/UserManagement/Roles/Roles.tsx deleted file mode 100644 index 0ed1f212db2..00000000000 --- a/frontend/src/routes/UserManagement/Roles/Roles.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright Contributors to the Open Cluster Management project */ -import { PageSection } from '@patternfly/react-core' -import { useTranslation } from '../../../lib/acm-i18next' -import { AcmPage, AcmPageContent, AcmPageHeader } from '../../../ui-components' - -const Roles = () => { - const { t } = useTranslation() - - return ( - }> - - Roles list - - - ) -} - -export { Roles } diff --git a/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx b/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx index 2c945cf0855..d02a7ee8d57 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx @@ -1,7 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ import { Navigate, Route, Routes } from 'react-router-dom-v5-compat' import { NavigationPath, createRoutePathFunction } from '../../../NavigationPath' -import { Roles } from './Roles' +import { RolesPage } from './RolesPage' import { RoleDetail } from './RoleDetail' import { RoleYaml } from './RoleYaml' import { RolePermissions } from './RolePermissions' @@ -12,15 +12,15 @@ const rolesChildPath = createRoutePathFunction(NavigationPath.roles) export default function RolesManagement() { return ( + {/* Main roles page */} + } /> + {/* Role detail routes */} } /> } /> } /> } /> - {/* Main roles page */} - } /> - } /> ) diff --git a/frontend/src/routes/UserManagement/Roles/Roles.test.tsx b/frontend/src/routes/UserManagement/Roles/RolesPage.test.tsx similarity index 91% rename from frontend/src/routes/UserManagement/Roles/Roles.test.tsx rename to frontend/src/routes/UserManagement/Roles/RolesPage.test.tsx index 2f371613c74..4c2e6be013d 100644 --- a/frontend/src/routes/UserManagement/Roles/Roles.test.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesPage.test.tsx @@ -4,13 +4,13 @@ import { render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom-v5-compat' import { RecoilRoot } from 'recoil' import { nockIgnoreRBAC } from '../../../lib/nock-util' -import { Roles } from './Roles' +import { RolesPage } from './RolesPage' function Component() { return ( - + ) diff --git a/frontend/src/routes/UserManagement/Roles/RolesPage.tsx b/frontend/src/routes/UserManagement/Roles/RolesPage.tsx new file mode 100644 index 00000000000..957f0dd1727 --- /dev/null +++ b/frontend/src/routes/UserManagement/Roles/RolesPage.tsx @@ -0,0 +1,28 @@ +/* Copyright Contributors to the Open Cluster Management project */ +import { useTranslation } from '../../../lib/acm-i18next' +import { AcmPage, AcmPageContent, AcmPageHeader } from '../../../ui-components' +import { RolesTable } from './RolesTable' + +const RolesPage = () => { + const { t } = useTranslation() + + // TODO: implement loading page? + // TODO: check page for rbac permission? + return ( + + } + > + + + + + ) +} + +export { RolesPage } diff --git a/frontend/src/routes/UserManagement/Roles/RolesTable.tsx b/frontend/src/routes/UserManagement/Roles/RolesTable.tsx new file mode 100644 index 00000000000..442b66fb3ae --- /dev/null +++ b/frontend/src/routes/UserManagement/Roles/RolesTable.tsx @@ -0,0 +1,62 @@ +/* Copyright Contributors to the Open Cluster Management project */ +import { PageSection } from '@patternfly/react-core' +import { useMemo, useCallback } from 'react' +import { useTranslation } from '../../../lib/acm-i18next' +import { AcmEmptyState, AcmTable, compareStrings } from '../../../ui-components' +import { rolesTableColumns, useFilters, Role } from './RolesTableHelper' + +const mockRoles: Role[] = [ + { + name: 'cluster-admin', + description: 'Full administrative access to the cluster', + category: 'System', + type: 'ClusterRole', + permissions: 156, + uid: 'cluster-admin-uid-1', + }, + { + name: 'view', + description: 'Read-only access to most objects in the cluster', + category: 'System', + type: 'ClusterRole', + permissions: 45, + uid: 'view-uid-2', + }, + { + name: 'edit', + description: 'Read and write access to most objects in a namespace', + category: 'System', + type: 'Role', + permissions: 78, + uid: 'edit-uid-3', + }, +] + +const RolesTable = () => { + const { t } = useTranslation() + const roles = useMemo(() => mockRoles?.sort((a, b) => compareStrings(a.name, b.name)) ?? [], []) + + const keyFn = useCallback((role: Role) => role.uid, []) + + const filters = useFilters() + const columns = rolesTableColumns({ t }) + + // TODO: implement loading page? + + return ( + + { + + key="roles-table" + filters={filters} + columns={columns} + keyFn={keyFn} + items={roles} + emptyState={} + /> + } + + ) +} + +export { RolesTable } diff --git a/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx b/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx new file mode 100644 index 00000000000..8157d430e93 --- /dev/null +++ b/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx @@ -0,0 +1,114 @@ +/* Copyright Contributors to the Open Cluster Management project */ +import { cellWidth } from '@patternfly/react-table' +import { useMemo } from 'react' +import { TFunction } from 'react-i18next' +import { generatePath, Link } from 'react-router-dom-v5-compat' +import { HighlightSearchText } from '../../../components/HighlightSearchText' +import { NavigationPath } from '../../../NavigationPath' +import { IAcmTableColumn } from '../../../ui-components/AcmTable/AcmTableTypes' + +import { Label } from '@patternfly/react-core' + +const EXPORT_FILE_PREFIX = 'roles-table' + +export interface Role { + name: string + description: string + category: string + type: string + permissions: number + uid: string +} + +type RolesTableHelperProps = { + t: TFunction +} + +const COLUMN_CELLS = { + NAME: (role: Role, search: string) => ( + + + + + + ), + DESCRIPTION: (role: Role) => {role.description}, + CATEGORY: (role: Role) => , + TYPE: (role: Role) => {role.type}, + PERMISSIONS: (role: Role) => {role.permissions}, +} + +export const rolesTableColumns = ({ t }: Pick): IAcmTableColumn[] => [ + { + header: t('Role'), + sort: 'name', + search: 'name', + transforms: [cellWidth(25)], + cell: (role, search) => COLUMN_CELLS.NAME(role, search), + exportContent: (role) => role.name, + }, + { + header: t('Description'), + sort: 'description', + search: 'description', + transforms: [cellWidth(30)], + cell: (role) => COLUMN_CELLS.DESCRIPTION(role), + exportContent: (role) => role.description, + }, + { + header: t('Category'), + sort: 'category', + transforms: [cellWidth(15)], + cell: (role) => COLUMN_CELLS.CATEGORY(role), + exportContent: (role) => role.category, + }, + { + header: t('Type'), + sort: 'type', + transforms: [cellWidth(15)], + cell: (role) => COLUMN_CELLS.TYPE(role), + exportContent: (role) => role.type, + }, + { + header: t('Permissions'), + sort: 'permissions', + transforms: [cellWidth(15)], + cell: (role) => COLUMN_CELLS.PERMISSIONS(role), + exportContent: (role) => role.permissions.toString(), + }, +] + +export const useFilters = () => { + return useMemo( + () => [ + { + id: 'category', + label: 'Category', + tableFilterFn: (selection: string[], role: Role) => { + if (selection.length === 0) return true + return selection.some((selected: string) => role.category === selected) + }, + options: [ + { label: 'System', value: 'System' }, + { label: 'Custom', value: 'Custom' }, + { label: 'Default', value: 'Default' }, + ], + }, + { + id: 'type', + label: 'Type', + tableFilterFn: (selection: string[], role: Role) => { + if (selection.length === 0) return true + return selection.some((selected: string) => role.type === selected) + }, + options: [ + { label: 'ClusterRole', value: 'ClusterRole' }, + { label: 'Role', value: 'Role' }, + ], + }, + ], + [] + ) +} + +export { EXPORT_FILE_PREFIX, COLUMN_CELLS } From 1dd79aa6d554988971c89e4faaca169f6b8770e9 Mon Sep 17 00:00:00 2001 From: Matthew Short Date: Wed, 13 Aug 2025 17:57:31 -0700 Subject: [PATCH 2/5] Implemented real clusterrole data by vm role label Signed-off-by: Matthew Short --- frontend/src/resources/rbac.ts | 4 ++ .../UserManagement/Roles/RolesTable.tsx | 58 ++++++++----------- .../UserManagement/Roles/RolesTableHelper.tsx | 2 +- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/frontend/src/resources/rbac.ts b/frontend/src/resources/rbac.ts index 2b5101d5cd6..83cc2395b36 100644 --- a/frontend/src/resources/rbac.ts +++ b/frontend/src/resources/rbac.ts @@ -152,3 +152,7 @@ export function listRoles() { export function listRoleBindings() { return listResources(RoleBindingDefinition) } + +export function listVirtualizationClusterRoles() { + return listResources(ClusterRoleDefinition, ['rbac.open-cluster-management.io/filter=vm-clusterroles']) +} diff --git a/frontend/src/routes/UserManagement/Roles/RolesTable.tsx b/frontend/src/routes/UserManagement/Roles/RolesTable.tsx index 442b66fb3ae..f58fbf7e5b8 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesTable.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesTable.tsx @@ -2,50 +2,42 @@ import { PageSection } from '@patternfly/react-core' import { useMemo, useCallback } from 'react' import { useTranslation } from '../../../lib/acm-i18next' -import { AcmEmptyState, AcmTable, compareStrings } from '../../../ui-components' +import { useQuery } from '../../../lib/useQuery' +import { listVirtualizationClusterRoles, ClusterRole } from '../../../resources/rbac' +import { AcmEmptyState, AcmTable, compareStrings, AcmLoadingPage } from '../../../ui-components' import { rolesTableColumns, useFilters, Role } from './RolesTableHelper' -const mockRoles: Role[] = [ - { - name: 'cluster-admin', - description: 'Full administrative access to the cluster', - category: 'System', - type: 'ClusterRole', - permissions: 156, - uid: 'cluster-admin-uid-1', - }, - { - name: 'view', - description: 'Read-only access to most objects in the cluster', - category: 'System', - type: 'ClusterRole', - permissions: 45, - uid: 'view-uid-2', - }, - { - name: 'edit', - description: 'Read and write access to most objects in a namespace', - category: 'System', - type: 'Role', - permissions: 78, - uid: 'edit-uid-3', - }, -] - const RolesTable = () => { const { t } = useTranslation() - const roles = useMemo(() => mockRoles?.sort((a, b) => compareStrings(a.name, b.name)) ?? [], []) + const { data: clusterRoles, loading } = useQuery(listVirtualizationClusterRoles) + + const roles = useMemo(() => { + if (!clusterRoles) return [] + + return clusterRoles + .map( + (clusterRole: ClusterRole): Role => ({ + name: clusterRole.metadata.name || '', + description: 'kubevirt.io cluster role', + category: 'Virtualization', + type: 'ClusterRole', + permissions: 'TBD', + uid: clusterRole.metadata.uid || clusterRole.metadata.name || '', + }) + ) + .sort((a, b) => compareStrings(a.name, b.name)) + }, [clusterRoles]) const keyFn = useCallback((role: Role) => role.uid, []) const filters = useFilters() const columns = rolesTableColumns({ t }) - // TODO: implement loading page? - return ( - { + {loading ? ( + + ) : ( key="roles-table" filters={filters} @@ -54,7 +46,7 @@ const RolesTable = () => { items={roles} emptyState={} /> - } + )} ) } diff --git a/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx b/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx index 8157d430e93..f0733a22edf 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx @@ -16,7 +16,7 @@ export interface Role { description: string category: string type: string - permissions: number + permissions: string uid: string } From f1ba367ad0e2cdfcf621f8fccef515fb858ec2f0 Mon Sep 17 00:00:00 2001 From: Matthew Short Date: Wed, 13 Aug 2025 21:46:06 -0700 Subject: [PATCH 3/5] Added main role page and connected existing role tab sections. Created roles context to reduce k8s api calls and implemented on all pages and tab sections. Improved route names and layout. Signed-off-by: Matthew Short --- frontend/src/NavigationPath.tsx | 8 +- .../UserManagement/Roles/RoleAssignments.tsx | 17 +--- .../UserManagement/Roles/RoleDetail.tsx | 17 +--- .../routes/UserManagement/Roles/RolePage.tsx | 92 +++++++++++++++++++ .../UserManagement/Roles/RolePermissions.tsx | 17 +--- .../routes/UserManagement/Roles/RoleYaml.tsx | 17 +--- .../UserManagement/Roles/RolesManagement.tsx | 34 ++++--- .../routes/UserManagement/Roles/RolesPage.tsx | 41 +++++++++ .../UserManagement/Roles/RolesTable.tsx | 6 +- .../UserManagement/Roles/RolesTableHelper.tsx | 2 +- 10 files changed, 184 insertions(+), 67 deletions(-) create mode 100644 frontend/src/routes/UserManagement/Roles/RolePage.tsx diff --git a/frontend/src/NavigationPath.tsx b/frontend/src/NavigationPath.tsx index 0fd88864686..4edb5f0e5d3 100644 --- a/frontend/src/NavigationPath.tsx +++ b/frontend/src/NavigationPath.tsx @@ -209,10 +209,10 @@ export enum NavigationPath { // RBAC Roles roles = '/multicloud/user-management/roles', - rolesDetails = '/multicloud/user-management/roles/:id', - rolesPermissions = '/multicloud/user-management/roles/:id/permissions', - rolesRoleAssignments = '/multicloud/user-management/roles/:id/role-assignments', - rolesYaml = '/multicloud/user-management/roles/:id/yaml', + roleDetails = '/multicloud/user-management/roles/:id', + rolePermissions = '/multicloud/user-management/roles/:id/permissions', + roleRoleAssignments = '/multicloud/user-management/roles/:id/role-assignments', + roleYaml = '/multicloud/user-management/roles/:id/yaml', emptyPath = '', } diff --git a/frontend/src/routes/UserManagement/Roles/RoleAssignments.tsx b/frontend/src/routes/UserManagement/Roles/RoleAssignments.tsx index 48340f10f0a..010f7b81a2e 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleAssignments.tsx +++ b/frontend/src/routes/UserManagement/Roles/RoleAssignments.tsx @@ -1,20 +1,13 @@ /* Copyright Contributors to the Open Cluster Management project */ import { PageSection } from '@patternfly/react-core' -import { useParams } from 'react-router-dom-v5-compat' -import { useTranslation } from '../../../lib/acm-i18next' -import { AcmPage, AcmPageContent, AcmPageHeader } from '../../../ui-components' +//import { useTranslation } from '../../../lib/acm-i18next' +import { useCurrentRole } from './RolesPage' const RoleAssignments = () => { - const { t } = useTranslation() - const { id = undefined } = useParams() + //const { t } = useTranslation() + const role = useCurrentRole() - return ( - }> - - Role assignments page for ID: {id} - - - ) + return Role assignments page for Role: {role?.metadata.name} } export { RoleAssignments } diff --git a/frontend/src/routes/UserManagement/Roles/RoleDetail.tsx b/frontend/src/routes/UserManagement/Roles/RoleDetail.tsx index 83bbd579803..89bfce91a8a 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleDetail.tsx +++ b/frontend/src/routes/UserManagement/Roles/RoleDetail.tsx @@ -1,20 +1,13 @@ /* Copyright Contributors to the Open Cluster Management project */ import { PageSection } from '@patternfly/react-core' -import { useParams } from 'react-router-dom-v5-compat' -import { useTranslation } from '../../../lib/acm-i18next' -import { AcmPage, AcmPageContent, AcmPageHeader } from '../../../ui-components' +//import { useTranslation } from '../../../lib/acm-i18next' +import { useCurrentRole } from './RolesPage' const RoleDetail = () => { - const { t } = useTranslation() - const { id = undefined } = useParams() + //const { t } = useTranslation() + const role = useCurrentRole() - return ( - }> - - Role detail page for ID: {id} - - - ) + return Role detail page for Role: {role?.metadata.name} } export { RoleDetail } diff --git a/frontend/src/routes/UserManagement/Roles/RolePage.tsx b/frontend/src/routes/UserManagement/Roles/RolePage.tsx new file mode 100644 index 00000000000..1c08edbe685 --- /dev/null +++ b/frontend/src/routes/UserManagement/Roles/RolePage.tsx @@ -0,0 +1,92 @@ +/* Copyright Contributors to the Open Cluster Management project */ +import { useParams, useLocation, Link, Outlet, useNavigate } from 'react-router-dom-v5-compat' +import { useTranslation } from '../../../lib/acm-i18next' +import { useRolesContext, useCurrentRole } from './RolesPage' +import { + AcmPage, + AcmPageHeader, + AcmSecondaryNav, + AcmSecondaryNavItem, + AcmLoadingPage, + AcmButton, +} from '../../../ui-components' +import { NavigationPath } from '../../../NavigationPath' +import { generatePath } from 'react-router-dom-v5-compat' +import { Page, PageSection } from '@patternfly/react-core' +import { ErrorPage } from '../../../components/ErrorPage' +import { ResourceError, ResourceErrorCode } from '../../../resources/utils' + +const RolePage = () => { + const { t } = useTranslation() + const { id = undefined } = useParams() + const location = useLocation() + const navigate = useNavigate() + const { loading } = useRolesContext() + const role = useCurrentRole() + + const isDetailsActive = location.pathname === generatePath(NavigationPath.roleDetails, { id: id ?? '' }) + const isPermissionsActive = location.pathname.includes('/permissions') + const isRoleAssignmentsActive = location.pathname.includes('/role-assignments') + const isYamlActive = location.pathname.includes('/yaml') + + switch (true) { + case loading: + return ( + + + + ) + case !role: + return ( + + navigate(NavigationPath.roles)} style={{ marginRight: '10px' }}> + {t('button.backToRoles')} + + } + /> + + ) + default: + return ( + + + {t('Details')} + + + {t('Permissions')} + + + + {t('Role assignments')} + + + + {t('YAML')} + + + } + /> + } + > + + + ) + } +} + +export { RolePage } diff --git a/frontend/src/routes/UserManagement/Roles/RolePermissions.tsx b/frontend/src/routes/UserManagement/Roles/RolePermissions.tsx index ea568270ba4..51f8fbebe73 100644 --- a/frontend/src/routes/UserManagement/Roles/RolePermissions.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolePermissions.tsx @@ -1,20 +1,13 @@ /* Copyright Contributors to the Open Cluster Management project */ import { PageSection } from '@patternfly/react-core' -import { useParams } from 'react-router-dom-v5-compat' -import { useTranslation } from '../../../lib/acm-i18next' -import { AcmPage, AcmPageContent, AcmPageHeader } from '../../../ui-components' +//import { useTranslation } from '../../../lib/acm-i18next' +import { useCurrentRole } from './RolesPage' const RolePermissions = () => { - const { t } = useTranslation() - const { id = undefined } = useParams() + //const { t } = useTranslation() + const role = useCurrentRole() - return ( - }> - - Role permissions page for ID: {id} - - - ) + return Role permissions page for Role: {role?.metadata.name} } export { RolePermissions } diff --git a/frontend/src/routes/UserManagement/Roles/RoleYaml.tsx b/frontend/src/routes/UserManagement/Roles/RoleYaml.tsx index 060021d83f8..8372558df44 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleYaml.tsx +++ b/frontend/src/routes/UserManagement/Roles/RoleYaml.tsx @@ -1,20 +1,13 @@ /* Copyright Contributors to the Open Cluster Management project */ import { PageSection } from '@patternfly/react-core' -import { useParams } from 'react-router-dom-v5-compat' -import { useTranslation } from '../../../lib/acm-i18next' -import { AcmPage, AcmPageContent, AcmPageHeader } from '../../../ui-components' +//import { useTranslation } from '../../../lib/acm-i18next' +import { useCurrentRole } from './RolesPage' const RoleYaml = () => { - const { t } = useTranslation() - const { id = undefined } = useParams() + //const { t } = useTranslation() + const role = useCurrentRole() - return ( - }> - - Role YAML page for ID: {id} - - - ) + return Role YAML page for Role: {role?.metadata.name} } export { RoleYaml } diff --git a/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx b/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx index d02a7ee8d57..3a928c316fe 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx @@ -1,7 +1,8 @@ /* Copyright Contributors to the Open Cluster Management project */ import { Navigate, Route, Routes } from 'react-router-dom-v5-compat' import { NavigationPath, createRoutePathFunction } from '../../../NavigationPath' -import { RolesPage } from './RolesPage' +import { RolesPage, RolesContextProvider } from './RolesPage' +import { RolePage } from './RolePage' import { RoleDetail } from './RoleDetail' import { RoleYaml } from './RoleYaml' import { RolePermissions } from './RolePermissions' @@ -11,17 +12,28 @@ const rolesChildPath = createRoutePathFunction(NavigationPath.roles) export default function RolesManagement() { return ( - - {/* Main roles page */} - } /> + + + {/* Individual role page tabs */} + }> + } /> + + }> + } /> + + }> + } /> + + }> + } /> + - {/* Role detail routes */} - } /> - } /> - } /> - } /> + {/* Main roles page with list of roles */} + } /> - } /> - + {/* Default redirect to roles */} + } /> + + ) } diff --git a/frontend/src/routes/UserManagement/Roles/RolesPage.tsx b/frontend/src/routes/UserManagement/Roles/RolesPage.tsx index 957f0dd1727..6f6c9fa3983 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesPage.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesPage.tsx @@ -1,8 +1,49 @@ /* Copyright Contributors to the Open Cluster Management project */ +import { createContext, useContext, ReactNode, useMemo } from 'react' +import { useParams } from 'react-router-dom-v5-compat' import { useTranslation } from '../../../lib/acm-i18next' +import { useQuery } from '../../../lib/useQuery' +import { listVirtualizationClusterRoles, ClusterRole } from '../../../resources/rbac' import { AcmPage, AcmPageContent, AcmPageHeader } from '../../../ui-components' import { RolesTable } from './RolesTable' +export type RolesContextType = { + clusterRoles?: ClusterRole[] + loading: boolean +} + +const RolesContext = createContext(undefined) + +export const useRolesContext = () => { + const context = useContext(RolesContext) + if (!context) { + throw new Error('useRolesContext must be used within RolesContextProvider') + } + return context +} + +export const useCurrentRole = () => { + const { id } = useParams() + const { clusterRoles } = useRolesContext() + + return useMemo(() => { + if (!clusterRoles || !id) return undefined + return clusterRoles.find((r) => r.metadata.uid === id || r.metadata.name === id) + }, [clusterRoles, id]) +} + +export const RolesContextProvider = ({ children }: { children: ReactNode }) => { + const { data: clusterRoles, loading } = useQuery(listVirtualizationClusterRoles) + //TODO: look into hanging apicalls at apiPaths that proceeds this api call which causes stuck loading page + + const contextValue: RolesContextType = { + clusterRoles, + loading, + } + + return {children} +} + const RolesPage = () => { const { t } = useTranslation() diff --git a/frontend/src/routes/UserManagement/Roles/RolesTable.tsx b/frontend/src/routes/UserManagement/Roles/RolesTable.tsx index f58fbf7e5b8..0d5323f5ee4 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesTable.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesTable.tsx @@ -2,14 +2,14 @@ import { PageSection } from '@patternfly/react-core' import { useMemo, useCallback } from 'react' import { useTranslation } from '../../../lib/acm-i18next' -import { useQuery } from '../../../lib/useQuery' -import { listVirtualizationClusterRoles, ClusterRole } from '../../../resources/rbac' +import { ClusterRole } from '../../../resources/rbac' import { AcmEmptyState, AcmTable, compareStrings, AcmLoadingPage } from '../../../ui-components' import { rolesTableColumns, useFilters, Role } from './RolesTableHelper' +import { useRolesContext } from './RolesPage' const RolesTable = () => { const { t } = useTranslation() - const { data: clusterRoles, loading } = useQuery(listVirtualizationClusterRoles) + const { clusterRoles, loading } = useRolesContext() const roles = useMemo(() => { if (!clusterRoles) return [] diff --git a/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx b/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx index f0733a22edf..69d9c8dad42 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesTableHelper.tsx @@ -27,7 +27,7 @@ type RolesTableHelperProps = { const COLUMN_CELLS = { NAME: (role: Role, search: string) => ( - + From 596015f50e9e8811348ff6251c1e073fb850e464 Mon Sep 17 00:00:00 2001 From: Matthew Short Date: Wed, 13 Aug 2025 21:48:44 -0700 Subject: [PATCH 4/5] Moved role files into dedicated directory. Signed-off-by: Matthew Short --- .../VirtualMachines/VirtualMachinesPage.tsx | 2 +- .../Roles/{ => Role}/RoleAssignments.test.tsx | 2 +- .../Roles/{ => Role}/RoleAssignments.tsx | 2 +- .../Roles/{ => Role}/RoleDetail.test.tsx | 2 +- .../UserManagement/Roles/{ => Role}/RoleDetail.tsx | 2 +- .../UserManagement/Roles/{ => Role}/RolePage.tsx | 12 ++++++------ .../Roles/{ => Role}/RolePermissions.test.tsx | 2 +- .../Roles/{ => Role}/RolePermissions.tsx | 2 +- .../Roles/{ => Role}/RoleYaml.test.tsx | 2 +- .../UserManagement/Roles/{ => Role}/RoleYaml.tsx | 2 +- .../routes/UserManagement/Roles/RolesManagement.tsx | 10 +++++----- 11 files changed, 20 insertions(+), 20 deletions(-) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RoleAssignments.test.tsx (93%) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RoleAssignments.tsx (89%) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RoleDetail.test.tsx (93%) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RoleDetail.tsx (89%) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RolePage.tsx (89%) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RolePermissions.test.tsx (93%) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RolePermissions.tsx (89%) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RoleYaml.test.tsx (92%) rename frontend/src/routes/UserManagement/Roles/{ => Role}/RoleYaml.tsx (89%) diff --git a/frontend/src/routes/Infrastructure/VirtualMachines/VirtualMachinesPage.tsx b/frontend/src/routes/Infrastructure/VirtualMachines/VirtualMachinesPage.tsx index 98bf797bb4d..3b62cf66d5f 100644 --- a/frontend/src/routes/Infrastructure/VirtualMachines/VirtualMachinesPage.tsx +++ b/frontend/src/routes/Infrastructure/VirtualMachines/VirtualMachinesPage.tsx @@ -20,7 +20,7 @@ import { useLocation, useNavigate, Outlet, Link } from 'react-router-dom-v5-comp import { Pages, usePageVisitMetricHandler } from '../../../hooks/console-metrics' import { useTranslation } from '../../../lib/acm-i18next' import { NavigationPath } from '../../../NavigationPath' -import { RoleAssignments } from '../../UserManagement/Roles/RoleAssignments' +import { RoleAssignments } from '../../UserManagement/Roles/Role/RoleAssignments' import { OCP_DOC } from '../../../lib/doc-util' import { PluginContext } from '../../../lib/PluginContext' import { ConfigMap } from '../../../resources' diff --git a/frontend/src/routes/UserManagement/Roles/RoleAssignments.test.tsx b/frontend/src/routes/UserManagement/Roles/Role/RoleAssignments.test.tsx similarity index 93% rename from frontend/src/routes/UserManagement/Roles/RoleAssignments.test.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RoleAssignments.test.tsx index 39b27893f1c..1fed8aa943a 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleAssignments.test.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RoleAssignments.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom-v5-compat' import { RecoilRoot } from 'recoil' -import { nockIgnoreRBAC } from '../../../lib/nock-util' +import { nockIgnoreRBAC } from '../../../../lib/nock-util' import { RoleAssignments } from './RoleAssignments' function Component({ roleId = 'test-role' }: { roleId?: string }) { diff --git a/frontend/src/routes/UserManagement/Roles/RoleAssignments.tsx b/frontend/src/routes/UserManagement/Roles/Role/RoleAssignments.tsx similarity index 89% rename from frontend/src/routes/UserManagement/Roles/RoleAssignments.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RoleAssignments.tsx index 010f7b81a2e..14574557f16 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleAssignments.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RoleAssignments.tsx @@ -1,7 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ import { PageSection } from '@patternfly/react-core' //import { useTranslation } from '../../../lib/acm-i18next' -import { useCurrentRole } from './RolesPage' +import { useCurrentRole } from '../RolesPage' const RoleAssignments = () => { //const { t } = useTranslation() diff --git a/frontend/src/routes/UserManagement/Roles/RoleDetail.test.tsx b/frontend/src/routes/UserManagement/Roles/Role/RoleDetail.test.tsx similarity index 93% rename from frontend/src/routes/UserManagement/Roles/RoleDetail.test.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RoleDetail.test.tsx index 8eb83d4f8d5..a8b0a758601 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleDetail.test.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RoleDetail.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom-v5-compat' import { RecoilRoot } from 'recoil' -import { nockIgnoreRBAC } from '../../../lib/nock-util' +import { nockIgnoreRBAC } from '../../../../lib/nock-util' import { RoleDetail } from './RoleDetail' function Component({ roleId = 'test-role' }: { roleId?: string }) { diff --git a/frontend/src/routes/UserManagement/Roles/RoleDetail.tsx b/frontend/src/routes/UserManagement/Roles/Role/RoleDetail.tsx similarity index 89% rename from frontend/src/routes/UserManagement/Roles/RoleDetail.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RoleDetail.tsx index 89bfce91a8a..ffe478add89 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleDetail.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RoleDetail.tsx @@ -1,7 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ import { PageSection } from '@patternfly/react-core' //import { useTranslation } from '../../../lib/acm-i18next' -import { useCurrentRole } from './RolesPage' +import { useCurrentRole } from '../RolesPage' const RoleDetail = () => { //const { t } = useTranslation() diff --git a/frontend/src/routes/UserManagement/Roles/RolePage.tsx b/frontend/src/routes/UserManagement/Roles/Role/RolePage.tsx similarity index 89% rename from frontend/src/routes/UserManagement/Roles/RolePage.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RolePage.tsx index 1c08edbe685..11b561fc9ce 100644 --- a/frontend/src/routes/UserManagement/Roles/RolePage.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RolePage.tsx @@ -1,7 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ import { useParams, useLocation, Link, Outlet, useNavigate } from 'react-router-dom-v5-compat' -import { useTranslation } from '../../../lib/acm-i18next' -import { useRolesContext, useCurrentRole } from './RolesPage' +import { useTranslation } from '../../../../lib/acm-i18next' +import { useRolesContext, useCurrentRole } from '../RolesPage' import { AcmPage, AcmPageHeader, @@ -9,12 +9,12 @@ import { AcmSecondaryNavItem, AcmLoadingPage, AcmButton, -} from '../../../ui-components' -import { NavigationPath } from '../../../NavigationPath' +} from '../../../../ui-components' +import { NavigationPath } from '../../../../NavigationPath' import { generatePath } from 'react-router-dom-v5-compat' import { Page, PageSection } from '@patternfly/react-core' -import { ErrorPage } from '../../../components/ErrorPage' -import { ResourceError, ResourceErrorCode } from '../../../resources/utils' +import { ErrorPage } from '../../../../components/ErrorPage' +import { ResourceError, ResourceErrorCode } from '../../../../resources/utils' const RolePage = () => { const { t } = useTranslation() diff --git a/frontend/src/routes/UserManagement/Roles/RolePermissions.test.tsx b/frontend/src/routes/UserManagement/Roles/Role/RolePermissions.test.tsx similarity index 93% rename from frontend/src/routes/UserManagement/Roles/RolePermissions.test.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RolePermissions.test.tsx index af381d0b947..44b83431269 100644 --- a/frontend/src/routes/UserManagement/Roles/RolePermissions.test.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RolePermissions.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom-v5-compat' import { RecoilRoot } from 'recoil' -import { nockIgnoreRBAC } from '../../../lib/nock-util' +import { nockIgnoreRBAC } from '../../../../lib/nock-util' import { RolePermissions } from './RolePermissions' function Component({ roleId = 'test-role' }: { roleId?: string }) { diff --git a/frontend/src/routes/UserManagement/Roles/RolePermissions.tsx b/frontend/src/routes/UserManagement/Roles/Role/RolePermissions.tsx similarity index 89% rename from frontend/src/routes/UserManagement/Roles/RolePermissions.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RolePermissions.tsx index 51f8fbebe73..e0c31a937ae 100644 --- a/frontend/src/routes/UserManagement/Roles/RolePermissions.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RolePermissions.tsx @@ -1,7 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ import { PageSection } from '@patternfly/react-core' //import { useTranslation } from '../../../lib/acm-i18next' -import { useCurrentRole } from './RolesPage' +import { useCurrentRole } from '../RolesPage' const RolePermissions = () => { //const { t } = useTranslation() diff --git a/frontend/src/routes/UserManagement/Roles/RoleYaml.test.tsx b/frontend/src/routes/UserManagement/Roles/Role/RoleYaml.test.tsx similarity index 92% rename from frontend/src/routes/UserManagement/Roles/RoleYaml.test.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RoleYaml.test.tsx index 8beafe3e0a1..2f57a0ad839 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleYaml.test.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RoleYaml.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom-v5-compat' import { RecoilRoot } from 'recoil' -import { nockIgnoreRBAC } from '../../../lib/nock-util' +import { nockIgnoreRBAC } from '../../../../lib/nock-util' import { RoleYaml } from './RoleYaml' function Component({ roleId = 'test-role' }: { roleId?: string }) { diff --git a/frontend/src/routes/UserManagement/Roles/RoleYaml.tsx b/frontend/src/routes/UserManagement/Roles/Role/RoleYaml.tsx similarity index 89% rename from frontend/src/routes/UserManagement/Roles/RoleYaml.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RoleYaml.tsx index 8372558df44..2a86e885ee5 100644 --- a/frontend/src/routes/UserManagement/Roles/RoleYaml.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RoleYaml.tsx @@ -1,7 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ import { PageSection } from '@patternfly/react-core' //import { useTranslation } from '../../../lib/acm-i18next' -import { useCurrentRole } from './RolesPage' +import { useCurrentRole } from '../RolesPage' const RoleYaml = () => { //const { t } = useTranslation() diff --git a/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx b/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx index 3a928c316fe..c80dbcb64c5 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx @@ -2,11 +2,11 @@ import { Navigate, Route, Routes } from 'react-router-dom-v5-compat' import { NavigationPath, createRoutePathFunction } from '../../../NavigationPath' import { RolesPage, RolesContextProvider } from './RolesPage' -import { RolePage } from './RolePage' -import { RoleDetail } from './RoleDetail' -import { RoleYaml } from './RoleYaml' -import { RolePermissions } from './RolePermissions' -import { RoleAssignments } from './RoleAssignments' +import { RolePage } from './Role/RolePage' +import { RoleDetail } from './Role/RoleDetail' +import { RoleYaml } from './Role/RoleYaml' +import { RolePermissions } from './Role/RolePermissions' +import { RoleAssignments } from './Role/RoleAssignments' const rolesChildPath = createRoutePathFunction(NavigationPath.roles) From ef398c60ae6597c8d8ec53029ee82c19f4de92f5 Mon Sep 17 00:00:00 2001 From: Matthew Short Date: Wed, 13 Aug 2025 21:49:59 -0700 Subject: [PATCH 5/5] Renamed role details files Signed-off-by: Matthew Short --- .../Roles/Role/{RoleDetail.test.tsx => RoleDetails.test.tsx} | 2 +- .../Roles/Role/{RoleDetail.tsx => RoleDetails.tsx} | 0 frontend/src/routes/UserManagement/Roles/RolesManagement.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename frontend/src/routes/UserManagement/Roles/Role/{RoleDetail.test.tsx => RoleDetails.test.tsx} (95%) rename frontend/src/routes/UserManagement/Roles/Role/{RoleDetail.tsx => RoleDetails.tsx} (100%) diff --git a/frontend/src/routes/UserManagement/Roles/Role/RoleDetail.test.tsx b/frontend/src/routes/UserManagement/Roles/Role/RoleDetails.test.tsx similarity index 95% rename from frontend/src/routes/UserManagement/Roles/Role/RoleDetail.test.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RoleDetails.test.tsx index a8b0a758601..294c74a01cc 100644 --- a/frontend/src/routes/UserManagement/Roles/Role/RoleDetail.test.tsx +++ b/frontend/src/routes/UserManagement/Roles/Role/RoleDetails.test.tsx @@ -4,7 +4,7 @@ import { render, screen } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom-v5-compat' import { RecoilRoot } from 'recoil' import { nockIgnoreRBAC } from '../../../../lib/nock-util' -import { RoleDetail } from './RoleDetail' +import { RoleDetail } from './RoleDetails' function Component({ roleId = 'test-role' }: { roleId?: string }) { return ( diff --git a/frontend/src/routes/UserManagement/Roles/Role/RoleDetail.tsx b/frontend/src/routes/UserManagement/Roles/Role/RoleDetails.tsx similarity index 100% rename from frontend/src/routes/UserManagement/Roles/Role/RoleDetail.tsx rename to frontend/src/routes/UserManagement/Roles/Role/RoleDetails.tsx diff --git a/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx b/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx index c80dbcb64c5..58b37788dea 100644 --- a/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx +++ b/frontend/src/routes/UserManagement/Roles/RolesManagement.tsx @@ -3,7 +3,7 @@ import { Navigate, Route, Routes } from 'react-router-dom-v5-compat' import { NavigationPath, createRoutePathFunction } from '../../../NavigationPath' import { RolesPage, RolesContextProvider } from './RolesPage' import { RolePage } from './Role/RolePage' -import { RoleDetail } from './Role/RoleDetail' +import { RoleDetail } from './Role/RoleDetails' import { RoleYaml } from './Role/RoleYaml' import { RolePermissions } from './Role/RolePermissions' import { RoleAssignments } from './Role/RoleAssignments'