-
Notifications
You must be signed in to change notification settings - Fork 13.1k
feat: admin ABAC rooms tab #37557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: admin ABAC rooms tab #37557
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
e8df925
feat: admin ABAC rooms tab
MartinSchoeler f58f7da
fix test
MartinSchoeler 5929ed5
review: unused if
MartinSchoeler bbec27e
review: query key
MartinSchoeler 60fc344
review: flatmap
MartinSchoeler 958a574
review: reset page when updating filters
MartinSchoeler 27f2618
review: translation fix
MartinSchoeler a58fef3
review: un-memoize select value
MartinSchoeler 7b1a395
review: attributesData
MartinSchoeler 39c26d2
review: missing handleSave
MartinSchoeler 1b2cebc
review: use Select and fix some toasts
MartinSchoeler 454ab00
review: don't pass setState to component
MartinSchoeler 1f18a93
review: swap controller for useControl
MartinSchoeler ce344a9
review: disable useABACAttributeList query when abac not available
MartinSchoeler 24676f8
review: useEndpointMutation
MartinSchoeler 0330116
review: Split delete modal component and tests
MartinSchoeler 8fece47
chore: ts fixes
MartinSchoeler d39dc67
review: useEndpointMutation on error
MartinSchoeler ecf0b93
review: remove unused fragment
MartinSchoeler 1c58610
review: add tests
MartinSchoeler bebe6eb
review: check for whole form validation for submit button
MartinSchoeler c9c3b4d
Rename query keys
tassoevan 9c28394
Apply test translations to Storybook too
tassoevan 3664222
Replace hook mock
tassoevan 3b261ff
Move deletion mutation to modal component
tassoevan 94b3eff
Use named exports for hooks
tassoevan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
43 changes: 43 additions & 0 deletions
43
apps/meteor/client/views/admin/ABAC/ABACAttributeField.spec.tsx
This file contains hidden or 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,43 @@ | ||
| import { composeStories } from '@storybook/react'; | ||
| import { render } from '@testing-library/react'; | ||
|
|
||
| import * as stories from './ABACAttributeField.stories'; | ||
|
|
||
| const mockAttribute1 = { | ||
| _id: 'attr1', | ||
| key: 'Department', | ||
| label: 'Department', | ||
| values: ['Engineering', 'Sales', 'Marketing'], | ||
| }; | ||
|
|
||
| const mockAttribute2 = { | ||
| _id: 'attr2', | ||
| key: 'Security-Level', | ||
| label: 'Security Level', | ||
| values: ['Public', 'Internal', 'Confidential'], | ||
| }; | ||
|
|
||
| const mockAttribute3 = { | ||
| _id: 'attr3', | ||
| key: 'Location', | ||
| label: 'Location', | ||
| values: ['US', 'EU', 'APAC'], | ||
| }; | ||
|
|
||
| jest.mock('./hooks/useABACAttributeList', () => ({ | ||
| useABACAttributeList: jest.fn(() => ({ | ||
| data: [mockAttribute1, mockAttribute2, mockAttribute3], | ||
| fetchNextPage: jest.fn(), | ||
| isLoading: false, | ||
| })), | ||
| })); | ||
|
|
||
| describe('ABACAttributeField', () => { | ||
| // TODO: Once the autocomplete components are a11y compliant, and testable, add more tests | ||
| const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); | ||
|
|
||
| test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { | ||
| const { baseElement } = render(<Story />); | ||
| expect(baseElement).toMatchSnapshot(); | ||
| }); | ||
| }); |
75 changes: 75 additions & 0 deletions
75
apps/meteor/client/views/admin/ABAC/ABACAttributeField.stories.tsx
This file contains hidden or 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,75 @@ | ||
| import { Field } from '@rocket.chat/fuselage'; | ||
| import { mockAppRoot } from '@rocket.chat/mock-providers'; | ||
| import { action } from '@storybook/addon-actions'; | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
| import { FormProvider, useForm } from 'react-hook-form'; | ||
|
|
||
| import ABACAttributeField from './ABACAttributeField'; | ||
| import type { AdminABACRoomFormData } from './AdminABACRoomForm'; | ||
|
|
||
| const mockAttribute1 = { | ||
| _id: 'attr1', | ||
| _updatedAt: new Date().toISOString(), | ||
| key: 'Department', | ||
| values: ['Engineering', 'Sales', 'Marketing'], | ||
| }; | ||
|
|
||
| const mockAttribute2 = { | ||
| _id: 'attr2', | ||
| _updatedAt: new Date().toISOString(), | ||
| key: 'Security-Level', | ||
| values: ['Public', 'Internal', 'Confidential'], | ||
| }; | ||
|
|
||
| const mockAttribute3 = { | ||
| _id: 'attr3', | ||
| _updatedAt: new Date().toISOString(), | ||
| key: 'Location', | ||
| values: ['US', 'EU', 'APAC'], | ||
| }; | ||
|
|
||
| const meta: Meta<typeof ABACAttributeField> = { | ||
| component: ABACAttributeField, | ||
| parameters: { | ||
| layout: 'padded', | ||
| }, | ||
| decorators: [ | ||
| (Story) => { | ||
| const AppRoot = mockAppRoot() | ||
| .withEndpoint('GET', '/v1/abac/attributes', () => ({ | ||
| attributes: [mockAttribute1, mockAttribute2, mockAttribute3], | ||
| count: 3, | ||
| offset: 0, | ||
| total: 3, | ||
| })) | ||
| .build(); | ||
|
|
||
| const methods = useForm<AdminABACRoomFormData>({ | ||
| defaultValues: { | ||
| room: '', | ||
| attributes: [{ key: '', values: [] }], | ||
| }, | ||
| mode: 'onChange', | ||
| }); | ||
|
|
||
| return ( | ||
| <AppRoot> | ||
| <FormProvider {...methods}> | ||
| <Field> | ||
| <Story /> | ||
| </Field> | ||
| </FormProvider> | ||
| </AppRoot> | ||
| ); | ||
| }, | ||
| ], | ||
| args: { | ||
| onRemove: action('onRemove'), | ||
| index: 0, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof ABACAttributeField>; | ||
|
|
||
| export const Default: Story = {}; |
99 changes: 99 additions & 0 deletions
99
apps/meteor/client/views/admin/ABAC/ABACAttributeField.tsx
This file contains hidden or 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,99 @@ | ||
| import { Box, Button, FieldError, FieldRow, InputBoxSkeleton, MultiSelect, PaginatedSelectFiltered } from '@rocket.chat/fuselage'; | ||
| import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; | ||
| import { useCallback, useMemo, useState } from 'react'; | ||
| import { useController, useFormContext } from 'react-hook-form'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import type { AdminABACRoomFormData } from './AdminABACRoomForm'; | ||
| import { useABACAttributeList } from './hooks/useABACAttributeList'; | ||
|
|
||
| type ABACAttributeAutocompleteProps = { | ||
| onRemove: () => void; | ||
| index: number; | ||
| }; | ||
|
|
||
| const ABACAttributeField = ({ onRemove, index }: ABACAttributeAutocompleteProps) => { | ||
| const { t } = useTranslation(); | ||
| const [filter, setFilter] = useState<string>(); | ||
| const filterDebounced = useDebouncedValue(filter, 300); | ||
|
|
||
| const { control, getValues, resetField } = useFormContext<AdminABACRoomFormData>(); | ||
|
|
||
| const { data: options, fetchNextPage, isLoading } = useABACAttributeList(filterDebounced || undefined); | ||
|
|
||
| const validateRepeatedAttributes = useCallback( | ||
| (value: string) => { | ||
| const attributes = getValues('attributes'); | ||
| // Only one instance of the same attribute is allowed to be in the form at a time | ||
| const repeatedAttributes = attributes.filter((attribute) => attribute.key === value).length > 1; | ||
| return repeatedAttributes ? t('ABAC_No_repeated_attributes') : undefined; | ||
| }, | ||
| [getValues, t], | ||
| ); | ||
|
|
||
| const { field: keyField, fieldState: keyFieldState } = useController({ | ||
| name: `attributes.${index}.key`, | ||
| control, | ||
| rules: { | ||
| required: t('Required_field', { field: t('Attribute') }), | ||
| validate: validateRepeatedAttributes, | ||
| }, | ||
| }); | ||
|
|
||
| const { field: valuesField, fieldState: valuesFieldState } = useController({ | ||
| name: `attributes.${index}.values`, | ||
| control, | ||
| rules: { required: t('Required_field', { field: t('Attribute_Values') }) }, | ||
| }); | ||
|
|
||
| const valueOptions: [string, string][] = useMemo(() => { | ||
| if (!keyField.value) { | ||
| return []; | ||
| } | ||
|
|
||
| const selectedAttributeData = options.find((option) => option.value === keyField.value); | ||
|
|
||
| return selectedAttributeData?.attributeValues.map((value) => [value, value]) || []; | ||
| }, [keyField.value, options]); | ||
|
|
||
| if (isLoading) { | ||
| return <InputBoxSkeleton />; | ||
| } | ||
| return ( | ||
tassoevan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <Box display='flex' flexDirection='column' w='full'> | ||
| <FieldRow> | ||
| <PaginatedSelectFiltered | ||
| {...keyField} | ||
| onChange={(val) => { | ||
| resetField(`attributes.${index}.values`); | ||
| keyField.onChange(val); | ||
| }} | ||
| filter={filter} | ||
| setFilter={setFilter as (value: string | number | undefined) => void} | ||
| options={options} | ||
| endReached={() => fetchNextPage()} | ||
| placeholder={t('ABAC_Search_Attribute')} | ||
| mbe={4} | ||
| error={keyFieldState.error?.message} | ||
| /> | ||
| </FieldRow> | ||
| <FieldError>{keyFieldState.error?.message || ''}</FieldError> | ||
|
|
||
| <FieldRow> | ||
| <MultiSelect | ||
| {...valuesField} | ||
| options={valueOptions} | ||
| placeholder={t('ABAC_Select_Attribute_Values')} | ||
| error={valuesFieldState.error?.message} | ||
| /> | ||
| </FieldRow> | ||
| <FieldError>{valuesFieldState.error?.message || ''}</FieldError> | ||
|
|
||
| <Button onClick={onRemove} title={t('Remove')} mbs={4}> | ||
| {t('Remove')} | ||
| </Button> | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export default ABACAttributeField; | ||
71 changes: 71 additions & 0 deletions
71
apps/meteor/client/views/admin/ABAC/ABACDeleteRoomModal.spec.tsx
This file contains hidden or 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,71 @@ | ||
| import { faker } from '@faker-js/faker'; | ||
| import { mockAppRoot } from '@rocket.chat/mock-providers'; | ||
| import { render, screen, waitFor } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
|
|
||
| import ABACDeleteRoomModal from './ABACDeleteRoomModal'; | ||
|
|
||
| const mockDispatchToastMessage = jest.fn(); | ||
|
|
||
| jest.mock('@rocket.chat/ui-contexts', () => ({ | ||
| ...jest.requireActual('@rocket.chat/ui-contexts'), | ||
| useToastMessageDispatch: () => mockDispatchToastMessage, | ||
| })); | ||
|
|
||
| const baseAppRoot = mockAppRoot().withTranslations('en', 'core', { | ||
| Edit: 'Edit', | ||
| Remove: 'Remove', | ||
| ABAC_Room_removed: 'Room {{roomName}} removed from ABAC management', | ||
| ABAC_Delete_room: 'Remove room from ABAC management', | ||
| ABAC_Delete_room_annotation: 'Proceed with caution', | ||
| ABAC_Delete_room_content: 'Removing <bold>{{roomName}}</bold> from ABAC management may result in unintended users gaining access.', | ||
| Cancel: 'Cancel', | ||
| }); | ||
|
|
||
| describe('ABACDeleteRoomModal', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| const rid = faker.database.mongodbObjectId(); | ||
| const roomName = 'Test Room'; | ||
|
|
||
| it('should render without crashing', () => { | ||
| const { baseElement } = render(<ABACDeleteRoomModal rid={rid} roomName={roomName} onClose={jest.fn()} />, { | ||
| wrapper: baseAppRoot.build(), | ||
| }); | ||
|
|
||
| expect(baseElement).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it('should call delete endpoint when delete is confirmed', async () => { | ||
| const deleteEndpointMock = jest.fn().mockResolvedValue(null); | ||
|
|
||
| render(<ABACDeleteRoomModal rid={rid} roomName={roomName} onClose={jest.fn()} />, { | ||
| wrapper: baseAppRoot.withEndpoint('DELETE', '/v1/abac/rooms/:rid/attributes', deleteEndpointMock).build(), | ||
| }); | ||
|
|
||
| await userEvent.click(screen.getByRole('button', { name: 'Remove' })); | ||
|
|
||
| await waitFor(() => { | ||
| expect(deleteEndpointMock).toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should show success toast when delete succeeds', async () => { | ||
| const deleteEndpointMock = jest.fn().mockResolvedValue(null); | ||
|
|
||
| render(<ABACDeleteRoomModal rid={rid} roomName={roomName} onClose={jest.fn()} />, { | ||
| wrapper: baseAppRoot.withEndpoint('DELETE', '/v1/abac/rooms/:rid/attributes', deleteEndpointMock).build(), | ||
| }); | ||
|
|
||
| await userEvent.click(screen.getByRole('button', { name: 'Remove' })); | ||
|
|
||
| await waitFor(() => { | ||
| expect(mockDispatchToastMessage).toHaveBeenCalledWith({ | ||
| type: 'success', | ||
| message: 'Room Test Room removed from ABAC management', | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
48 changes: 48 additions & 0 deletions
48
apps/meteor/client/views/admin/ABAC/ABACDeleteRoomModal.tsx
This file contains hidden or 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,48 @@ | ||
| import type { IRoom } from '@rocket.chat/core-typings'; | ||
| import { Box } from '@rocket.chat/fuselage'; | ||
| import { GenericModal } from '@rocket.chat/ui-client'; | ||
| import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; | ||
| import { useQueryClient } from '@tanstack/react-query'; | ||
| import { Trans, useTranslation } from 'react-i18next'; | ||
|
|
||
| import { useEndpointMutation } from '../../../hooks/useEndpointMutation'; | ||
| import { ABACQueryKeys } from '../../../lib/queryKeys'; | ||
|
|
||
| type ABACDeleteRoomModalProps = { | ||
| rid: IRoom['_id']; | ||
| roomName: string; | ||
| onClose: () => void; | ||
| }; | ||
|
|
||
| const ABACDeleteRoomModal = ({ rid, roomName, onClose }: ABACDeleteRoomModalProps) => { | ||
| const { t } = useTranslation(); | ||
|
|
||
| const queryClient = useQueryClient(); | ||
| const dispatchToastMessage = useToastMessageDispatch(); | ||
| const deleteMutation = useEndpointMutation('DELETE', '/v1/abac/rooms/:rid/attributes', { | ||
| keys: { rid }, | ||
| onSuccess: () => { | ||
| dispatchToastMessage({ type: 'success', message: t('ABAC_Room_removed', { roomName }) }); | ||
| }, | ||
| onSettled: () => { | ||
| queryClient.invalidateQueries({ queryKey: ABACQueryKeys.rooms.all() }); | ||
| onClose(); | ||
| }, | ||
| }); | ||
|
|
||
| return ( | ||
| <GenericModal | ||
| variant='danger' | ||
| icon={null} | ||
| title={t('ABAC_Delete_room')} | ||
| annotation={t('ABAC_Delete_room_annotation')} | ||
| confirmText={t('Remove')} | ||
| onConfirm={() => deleteMutation.mutate(undefined)} | ||
| onCancel={onClose} | ||
| > | ||
| <Trans i18nKey='ABAC_Delete_room_content' values={{ roomName }} components={{ bold: <Box is='span' fontWeight='bold' /> }} /> | ||
| </GenericModal> | ||
| ); | ||
| }; | ||
|
|
||
| export default ABACDeleteRoomModal; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.