0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat(console): retrieve applications from api (#320)

* feat(console): retrieve applications from api

* fix(console): i18n key

* fix(console): update per review
This commit is contained in:
Gao Sun 2022-03-04 17:26:34 +08:00 committed by GitHub
parent 06fd253754
commit bb1d3c0a37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 116 additions and 31 deletions

View file

@ -25,7 +25,8 @@
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-i18next": "^11.15.4", "react-i18next": "^11.15.4",
"react-router-dom": "^6.2.2" "react-router-dom": "^6.2.2",
"swr": "^1.2.2"
}, },
"devDependencies": { "devDependencies": {
"@parcel/core": "^2.3.1", "@parcel/core": "^2.3.1",

View file

@ -1,7 +1,8 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { SWRConfig } from 'swr';
import './scss/normalized.scss'; import './scss/normalized.scss';
import * as styles from './App.module.scss'; import * as styles from './App.module.scss';
import AppContent from './components/AppContent'; import AppContent from './components/AppContent';
import Content from './components/Content'; import Content from './components/Content';
@ -10,6 +11,7 @@ import Topbar from './components/Topbar';
import initI18n from './i18n/init'; import initI18n from './i18n/init';
import ApiResources from './pages/ApiResources'; import ApiResources from './pages/ApiResources';
import Applications from './pages/Applications'; import Applications from './pages/Applications';
import { fetcher } from './swr';
const isBasenameNeeded = process.env.NODE_ENV !== 'development' || process.env.PORT === '5002'; const isBasenameNeeded = process.env.NODE_ENV !== 'development' || process.env.PORT === '5002';
@ -26,18 +28,20 @@ const Main = () => {
}, [location.pathname, navigate]); }, [location.pathname, navigate]);
return ( return (
<AppContent theme="light"> <SWRConfig value={{ fetcher }}>
<Topbar /> <AppContent theme="light">
<div className={styles.content}> <Topbar />
<Sidebar /> <div className={styles.content}>
<Content> <Sidebar />
<Routes> <Content>
<Route path="api-resources" element={<ApiResources />} /> <Routes>
<Route path="applications" element={<Applications />} /> <Route path="api-resources" element={<ApiResources />} />
</Routes> <Route path="applications" element={<Applications />} />
</Content> </Routes>
</div> </Content>
</AppContent> </div>
</AppContent>
</SWRConfig>
); );
}; };

View file

@ -14,7 +14,7 @@ const ItemPreview = ({ title, subtitle, icon }: Props) => {
{icon} {icon}
<div> <div>
<div className={styles.title}>{title}</div> <div className={styles.title}>{title}</div>
{subtitle && <div className={styles.subtitle}>{subtitle}</div>} {subtitle && <div className={styles.subtitle}>{String(subtitle)}</div>}
</div> </div>
</div> </div>
); );

View file

@ -9,7 +9,7 @@
margin-top: _.unit(6); margin-top: _.unit(6);
width: 100%; width: 100%;
tbody tr { tbody tr.clickable {
cursor: pointer; cursor: pointer;
&:hover { &:hover {

View file

@ -1,4 +1,7 @@
import { Application } from '@logto/schemas';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Card from '@/components/Card'; import Card from '@/components/Card';
@ -6,10 +9,16 @@ import CardTitle from '@/components/CardTitle';
import CopyToClipboard from '@/components/CopyToClipboard'; import CopyToClipboard from '@/components/CopyToClipboard';
import ImagePlaceholder from '@/components/ImagePlaceholder'; import ImagePlaceholder from '@/components/ImagePlaceholder';
import ItemPreview from '@/components/ItemPreview'; import ItemPreview from '@/components/ItemPreview';
import { RequestError } from '@/swr';
import { applicationTypeI18nKey } from '@/types/applications';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
const Applications = () => { const Applications = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data, error } = useSWR<Application[], RequestError>('/api/applications');
const isLoading = !data && !error;
return ( return (
<Card> <Card>
<div className={styles.headline}> <div className={styles.headline}>
@ -19,23 +28,35 @@ const Applications = () => {
<table className={styles.table}> <table className={styles.table}>
<thead> <thead>
<tr> <tr>
<td className={styles.applicationName}>Application Name</td> <td className={styles.applicationName}>{t('applications.application_name')}</td>
<td>Client ID</td> <td>{t('applications.client_id')}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> {error && (
<td> <tr>
<ItemPreview <td colSpan={2}>error occurred: {error.metadata.code}</td>
title="Default App" </tr>
subtitle="Single Page Application" )}
icon={<ImagePlaceholder />} {isLoading && (
/> <tr>
</td> <td colSpan={2}>loading</td>
<td> </tr>
<CopyToClipboard value="RUMatENw0rFWO5aGbMI8tY2Qol50eOg3" /> )}
</td> {data?.map(({ id, name, type }) => (
</tr> <tr key={id}>
<td>
<ItemPreview
title={name}
subtitle={String(t(applicationTypeI18nKey[type]))}
icon={<ImagePlaceholder />}
/>
</td>
<td>
<CopyToClipboard value={id} />
</td>
</tr>
))}
</tbody> </tbody>
</table> </table>
</Card> </Card>

View file

@ -0,0 +1,22 @@
import { RequestErrorMetadata } from '@logto/schemas';
import { BareFetcher } from 'swr';
export class RequestError extends Error {
metadata: RequestErrorMetadata;
constructor(metadata: RequestErrorMetadata) {
super('Request error occurred.');
this.metadata = metadata;
}
}
export const fetcher: BareFetcher = async (resource, init) => {
const response = await fetch(resource, init);
if (!response.ok) {
const metadata = (await response.json()) as RequestErrorMetadata;
throw new RequestError(metadata);
}
return response.json();
};

View file

@ -0,0 +1,8 @@
import { AdminConsoleKey } from '@logto/phrases';
import { ApplicationType } from '@logto/schemas';
export const applicationTypeI18nKey: Record<ApplicationType, AdminConsoleKey> = {
[ApplicationType.Native]: 'applications.type.native',
[ApplicationType.SPA]: 'applications.type.spa',
[ApplicationType.Traditional]: 'applications.type.tranditional',
};

View file

@ -1,4 +1,4 @@
import { DefaultState, DefaultContext, ParameterizedContext, Next } from 'koa'; import { DefaultState, DefaultContext, ParameterizedContext, Next, BaseRequest } from 'koa';
declare module 'koa' { declare module 'koa' {
// Have to do this patch since `compose.Middleware` returns `any`. // Have to do this patch since `compose.Middleware` returns `any`.
@ -10,4 +10,9 @@ declare module 'koa' {
ResponseBodyT = any, ResponseBodyT = any,
NextT = void NextT = void
> = KoaMiddleware<ParameterizedContext<StateT, ContextT, ResponseBodyT>, NextT>; > = KoaMiddleware<ParameterizedContext<StateT, ContextT, ResponseBodyT>, NextT>;
interface Request extends BaseRequest {
body?: any;
files?: Files;
}
} }

View file

@ -38,6 +38,13 @@ const translation = {
subtitle: subtitle:
'Setup a mobile, single page or traditional application to use Logto for authentication.', 'Setup a mobile, single page or traditional application to use Logto for authentication.',
create: 'Create Application', create: 'Create Application',
application_name: 'Application Name',
client_id: 'Client ID',
type: {
native: 'Native App',
spa: 'Single Page App',
tranditional: 'Tranditional Web App',
},
}, },
}, },
}; };

View file

@ -40,6 +40,13 @@ const translation = {
subtitle: subtitle:
'Setup a mobile, single page or traditional application to use Logto for authentication.', 'Setup a mobile, single page or traditional application to use Logto for authentication.',
create: 'Create Application', create: 'Create Application',
application_name: 'Application Name',
client_id: 'Client ID',
type: {
native: 'Native App',
spa: 'Single Page App',
tranditional: 'Tranditional Web App',
},
}, },
}, },
}; };

10
pnpm-lock.yaml generated
View file

@ -46,6 +46,7 @@ importers:
react-i18next: ^11.15.4 react-i18next: ^11.15.4
react-router-dom: ^6.2.2 react-router-dom: ^6.2.2
stylelint: ^13.13.1 stylelint: ^13.13.1
swr: ^1.2.2
typescript: ^4.5.5 typescript: ^4.5.5
dependencies: dependencies:
'@logto/phrases': link:../phrases '@logto/phrases': link:../phrases
@ -58,6 +59,7 @@ importers:
react-dom: 17.0.2_react@17.0.2 react-dom: 17.0.2_react@17.0.2
react-i18next: 11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5 react-i18next: 11.15.4_2c37a602a29bb6bd53f3de707a8cfcc5
react-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2 react-router-dom: 6.2.2_react-dom@17.0.2+react@17.0.2
swr: 1.2.2_react@17.0.2
devDependencies: devDependencies:
'@parcel/core': 2.3.1 '@parcel/core': 2.3.1
'@parcel/transformer-sass': 2.3.1_@parcel+core@2.3.1 '@parcel/transformer-sass': 2.3.1_@parcel+core@2.3.1
@ -12547,6 +12549,14 @@ packages:
stable: 0.1.8 stable: 0.1.8
dev: true dev: true
/swr/1.2.2_react@17.0.2:
resolution: {integrity: sha512-ky0BskS/V47GpW8d6RU7CPsr6J8cr7mQD6+do5eky3bM0IyJaoi3vO8UhvrzJaObuTlGhPl2szodeB2dUd76Xw==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 17.0.2
dev: false
/symbol-tree/3.2.4: /symbol-tree/3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
dev: true dev: true