Skip to content

Commit

Permalink
feat(rbac): save and display role description in the frontend (#1206)
Browse files Browse the repository at this point in the history
fix(rbac): save and display role description in the frontend

Signed-off-by: Oleksandr Andriienko <[email protected]>
  • Loading branch information
AndrienkoAleksandr authored Feb 26, 2024
1 parent 2855713 commit ff61266
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 79 deletions.
11 changes: 4 additions & 7 deletions plugins/rbac/src/components/CreateRole/EditRolePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { RoleFormValues } from './types';
export const EditRolePage = () => {
const { roleName, roleNamespace, roleKind } = useParams();
const [queryParamState] = useQueryParamState<number>('activeStep');
const { selectedMembers, members, loading, roleError, membersError } =
const { selectedMembers, members, role, loading, roleError, membersError } =
useSelectedMembers(
roleName ? `${roleKind}:${roleNamespace}/${roleName}` : '',
);
Expand All @@ -33,19 +33,16 @@ export const EditRolePage = () => {
name: roleName || '',
namespace: roleNamespace || 'default',
kind: roleKind || 'role',
description: '',
description: role?.metadata?.description ?? '',
selectedMembers,
permissionPoliciesRows: permissionPolicies,
};
const renderPage = () => {
if (loading) {
return <Progress />;
} else if (roleError?.name) {
} else if (roleError.name) {
return (
<ErrorPage
status={roleError?.name}
statusMessage={roleError?.message}
/>
<ErrorPage status={roleError.name} statusMessage={roleError.message} />
);
}
return (
Expand Down
84 changes: 84 additions & 0 deletions plugins/rbac/src/components/RoleOverview/AboutCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';

import { renderInTestApp } from '@backstage/test-utils';

import { Role } from '@janus-idp/backstage-plugin-rbac-common';

import { useRole } from '../../hooks/useRole';
import { AboutCard } from './AboutCard';

jest.mock('../../hooks/useRole', () => ({
useRole: jest.fn(),
}));

const mockRole: Role = {
name: 'role:default/rbac-admin',
memberReferences: ['user:default/tom', 'group:default/performance-dev-team'],
metadata: {
source: 'rest',
description: 'performance dev team',
},
};

const mockRoleWithoutDescription: Role = {
name: 'role:default/rbac-admin',
memberReferences: ['user:default/tom', 'group:default/performance-dev-team'],
metadata: {
source: 'rest',
description: undefined,
},
};

const mockUseRole = useRole as jest.MockedFunction<typeof useRole>;

describe('AboutCard', () => {
it('should show role metadata information', async () => {
mockUseRole.mockReturnValue({
loading: false,
role: mockRole,
roleError: {
name: '',
message: '',
},
});
const { queryByText } = await renderInTestApp(
<AboutCard roleName="role:default/rbac_admin" />,
);
expect(queryByText('About')).not.toBeNull();
expect(queryByText('performance dev team')).not.toBeNull();
});

it('should display stub, when role description is absent', async () => {
mockUseRole.mockReturnValue({
loading: false,
role: mockRoleWithoutDescription,
roleError: {
name: '',
message: '',
},
});
const { queryByText } = await renderInTestApp(
<AboutCard roleName="role:default/rbac_admin" />,
);
expect(queryByText('About')).not.toBeNull();
expect(queryByText('No description')).not.toBeNull();
});

it('should show an error if api call fails', async () => {
mockUseRole.mockReturnValue({
loading: false,
role: mockRole,
roleError: {
name: 'Role not found',
message: 'Role not found',
},
});
const { queryByText } = await renderInTestApp(
<AboutCard roleName="role:default/rbac_admin" />,
);
expect(
queryByText('Error: Something went wrong while fetching role'),
).not.toBeNull();
expect(queryByText('Role not found')).not.toBeNull();
});
});
69 changes: 48 additions & 21 deletions plugins/rbac/src/components/RoleOverview/AboutCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React from 'react';

import { MarkdownContent } from '@backstage/core-components';
import {
MarkdownContent,
Progress,
WarningPanel,
} from '@backstage/core-components';
import { AboutField } from '@backstage/plugin-catalog';

import {
Expand All @@ -11,6 +15,8 @@ import {
makeStyles,
} from '@material-ui/core';

import { useRole } from '../../hooks/useRole';

const useStyles = makeStyles({
gridItemCard: {
display: 'flex',
Expand All @@ -34,35 +40,56 @@ const useStyles = makeStyles({
},
});

export const AboutCard = () => {
type AboutCardProps = {
roleName: string;
};

export const AboutCard = ({ roleName }: AboutCardProps) => {
const classes = useStyles();
const cardClass = classes.gridItemCard;
const cardContentClass = classes.gridItemCardContent;

const { role, roleError, loading } = useRole(roleName);
if (loading) {
return <Progress />;
}
return (
<Card className={cardClass}>
<CardHeader title="About" />
<CardContent className={cardContentClass}>
<Grid container>
<AboutField label="Description" gridSizes={{ xs: 4, sm: 8, lg: 4 }}>
<MarkdownContent
className={classes.text}
content="No description"
/>
</AboutField>
<AboutField label="Modified By" gridSizes={{ xs: 4, sm: 8, lg: 4 }}>
<MarkdownContent
className={classes.text}
content="No information"
/>
</AboutField>
<AboutField label="Last Modified" gridSizes={{ xs: 4, sm: 8, lg: 4 }}>
<MarkdownContent
className={classes.text}
content="No information"
{roleError.name ? (
<div style={{ paddingBottom: '16px' }}>
<WarningPanel
message={roleError?.message}
title="Something went wrong while fetching role"
severity="error"
/>
</AboutField>
</Grid>
</div>
) : (
<Grid container>
<AboutField label="Description" gridSizes={{ xs: 4, sm: 8, lg: 4 }}>
<MarkdownContent
className={classes.text}
content={role?.metadata?.description ?? 'No description'}
/>
</AboutField>
<AboutField label="Modified By" gridSizes={{ xs: 4, sm: 8, lg: 4 }}>
<MarkdownContent
className={classes.text}
content={role?.metadata?.modifiedBy ?? 'No information'}
/>
</AboutField>
<AboutField
label="Last Modified"
gridSizes={{ xs: 4, sm: 8, lg: 4 }}
>
<MarkdownContent
className={classes.text}
content={role?.metadata?.lastModified ?? 'No information'}
/>
</AboutField>
</Grid>
)}
</CardContent>
</Card>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export const RoleOverviewPage = () => {
<TabbedLayout.Route path="" title="Overview">
<Grid container direction="row">
<Grid item lg={12} xs={12}>
<AboutCard />
<AboutCard
roleName={`${roleKind}:${roleNamespace}/${roleName}`}
/>
</Grid>
<Grid item lg={6} xs={12}>
<MembersCard
Expand Down
77 changes: 77 additions & 0 deletions plugins/rbac/src/hooks/useRole.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { renderHook, waitFor } from '@testing-library/react';

import { mockMembers } from '../__fixtures__/mockMembers';
import { useRole } from './useRole';

const apiMock = {
getRole: jest.fn().mockImplementation(),
getMembers: jest.fn().mockImplementation(),
};

jest.mock('@backstage/core-plugin-api', () => {
const actualApi = jest.requireActual('@backstage/core-plugin-api');
return {
...actualApi,
useApi: jest.fn().mockImplementation(() => {
return apiMock;
}),
};
});

describe('useRole', () => {
beforeEach(() => {
apiMock.getRole = jest.fn().mockImplementation(async () => {
return [
{
memberReferences: [
'group:default/admins',
'user:default/amelia.park',
'user:default/calum.leavy',
'group:default/team-b',
'group:default/team-c',
],
name: 'role:default/rbac_admin',
metadata: {
source: 'rest',
description: 'default rbac admin group',
},
},
];
});
apiMock.getMembers = jest.fn().mockImplementation(async () => mockMembers);
});

describe('useRole', () => {
it('should throw an error on get role', async () => {
apiMock.getRole = jest.fn().mockImplementation(() => {
throw new Error('Some error message');
});
const { result } = renderHook(() => useRole('role:default/rbac_admin'));
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current.roleError.message).toEqual('Some error message');
});
});

it('should return role', async () => {
const { result } = renderHook(() => useRole('role:default/rbac_admin'));
await waitFor(() => {
expect(result.current.loading).toBeFalsy();
expect(result.current.role).toEqual({
memberReferences: [
'group:default/admins',
'user:default/amelia.park',
'user:default/calum.leavy',
'group:default/team-b',
'group:default/team-c',
],
name: 'role:default/rbac_admin',
metadata: {
source: 'rest',
description: 'default rbac admin group',
},
});
});
});
});
});
31 changes: 31 additions & 0 deletions plugins/rbac/src/hooks/useRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useAsync } from 'react-use';

import { useApi } from '@backstage/core-plugin-api';

import { Role } from '@janus-idp/backstage-plugin-rbac-common';

import { rbacApiRef } from '../api/RBACBackendClient';

export const useRole = (
roleEntityRef: string,
): {
loading: boolean;
role: Role | undefined;
roleError: Error;
} => {
const rbacApi = useApi(rbacApiRef);
const {
value: roles,
loading,
error: roleError,
} = useAsync(async () => await rbacApi.getRole(roleEntityRef));

return {
loading,
role: Array.isArray(roles) ? roles[0] : undefined,
roleError: (roleError as Error) || {
name: (roles as Response)?.status,
message: `Error fetching the role. ${(roles as Response)?.statusText}`,
},
};
};
2 changes: 1 addition & 1 deletion plugins/rbac/src/hooks/useRoles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const useRoles = (
{
id: role.name,
name: role.name,
description: '-',
description: role.metadata?.description ?? '-',
members: role.memberReferences,
permissions,
modifiedBy: '-',
Expand Down
Loading

0 comments on commit ff61266

Please sign in to comment.