forked from janus-idp/backstage-plugins
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rbac): add support for creation of role (janus-idp#974)
- Loading branch information
1 parent
781cae3
commit 7cb9cbd
Showing
25 changed files
with
943 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 116 additions & 0 deletions
116
plugins/rbac/src/components/CreateRole/AddMembersForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import React from 'react'; | ||
import { useAsync } from 'react-use'; | ||
|
||
import { useApi } from '@backstage/core-plugin-api'; | ||
|
||
import { LinearProgress, TextField } from '@material-ui/core'; | ||
import Autocomplete from '@material-ui/lab/Autocomplete'; | ||
import FormHelperText from '@mui/material/FormHelperText'; | ||
import { FormikErrors } from 'formik'; | ||
|
||
import { rbacApiRef } from '../../api/RBACBackendClient'; | ||
import { MemberEntity } from '../../types'; | ||
import { | ||
getChildGroupsCount, | ||
getMembersCount, | ||
getParentGroupsCount, | ||
} from '../../utils/create-role-utils'; | ||
import { MembersDropdownOption } from './MembersDropdownOption'; | ||
import { CreateRoleFormValues, SelectedMember } from './types'; | ||
|
||
type AddMembersFormProps = { | ||
selectedMembers: SelectedMember[]; | ||
selectedMembersError?: string; | ||
setFieldValue: ( | ||
field: string, | ||
value: any, | ||
shouldValidate?: boolean, | ||
) => Promise<FormikErrors<CreateRoleFormValues>> | Promise<void>; | ||
}; | ||
|
||
export const AddMembersForm = ({ | ||
selectedMembers, | ||
selectedMembersError, | ||
setFieldValue, | ||
}: AddMembersFormProps) => { | ||
const rbacApi = useApi(rbacApiRef); | ||
const [search, setSearch] = React.useState<string>(''); | ||
const { | ||
loading: membersLoading, | ||
value: members, | ||
error, | ||
} = useAsync(async () => { | ||
return await rbacApi.getMembers(); | ||
}); | ||
|
||
const getDescription = (member: MemberEntity) => { | ||
const memberCount = getMembersCount(member); | ||
const parentCount = getParentGroupsCount(member); | ||
const childCount = getChildGroupsCount(member); | ||
|
||
return member.kind === 'Group' | ||
? `${memberCount} members, ${parentCount} parent group, ${childCount} child groups` | ||
: undefined; | ||
}; | ||
|
||
const membersOptions: SelectedMember[] = members | ||
? members.map((member: MemberEntity, index) => ({ | ||
label: member.metadata.name, | ||
description: getDescription(member), | ||
etag: | ||
member.metadata.etag ?? | ||
`${member.metadata.name}-${member.kind}-${index}`, | ||
type: member.kind, | ||
namespace: member.metadata.namespace, | ||
members: getMembersCount(member), | ||
})) | ||
: []; | ||
|
||
return ( | ||
<> | ||
<FormHelperText> | ||
Search and select users and groups to be added. Selected users and | ||
groups will appear in the members table. | ||
</FormHelperText> | ||
<br /> | ||
<Autocomplete | ||
options={membersOptions} | ||
getOptionLabel={(option: SelectedMember) => option.label} | ||
getOptionSelected={(option: SelectedMember, value: SelectedMember) => | ||
option.etag === value.etag | ||
} | ||
loading={membersLoading} | ||
loadingText={<LinearProgress />} | ||
onChange={(_e, value: SelectedMember) => | ||
setFieldValue('selectedMembers', [...selectedMembers, value]) | ||
} | ||
disableClearable | ||
getOptionDisabled={(option: SelectedMember) => | ||
!!selectedMembers.find( | ||
(sm: SelectedMember) => sm.etag === option.etag, | ||
) | ||
} | ||
renderOption={(option: SelectedMember, state) => ( | ||
<MembersDropdownOption option={option} state={state} /> | ||
)} | ||
renderInput={params => ( | ||
<TextField | ||
{...params} | ||
variant="outlined" | ||
label="Users and groups" | ||
placeholder="Search by user name or group name" | ||
error={!!selectedMembersError} | ||
helperText={selectedMembersError ?? ''} | ||
value={search} | ||
onChange={e => setSearch(e.target.value)} | ||
required | ||
/> | ||
)} | ||
/> | ||
<br /> | ||
{error?.message && ( | ||
<FormHelperText error={!error}>{error.message}</FormHelperText> | ||
)} | ||
</> | ||
); | ||
}; |
50 changes: 50 additions & 0 deletions
50
plugins/rbac/src/components/CreateRole/AddedMembersTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from 'react'; | ||
|
||
import { Table } from '@backstage/core-components'; | ||
|
||
import { makeStyles } from '@material-ui/core'; | ||
import { FormikErrors } from 'formik'; | ||
|
||
import { getMembers } from '../../utils/rbac-utils'; | ||
import { selectedMembersColumns } from './AddedMembersTableColumn'; | ||
import { CreateRoleFormValues, SelectedMember } from './types'; | ||
|
||
const useStyles = makeStyles(theme => ({ | ||
empty: { | ||
padding: theme.spacing(2), | ||
display: 'flex', | ||
justifyContent: 'center', | ||
}, | ||
})); | ||
|
||
type AddedMembersTableProps = { | ||
selectedMembers: SelectedMember[]; | ||
setFieldValue: ( | ||
field: string, | ||
value: any, | ||
shouldValidate?: boolean, | ||
) => Promise<FormikErrors<CreateRoleFormValues>> | Promise<void>; | ||
}; | ||
|
||
export const AddedMembersTable = ({ | ||
selectedMembers, | ||
setFieldValue, | ||
}: AddedMembersTableProps) => { | ||
const classes = useStyles(); | ||
return ( | ||
<Table | ||
title={ | ||
selectedMembers.length > 0 | ||
? `Users and groups (${getMembers(selectedMembers)})` | ||
: 'Users and groups' | ||
} | ||
data={selectedMembers} | ||
columns={selectedMembersColumns(selectedMembers, setFieldValue)} | ||
emptyContent={ | ||
<div className={classes.empty}> | ||
No records. Selected users and groups appear here. | ||
</div> | ||
} | ||
/> | ||
); | ||
}; |
62 changes: 62 additions & 0 deletions
62
plugins/rbac/src/components/CreateRole/AddedMembersTableColumn.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import React from 'react'; | ||
|
||
import { TableColumn } from '@backstage/core-components'; | ||
|
||
import { IconButton } from '@material-ui/core'; | ||
import Delete from '@mui/icons-material/Delete'; | ||
import { FormikErrors } from 'formik'; | ||
|
||
import { CreateRoleFormValues, SelectedMember } from './types'; | ||
|
||
export const selectedMembersColumns = ( | ||
selectedMembers: SelectedMember[], | ||
setFieldValue: ( | ||
field: string, | ||
value: any, | ||
shouldValidate?: boolean, | ||
) => Promise<FormikErrors<CreateRoleFormValues>> | Promise<void>, | ||
): TableColumn<SelectedMember>[] => { | ||
const onRemove = (etag: string) => { | ||
const updatedMembers = selectedMembers.filter( | ||
(mem: SelectedMember) => mem.etag !== etag, | ||
); | ||
setFieldValue('selectedMembers', updatedMembers); | ||
}; | ||
|
||
return [ | ||
{ | ||
title: 'Name', | ||
field: 'label', | ||
type: 'string', | ||
}, | ||
{ | ||
title: 'Type', | ||
field: 'type', | ||
type: 'string', | ||
}, | ||
{ | ||
title: 'Members', | ||
field: 'members', | ||
type: 'numeric', | ||
align: 'left', | ||
emptyValue: '-', | ||
}, | ||
{ | ||
title: 'Actions', | ||
sorting: false, | ||
render: (mem: SelectedMember) => { | ||
return ( | ||
<span key={mem.etag}> | ||
<IconButton | ||
onClick={() => onRemove(mem.etag)} | ||
aria-label="Remove" | ||
title="Remove member" | ||
> | ||
<Delete /> | ||
</IconButton> | ||
</span> | ||
); | ||
}, | ||
}, | ||
]; | ||
}; |
45 changes: 45 additions & 0 deletions
45
plugins/rbac/src/components/CreateRole/CreateRoleForm.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import React from 'react'; | ||
|
||
import { render, screen } from '@testing-library/react'; | ||
|
||
import { CreateRoleForm } from './CreateRoleForm'; | ||
|
||
jest.mock('react-router-dom', () => ({ | ||
useNavigate: jest.fn(), | ||
})); | ||
|
||
jest.mock('@backstage/core-plugin-api', () => ({ | ||
...jest.requireActual('@backstage/core-plugin-api'), | ||
useApi: jest.fn(), | ||
})); | ||
|
||
jest.mock('formik', () => ({ | ||
...jest.requireActual('formik'), | ||
useFormik: jest.fn().mockReturnValue({ | ||
errors: {}, | ||
values: {}, | ||
// mocked useFormik to return formik status with submitError | ||
status: { submitError: 'Unexpected error' }, | ||
}), | ||
})); | ||
|
||
describe('CreateRoleForm', () => { | ||
it('renders create role form correctly', async () => { | ||
render(<CreateRoleForm />); | ||
|
||
expect( | ||
screen.getByText(/enter name and description of role/i), | ||
).toBeInTheDocument(); | ||
expect(screen.getByTestId(/role-name/i)).toBeInTheDocument(); | ||
expect(screen.getByTestId(/role-description/i)).toBeInTheDocument(); | ||
expect(screen.getByText(/add users and groups/i)).toBeInTheDocument(); | ||
}); | ||
|
||
it('shows error if there is any error in formik status', async () => { | ||
render(<CreateRoleForm />); | ||
|
||
expect( | ||
screen.getByText(/unable to create role. unexpected error/i), | ||
).toBeInTheDocument(); | ||
}); | ||
}); |
Oops, something went wrong.