0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(console): api resource details (#352)

This commit is contained in:
Xiao Yijun 2022-03-09 17:56:50 +08:00 committed by GitHub
parent 17e218f77d
commit 05bbc9c2d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 215 additions and 2 deletions

View file

@ -9,6 +9,7 @@ import Content from './components/Content';
import Sidebar, { getPath, sections } from './components/Sidebar';
import Topbar from './components/Topbar';
import initI18n from './i18n/init';
import ApiResourceDetails from './pages/ApiResourceDetails';
import ApiResources from './pages/ApiResources';
import ApplicationDetails from './pages/ApplicationDetails';
import Applications from './pages/Applications';
@ -38,11 +39,14 @@ const Main = () => {
<Sidebar />
<Content>
<Routes>
<Route path="api-resources" element={<ApiResources />} />
<Route path="applications">
<Route index element={<Applications />} />
<Route path=":id" element={<ApplicationDetails />} />
</Route>
<Route path="api-resources">
<Route index element={<ApiResources />} />
<Route path=":id" element={<ApiResourceDetails />} />
</Route>
<Route path="connectors">
<Route index element={<Connectors />} />
<Route path="social" element={<Connectors />} />

View file

@ -0,0 +1,68 @@
@use '@/scss/underscore' as _;
.container {
> *:not(:first-child) {
margin-top: _.unit(4);
}
}
.container .backButton {
display: flex;
align-items: center;
> *:not(:first-child) {
margin-left: _.unit(1);
}
}
.container .header {
display: flex;
justify-content: space-between;
align-items: center;
.info {
display: flex;
.meta {
margin-left: _.unit(6);
display: flex;
flex-direction: column;
justify-content: space-between;
.name {
font: var(--font-title-large);
color: var(--color-component-text);
}
}
}
.operation {
> *:not(:first-child) {
margin-left: _.unit(3);
}
}
}
.container .body {
> :first-child {
margin-top: 0;
}
.form {
margin-top: _.unit(8);
}
.fields {
padding-bottom: _.unit(16);
border-bottom: 1px solid var(--color-border);
}
.textField {
@include _.form-text-field;
}
.submit {
margin-top: _.unit(6);
text-align: right;
}
}

View file

@ -0,0 +1,124 @@
import { Resource } from '@logto/schemas';
import ky from 'ky';
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import useSWR from 'swr';
import BackLink from '@/components/BackLink';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CopyToClipboard from '@/components/CopyToClipboard';
import FormField from '@/components/FormField';
import ImagePlaceholder from '@/components/ImagePlaceholder';
import TabNav, { TabNavLink } from '@/components/TabNav';
import TextInput from '@/components/TextInput';
import { RequestError } from '@/swr';
import * as styles from './index.module.scss';
type FormData = {
name: string;
accessTokenTtl: number;
};
const ApiResourceDetails = () => {
const location = useLocation();
const { id } = useParams();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data, error, mutate } = useSWR<Resource, RequestError>(id && `/api/resources/${id}`);
const isLoading = !data && !error;
const { handleSubmit, register, reset } = useForm<FormData>({
defaultValues: data,
});
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (!data) {
return;
}
reset(data);
}, [data, reset]);
const onSubmit = handleSubmit(async (formData) => {
if (!data || submitting) {
return;
}
setSubmitting(true);
try {
const updatedApiResource = await ky
.patch(`/api/resources/${data.id}`, { json: formData })
.json<Resource>();
void mutate(updatedApiResource);
} catch (error: unknown) {
console.error(error);
} finally {
setSubmitting(false);
}
});
return (
<div className={styles.container}>
<BackLink to="/api-resources">{t('api_resource_details.back_to_api_resources')}</BackLink>
{isLoading && <div>loading</div>}
{error && <div>{`error occurred: ${error.metadata.code}`}</div>}
{data && (
<>
<Card className={styles.header}>
<div className={styles.info}>
<ImagePlaceholder size={76} borderRadius={16} />
<div className={styles.meta}>
<div className={styles.name}>{data.name}</div>
<CopyToClipboard value={data.indicator} />
</div>
</div>
<div className={styles.operation}>
<Button title="admin_console.api_resource_details.check_help_guide" />
</div>
</Card>
<Card className={styles.body}>
<TabNav>
<TabNavLink href={location.pathname}>{t('api_resource_details.settings')}</TabNavLink>
</TabNav>
<form className={styles.form} onSubmit={onSubmit}>
<div className={styles.fields}>
<FormField
isRequired
title="admin_console.api_resources.api_name"
className={styles.textField}
>
<TextInput {...register('name', { required: true })} />
</FormField>
<FormField
isRequired
title="admin_console.api_resource_details.token_expiration_time_in_seconds"
className={styles.textField}
>
<TextInput
{...register('accessTokenTtl', { required: true, valueAsNumber: true })}
/>
</FormField>
</div>
<div className={styles.submit}>
<Button
disabled={submitting}
htmlType="submit"
type="primary"
title="admin_console.api_resource_details.save_changes"
size="large"
/>
</div>
</form>
</Card>
</>
)}
</div>
);
};
export default ApiResourceDetails;

View file

@ -3,6 +3,7 @@ import { conditional } from '@silverhand/essentials/lib/utilities/conditional.js
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Modal from 'react-modal';
import { useNavigate } from 'react-router-dom';
import useSWR from 'swr';
import Button from '@/components/Button';
@ -22,6 +23,7 @@ const ApiResources = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data, error, mutate } = useSWR<Resource[], RequestError>('/api/resources');
const isLoading = !data && !error;
const navigate = useNavigate();
return (
<Card>
@ -45,6 +47,7 @@ const ApiResources = () => {
if (createdApiResource) {
void mutate(conditional(data && [...data, createdApiResource]));
navigate(createdApiResource.id);
}
}}
/>
@ -71,7 +74,7 @@ const ApiResources = () => {
{data?.map(({ id, name, indicator }) => (
<tr key={id} className={styles.clickable}>
<td>
<ItemPreview title={name} icon={<ImagePlaceholder />} />
<ItemPreview title={name} icon={<ImagePlaceholder />} to={`/api-resources/${id}`} />
</td>
<td>
<CopyToClipboard value={indicator} />

View file

@ -79,6 +79,13 @@ const translation = {
api_name: 'API Name',
api_identifier: 'API Identifier',
},
api_resource_details: {
back_to_api_resources: 'Back to my API resources',
check_help_guide: 'Check Help Guide',
settings: 'Settings',
save_changes: 'Save Changes',
token_expiration_time_in_seconds: 'Token Expiration Time (in seconds)',
},
connectors: {
title: 'Connectors',
subtitle: 'Setup connectors to enable passwordless and social sign in experience.',

View file

@ -81,6 +81,13 @@ const translation = {
api_name: 'API Name',
api_identifier: 'API Identifier',
},
api_resource_details: {
back_to_api_resources: 'Back to my API resources',
check_help_guide: 'Check Help Guide',
settings: 'Settings',
save_changes: 'Save Changes',
token_expiration_time_in_seconds: 'Token Expiration Time (in seconds)',
},
connectors: {
title: '连接器',
subtitle: 'Setup connectors to enable passwordless and social sign in experience.',