0
Fork 0
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:
Xiao Yijun 2023-01-06 23:03:53 +08:00 committed by GitHub
parent 8bcb4de1c0
commit fdd5af2582
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 202 additions and 43 deletions

View file

@ -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 {

View file

@ -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 }) => (

View file

@ -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;

View file

@ -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);

View file

@ -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,50 +30,82 @@ const ApiResourcePermissions = () => {
const isLoading = !scopes && !error;
const [isCreateFormOpen, setIsCreateFormOpen] = useState(false);
return (
<Table
className={styles.permissionTable}
rowIndexKey="id"
rowGroups={[{ key: 'scopes', data: scopes }]}
columns={[
{
title: t('api_resource_details.permission.name_column'),
dataIndex: 'name',
colSpan: 6,
render: ({ name }) => <div className={styles.name}>{name}</div>,
},
{
title: t('api_resource_details.permission.description_column'),
dataIndex: 'description',
colSpan: 9,
render: ({ description }) => <div className={styles.description}>{description}</div>,
},
{
title: null,
dataIndex: 'delete',
colSpan: 1,
render: () => (
<IconButton>
<Delete />
</IconButton>
<>
<Table
className={styles.permissionTable}
rowIndexKey="id"
rowGroups={[{ key: 'scopes', data: scopes }]}
columns={[
{
title: t('api_resource_details.permission.name_column'),
dataIndex: 'name',
colSpan: 6,
render: ({ name }) => <div className={styles.name}>{name}</div>,
},
{
title: t('api_resource_details.permission.description_column'),
dataIndex: 'description',
colSpan: 9,
render: ({ description }) => <div className={styles.description}>{description}</div>,
},
{
title: null,
dataIndex: 'delete',
colSpan: 1,
render: () => (
<IconButton>
<Delete />
</IconButton>
),
},
]}
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: (
<Button
title="api_resource_details.permission.create_button"
type="outline"
onClick={() => {
setIsCreateFormOpen(true);
}}
/>
),
},
]}
isLoading={isLoading}
placeholder={{
content: (
<Button
title="api_resource_details.permission.create_button"
type="outline"
onClick={() => {
// TODO @xiaoyijun Create Permission
}}
/>
),
}}
errorMessage={error?.body?.message ?? error?.message}
onRetry={async () => mutate(undefined, true)}
/>
}}
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);
}}
/>
)}
</>
);
};