Skip to content

Commit 7bf22ed

Browse files
authored
Add roles table for assigning/managing user/group roles (opendatahub-io#6099)
* Add roles table for assigning/managing user/group roles * update UX * change assigning label color to yellow
1 parent f49b7d9 commit 7bf22ed

25 files changed

+763
-640
lines changed

frontend/src/concepts/permissions/__tests__/utils.spec.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
buildGroupRoleMap,
33
buildUserRoleMap,
44
getRoleByRef,
5+
getRoleLabelTypeForRole,
56
getRoleLabelTypeForRoleRef,
67
getRoleRefsForSubject,
78
getRoleRefKey,
@@ -85,37 +86,22 @@ describe('permissions utils', () => {
8586
});
8687

8788
it('classifies role label type', () => {
88-
expect(
89-
getRoleLabelTypeForRoleRef(
90-
{ kind: 'Role', name: dashboardRole.metadata.name },
91-
dashboardRole,
92-
),
93-
).toBe(RoleLabelType.Dashboard);
94-
expect(
95-
getRoleLabelTypeForRoleRef(
96-
{ kind: 'Role', name: openshiftDefaultRole.metadata.name },
97-
openshiftDefaultRole,
98-
),
99-
).toBe(RoleLabelType.OpenshiftDefault);
100-
expect(
101-
getRoleLabelTypeForRoleRef(
102-
{ kind: 'Role', name: openshiftCustomRole.metadata.name },
103-
openshiftCustomRole,
104-
),
105-
).toBe(RoleLabelType.OpenshiftCustom);
89+
expect(getRoleLabelTypeForRole(dashboardRole)).toBe(RoleLabelType.Dashboard);
90+
expect(getRoleLabelTypeForRole(openshiftDefaultRole)).toBe(RoleLabelType.OpenshiftDefault);
91+
expect(getRoleLabelTypeForRole(openshiftCustomRole)).toBe(RoleLabelType.OpenshiftCustom);
10692
});
10793

10894
it('falls back to OpenShift label type when role object is not readable', () => {
109-
expect(getRoleLabelTypeForRoleRef({ kind: 'ClusterRole', name: 'admin' }, undefined)).toBe(
95+
expect(getRoleLabelTypeForRoleRef({ kind: 'ClusterRole', name: 'admin' })).toBe(
11096
RoleLabelType.OpenshiftDefault,
11197
);
112-
expect(getRoleLabelTypeForRoleRef({ kind: 'ClusterRole', name: 'edit' }, undefined)).toBe(
98+
expect(getRoleLabelTypeForRoleRef({ kind: 'ClusterRole', name: 'edit' })).toBe(
11399
RoleLabelType.OpenshiftDefault,
114100
);
115-
expect(getRoleLabelTypeForRoleRef({ kind: 'ClusterRole', name: 'unknown' }, undefined)).toBe(
101+
expect(getRoleLabelTypeForRoleRef({ kind: 'ClusterRole', name: 'unknown' })).toBe(
116102
RoleLabelType.OpenshiftCustom,
117103
);
118-
expect(getRoleLabelTypeForRoleRef({ kind: 'Role', name: 'unknown' }, undefined)).toBe(
104+
expect(getRoleLabelTypeForRoleRef({ kind: 'Role', name: 'unknown' })).toBe(
119105
RoleLabelType.OpenshiftCustom,
120106
);
121107
});

frontend/src/concepts/permissions/const.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ export const DEFAULT_ROLE_DESCRIPTIONS: Partial<Record<string, string>> = {
1010
'ClusterRole:admin': 'Edit the project and manage user access',
1111
'ClusterRole:edit': 'View and edit the project components',
1212
};
13+
14+
export const DEFAULT_CLUSTER_ROLE_NAMES: string[] = ['admin', 'edit'];

frontend/src/concepts/permissions/utils.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
import { KnownLabels } from '#~/k8sTypes';
99
import { getDisplayNameFromK8sResource } from '#~/concepts/k8s/utils';
1010
import {
11+
DEFAULT_CLUSTER_ROLE_NAMES,
1112
DEFAULT_ROLE_DESCRIPTIONS,
1213
OPENSHIFT_BOOTSTRAPPING_DEFAULT_VALUE,
1314
OPENSHIFT_BOOTSTRAPPING_LABEL_KEY,
@@ -78,7 +79,7 @@ export const hasRoleRef = (roleRefs: RoleRef[], target: RoleRef): boolean =>
7879
* - OpenShift default roles: RoleLabelType.OpenshiftDefault (bootstrapped RBAC defaults).
7980
* - OpenShift custom roles: RoleLabelType.OpenshiftCustom (everything else).
8081
*/
81-
const getRoleLabelType = (role: RoleKind | ClusterRoleKind): RoleLabelType => {
82+
export const getRoleLabelTypeForRole = (role: RoleKind | ClusterRoleKind): RoleLabelType => {
8283
const labels = role.metadata.labels ?? {};
8384
if (labels[KnownLabels.DASHBOARD_RESOURCE] === 'true') {
8485
return RoleLabelType.Dashboard;
@@ -97,15 +98,9 @@ const getRoleLabelType = (role: RoleKind | ClusterRoleKind): RoleLabelType => {
9798
* - admin/edit ClusterRoles are treated as OpenShift default even when unreadable.
9899
* - other unreadable roles are treated as OpenShift custom.
99100
*/
100-
export const getRoleLabelTypeForRoleRef = (
101-
roleRef: RoleRef,
102-
role?: RoleKind | ClusterRoleKind,
103-
): RoleLabelType => {
104-
if (role) {
105-
return getRoleLabelType(role);
106-
}
101+
export const getRoleLabelTypeForRoleRef = (roleRef: RoleRef): RoleLabelType => {
107102
const name = roleRef.name.toLowerCase();
108-
if (roleRef.kind === 'ClusterRole' && (name === 'admin' || name === 'edit')) {
103+
if (roleRef.kind === 'ClusterRole' && DEFAULT_CLUSTER_ROLE_NAMES.includes(name)) {
109104
return RoleLabelType.OpenshiftDefault;
110105
}
111106
return RoleLabelType.OpenshiftCustom;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createIcon } from '@patternfly/react-icons/dist/esm/createIcon';
2+
3+
const AiExperienceIcon = createIcon({
4+
name: 'AiExperienceIcon',
5+
width: 32,
6+
height: 32,
7+
svgPath:
8+
'M26.03125,16.96191c-5.90527-.46875-10.53076-5.09473-10.99902-11-.0415-.51953-.5166-.96094-1.03809-.96094s-.99658.44238-1.03809.96191c-.46777,5.9043-5.09375,10.53027-10.99121,10.99902-.52344.03711-.96973.51367-.96973,1.03809,0,.52148.44189.99707.96191,1.03809,5.90527.46875,10.53125,5.09473,10.99951,11,.0415.51953.5166.96094,1.0376.96094.52197,0,.99707-.44238,1.03857-.96191.46777-5.9043,5.09326-10.53027,10.99854-10.99902.51953-.04199.96191-.51562.96191-1.03809,0-.52148-.44238-.99707-.96191-1.03809ZM13.99414,25.76465c-1.4126-3.54492-4.21875-6.35059-7.7666-7.76367,3.54639-1.41309,6.354-4.2207,7.76709-7.76562,1.4126,3.54492,4.21826,6.35059,7.76611,7.76367-3.5459,1.41309-6.35352,4.2207-7.7666,7.76562ZM30.50195,7c0,.28906-.20898.53613-.49805.58984-2.2334.4082-4.00879,2.18359-4.41699,4.41699-.05371.28906-.30078.49805-.58984.49805s-.53613-.20898-.58984-.49805c-.40723-2.2334-2.18262-4.00879-4.41699-4.41699-.28906-.05371-.49805-.30078-.49805-.58984s.20898-.53613.49805-.58984c2.23438-.4082,4.00977-2.18359,4.41699-4.41699.05371-.28906.30078-.49805.58984-.49805s.53613.20898.58984.49805c.4082,2.2334,2.18359,4.00879,4.41699,4.41699.28906.05371.49805.30078.49805.58984Z',
9+
xOffset: 0,
10+
yOffset: 0,
11+
});
12+
13+
export default AiExperienceIcon;

frontend/src/pages/projects/projectPermissions/ProjectPermissions.tsx

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ import { ProjectSectionID } from '#~/pages/projects/screens/detail/types';
1919
import { usePermissionsContext } from '#~/concepts/permissions/PermissionsContext';
2020
import FilterToolbar from '#~/components/FilterToolbar';
2121
import SimpleSelect from '#~/components/SimpleSelect';
22-
import type { RoleRef } from '#~/concepts/permissions/types';
2322
import { ProjectDetailsContext } from '#~/pages/projects/ProjectDetailsContext';
2423
import SubjectRolesTableSection from './SubjectRolesTableSection';
25-
import RoleDetailsModal from './roleDetails/RoleDetailsModal';
2624
import {
2725
FilterDataType,
2826
initialFilterData,
@@ -41,8 +39,6 @@ const ProjectPermissions: React.FC = () => {
4139
SubjectScopeFilter.all,
4240
);
4341
const [filterData, setFilterData] = React.useState<FilterDataType>(initialFilterData);
44-
const [selectedRoleRef, setSelectedRoleRef] = React.useState<RoleRef>();
45-
4642
const clearFilters = React.useCallback(() => {
4743
setFilterData(initialFilterData);
4844
}, []);
@@ -70,12 +66,6 @@ const ProjectPermissions: React.FC = () => {
7066
</StackItem>
7167
) : (
7268
<>
73-
{selectedRoleRef ? (
74-
<RoleDetailsModal
75-
roleRef={selectedRoleRef}
76-
onClose={() => setSelectedRoleRef(undefined)}
77-
/>
78-
) : null}
7969
<StackItem>
8070
<Toolbar clearAllFilters={clearFilters}>
8171
<ToolbarContent>
@@ -147,7 +137,6 @@ const ProjectPermissions: React.FC = () => {
147137
subjectKind="user"
148138
filterData={filterData}
149139
onClearFilters={clearFilters}
150-
onRoleClick={setSelectedRoleRef}
151140
/>
152141
</StackItem>
153142
) : null}
@@ -157,7 +146,6 @@ const ProjectPermissions: React.FC = () => {
157146
subjectKind="group"
158147
filterData={filterData}
159148
onClearFilters={clearFilters}
160-
onRoleClick={setSelectedRoleRef}
161149
/>
162150
</StackItem>
163151
) : null}

frontend/src/pages/projects/projectPermissions/ProjectPermissionsAssignRoles.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ActionListItem,
1313
ActionListGroup,
1414
Bullseye,
15+
Icon,
1516
Spinner,
1617
FormHelperText,
1718
HelperText,
@@ -20,6 +21,8 @@ import {
2021
Content,
2122
ContentVariants,
2223
Alert,
24+
FlexItem,
25+
Flex,
2326
} from '@patternfly/react-core';
2427
import { Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
2528
import {
@@ -30,7 +33,10 @@ import ApplicationsPage from '#~/pages/ApplicationsPage';
3033
import { ProjectDetailsContext } from '#~/pages/projects/ProjectDetailsContext';
3134
import { useAccessReview } from '#~/api/useAccessReview.ts';
3235
import { getDisplayNameFromK8sResource } from '#~/concepts/k8s/utils.ts';
36+
import { ProjectObjectType, typedBackgroundColor } from '#~/concepts/design/utils.ts';
37+
import TypedObjectIcon from '#~/concepts/design/TypedObjectIcon.tsx';
3338
import SubjectNameTypeaheadSelect from './components/SubjectNameTypeaheadSelect';
39+
import ManageRolesTable from './manageRoles/ManageRolesTable';
3440
import { useRoleAssignmentData } from './useRoleAssignmentData';
3541

3642
const ProjectPermissionsAssignRolesForm: React.FC = () => {
@@ -134,7 +140,36 @@ const ProjectPermissionsAssignRolesForm: React.FC = () => {
134140
>
135141
{isManageMode ? (
136142
<Content component={ContentVariants.p} data-testid="assign-roles-subject-readonly">
137-
{subjectName}
143+
<Flex
144+
spaceItems={{ default: 'spaceItemsSm' }}
145+
alignItems={{ default: 'alignItemsCenter' }}
146+
>
147+
<FlexItem>
148+
<Bullseye
149+
style={{
150+
background: typedBackgroundColor(
151+
subjectKind === 'user'
152+
? ProjectObjectType.user
153+
: ProjectObjectType.group,
154+
),
155+
borderRadius: 14,
156+
width: 28,
157+
height: 28,
158+
}}
159+
>
160+
<Icon size="lg">
161+
<TypedObjectIcon
162+
resourceType={
163+
subjectKind === 'user'
164+
? ProjectObjectType.user
165+
: ProjectObjectType.group
166+
}
167+
/>
168+
</Icon>
169+
</Bullseye>
170+
</FlexItem>
171+
<FlexItem>{subjectName}</FlexItem>
172+
</Flex>
138173
</Content>
139174
) : (
140175
<>
@@ -167,6 +202,11 @@ const ProjectPermissionsAssignRolesForm: React.FC = () => {
167202
)}
168203
</FormGroup>
169204
</FormSection>
205+
<ManageRolesTable
206+
subjectKind={subjectKind}
207+
subjectName={subjectName}
208+
existingSubjectNames={existingSubjectNames}
209+
/>
170210
</Form>
171211
</PageSection>
172212
<PageSection hasBodyWrapper={false} stickyOnBreakpoint={{ default: 'bottom' }}>

0 commit comments

Comments
 (0)