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:
parent
17e218f77d
commit
05bbc9c2d3
6 changed files with 215 additions and 2 deletions
|
@ -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 />} />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
124
packages/console/src/pages/ApiResourceDetails/index.tsx
Normal file
124
packages/console/src/pages/ApiResourceDetails/index.tsx
Normal 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;
|
|
@ -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} />
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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.',
|
||||
|
|
Loading…
Reference in a new issue