mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
feat(console): init dashboard (#1006)
This commit is contained in:
parent
2812d6de46
commit
28e09b6994
10 changed files with 201 additions and 8 deletions
|
@ -20,6 +20,7 @@ import Applications from './pages/Applications';
|
|||
import Callback from './pages/Callback';
|
||||
import ConnectorDetails from './pages/ConnectorDetails';
|
||||
import Connectors from './pages/Connectors';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import GetStarted from './pages/GetStarted';
|
||||
import NotFound from './pages/NotFound';
|
||||
import Settings from './pages/Settings';
|
||||
|
@ -67,6 +68,7 @@ const Main = () => {
|
|||
<Route path=":tab" element={<SignInExperience />} />
|
||||
</Route>
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</SWRConfig>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ErrorImage from '@/assets/images/warning.svg';
|
||||
import { ArrowDown, ArrowUp } from '@/icons/Arrow';
|
||||
import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -35,7 +35,7 @@ const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props
|
|||
}}
|
||||
>
|
||||
{t('errors.more_details')}
|
||||
{isDetailsOpen ? <ArrowUp /> : <ArrowDown />}
|
||||
{isDetailsOpen ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { ReactNode, useRef, useState } from 'react';
|
||||
|
||||
import { ArrowDown, ArrowUp } from '@/icons/Arrow';
|
||||
import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow';
|
||||
|
||||
import Dropdown, { DropdownItem } from '../Dropdown';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -47,7 +47,7 @@ const Select = ({ value, options, onChange, isReadOnly, hasError }: Props) => {
|
|||
}}
|
||||
>
|
||||
{current?.title}
|
||||
<div className={styles.arrow}>{isOpen ? <ArrowUp /> : <ArrowDown />}</div>
|
||||
<div className={styles.arrow}>{isOpen ? <KeyboardArrowUp /> : <KeyboardArrowDown />}</div>
|
||||
</div>
|
||||
<Dropdown
|
||||
isFullWidth
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { SVGProps } from 'react';
|
||||
|
||||
export const ArrowDown = (props: SVGProps<SVGSVGElement>) => {
|
||||
export const KeyboardArrowDown = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
|
@ -18,7 +18,7 @@ export const ArrowDown = (props: SVGProps<SVGSVGElement>) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const ArrowUp = (props: SVGProps<SVGSVGElement>) => {
|
||||
export const KeyboardArrowUp = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
|
@ -35,3 +35,39 @@ export const ArrowUp = (props: SVGProps<SVGSVGElement>) => {
|
|||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ArrowUp = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M14.7578 9.41199L10.5912 5.24532C10.5119 5.16946 10.4185 5.10999 10.3162 5.07032C10.1133 4.98698 9.88572 4.98698 9.68284 5.07032C9.58054 5.10999 9.48709 5.16946 9.40784 5.24532L5.24117 9.41199C5.16347 9.48969 5.10184 9.58193 5.05979 9.68345C5.01774 9.78497 4.99609 9.89377 4.99609 10.0037C4.99609 10.2256 5.08425 10.4384 5.24117 10.5953C5.39809 10.7522 5.61092 10.8404 5.83284 10.8404C6.05475 10.8404 6.26758 10.7522 6.4245 10.5953L9.16617 7.84532V14.1703C9.16617 14.3913 9.25397 14.6033 9.41025 14.7596C9.56653 14.9159 9.77849 15.0037 9.9995 15.0037C10.2205 15.0037 10.4325 14.9159 10.5888 14.7596C10.745 14.6033 10.8328 14.3913 10.8328 14.1703V7.84532L13.5745 10.5953C13.652 10.6734 13.7441 10.7354 13.8457 10.7777C13.9472 10.82 14.0562 10.8418 14.1662 10.8418C14.2762 10.8418 14.3851 10.82 14.4867 10.7777C14.5882 10.7354 14.6804 10.6734 14.7578 10.5953C14.8359 10.5179 14.8979 10.4257 14.9402 10.3241C14.9826 10.2226 15.0043 10.1137 15.0043 10.0037C15.0043 9.89365 14.9826 9.78473 14.9402 9.68318C14.8979 9.58163 14.8359 9.48946 14.7578 9.41199Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ArrowDown = (props: SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M14.7578 10.588L10.5912 14.7547C10.5119 14.8305 10.4185 14.89 10.3162 14.9297C10.1133 15.013 9.88572 15.013 9.68284 14.9297C9.58054 14.89 9.48709 14.8305 9.40784 14.7547L5.24117 10.588C5.16347 10.5103 5.10184 10.4181 5.05979 10.3166C5.01774 10.215 4.99609 10.1062 4.99609 9.99634C4.99609 9.77442 5.08425 9.5616 5.24117 9.40468C5.39809 9.24776 5.61092 9.1596 5.83284 9.1596C6.05475 9.1596 6.26758 9.24776 6.4245 9.40468L9.16617 12.1547V5.82968C9.16617 5.60866 9.25397 5.3967 9.41025 5.24042C9.56653 5.08414 9.77849 4.99634 9.9995 4.99634C10.2205 4.99634 10.4325 5.08414 10.5888 5.24042C10.745 5.3967 10.8328 5.60866 10.8328 5.82968V12.1547L13.5745 9.40468C13.652 9.32657 13.7441 9.26457 13.8457 9.22227C13.9472 9.17996 14.0562 9.15818 14.1662 9.15818C14.2762 9.15818 14.3851 9.17996 14.4867 9.22227C14.5882 9.26457 14.6804 9.32657 14.7578 9.40468C14.8359 9.48214 14.8979 9.57431 14.9402 9.67586C14.9826 9.77741 15.0043 9.88633 15.0043 9.99634C15.0043 10.1064 14.9826 10.2153 14.9402 10.3168C14.8979 10.4184 14.8359 10.5105 14.7578 10.588Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import DangerousRaw from '@/components/DangerousRaw';
|
|||
import IconButton from '@/components/IconButton';
|
||||
import Index from '@/components/Index';
|
||||
import Spacer from '@/components/Spacer';
|
||||
import { ArrowDown, ArrowUp } from '@/icons/Arrow';
|
||||
import { KeyboardArrowDown, KeyboardArrowUp } from '@/icons/Arrow';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -74,7 +74,7 @@ const Step = ({
|
|||
subtitle={<DangerousRaw>{subtitle}</DangerousRaw>}
|
||||
/>
|
||||
<Spacer />
|
||||
<IconButton>{isExpanded ? <ArrowUp /> : <ArrowDown />}</IconButton>
|
||||
<IconButton>{isExpanded ? <KeyboardArrowUp /> : <KeyboardArrowDown />}</IconButton>
|
||||
</div>
|
||||
<div className={classNames(styles.content, isExpanded && styles.expanded)}>
|
||||
{children}
|
||||
|
|
62
packages/console/src/pages/Dashboard/index.module.scss
Normal file
62
packages/console/src/pages/Dashboard/index.module.scss
Normal file
|
@ -0,0 +1,62 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: _.unit(6);
|
||||
|
||||
.title {
|
||||
font: var(--font-title-large);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: _.unit(1);
|
||||
padding-right: _.unit(6);
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-caption);
|
||||
}
|
||||
}
|
||||
|
||||
.topBlocks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.block {
|
||||
flex: 1;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: _.unit(4);
|
||||
}
|
||||
|
||||
.title {
|
||||
font: var(--font-title-medium);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.number {
|
||||
flex: 1;
|
||||
font: var(--font-headline-small);
|
||||
}
|
||||
|
||||
.delta {
|
||||
font: var(--font-title-medium);
|
||||
color: var(--color-success-50);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.down {
|
||||
color: var(--color-error-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
60
packages/console/src/pages/Dashboard/index.tsx
Normal file
60
packages/console/src/pages/Dashboard/index.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
import { ArrowDown, ArrowUp } from '@/icons/Arrow';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import { NewUsersResponse, TotalUsersResponse } from './types';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { data: totalData } = useSWR<TotalUsersResponse>('/api/dashboard/users/total');
|
||||
const { data: newData } = useSWR<NewUsersResponse>('/api/dashboard/users/new');
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const isLoading = !totalData || !newData;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{t('dashboard.title')}</div>
|
||||
<div className={styles.subtitle}>{t('dashboard.description')}</div>
|
||||
</div>
|
||||
{!isLoading && (
|
||||
<div className={styles.topBlocks}>
|
||||
<Card className={styles.block}>
|
||||
<div className={styles.title}>{t('dashboard.total_users')}</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.number}>{totalData.totalUserCount}</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={styles.block}>
|
||||
<div className={styles.title}>{t('dashboard.new_users_today')}</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.number}>{newData.today.count}</div>
|
||||
<div className={classNames(styles.delta, newData.today.delta < 0 && styles.down)}>
|
||||
({newData.today.delta > 0 && '+'}
|
||||
{newData.today.delta}){newData.today.delta > 0 ? <ArrowUp /> : <ArrowDown />}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={styles.block}>
|
||||
<div className={styles.title}>{t('dashboard.new_users_7_days')}</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.number}>{newData.last7Days.count}</div>
|
||||
<div className={classNames(styles.delta, newData.last7Days.delta < 0 && styles.down)}>
|
||||
({newData.last7Days.delta > 0 && '+'}
|
||||
{newData.last7Days.delta})
|
||||
{newData.last7Days.delta > 0 ? <ArrowUp /> : <ArrowDown />}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
13
packages/console/src/pages/Dashboard/types.ts
Normal file
13
packages/console/src/pages/Dashboard/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export type CountAndDelta = {
|
||||
count: number;
|
||||
delta: number;
|
||||
};
|
||||
|
||||
export type TotalUsersResponse = {
|
||||
totalUserCount: number;
|
||||
};
|
||||
|
||||
export type NewUsersResponse = {
|
||||
today: CountAndDelta;
|
||||
last7Days: CountAndDelta;
|
||||
};
|
|
@ -520,6 +520,16 @@ const translation = {
|
|||
appearance_dark: 'Dark mode',
|
||||
saved: 'Saved!',
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
description: 'Get an overview about your app performace',
|
||||
total_users: 'Total users',
|
||||
new_users_today: 'New users today',
|
||||
new_users_7_days: 'New users past 7 days',
|
||||
daily_active_users: 'Daily active users',
|
||||
weekly_active_users: 'Weeky active users',
|
||||
monthly_active_users: 'Monthly active users',
|
||||
},
|
||||
session_expired: {
|
||||
title: 'Session Expired',
|
||||
subtitle:
|
||||
|
|
|
@ -516,6 +516,16 @@ const translation = {
|
|||
appearance_dark: '深色模式',
|
||||
saved: '已保存',
|
||||
},
|
||||
dashboard: {
|
||||
title: '仪表盘',
|
||||
description: '查看运行总览',
|
||||
total_users: '总用户',
|
||||
new_users_today: '今日新增',
|
||||
new_users_7_days: '7日新增',
|
||||
daily_active_users: '日活用户',
|
||||
weekly_active_users: '周活用户',
|
||||
monthly_active_users: '月活用户',
|
||||
},
|
||||
session_expired: {
|
||||
title: '会话已过期',
|
||||
subtitle: '会话可能已过期,您已被登出. 请点击下方按钮重新登录到管理界面.',
|
||||
|
|
Loading…
Add table
Reference in a new issue