mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
feat(console): display api resources in org role permission table (#5671)
This commit is contained in:
parent
01fee1dd9d
commit
dd08efe050
7 changed files with 219 additions and 60 deletions
|
@ -1,4 +1,5 @@
|
||||||
import { type ScopeResponse } from '@logto/schemas';
|
import { type AdminConsoleKey } from '@logto/phrases';
|
||||||
|
import { type Nullable } from '@silverhand/essentials';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
|
@ -10,24 +11,43 @@ import TextInput from '@/ds-components/TextInput';
|
||||||
import * as modalStyles from '@/scss/modal.module.scss';
|
import * as modalStyles from '@/scss/modal.module.scss';
|
||||||
import { trySubmitSafe } from '@/utils/form';
|
import { trySubmitSafe } from '@/utils/form';
|
||||||
|
|
||||||
type Props = {
|
export type EditScopeData = {
|
||||||
data: ScopeResponse;
|
/** Only `description` is editable for all kinds of scopes */
|
||||||
onClose: () => void;
|
description: Nullable<string>;
|
||||||
onSubmit: (scope: ScopeResponse) => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function EditPermissionModal({ data, onClose, onSubmit }: Props) {
|
type Props = {
|
||||||
|
/** The scope name displayed in the name input field */
|
||||||
|
scopeName: string;
|
||||||
|
/** The data to edit */
|
||||||
|
data: EditScopeData;
|
||||||
|
/** Determines the translation keys for texts in the editor modal */
|
||||||
|
text: {
|
||||||
|
/** The translation key of the modal title */
|
||||||
|
title: AdminConsoleKey;
|
||||||
|
/** The field name translation key for the name input */
|
||||||
|
nameField: AdminConsoleKey;
|
||||||
|
/** The field name translation key for the description input */
|
||||||
|
descriptionField: AdminConsoleKey;
|
||||||
|
/** The placeholder translation key for the description input */
|
||||||
|
descriptionPlaceholder: AdminConsoleKey;
|
||||||
|
};
|
||||||
|
onSubmit: (editedData: EditScopeData) => Promise<void>;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function EditScopeModal({ scopeName, data, text, onClose, onSubmit }: Props) {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
} = useForm<ScopeResponse>({ defaultValues: data });
|
} = useForm<EditScopeData>({ defaultValues: data });
|
||||||
|
|
||||||
const onSubmitHandler = handleSubmit(
|
const onSubmitHandler = handleSubmit(
|
||||||
trySubmitSafe(async (formData) => {
|
trySubmitSafe(async (formData) => {
|
||||||
await onSubmit({ ...data, ...formData });
|
await onSubmit(formData);
|
||||||
onClose();
|
onClose();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -43,7 +63,7 @@ function EditPermissionModal({ data, onClose, onSubmit }: Props) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalLayout
|
<ModalLayout
|
||||||
title="permissions.edit_title"
|
title={text.title}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button isLoading={isSubmitting} title="general.cancel" onClick={onClose} />
|
<Button isLoading={isSubmitting} title="general.cancel" onClick={onClose} />
|
||||||
|
@ -59,14 +79,14 @@ function EditPermissionModal({ data, onClose, onSubmit }: Props) {
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<form>
|
<form>
|
||||||
<FormField title="api_resource_details.permission.name">
|
<FormField title={text.nameField}>
|
||||||
<TextInput readOnly value={data.name} />
|
<TextInput readOnly value={scopeName} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField title="api_resource_details.permission.description">
|
<FormField title={text.descriptionField}>
|
||||||
<TextInput
|
<TextInput
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder={t('api_resource_details.permission.description_placeholder')}
|
placeholder={String(t(text.descriptionPlaceholder))}
|
||||||
{...register('description')}
|
{...register('description')}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
@ -76,4 +96,4 @@ function EditPermissionModal({ data, onClose, onSubmit }: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditPermissionModal;
|
export default EditScopeModal;
|
|
@ -21,9 +21,9 @@ import useApi from '@/hooks/use-api';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
|
|
||||||
import ActionsButton from '../ActionsButton';
|
import ActionsButton from '../ActionsButton';
|
||||||
|
import EditScopeModal, { type EditScopeData } from '../EditScopeModal';
|
||||||
import EmptyDataPlaceholder from '../EmptyDataPlaceholder';
|
import EmptyDataPlaceholder from '../EmptyDataPlaceholder';
|
||||||
|
|
||||||
import EditPermissionModal from './EditPermissionModal';
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type SearchProps = {
|
type SearchProps = {
|
||||||
|
@ -98,9 +98,9 @@ function PermissionsTable({
|
||||||
|
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
const handleEdit = async (scope: ScopeResponse) => {
|
const handleEdit = async (scope: ScopeResponse, editedData: EditScopeData) => {
|
||||||
const patchApiEndpoint = `api/resources/${scope.resourceId}/scopes/${scope.id}`;
|
const patchApiEndpoint = `api/resources/${scope.resourceId}/scopes/${scope.id}`;
|
||||||
await api.patch(patchApiEndpoint, { json: scope });
|
await api.patch(patchApiEndpoint, { json: editedData });
|
||||||
toast.success(t('permissions.updated'));
|
toast.success(t('permissions.updated'));
|
||||||
onPermissionUpdated();
|
onPermissionUpdated();
|
||||||
};
|
};
|
||||||
|
@ -236,12 +236,21 @@ function PermissionsTable({
|
||||||
onRetry={retryHandler}
|
onRetry={retryHandler}
|
||||||
/>
|
/>
|
||||||
{editingScope && (
|
{editingScope && (
|
||||||
<EditPermissionModal
|
<EditScopeModal
|
||||||
|
scopeName={editingScope.name}
|
||||||
data={editingScope}
|
data={editingScope}
|
||||||
|
text={{
|
||||||
|
title: 'permissions.edit_title',
|
||||||
|
nameField: 'api_resource_details.permission.name',
|
||||||
|
descriptionField: 'api_resource_details.permission.description',
|
||||||
|
descriptionPlaceholder: 'api_resource_details.permission.description_placeholder',
|
||||||
|
}}
|
||||||
|
onSubmit={async (editedData) => {
|
||||||
|
await handleEdit(editingScope, editedData);
|
||||||
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setEditingScope(undefined);
|
setEditingScope(undefined);
|
||||||
}}
|
}}
|
||||||
onSubmit={handleEdit}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-left: _.unit(2);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: _.unit(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { type Resource } from '@logto/schemas';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import ResourceIcon from '@/assets/icons/resource.svg';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
resourceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ResourceName({ resourceId }: Props) {
|
||||||
|
const { data, isLoading } = useSWR<Resource>(`api/resources/${resourceId}`);
|
||||||
|
|
||||||
|
if (isLoading || !data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.container}>
|
||||||
|
<ResourceIcon className={styles.icon} />
|
||||||
|
{data.name}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResourceName;
|
|
@ -1,77 +1,92 @@
|
||||||
import { type OrganizationScope } from '@logto/schemas';
|
import { type Scope, type OrganizationScope } from '@logto/schemas';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
import Plus from '@/assets/icons/plus.svg';
|
import Plus from '@/assets/icons/plus.svg';
|
||||||
import ActionsButton from '@/components/ActionsButton';
|
import ActionsButton from '@/components/ActionsButton';
|
||||||
import Breakable from '@/components/Breakable';
|
import Breakable from '@/components/Breakable';
|
||||||
|
import EditScopeModal, { type EditScopeData } from '@/components/EditScopeModal';
|
||||||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
||||||
import ManageOrganizationPermissionModal from '@/components/ManageOrganizationPermissionModal';
|
|
||||||
import Button from '@/ds-components/Button';
|
import Button from '@/ds-components/Button';
|
||||||
import DynamicT from '@/ds-components/DynamicT';
|
import DynamicT from '@/ds-components/DynamicT';
|
||||||
import Search from '@/ds-components/Search';
|
import Search from '@/ds-components/Search';
|
||||||
import Table from '@/ds-components/Table';
|
import Table from '@/ds-components/Table';
|
||||||
import Tag from '@/ds-components/Tag';
|
import Tag from '@/ds-components/Tag';
|
||||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
import useApi from '@/hooks/use-api';
|
||||||
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
||||||
|
|
||||||
|
import ResourceName from './ResourceName';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
import useOrganizationRoleScopes from './use-organization-role-scopes';
|
||||||
|
|
||||||
const organizationRolesPath = 'api/organization-roles';
|
type OrganizationRoleScope = OrganizationScope | Scope;
|
||||||
|
|
||||||
|
const isResourceScope = (scope: OrganizationRoleScope): scope is Scope => 'resourceId' in scope;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
organizationRoleId: string;
|
organizationRoleId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Permissions({ organizationRoleId }: Props) {
|
function Permissions({ organizationRoleId }: Props) {
|
||||||
|
const organizationRolePath = `api/organization-roles/${organizationRoleId}`;
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
const { data, error, isLoading, mutate } = useSWR<OrganizationScope[], RequestError>(
|
const { organizationScopes, resourceScopes, error, isLoading, mutate } =
|
||||||
`${organizationRolesPath}/${organizationRoleId}/scopes`
|
useOrganizationRoleScopes(organizationRoleId);
|
||||||
);
|
|
||||||
|
|
||||||
const [{ keyword }, updateSearchParameters] = useSearchParametersWatcher({
|
const [{ keyword }, updateSearchParameters] = useSearchParametersWatcher({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filterScopesByKeyword = useCallback(
|
||||||
if (keyword) {
|
(scopes: OrganizationRoleScope[]) => scopes.filter(({ name }) => name.includes(keyword)),
|
||||||
return data?.filter((roleScope) => roleScope.name.includes(keyword));
|
[keyword]
|
||||||
}
|
);
|
||||||
return data;
|
|
||||||
}, [data, keyword]);
|
|
||||||
|
|
||||||
const [editPermission, setEditPermission] = useState<OrganizationScope>();
|
const scopesData = useMemo(
|
||||||
|
() =>
|
||||||
|
keyword
|
||||||
|
? [...filterScopesByKeyword(resourceScopes), ...filterScopesByKeyword(organizationScopes)]
|
||||||
|
: [...resourceScopes, ...organizationScopes],
|
||||||
|
[filterScopesByKeyword, keyword, organizationScopes, resourceScopes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [editingScope, setEditingScope] = useState<OrganizationScope>();
|
||||||
|
|
||||||
|
const removeScopeHandler = useCallback(
|
||||||
|
(scopeToRemove: OrganizationRoleScope) => async () => {
|
||||||
|
const deleteSubpath = isResourceScope(scopeToRemove) ? 'resource-scopes' : 'scopes';
|
||||||
|
await api.delete(`${organizationRolePath}/${deleteSubpath}/${scopeToRemove.id}`);
|
||||||
|
|
||||||
const scopeRemoveHandler = useCallback(
|
|
||||||
(scopeToRemove: OrganizationScope) => async () => {
|
|
||||||
await api.put(`${organizationRolesPath}/${organizationRoleId}/scopes`, {
|
|
||||||
json: {
|
|
||||||
organizationScopeIds:
|
|
||||||
data?.filter((scope) => scope.id !== scopeToRemove.id).map(({ id }) => id) ?? [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
toast.success(
|
toast.success(
|
||||||
t('organization_role_details.permissions.removed', { name: scopeToRemove.name })
|
t('organization_role_details.permissions.removed', { name: scopeToRemove.name })
|
||||||
);
|
);
|
||||||
void mutate();
|
mutate();
|
||||||
},
|
},
|
||||||
[api, data, mutate, organizationRoleId, t]
|
[api, mutate, organizationRolePath, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleEdit = async (scope: OrganizationRoleScope, editedData: EditScopeData) => {
|
||||||
|
const patchApiEndpoint = isResourceScope(scope)
|
||||||
|
? `api/resources/${scope.resourceId}/scopes/${scope.id}`
|
||||||
|
: `api/organization-scopes/${scope.id}`;
|
||||||
|
await api.patch(patchApiEndpoint, { json: editedData });
|
||||||
|
toast.success(t('permissions.updated'));
|
||||||
|
mutate();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Table
|
<Table
|
||||||
rowGroups={[{ key: 'organizationRolePermissions', data: filteredData }]}
|
rowGroups={[{ key: 'organizationRolePermissions', data: scopesData }]}
|
||||||
rowIndexKey="id"
|
rowIndexKey="id"
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
title: <DynamicT forKey="organization_role_details.permissions.name_column" />,
|
title: <DynamicT forKey="organization_role_details.permissions.name_column" />,
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
colSpan: 7,
|
colSpan: 5,
|
||||||
render: ({ name }) => {
|
render: ({ name }) => {
|
||||||
return (
|
return (
|
||||||
<Tag variant="cell">
|
<Tag variant="cell">
|
||||||
|
@ -83,9 +98,28 @@ function Permissions({ organizationRoleId }: Props) {
|
||||||
{
|
{
|
||||||
title: <DynamicT forKey="organization_role_details.permissions.description_column" />,
|
title: <DynamicT forKey="organization_role_details.permissions.description_column" />,
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
colSpan: 8,
|
colSpan: 5,
|
||||||
render: ({ description }) => <Breakable>{description ?? '-'}</Breakable>,
|
render: ({ description }) => <Breakable>{description ?? '-'}</Breakable>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: <DynamicT forKey="organization_role_details.permissions.type_column" />,
|
||||||
|
dataIndex: 'type',
|
||||||
|
colSpan: 5,
|
||||||
|
render: (scope) => {
|
||||||
|
return (
|
||||||
|
<Breakable>
|
||||||
|
{isResourceScope(scope) ? (
|
||||||
|
<>
|
||||||
|
<DynamicT forKey="organization_role_details.permissions.type.api" />
|
||||||
|
<ResourceName resourceId={scope.resourceId} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<DynamicT forKey="organization_role_details.permissions.type.org" />
|
||||||
|
)}
|
||||||
|
</Breakable>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: null,
|
title: null,
|
||||||
dataIndex: 'action',
|
dataIndex: 'action',
|
||||||
|
@ -99,11 +133,9 @@ function Permissions({ organizationRoleId }: Props) {
|
||||||
deleteConfirmation: 'general.remove',
|
deleteConfirmation: 'general.remove',
|
||||||
}}
|
}}
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
setEditPermission(scope);
|
setEditingScope(scope);
|
||||||
}}
|
|
||||||
onDelete={async () => {
|
|
||||||
await scopeRemoveHandler(scope)();
|
|
||||||
}}
|
}}
|
||||||
|
onDelete={removeScopeHandler(scope)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -137,14 +169,33 @@ function Permissions({ organizationRoleId }: Props) {
|
||||||
placeholder={<EmptyDataPlaceholder />}
|
placeholder={<EmptyDataPlaceholder />}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
errorMessage={error?.body?.message ?? error?.message}
|
errorMessage={error?.body?.message ?? error?.message}
|
||||||
onRetry={async () => mutate(undefined, true)}
|
onRetry={mutate}
|
||||||
/>
|
/>
|
||||||
{editPermission && (
|
{editingScope && (
|
||||||
<ManageOrganizationPermissionModal
|
<EditScopeModal
|
||||||
data={editPermission}
|
scopeName={editingScope.name}
|
||||||
|
data={editingScope}
|
||||||
|
text={
|
||||||
|
isResourceScope(editingScope)
|
||||||
|
? {
|
||||||
|
title: 'permissions.edit_title',
|
||||||
|
nameField: 'api_resource_details.permission.name',
|
||||||
|
descriptionField: 'api_resource_details.permission.description',
|
||||||
|
descriptionPlaceholder: 'api_resource_details.permission.description_placeholder',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: 'organization_template.permissions.edit_title',
|
||||||
|
nameField: 'organization_template.permissions.permission_field_name',
|
||||||
|
descriptionField: 'organization_template.permissions.description_field_name',
|
||||||
|
descriptionPlaceholder:
|
||||||
|
'organization_template.permissions.description_field_placeholder',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSubmit={async (editedData) => {
|
||||||
|
await handleEdit(editingScope, editedData);
|
||||||
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setEditPermission(undefined);
|
setEditingScope(undefined);
|
||||||
void mutate();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { type Scope, type OrganizationScope } from '@logto/schemas';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import { type RequestError } from '@/hooks/use-api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the organization and resource scopes of an organization role.
|
||||||
|
*/
|
||||||
|
function useOrganizationRoleScopes(organizationRoleId: string) {
|
||||||
|
const organizationRolePath = `api/organization-roles/${organizationRoleId}`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: organizationScopes = [],
|
||||||
|
error: fetchOrganizationScopesError,
|
||||||
|
isLoading: isOrganizationScopesLoading,
|
||||||
|
mutate: mutateOrganizationScopes,
|
||||||
|
} = useSWR<OrganizationScope[], RequestError>(`${organizationRolePath}/scopes`);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: resourceScopes = [],
|
||||||
|
error: fetchResourceScopesError,
|
||||||
|
isLoading: isResourceScopesLoading,
|
||||||
|
mutate: mutateResourceScopes,
|
||||||
|
} = useSWR<Scope[], RequestError>(`${organizationRolePath}/resource-scopes`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
organizationScopes,
|
||||||
|
resourceScopes,
|
||||||
|
error: fetchOrganizationScopesError ?? fetchResourceScopesError,
|
||||||
|
isLoading: isOrganizationScopesLoading || isResourceScopesLoading,
|
||||||
|
mutate: () => {
|
||||||
|
void mutateOrganizationScopes();
|
||||||
|
void mutateResourceScopes();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useOrganizationRoleScopes;
|
|
@ -70,12 +70,14 @@ function OrganizationRoles() {
|
||||||
title: <DynamicT forKey="organization_template.roles.permissions_column" />,
|
title: <DynamicT forKey="organization_template.roles.permissions_column" />,
|
||||||
dataIndex: 'scopes',
|
dataIndex: 'scopes',
|
||||||
colSpan: 12,
|
colSpan: 12,
|
||||||
render: ({ scopes }) => {
|
render: ({ scopes, resourceScopes }) => {
|
||||||
return scopes.length === 0 ? (
|
const roleScopes = [...scopes, ...resourceScopes];
|
||||||
|
|
||||||
|
return roleScopes.length === 0 ? (
|
||||||
'-'
|
'-'
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.permissions}>
|
<div className={styles.permissions}>
|
||||||
{scopes.map(({ id, name }) => (
|
{roleScopes.map(({ id, name }) => (
|
||||||
<Tag key={id} variant="cell">
|
<Tag key={id} variant="cell">
|
||||||
<Breakable>{name}</Breakable>
|
<Breakable>{name}</Breakable>
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|
Loading…
Add table
Reference in a new issue