Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/meteor/client/lib/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
40 changes: 38 additions & 2 deletions apps/meteor/client/views/admin/ABAC/AdminABACPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<Page flexDirection='row'>
Expand All @@ -38,8 +62,20 @@ const AdminABACPage = ({ shouldShowWarning }: AdminABACPageProps) => {
</Box>
)}
<AdminABACTabs />
<PageContent>{tab === 'settings' && <AdminABACSettings />}</PageContent>
<PageContent>
{tab === 'settings' && <AdminABACSettings />}
{tab === 'room-attributes' && <AdminABACRoomAttributes />}
</PageContent>
</Page>
{tab === 'room-attributes' && context !== undefined && (
<ContextualbarDialog onClose={() => handleCloseContextualbar()}>
{isABACAvailable === 'loading' && <ContextualbarSkeletonBody />}
{context === 'new' && isABACAvailable === true && <RoomAttributesContextualBar onClose={() => handleCloseContextualbar()} />}
{context === 'edit' && _id && isABACAvailable === true && (
<RoomAttributesContextualBarWithData id={_id} onClose={() => handleCloseContextualbar()} />
)}
</ContextualbarDialog>
)}
</Page>
);
};
Expand Down
28 changes: 28 additions & 0 deletions apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributeMenu.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<GenericMenu
title={t('Options')}
icon='kebab'
sections={[
{
items,
},
]}
/>
);
};

export default AdminABACRoomAttributeMenu;
110 changes: 110 additions & 0 deletions apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributes.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Margins block={24}>
<Box display='flex'>
<TextInput
addon={<Icon name='magnifier' size='x20' />}
placeholder={t('ABAC_Search_attributes')}
value={text}
onChange={(e) => setText((e.target as HTMLInputElement).value)}
/>
<Button onClick={handleNewAttribute} primary mis={8} disabled={isABACAvailable !== true}>
{t('ABAC_New_attribute')}
</Button>
</Box>
</Margins>
{(!data || data.attributes.length === 0) && !isLoading ? (
<Box display='flex' justifyContent='center' height='full'>
<GenericNoResults icon='list-alt' title={t('No_attributes')} description={t('No_attributes_description')} />
</Box>
) : (
<>
<GenericTable>
<GenericTableHeader>
<GenericTableHeaderCell>{t('Name')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Value')}</GenericTableHeaderCell>
<GenericTableHeaderCell key='spacer' w={40} />
</GenericTableHeader>
<GenericTableBody>
{data?.attributes.map((attribute) => (
<GenericTableRow key={attribute._id}>
<GenericTableCell>{attribute.key}</GenericTableCell>
<GenericTableCell>{attribute.values.join(', ')}</GenericTableCell>
<GenericTableCell>
<AdminABACRoomAttributeMenu attribute={attribute} />
</GenericTableCell>
</GenericTableRow>
))}
</GenericTableBody>
</GenericTable>
<Pagination
divider
current={current}
itemsPerPage={itemsPerPage}
count={data?.total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
{...paginationProps}
/>
</>
)}
</>
);
};

export default AdminABACRoomAttributes;
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();
Expand Down Expand Up @@ -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' });
Expand Down Expand Up @@ -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);
});
});
108 changes: 57 additions & 51 deletions apps/meteor/client/views/admin/ABAC/AdminABACRoomAttributesForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export type AdminABACRoomAttributesFormFormData = {
};

type AdminABACRoomAttributesFormProps = {
onSave: (data: unknown) => void;
onSave: (data: AdminABACRoomAttributesFormFormData) => void;
onCancel: () => void;
description: string;
};
Expand Down Expand Up @@ -68,68 +68,74 @@ const AdminABACRoomAttributesForm = ({ onSave, onCancel, description }: AdminABA
}, [errors.attributeValues, errors.lockedAttributes]);

return (
<Box is='form' onSubmit={handleSubmit(onSave)} id={formId}>
<>
<ContextualbarScrollableContent>
<Box>{description}</Box>
<Field mb={16}>
<FieldLabel htmlFor={nameField} required>
{t('Name')}
</FieldLabel>
<FieldRow>
<TextInput
error={errors.name?.message}
id={nameField}
{...register('name', { required: t('Required_field', { field: t('Name') }) })}
/>
</FieldRow>
<FieldError>{errors.name?.message || ''}</FieldError>
</Field>
<Field mb={16}>
<FieldLabel required id={valuesField}>
{t('Values')}
</FieldLabel>
{lockedAttributesFields.map((field, index) => (
<FieldRow key={field.id}>
<Box is='form' onSubmit={handleSubmit(onSave)} id={formId}>
<Box>{description}</Box>
<Field mb={16}>
<FieldLabel htmlFor={nameField} required>
{t('Name')}
</FieldLabel>
<FieldRow>
<TextInput
disabled
aria-labelledby={valuesField}
error={errors.lockedAttributes?.[index]?.value?.message || ''}
{...register(`lockedAttributes.${index}.value`, { required: t('Required_field', { field: t('Values') }) })}
error={errors.name?.message}
id={nameField}
{...register('name', { required: t('Required_field', { field: t('Name') }) })}
/>
{index !== 0 && <IconButton aria-label={t('Remove')} icon='trash' onClick={() => removeLockedAttribute(index)} />}
</FieldRow>
))}
{fields.map((field, index) => (
<FieldRow key={field.id}>
<TextInput
aria-labelledby={valuesField}
error={errors.attributeValues?.[index]?.value?.message || ''}
{...register(`attributeValues.${index}.value`, { required: t('Required_field', { field: t('Values') }) })}
/>
{(index !== 0 || lockedAttributesFields.length > 0) && (
<IconButton aria-label={t('Remove')} icon='trash' onClick={() => remove(index)} />
)}
</FieldRow>
))}
<FieldError>{getAttributeValuesError()}</FieldError>
<Button
onClick={() => 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')}
</Button>
</Field>
<FieldError>{errors.name?.message || ''}</FieldError>
</Field>
<Field mb={16}>
<FieldLabel required id={valuesField}>
{t('Values')}
</FieldLabel>
{lockedAttributesFields.map((field, index) => (
<FieldRow key={field.id}>
<TextInput
disabled
aria-labelledby={valuesField}
error={errors.lockedAttributes?.[index]?.value?.message || ''}
{...register(`lockedAttributes.${index}.value`, { required: t('Required_field', { field: t('Values') }) })}
/>
{index !== 0 && <IconButton title={t('ABAC_Remove_attribute')} icon='trash' onClick={() => removeLockedAttribute(index)} />}
</FieldRow>
))}
{fields.map((field, index) => (
<FieldRow key={field.id}>
<TextInput
aria-labelledby={valuesField}
error={errors.attributeValues?.[index]?.value?.message || ''}
{...register(`attributeValues.${index}.value`, { required: t('Required_field', { field: t('Values') }) })}
/>
{(index !== 0 || lockedAttributesFields.length > 0) && (
<IconButton title={t('ABAC_Remove_attribute')} icon='trash' onClick={() => remove(index)} />
)}
</FieldRow>
))}
<FieldError>{getAttributeValuesError()}</FieldError>
<Button
onClick={() => 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')}
</Button>
</Field>
</Box>
</ContextualbarScrollableContent>
<ContextualbarFooter>
<ButtonGroup stretch>
<Button onClick={() => onCancel()}>{t('Cancel')}</Button>
<Button type='submit' disabled={hasValuesErrors || !!errors.name} primary>
<Button type='submit' form={formId} disabled={hasValuesErrors || !!errors.name} primary>
{t('Save')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</Box>
</>
);
};

Expand Down
3 changes: 3 additions & 0 deletions apps/meteor/client/views/admin/ABAC/AdminABACTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const AdminABACTabs = () => {
<TabsItem selected={tab === 'settings'} onClick={() => handleTabClick('settings')}>
{t('Settings')}
</TabsItem>
<TabsItem selected={tab === 'room-attributes'} onClick={() => handleTabClick('room-attributes')}>
{t('ABAC_Room_Attributes')}
</TabsItem>
</Tabs>
);
};
Expand Down
Loading
Loading