mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): create permission (#2830)
This commit is contained in:
parent
8bcb4de1c0
commit
fdd5af2582
5 changed files with 202 additions and 43 deletions
|
@ -5,6 +5,17 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.filterContainer {
|
||||
background-color: var(--color-layer-1);
|
||||
border-radius: 12px 12px 0 0;
|
||||
padding: _.unit(3) _.unit(3) 0;
|
||||
|
||||
.filter {
|
||||
border-bottom: 1px solid var(--color-divider);
|
||||
padding-bottom: _.unit(3);
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border: none;
|
||||
}
|
||||
|
@ -25,6 +36,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hideTopBorderRadius {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bodyTable {
|
||||
|
|
|
@ -24,6 +24,7 @@ type Props<
|
|||
rowGroups: Array<RowGroup<TFieldValues>>;
|
||||
columns: Array<Column<TFieldValues>>;
|
||||
rowIndexKey: TName;
|
||||
filter?: ReactNode;
|
||||
isRowClickable?: (row: TFieldValues) => boolean;
|
||||
rowClickHandler?: (row: TFieldValues) => void;
|
||||
className?: string;
|
||||
|
@ -42,6 +43,7 @@ const Table = <
|
|||
rowGroups,
|
||||
columns,
|
||||
rowIndexKey,
|
||||
filter,
|
||||
rowClickHandler,
|
||||
isRowClickable = () => Boolean(rowClickHandler),
|
||||
className,
|
||||
|
@ -60,7 +62,18 @@ const Table = <
|
|||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<table className={classNames(styles.headerTable, headerClassName)}>
|
||||
{filter && (
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filter}>{filter}</div>
|
||||
</div>
|
||||
)}
|
||||
<table
|
||||
className={classNames(
|
||||
styles.headerTable,
|
||||
filter && styles.hideTopBorderRadius,
|
||||
headerClassName
|
||||
)}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ title, colSpan, dataIndex }) => (
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import type { Scope } from '@logto/schemas';
|
||||
import { useForm } from 'react-hook-form';
|
||||
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 TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
type Props = {
|
||||
resourceId: string;
|
||||
onClose: (scope?: Scope) => void;
|
||||
};
|
||||
|
||||
type CreatePermissionFormData = Pick<Scope, 'name' | 'description'>;
|
||||
|
||||
const CreatePermissionModal = ({ resourceId, onClose }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<CreatePermissionFormData>();
|
||||
|
||||
const api = useApi();
|
||||
|
||||
const onSubmit = handleSubmit(async (formData) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createdScope = await api
|
||||
.post(`/api/resources/${resourceId}/scopes`, { json: formData })
|
||||
.json<Scope>();
|
||||
|
||||
onClose(createdScope);
|
||||
});
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen
|
||||
shouldCloseOnEsc
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
onRequestClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ModalLayout
|
||||
title="api_resource_details.permission.create_title"
|
||||
subtitle="api_resource_details.permission.create_subtitle"
|
||||
footer={
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
htmlType="submit"
|
||||
title="api_resource_details.permission.confirm_create"
|
||||
size="large"
|
||||
type="primary"
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<form>
|
||||
<FormField isRequired title="api_resource_details.permission.name">
|
||||
<TextInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
placeholder={t('api_resource_details.permission.name_placeholder')}
|
||||
{...register('name', { required: true })}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="api_resource_details.permission.description">
|
||||
<TextInput
|
||||
placeholder={t('api_resource_details.permission.description_placeholder')}
|
||||
{...register('description')}
|
||||
/>
|
||||
</FormField>
|
||||
</form>
|
||||
</ModalLayout>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePermissionModal;
|
|
@ -4,6 +4,12 @@
|
|||
margin-bottom: _.unit(6);
|
||||
color: var(--color-text);
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline-block;
|
||||
font: var(--font-body-medium);
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import type { Scope } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Delete from '@/assets/images/delete.svg';
|
||||
import Plus from '@/assets/images/plus.svg';
|
||||
import Button from '@/components/Button';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import Search from '@/components/Search';
|
||||
import Table from '@/components/Table';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
|
||||
import type { ApiResourceDetailsOutletContext } from '../types';
|
||||
import CreatePermissionModal from './components/CreatePermissionModal';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const ApiResourcePermissions = () => {
|
||||
|
@ -25,7 +30,10 @@ const ApiResourcePermissions = () => {
|
|||
|
||||
const isLoading = !scopes && !error;
|
||||
|
||||
const [isCreateFormOpen, setIsCreateFormOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
className={styles.permissionTable}
|
||||
rowIndexKey="id"
|
||||
|
@ -54,6 +62,20 @@ const ApiResourcePermissions = () => {
|
|||
),
|
||||
},
|
||||
]}
|
||||
filter={
|
||||
<div className={styles.filter}>
|
||||
<Search />
|
||||
<Button
|
||||
title="api_resource_details.permission.create_button"
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<Plus />}
|
||||
onClick={() => {
|
||||
setIsCreateFormOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
isLoading={isLoading}
|
||||
placeholder={{
|
||||
content: (
|
||||
|
@ -61,7 +83,7 @@ const ApiResourcePermissions = () => {
|
|||
title="api_resource_details.permission.create_button"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
// TODO @xiaoyijun Create Permission
|
||||
setIsCreateFormOpen(true);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
@ -69,6 +91,21 @@ const ApiResourcePermissions = () => {
|
|||
errorMessage={error?.body?.message ?? error?.message}
|
||||
onRetry={async () => mutate(undefined, true)}
|
||||
/>
|
||||
{isCreateFormOpen && (
|
||||
<CreatePermissionModal
|
||||
resourceId={resourceId}
|
||||
onClose={(scope) => {
|
||||
if (scope) {
|
||||
toast.success(
|
||||
t('api_resource_details.permission.permission_created', { name: scope.name })
|
||||
);
|
||||
void mutate();
|
||||
}
|
||||
setIsCreateFormOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue