diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index 904fc4b2f90e3..31b7930719a46 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -117,3 +117,12 @@ export const teamsQueryKeys = { [...teamsQueryKeys.team(teamId), 'rooms-of-user', userId, options] as const, listUserTeams: (userId: IUser['_id']) => [...teamsQueryKeys.all, 'listUserTeams', userId] as const, }; + +export const ABACQueryKeys = { + all: ['abac'] as const, + roomAttributes: { + all: () => [...ABACQueryKeys.all, 'room-attributes'] as const, + roomAttributesList: (query?: PaginatedRequest) => [...ABACQueryKeys.roomAttributes.all(), 'room-attributes-list', query] as const, + attribute: (attributeId: string) => [...ABACQueryKeys.roomAttributes.all(), 'attribute', attributeId] as const, + }, +}; diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx index d8352181e4df9..808eadae71b18 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx @@ -1,9 +1,15 @@ import { Box, Button, Callout } from '@rocket.chat/fuselage'; -import { useRouteParameter } from '@rocket.chat/ui-contexts'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import { Trans, useTranslation } from 'react-i18next'; +import AdminABACRoomAttributes from './AdminABACRoomAttributes'; import AdminABACSettings from './AdminABACSettings'; import AdminABACTabs from './AdminABACTabs'; +import RoomAttributesContextualBar from './RoomAttributesContextualBar'; +import RoomAttributesContextualBarWithData from './RoomAttributesContextualBarWithData'; +import useIsABACAvailable from './hooks/useIsABACAvailable'; +import { ContextualbarDialog, ContextualbarSkeletonBody } from '../../../components/Contextualbar'; import { Page, PageContent, PageHeader } from '../../../components/Page'; import { useExternalLink } from '../../../hooks/useExternalLink'; import { links } from '../../../lib/links'; @@ -14,8 +20,26 @@ type AdminABACPageProps = { const AdminABACPage = ({ shouldShowWarning }: AdminABACPageProps) => { const { t } = useTranslation(); + const router = useRouter(); const tab = useRouteParameter('tab'); + const _id = useRouteParameter('id'); + const context = useRouteParameter('context'); const learnMore = useExternalLink(); + const isABACAvailable = useIsABACAvailable(); + + const handleCloseContextualbar = useEffectEvent((): void => { + if (!context) { + return; + } + + router.navigate( + { + name: 'admin-ABAC', + params: { ...router.getRouteParameters(), context: '', id: '' }, + }, + { replace: true }, + ); + }); return ( @@ -38,8 +62,20 @@ const AdminABACPage = ({ shouldShowWarning }: AdminABACPageProps) => { )} - {tab === 'settings' && } + + {tab === 'settings' && } + {tab === 'room-attributes' && } + + {tab === 'room-attributes' && context !== undefined && ( + handleCloseContextualbar()}> + {isABACAvailable === 'loading' && } + {context === 'new' && isABACAvailable === true && handleCloseContextualbar()} />} + {context === 'edit' && _id && isABACAvailable === true && ( + handleCloseContextualbar()} /> + )} + + )} ); }; diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributeMenu.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributeMenu.tsx new file mode 100644 index 0000000000000..ac6d4eaf120ee --- /dev/null +++ b/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributeMenu.tsx @@ -0,0 +1,28 @@ +import { GenericMenu } from '@rocket.chat/ui-client'; +import { useTranslation } from 'react-i18next'; + +import useRoomAttributeItems from './useRoomAttributeOptions'; + +type AdminABACRoomAttributeMenuProps = { + attribute: { _id: string; key: string }; +}; + +const AdminABACRoomAttributeMenu = ({ attribute }: AdminABACRoomAttributeMenuProps) => { + const { t } = useTranslation(); + + const items = useRoomAttributeItems(attribute); + + return ( + + ); +}; + +export default AdminABACRoomAttributeMenu; diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributes.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributes.tsx new file mode 100644 index 0000000000000..6c4dcdd97347c --- /dev/null +++ b/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributes.tsx @@ -0,0 +1,110 @@ +import { Box, Button, Icon, Margins, Pagination, TextInput } from '@rocket.chat/fuselage'; +import { useDebouncedValue, useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import AdminABACRoomAttributeMenu from './AdminABACRoomAttributeMenu'; +import useIsABACAvailable from './hooks/useIsABACAvailable'; +import GenericNoResults from '../../../components/GenericNoResults'; +import { + GenericTable, + GenericTableBody, + GenericTableCell, + GenericTableHeader, + GenericTableHeaderCell, + GenericTableRow, +} from '../../../components/GenericTable'; +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; +import { ABACQueryKeys } from '../../../lib/queryKeys'; + +const AdminABACRoomAttributes = () => { + const { t } = useTranslation(); + + const [text, setText] = useState(''); + const debouncedText = useDebouncedValue(text, 200); + const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination(); + const getAttributes = useEndpoint('GET', '/v1/abac/attributes'); + const isABACAvailable = useIsABACAvailable(); + + const router = useRouter(); + const handleNewAttribute = useEffectEvent(() => { + router.navigate({ + name: 'admin-ABAC', + params: { + tab: 'room-attributes', + context: 'new', + }, + }); + }); + + const query = useMemo( + () => ({ + ...(debouncedText ? { key: debouncedText, values: debouncedText } : {}), + offset: current, + count: itemsPerPage, + }), + [debouncedText, current, itemsPerPage], + ); + + const { data, isLoading } = useQuery({ + queryKey: ABACQueryKeys.roomAttributes.roomAttributesList(query), + queryFn: () => getAttributes(query), + }); + + return ( + <> + + + } + placeholder={t('ABAC_Search_attributes')} + value={text} + onChange={(e) => setText((e.target as HTMLInputElement).value)} + /> + + {t('ABAC_New_attribute')} + + + + {(!data || data.attributes.length === 0) && !isLoading ? ( + + + + ) : ( + <> + + + {t('Name')} + {t('Value')} + + + + {data?.attributes.map((attribute) => ( + + {attribute.key} + {attribute.values.join(', ')} + + + + + ))} + + + + > + )} + > + ); +}; + +export default AdminABACRoomAttributes; diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.spec.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.spec.tsx index 7ffa9ca665d9c..bfde0331c6bae 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.spec.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.spec.tsx @@ -135,7 +135,7 @@ describe('AdminABACRoomAttributesForm', () => { { wrapper: appRoot }, ); - const trashButtons = screen.getAllByRole('button', { name: 'Remove' }); + const trashButtons = screen.getAllByRole('button', { name: 'ABAC_Remove_attribute' }); expect(screen.getByDisplayValue('Value 1')).toBeInTheDocument(); expect(screen.getByDisplayValue('Value 2')).toBeInTheDocument(); @@ -158,7 +158,7 @@ describe('AdminABACRoomAttributesForm', () => { { wrapper: appRoot }, ); - const trashButtons = screen.queryAllByRole('button', { name: 'Remove' }); + const trashButtons = screen.queryAllByRole('button', { name: 'ABAC_Remove_attribute' }); expect(screen.getByDisplayValue('Locked Value 1')).toBeInTheDocument(); expect(screen.getByDisplayValue('Locked Value 2')).toBeInTheDocument(); @@ -211,7 +211,7 @@ describe('AdminABACRoomAttributesForm', () => { { wrapper: appRoot }, ); - const trashButtons = screen.queryAllByRole('button', { name: 'Remove' }); + const trashButtons = screen.queryAllByRole('button', { name: 'ABAC_Remove_attribute' }); await userEvent.click(trashButtons[0]); const saveButton = screen.getByRole('button', { name: 'Save' }); @@ -253,7 +253,7 @@ describe('AdminABACRoomAttributesForm', () => { expect(screen.getByDisplayValue('Locked Value')).toBeDisabled(); expect(screen.getByDisplayValue('Regular Value')).not.toBeDisabled(); - const trashButtons = screen.getAllByRole('button', { name: 'Remove' }); + const trashButtons = screen.getAllByRole('button', { name: 'ABAC_Remove_attribute' }); expect(trashButtons).toHaveLength(1); }); }); diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.tsx index fa30bf880a5d9..664050e5f3d02 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.tsx @@ -23,7 +23,7 @@ export type AdminABACRoomAttributesFormFormData = { }; type AdminABACRoomAttributesFormProps = { - onSave: (data: unknown) => void; + onSave: (data: AdminABACRoomAttributesFormFormData) => void; onCancel: () => void; description: string; }; @@ -68,68 +68,74 @@ const AdminABACRoomAttributesForm = ({ onSave, onCancel, description }: AdminABA }, [errors.attributeValues, errors.lockedAttributes]); return ( - + <> - {description} - - - {t('Name')} - - - - - {errors.name?.message || ''} - - - - {t('Values')} - - {lockedAttributesFields.map((field, index) => ( - + + {description} + + + {t('Name')} + + - {index !== 0 && removeLockedAttribute(index)} />} - ))} - {fields.map((field, index) => ( - - - {(index !== 0 || lockedAttributesFields.length > 0) && ( - remove(index)} /> - )} - - ))} - {getAttributeValuesError()} - append({ value: '' })} - // Checking for values since rhf does consider the newly added field as dirty after an append() call - disabled={!!getAttributeValuesError() || attributeValues?.some((value: { value: string }) => value?.value === '')} - > - {t('Add Value')} - - + {errors.name?.message || ''} + + + + {t('Values')} + + {lockedAttributesFields.map((field, index) => ( + + + {index !== 0 && removeLockedAttribute(index)} />} + + ))} + {fields.map((field, index) => ( + + + {(index !== 0 || lockedAttributesFields.length > 0) && ( + remove(index)} /> + )} + + ))} + {getAttributeValuesError()} + append({ value: '' })} + // Checking for values since rhf does consider the newly added field as dirty after an append() call + disabled={ + !!getAttributeValuesError() || + attributeValues?.some((value: { value: string }) => value?.value === '') || + lockedAttributesFields.length + fields.length >= 10 + } + > + {t('Add Value')} + + + onCancel()}>{t('Cancel')} - + {t('Save')} - + > ); }; diff --git a/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx b/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx index 4fc73a491f44e..e4733b8be5114 100644 --- a/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx +++ b/apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx @@ -17,6 +17,9 @@ const AdminABACTabs = () => { handleTabClick('settings')}> {t('Settings')} + handleTabClick('room-attributes')}> + {t('ABAC_Room_Attributes')} + ); }; diff --git a/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBar.tsx b/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBar.tsx new file mode 100644 index 0000000000000..ace7a9ea0c022 --- /dev/null +++ b/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBar.tsx @@ -0,0 +1,99 @@ +import { ContextualbarTitle } from '@rocket.chat/fuselage'; +import { useEndpoint, useRouteParameter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import type { AdminABACRoomAttributesFormFormData } from './AdminABACRoomAttributesForm'; +import AdminABACRoomAttributesForm from './AdminABACRoomAttributesForm'; +import { ContextualbarClose, ContextualbarHeader } from '../../../components/Contextualbar'; +import { ABACQueryKeys } from '../../../lib/queryKeys'; + +type RoomAttributesContextualBarProps = { + attributeId?: string; + attributeData?: { + key: string; + values: string[]; + usage: Record; + }; + onClose: () => void; +}; + +const RoomAttributesContextualBar = ({ attributeData, onClose }: RoomAttributesContextualBarProps) => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const methods = useForm<{ + name: string; + attributeValues: { value: string }[]; + lockedAttributes: { value: string }[]; + }>({ + defaultValues: attributeData + ? { + name: attributeData.key, + attributeValues: [{ value: '' }], + lockedAttributes: attributeData.values.map((value) => ({ value })), + } + : { + name: '', + attributeValues: [{ value: '' }], + lockedAttributes: [], + }, + }); + + const { getValues } = methods; + + const attributeId = useRouteParameter('id'); + const createAttribute = useEndpoint('POST', '/v1/abac/attributes'); + const updateAttribute = useEndpoint('PUT', '/v1/abac/attributes/:_id', { + _id: attributeId ?? '', + }); + + const dispatchToastMessage = useToastMessageDispatch(); + + const saveMutation = useMutation({ + mutationFn: async (data: AdminABACRoomAttributesFormFormData) => { + const payload = { + key: data.name, + values: [...data.lockedAttributes.map((attribute) => attribute.value), ...data.attributeValues.map((attribute) => attribute.value)], + }; + if (attributeId) { + await updateAttribute(payload); + } else { + await createAttribute(payload); + } + }, + onSuccess: () => { + if (attributeId) { + dispatchToastMessage({ type: 'success', message: t('ABAC_Attribute_updated', { attributeName: getValues('name') }) }); + } else { + dispatchToastMessage({ type: 'success', message: t('ABAC_Attribute_created', { attributeName: getValues('name') }) }); + } + onClose(); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ABACQueryKeys.roomAttributes.all() }); + }, + }); + + return ( + <> + + {t(attributeId ? 'ABAC_Edit_attribute' : 'ABAC_New_attribute')} + + + + saveMutation.mutateAsync(values)} + onCancel={onClose} + description={t(attributeId ? 'ABAC_Edit_attribute_description' : 'ABAC_New_attribute_description')} + /> + + > + ); +}; + +export default RoomAttributesContextualBar; diff --git a/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBarWithData.tsx b/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBarWithData.tsx new file mode 100644 index 0000000000000..8b334fdcadceb --- /dev/null +++ b/apps/meteor/client/views/admin/ABAC/RoomAttributesContextualBarWithData.tsx @@ -0,0 +1,28 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +import RoomAttributesContextualBar from './RoomAttributesContextualBar'; +import { ContextualbarSkeletonBody } from '../../../components/Contextualbar'; +import { ABACQueryKeys } from '../../../lib/queryKeys'; + +type RoomAttributesContextualBarWithDataProps = { + id: string; + onClose: () => void; +}; + +const RoomAttributesContextualBarWithData = ({ id, onClose }: RoomAttributesContextualBarWithDataProps) => { + const getAttributes = useEndpoint('GET', '/v1/abac/attributes/:_id', { _id: id }); + const { data, isLoading, isFetching } = useQuery({ + queryKey: ABACQueryKeys.roomAttributes.attribute(id), + queryFn: () => getAttributes(), + staleTime: 0, + }); + + if (isLoading || isFetching) { + return ; + } + + return ; +}; + +export default RoomAttributesContextualBarWithData; diff --git a/apps/meteor/client/views/admin/ABAC/__snapshots__/AdminABACRoomAttributesForm.spec.tsx.snap b/apps/meteor/client/views/admin/ABAC/__snapshots__/AdminABACRoomAttributesForm.spec.tsx.snap index ab8d65693d3bb..83c3c4434d048 100644 --- a/apps/meteor/client/views/admin/ABAC/__snapshots__/AdminABACRoomAttributesForm.spec.tsx.snap +++ b/apps/meteor/client/views/admin/ABAC/__snapshots__/AdminABACRoomAttributesForm.spec.tsx.snap @@ -6,40 +6,40 @@ exports[`AdminABACRoomAttributesForm renders NewAttribute without crashing 1`] = - - - + class="os-size-observer-listener" + /> + + - Create an attribute that can later be assigned to rooms. - + + + - - + class="os-scrollbar-handle" + /> + + - - + class="os-scrollbar-handle" + /> + + - - - - Cancel - - - + + + - - Save - - - + Save + + - +