mirror of
https://github.com/logto-io/logto.git
synced 2025-03-03 22:15:32 -05:00
feat(console): assign permissions to a role (#2882)
This commit is contained in:
parent
1f293292b8
commit
751d6117c1
24 changed files with 627 additions and 8 deletions
3
packages/console/src/assets/images/caret-expanded.svg
Normal file
3
packages/console/src/assets/images/caret-expanded.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5917 12.3583L14.125 8.825C14.2031 8.74753 14.2651 8.65536 14.3074 8.55381C14.3497 8.45226 14.3715 8.34334 14.3715 8.23333C14.3715 8.12332 14.3497 8.0144 14.3074 7.91285C14.2651 7.8113 14.2031 7.71913 14.125 7.64166C13.9689 7.48646 13.7577 7.39934 13.5375 7.39934C13.3174 7.39934 13.1062 7.48646 12.95 7.64166L10 10.5917L7.05002 7.64166C6.89389 7.48646 6.68268 7.39934 6.46252 7.39934C6.24237 7.39934 6.03116 7.48646 5.87502 7.64166C5.79779 7.71953 5.73668 7.81188 5.69521 7.91341C5.65374 8.01494 5.63272 8.12366 5.63336 8.23333C5.63272 8.343 5.65374 8.45172 5.69521 8.55325C5.73668 8.65478 5.79779 8.74713 5.87502 8.825L9.40836 12.3583C9.48582 12.4364 9.57799 12.4984 9.67954 12.5407C9.78109 12.583 9.89001 12.6048 10 12.6048C10.11 12.6048 10.219 12.583 10.3205 12.5407C10.4221 12.4984 10.5142 12.4364 10.5917 12.3583Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 958 B |
3
packages/console/src/assets/images/caret-folded.svg
Normal file
3
packages/console/src/assets/images/caret-folded.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.3583 9.40833L8.82501 5.875C8.74754 5.79689 8.65538 5.7349 8.55383 5.69259C8.45228 5.65028 8.34336 5.6285 8.23335 5.6285C8.12334 5.6285 8.01442 5.65028 7.91287 5.69259C7.81132 5.7349 7.71915 5.79689 7.64168 5.875C7.48647 6.03114 7.39935 6.24235 7.39935 6.4625C7.39935 6.68266 7.48647 6.89387 7.64168 7.05L10.5917 10L7.64168 12.95C7.48647 13.1061 7.39935 13.3173 7.39935 13.5375C7.39935 13.7577 7.48647 13.9689 7.64168 14.125C7.71955 14.2022 7.81189 14.2633 7.91342 14.3048C8.01496 14.3463 8.12367 14.3673 8.23335 14.3667C8.34302 14.3673 8.45174 14.3463 8.55327 14.3048C8.6548 14.2633 8.74715 14.2022 8.82501 14.125L12.3583 10.5917C12.4365 10.5142 12.4984 10.422 12.5408 10.3205C12.5831 10.2189 12.6048 10.11 12.6048 10C12.6048 9.88999 12.5831 9.78107 12.5408 9.67952C12.4984 9.57797 12.4365 9.4858 12.3583 9.40833Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 953 B |
|
@ -0,0 +1,116 @@
|
|||
import type { ResourceResponse, ScopeResponse } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Search from '@/assets/images/search.svg';
|
||||
|
||||
import TextInput from '../TextInput';
|
||||
import ResourceItem from './components/ResourceItem';
|
||||
import * as styles from './index.module.scss';
|
||||
import type { DetailedResourceResponse } from './types';
|
||||
|
||||
type Props = {
|
||||
excludeScopeIds: string[];
|
||||
selectedPermissions: ScopeResponse[];
|
||||
onChange: (value: ScopeResponse[]) => void;
|
||||
};
|
||||
|
||||
const SourcePermissionsBox = ({ excludeScopeIds, selectedPermissions, onChange }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { data = [] } = useSWR<ResourceResponse[]>(`/api/resources?includeScopes=true`);
|
||||
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const handleSearchInput = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setKeyword(event.target.value);
|
||||
};
|
||||
|
||||
const isPermissionAdded = (scope: ScopeResponse) =>
|
||||
selectedPermissions.findIndex(({ id }) => id === scope.id) >= 0;
|
||||
|
||||
const onSelectPermission = (scope: ScopeResponse) => {
|
||||
const permissionAdded = isPermissionAdded(scope);
|
||||
|
||||
if (permissionAdded) {
|
||||
onChange(selectedPermissions.filter(({ id }) => id !== scope.id));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onChange([scope, ...selectedPermissions]);
|
||||
};
|
||||
|
||||
const onSelectResource = ({ scopes }: DetailedResourceResponse) => {
|
||||
const isAllSelected = scopes.every((scope) => isPermissionAdded(scope));
|
||||
const scopesIds = new Set(scopes.map(({ id }) => id));
|
||||
const basePermissions = selectedPermissions.filter(({ id }) => !scopesIds.has(id));
|
||||
|
||||
if (isAllSelected) {
|
||||
onChange(basePermissions);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onChange([...scopes, ...basePermissions]);
|
||||
};
|
||||
|
||||
const getResourceSelectedPermissions = ({ scopes }: DetailedResourceResponse) =>
|
||||
scopes.filter((scope) => selectedPermissions.findIndex(({ id }) => id === scope.id) >= 0);
|
||||
|
||||
const resources = data
|
||||
.filter(({ scopes }) => scopes.some(({ id }) => !excludeScopeIds.includes(id)))
|
||||
.map(({ scopes, ...resource }) => ({
|
||||
...resource,
|
||||
scopes: scopes
|
||||
.filter(({ id }) => !excludeScopeIds.includes(id))
|
||||
.map((scope) => ({
|
||||
...scope,
|
||||
resource,
|
||||
})),
|
||||
}));
|
||||
|
||||
const dataSource =
|
||||
conditional(
|
||||
keyword &&
|
||||
resources
|
||||
.filter(({ name, scopes }) => {
|
||||
return name.includes(keyword) || scopes.some(({ name }) => name.includes(keyword));
|
||||
})
|
||||
.map(({ scopes, ...resource }) => ({
|
||||
...resource,
|
||||
scopes: scopes.filter(
|
||||
({ name, resource }) => name.includes(keyword) || resource.name.includes(keyword)
|
||||
),
|
||||
}))
|
||||
.filter(({ scopes }) => scopes.length > 0)
|
||||
) ?? resources;
|
||||
|
||||
return (
|
||||
<div className={styles.box}>
|
||||
<div className={styles.top}>
|
||||
<TextInput
|
||||
className={styles.search}
|
||||
icon={<Search className={styles.icon} />}
|
||||
placeholder={t('general.search_placeholder')}
|
||||
onChange={handleSearchInput}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{dataSource.map((resource) => (
|
||||
<ResourceItem
|
||||
key={resource.id}
|
||||
resource={resource}
|
||||
selectedPermissions={getResourceSelectedPermissions(resource)}
|
||||
onSelectResource={onSelectResource}
|
||||
onSelectPermission={onSelectPermission}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourcePermissionsBox;
|
|
@ -0,0 +1,38 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import TargetPermissionItem from './components/TargetPermissionItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
selectedScopes: ScopeResponse[];
|
||||
onRemovePermission: (scope: ScopeResponse) => void;
|
||||
};
|
||||
|
||||
const TargetPermissionsBox = ({ selectedScopes, onRemovePermission }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div className={styles.box}>
|
||||
<div className={classNames(styles.top, styles.added)}>
|
||||
<span>{t('role_details.permission.added_text', { value: selectedScopes.length })}</span>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{selectedScopes.map((scope) => {
|
||||
return (
|
||||
<TargetPermissionItem
|
||||
key={scope.id}
|
||||
scope={scope}
|
||||
onDelete={() => {
|
||||
onRemovePermission(scope);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetPermissionsBox;
|
|
@ -0,0 +1,38 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.resourceItem {
|
||||
padding: 0 _.unit(4);
|
||||
user-select: none;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: _.unit(1.5);
|
||||
|
||||
.resource {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.caret {
|
||||
margin-right: _.unit(2);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.name {
|
||||
font: var(--font-label-2);
|
||||
@include _.text-ellipsis;
|
||||
}
|
||||
|
||||
.permissionInfo {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text-secondary);
|
||||
margin-left: _.unit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invisible {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CaretExpanded from '@/assets/images/caret-expanded.svg';
|
||||
import CaretFolded from '@/assets/images/caret-folded.svg';
|
||||
import Checkbox from '@/components/Checkbox';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import type { DetailedResourceResponse } from '../../types';
|
||||
import SourcePermissionItem from '../SourcePermissionItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
resource: DetailedResourceResponse;
|
||||
selectedPermissions: ScopeResponse[];
|
||||
onSelectResource: (resource: DetailedResourceResponse) => void;
|
||||
onSelectPermission: (scope: ScopeResponse) => void;
|
||||
};
|
||||
|
||||
const ResourceItem = ({
|
||||
resource,
|
||||
selectedPermissions,
|
||||
onSelectResource,
|
||||
onSelectPermission,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { name, scopes } = resource;
|
||||
const selectedScopesCount = selectedPermissions.length;
|
||||
const totalScopesCount = scopes.length;
|
||||
const [isScopesInvisible, setIsScopesInvisible] = useState(true);
|
||||
|
||||
return (
|
||||
<div className={styles.resourceItem}>
|
||||
<div className={styles.title}>
|
||||
<Checkbox
|
||||
checked={selectedScopesCount === totalScopesCount}
|
||||
indeterminate={selectedScopesCount > 0 && selectedScopesCount < totalScopesCount}
|
||||
disabled={false}
|
||||
onChange={() => {
|
||||
onSelectResource(resource);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.resource}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
setIsScopesInvisible(!isScopesInvisible);
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsScopesInvisible(!isScopesInvisible);
|
||||
}}
|
||||
>
|
||||
{isScopesInvisible ? (
|
||||
<CaretFolded className={styles.caret} />
|
||||
) : (
|
||||
<CaretExpanded className={styles.caret} />
|
||||
)}
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.permissionInfo}>
|
||||
({t('role_details.permission.api_permission_count', { value: scopes.length })})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames(isScopesInvisible && styles.invisible)}>
|
||||
{scopes.map((scope) => (
|
||||
<SourcePermissionItem
|
||||
key={scope.id}
|
||||
scope={scope}
|
||||
isSelected={selectedPermissions.findIndex(({ id }) => scope.id === id) >= 0}
|
||||
onSelectPermission={onSelectPermission}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourceItem;
|
|
@ -0,0 +1,19 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.sourcePermissionItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: _.unit(1.5) _.unit(7);
|
||||
cursor: pointer;
|
||||
|
||||
.name {
|
||||
padding: _.unit(1) _.unit(2);
|
||||
border-radius: 6px;
|
||||
background: var(--color-neutral-95);
|
||||
@include _.text-ellipsis;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
|
||||
import Checkbox from '@/components/Checkbox';
|
||||
import { onKeyDownHandler } from '@/utilities/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
scope: ScopeResponse;
|
||||
isSelected: boolean;
|
||||
onSelectPermission: (scope: ScopeResponse) => void;
|
||||
};
|
||||
|
||||
const SourcePermissionItem = ({ scope, isSelected, onSelectPermission }: Props) => {
|
||||
const { name } = scope;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.sourcePermissionItem}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={onKeyDownHandler(() => {
|
||||
onSelectPermission(scope);
|
||||
})}
|
||||
onClick={() => {
|
||||
onSelectPermission(scope);
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={false}
|
||||
onChange={() => {
|
||||
onSelectPermission(scope);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.name}>{name}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourcePermissionItem;
|
|
@ -0,0 +1,31 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.targetPermissionItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: _.unit(1.5) _.unit(4);
|
||||
|
||||
.title {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font: var(--font-body-medium);
|
||||
overflow: hidden;
|
||||
|
||||
.name {
|
||||
padding: _.unit(1) _.unit(2);
|
||||
border-radius: 6px;
|
||||
background: var(--color-neutral-95);
|
||||
@include _.text-ellipsis;
|
||||
}
|
||||
|
||||
.resourceName {
|
||||
margin: 0 _.unit(2);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
import type { Key } from 'react';
|
||||
|
||||
import Close from '@/assets/images/close.svg';
|
||||
import IconButton from '@/components/IconButton';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
export type Props = {
|
||||
key: Key;
|
||||
scope: ScopeResponse;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
const TargetPermissionItem = ({ key, scope, onDelete }: Props) => {
|
||||
const {
|
||||
name,
|
||||
resource: { name: resourceName },
|
||||
} = scope;
|
||||
|
||||
return (
|
||||
<div key={key} className={styles.targetPermissionItem}>
|
||||
<div className={styles.title}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.resourceName}>{`(${resourceName})`}</div>
|
||||
</div>
|
||||
<IconButton size="small" iconClassName={styles.icon} onClick={onDelete}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetPermissionItem;
|
|
@ -0,0 +1,47 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
height: 360px;
|
||||
|
||||
.verticalBar {
|
||||
@include _.vertical-bar;
|
||||
}
|
||||
|
||||
.box {
|
||||
flex: 1 1 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.top {
|
||||
height: 52px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 _.unit(4);
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.added {
|
||||
font: var(--font-label-large);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
|
||||
import SourcePermissionsBox from './SourcePermissionsBox';
|
||||
import TargetPermissionsBox from './TargetPermissionsBox';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
value: ScopeResponse[];
|
||||
onChange: (value: ScopeResponse[]) => void;
|
||||
excludeScopeIds?: string[];
|
||||
};
|
||||
|
||||
const RolePermissionsTransfer = ({ excludeScopeIds = [], value, onChange }: Props) => (
|
||||
<div className={styles.container}>
|
||||
<SourcePermissionsBox
|
||||
excludeScopeIds={excludeScopeIds}
|
||||
selectedPermissions={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className={styles.verticalBar} />
|
||||
<TargetPermissionsBox
|
||||
selectedScopes={value}
|
||||
onRemovePermission={(scope) => {
|
||||
onChange(value.filter(({ id }) => id !== scope.id));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RolePermissionsTransfer;
|
|
@ -0,0 +1,5 @@
|
|||
import type { ResourceResponse, ScopeResponse } from '@logto/schemas';
|
||||
|
||||
export type DetailedResourceResponse = Omit<ResourceResponse, 'scopes'> & {
|
||||
scopes: ScopeResponse[];
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
import type { ScopeResponse } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import RolePermissionsTransfer from '@/components/RolePermissionsTransfer';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
type Props = {
|
||||
roleId: string;
|
||||
excludeScopeIds: string[];
|
||||
onClose: (success?: boolean) => void;
|
||||
};
|
||||
|
||||
const AssignPermissionsModal = ({ roleId, excludeScopeIds, onClose }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [scopes, setScopes] = useState<ScopeResponse[]>([]);
|
||||
|
||||
const api = useApi();
|
||||
|
||||
const handleAssign = async () => {
|
||||
if (isSubmitting || scopes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await api.post(`/api/roles/${roleId}/scopes`, {
|
||||
json: { scopeIds: scopes.map(({ id }) => id) },
|
||||
});
|
||||
toast.success(t('role_details.permission.permission_assigned'));
|
||||
onClose(true);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen
|
||||
shouldCloseOnEsc
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
onRequestClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ModalLayout
|
||||
title="role_details.permission.assign_title"
|
||||
subtitle="role_details.permission.assign_subtitle"
|
||||
size="large"
|
||||
footer={
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
disabled={scopes.length === 0}
|
||||
htmlType="submit"
|
||||
title="role_details.permission.confirm_assign"
|
||||
size="large"
|
||||
type="primary"
|
||||
onClick={handleAssign}
|
||||
/>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<FormField title="role_details.permission.assign_form_filed">
|
||||
<RolePermissionsTransfer
|
||||
value={scopes}
|
||||
excludeScopeIds={excludeScopeIds}
|
||||
onChange={(scopes) => {
|
||||
setScopes(scopes);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
</ModalLayout>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssignPermissionsModal;
|
|
@ -11,6 +11,7 @@ import type { RequestError } from '@/hooks/use-api';
|
|||
import useApi from '@/hooks/use-api';
|
||||
|
||||
import type { RoleDetailsOutletContext } from '../types';
|
||||
import AssignPermissionsModal from './components/AssignPermissionsModal';
|
||||
|
||||
const RolePermissions = () => {
|
||||
const {
|
||||
|
@ -27,6 +28,7 @@ const RolePermissions = () => {
|
|||
|
||||
const isLoading = !scopes && !error;
|
||||
|
||||
const [isAssignPermissionsModalOpen, setIsAssignPermissionsModalOpen] = useState(false);
|
||||
const [scopeToBeDeleted, setScopeToBeDeleted] = useState<Scope>();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
|
@ -58,7 +60,7 @@ const RolePermissions = () => {
|
|||
isLoading={isLoading}
|
||||
createButtonTitle="role_details.permission.assign_button"
|
||||
createHandler={() => {
|
||||
// TODO @xiaoyijun Assign Permissions to Role
|
||||
setIsAssignPermissionsModalOpen(true);
|
||||
}}
|
||||
deleteHandler={setScopeToBeDeleted}
|
||||
errorMessage={error?.body?.message ?? error?.message}
|
||||
|
@ -77,6 +79,18 @@ const RolePermissions = () => {
|
|||
{t('role_details.permission.deletion_description')}
|
||||
</ConfirmModal>
|
||||
)}
|
||||
{isAssignPermissionsModalOpen && (
|
||||
<AssignPermissionsModal
|
||||
excludeScopeIds={scopes?.map(({ id }) => id) ?? []}
|
||||
roleId={roleId}
|
||||
onClose={(success) => {
|
||||
if (success) {
|
||||
void mutate();
|
||||
}
|
||||
setIsAssignPermissionsModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import type { Role } from '@logto/schemas';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Role, ScopeResponse } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import FormField from '@/components/FormField';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import RolePermissionsTransfer from '@/components/RolePermissionsTransfer';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
|
||||
|
@ -12,11 +13,17 @@ export type Props = {
|
|||
onClose: (createdRole?: Role) => void;
|
||||
};
|
||||
|
||||
type CreateRoleFormData = Pick<Role, 'name' | 'description'>;
|
||||
type CreateRoleFormData = Pick<Role, 'name' | 'description'> & {
|
||||
scopes: ScopeResponse[];
|
||||
};
|
||||
|
||||
type CreateRolePayload = Pick<Role, 'name' | 'description'> & {
|
||||
scopeIds?: string[];
|
||||
};
|
||||
|
||||
const CreateRoleForm = ({ onClose }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isSubmitting },
|
||||
|
@ -24,12 +31,18 @@ const CreateRoleForm = ({ onClose }: Props) => {
|
|||
|
||||
const api = useApi();
|
||||
|
||||
const onSubmit = handleSubmit(async (formData) => {
|
||||
const onSubmit = handleSubmit(async ({ name, description, scopes }) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createdRole = await api.post('/api/roles', { json: formData }).json<Role>();
|
||||
const payload: CreateRolePayload = {
|
||||
name,
|
||||
description,
|
||||
scopeIds: conditional(scopes.length > 0 && scopes.map(({ id }) => id)),
|
||||
};
|
||||
|
||||
const createdRole = await api.post('/api/roles', { json: payload }).json<Role>();
|
||||
onClose(createdRole);
|
||||
});
|
||||
|
||||
|
@ -37,6 +50,7 @@ const CreateRoleForm = ({ onClose }: Props) => {
|
|||
<ModalLayout
|
||||
title="roles.create_role_title"
|
||||
subtitle="roles.create_role_description"
|
||||
size="large"
|
||||
footer={
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
|
@ -60,6 +74,16 @@ const CreateRoleForm = ({ onClose }: Props) => {
|
|||
<FormField title="roles.role_description">
|
||||
<TextInput {...register('description')} />
|
||||
</FormField>
|
||||
<FormField title="roles.assign_permissions">
|
||||
<Controller
|
||||
control={control}
|
||||
name="scopes"
|
||||
defaultValue={[]}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<RolePermissionsTransfer value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</form>
|
||||
</ModalLayout>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,7 @@ const roles = {
|
|||
create: 'Add Roles', // UNTRANSLATED
|
||||
role_name: 'Role', // UNTRANSLATED
|
||||
role_description: 'Description', // UNTRANSLATED
|
||||
assign_permissions: 'Assign permissions', // UNTRANSLATED
|
||||
create_role_title: 'Create a role', // UNTRANSLATED
|
||||
create_role_description:
|
||||
'Create and manage Roles for your applications. Roles contain collections of Permissions and can be assigned to Users.', // UNTRANSLATED
|
||||
|
|
|
@ -5,6 +5,7 @@ const roles = {
|
|||
create: 'Add Roles',
|
||||
role_name: 'Role',
|
||||
role_description: 'Description',
|
||||
assign_permissions: 'Assign permissions',
|
||||
create_role_title: 'Create a role',
|
||||
create_role_description:
|
||||
'Create and manage Roles for your applications. Roles contain collections of Permissions and can be assigned to Users.',
|
||||
|
|
|
@ -5,6 +5,7 @@ const roles = {
|
|||
create: 'Add Roles', // UNTRANSLATED
|
||||
role_name: 'Role', // UNTRANSLATED
|
||||
role_description: 'Description', // UNTRANSLATED
|
||||
assign_permissions: 'Assign permissions', // UNTRANSLATED
|
||||
create_role_title: 'Create a role', // UNTRANSLATED
|
||||
create_role_description:
|
||||
'Create and manage Roles for your applications. Roles contain collections of Permissions and can be assigned to Users.', // UNTRANSLATED
|
||||
|
|
|
@ -5,6 +5,7 @@ const roles = {
|
|||
create: 'Add Roles', // UNTRANSLATED
|
||||
role_name: 'Role', // UNTRANSLATED
|
||||
role_description: 'Description', // UNTRANSLATED
|
||||
assign_permissions: 'Assign permissions', // UNTRANSLATED
|
||||
create_role_title: 'Create a role', // UNTRANSLATED
|
||||
create_role_description:
|
||||
'Create and manage Roles for your applications. Roles contain collections of Permissions and can be assigned to Users.', // UNTRANSLATED
|
||||
|
|
|
@ -5,6 +5,7 @@ const roles = {
|
|||
create: 'Add Roles', // UNTRANSLATED
|
||||
role_name: 'Role', // UNTRANSLATED
|
||||
role_description: 'Description', // UNTRANSLATED
|
||||
assign_permissions: 'Assign permissions', // UNTRANSLATED
|
||||
create_role_title: 'Create a role', // UNTRANSLATED
|
||||
create_role_description:
|
||||
'Create and manage Roles for your applications. Roles contain collections of Permissions and can be assigned to Users.', // UNTRANSLATED
|
||||
|
|
|
@ -5,6 +5,7 @@ const roles = {
|
|||
create: 'Add Roles', // UNTRANSLATED
|
||||
role_name: 'Role', // UNTRANSLATED
|
||||
role_description: 'Description', // UNTRANSLATED
|
||||
assign_permissions: 'Assign permissions', // UNTRANSLATED
|
||||
create_role_title: 'Create a role', // UNTRANSLATED
|
||||
create_role_description:
|
||||
'Create and manage Roles for your applications. Roles contain collections of Permissions and can be assigned to Users.', // UNTRANSLATED
|
||||
|
|
|
@ -5,6 +5,7 @@ const roles = {
|
|||
create: 'Add Roles', // UNTRANSLATED
|
||||
role_name: 'Role', // UNTRANSLATED
|
||||
role_description: 'Description', // UNTRANSLATED
|
||||
assign_permissions: 'Assign permissions', // UNTRANSLATED
|
||||
create_role_title: 'Create a role', // UNTRANSLATED
|
||||
create_role_description:
|
||||
'Create and manage Roles for your applications. Roles contain collections of Permissions and can be assigned to Users.', // UNTRANSLATED
|
||||
|
|
|
@ -5,6 +5,7 @@ const roles = {
|
|||
create: 'Add Roles', // UNTRANSLATED
|
||||
role_name: 'Role', // UNTRANSLATED
|
||||
role_description: 'Description', // UNTRANSLATED
|
||||
assign_permissions: 'Assign permissions', // UNTRANSLATED
|
||||
create_role_title: 'Create a role', // UNTRANSLATED
|
||||
create_role_description:
|
||||
'Create and manage Roles for your applications. Roles contain collections of Permissions and can be assigned to Users.', // UNTRANSLATED
|
||||
|
|
Loading…
Add table
Reference in a new issue