Skip to content

Commit 1ddd1e6

Browse files
committed
Update admin service status navigation
1 parent 83db435 commit 1ddd1e6

10 files changed

Lines changed: 202 additions & 38 deletions

File tree

src/components/top-navigation.tsx

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,55 @@ export default function TopNavigation({
8989
const { user } = useUserInfo();
9090
const { pathname } = useLocation();
9191
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
92+
const [appDrawerPlacement, setAppDrawerPlacementState] = useState(
93+
getManageAppDrawerPlacement,
94+
);
9295
const selectedKeys = useMemo(() => getSelectedKeys(pathname), [pathname]);
96+
const shouldShowAppsTopTab =
97+
showAuthenticatedChrome &&
98+
user &&
99+
!isMobile &&
100+
appDrawerPlacement !== 'hidden';
101+
102+
useEffect(() => {
103+
const syncPlacement = () => {
104+
setAppDrawerPlacementState(getManageAppDrawerPlacement());
105+
};
106+
107+
window.addEventListener(manageAppDrawerPlacementChangeEvent, syncPlacement);
108+
window.addEventListener('storage', syncPlacement);
109+
return () => {
110+
window.removeEventListener(
111+
manageAppDrawerPlacementChangeEvent,
112+
syncPlacement,
113+
);
114+
window.removeEventListener('storage', syncPlacement);
115+
};
116+
}, []);
93117

94118
const authenticatedItems: MenuItems =
95119
showAuthenticatedChrome && user
96120
? [
121+
...(shouldShowAppsTopTab
122+
? [
123+
{
124+
key: 'apps',
125+
icon: <AppstoreOutlined />,
126+
label: <Link to={rootRouterPath.apps}>应用列表</Link>,
127+
},
128+
]
129+
: []),
130+
...(user.admin
131+
? [
132+
{
133+
key: 'admin-service-status',
134+
icon: <DashboardOutlined />,
135+
label: (
136+
<Link to={rootRouterPath.adminServiceStatus}>服务状态</Link>
137+
),
138+
},
139+
]
140+
: []),
97141
{
98142
key: 'audit-logs',
99143
icon: <FileTextOutlined />,
@@ -140,15 +184,6 @@ export default function TopNavigation({
140184
<Link to={rootRouterPath.adminMetrics}>全局统计</Link>
141185
),
142186
},
143-
{
144-
key: 'admin-service-status',
145-
icon: <DashboardOutlined />,
146-
label: (
147-
<Link to={rootRouterPath.adminServiceStatus}>
148-
服务状态
149-
</Link>
150-
),
151-
},
152187
],
153188
},
154189
]
@@ -174,7 +209,10 @@ export default function TopNavigation({
174209

175210
return (
176211
<div className="flex min-h-16 w-full min-w-0 items-center gap-1.5 md:gap-3">
177-
<Link to="/" className="flex shrink-0 items-center no-underline">
212+
<Link
213+
to={rootRouterPath.home}
214+
className="flex shrink-0 items-center no-underline"
215+
>
178216
<LogoH className="h-7 w-auto max-w-[88px] sm:max-w-[130px] md:max-w-[150px]" />
179217
</Link>
180218
{showAuthenticatedChrome && user && <AppSwitcher compact={isMobile} />}
@@ -660,6 +698,9 @@ function formatAppKey(appKey?: string | null) {
660698
}
661699

662700
function getSelectedKeys(pathname: string) {
701+
if (pathname === rootRouterPath.home || pathname === rootRouterPath.apps) {
702+
return ['apps'];
703+
}
663704
if (pathname === rootRouterPath.user) {
664705
return ['user'];
665706
}

src/globals.d.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,27 @@ declare module '*.css' {
2323

2424
declare module 'bun:test' {
2525
type TestHandler = () => void | Promise<void>;
26-
27-
export function describe(name: string, fn: TestHandler): void;
28-
export function it(name: string, fn: TestHandler): void;
29-
export function test(name: string, fn: TestHandler): void;
30-
export function expect<T>(actual: T): {
26+
type ExpectAssertions = {
3127
toBe(expected: unknown): void;
28+
toBeGreaterThan(expected: number): void;
3229
toBeNull(): void;
3330
toContain(expected: unknown): void;
31+
toEqual(expected: unknown): void;
3432
toHaveBeenCalledWith(...args: unknown[]): void;
3533
toHaveBeenCalled(): void;
36-
not: {
37-
toHaveBeenCalledWith(...args: unknown[]): void;
38-
toHaveBeenCalled(): void;
39-
};
34+
toHaveLength(expected: number): void;
35+
toThrow(expected?: unknown): void;
36+
};
37+
type ExpectMatchers = ExpectAssertions & {
38+
not: ExpectAssertions;
39+
rejects: ExpectAssertions;
40+
resolves: ExpectAssertions;
4041
};
42+
43+
export function describe(name: string, fn: TestHandler): void;
44+
export function it(name: string, fn: TestHandler): void;
45+
export function test(name: string, fn: TestHandler): void;
46+
export function expect<T>(actual: T): ExpectMatchers;
4147
export function beforeEach(fn: () => void | Promise<void>): void;
4248
export function afterEach(fn: () => void | Promise<void>): void;
4349
export function setSystemTime(time: Date | number | null): void;

src/pages/admin-service-status.tsx

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import {
99
Spin,
1010
Statistic,
1111
Table,
12+
Tabs,
1213
Tag,
1314
Typography,
1415
} from 'antd';
1516
import type { ColumnsType } from 'antd/es/table';
1617
import dayjs from 'dayjs';
17-
import { useMemo } from 'react';
18+
import { useMemo, useState } from 'react';
1819
import {
1920
api,
2021
type InternalMetricCounter,
@@ -53,6 +54,48 @@ type EndpointRow = {
5354
total: number;
5455
};
5556

57+
const SERVICE_STATUS_TARGETS = [
58+
{
59+
key: 'jd1',
60+
label: 'jd1',
61+
host: '1.rnupdate.online',
62+
baseUrl: 'https://1.rnupdate.online/api',
63+
},
64+
{
65+
key: 'jd2',
66+
label: 'jd2',
67+
host: '2.rnupdate.online',
68+
baseUrl: 'https://2.rnupdate.online/api',
69+
},
70+
{
71+
key: 'jd3',
72+
label: 'jd3',
73+
host: '3.rnupdate.online',
74+
baseUrl: 'https://3.rnupdate.online/api',
75+
},
76+
{
77+
key: 'jd4',
78+
label: 'jd4',
79+
host: '4.rnupdate.online',
80+
baseUrl: 'https://4.rnupdate.online/api',
81+
},
82+
{
83+
key: 's1',
84+
label: 's1',
85+
host: 's1.reactnative.cn',
86+
baseUrl: 'https://s1.reactnative.cn/api',
87+
},
88+
{
89+
key: 'p',
90+
label: 'p',
91+
host: 'p.reactnative.cn',
92+
baseUrl: 'https://p.reactnative.cn/api',
93+
},
94+
] as const;
95+
96+
type ServiceStatusTarget = (typeof SERVICE_STATUS_TARGETS)[number];
97+
type ServiceStatusTargetKey = ServiceStatusTarget['key'];
98+
5699
const counterLabels: Record<string, string> = {
57100
'api.request.error': '5xx',
58101
'api.request.total': '请求',
@@ -449,15 +492,19 @@ const endpointColumns: ColumnsType<EndpointRow> = [
449492
},
450493
];
451494

452-
export const Component = () => {
495+
function ServiceStatusPanel({ target }: { target: ServiceStatusTarget }) {
453496
const {
454497
data: snapshot,
455498
error,
456499
isFetching,
457500
refetch,
458501
} = useQuery({
459-
queryFn: () => api.getInternalMetrics(),
460-
queryKey: ['internalMetrics'],
502+
queryFn: () =>
503+
api.getInternalMetrics({
504+
baseUrl: target.baseUrl,
505+
suppressErrorToast: true,
506+
}),
507+
queryKey: ['internalMetrics', target.key],
461508
refetchInterval: 30_000,
462509
});
463510

@@ -505,13 +552,14 @@ export const Component = () => {
505552
: 0;
506553

507554
return (
508-
<div className="page-section">
555+
<>
509556
<div className="mb-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
510557
<div>
511-
<Title level={4} className="m-0!">
512-
服务状态
558+
<Title level={5} className="m-0!">
559+
{target.label}
513560
</Title>
514561
<div className="mt-1 flex flex-wrap gap-2">
562+
<Tag>{target.host}</Tag>
515563
{snapshot?.generatedAt && (
516564
<Tag>
517565
{dayjs(snapshot.generatedAt).format('YYYY-MM-DD HH:mm:ss')}
@@ -638,6 +686,42 @@ export const Component = () => {
638686
/>
639687
</Card>
640688
</Spin>
689+
</>
690+
);
691+
}
692+
693+
export const Component = () => {
694+
const [activeTargetKey, setActiveTargetKey] =
695+
useState<ServiceStatusTargetKey>(SERVICE_STATUS_TARGETS[0].key);
696+
const activeTarget =
697+
SERVICE_STATUS_TARGETS.find((target) => target.key === activeTargetKey) ??
698+
SERVICE_STATUS_TARGETS[0];
699+
700+
return (
701+
<div className="page-section">
702+
<div className="mb-4">
703+
<Title level={4} className="m-0!">
704+
服务状态
705+
</Title>
706+
<Text type="secondary">按节点查看内部指标和运行状态。</Text>
707+
</div>
708+
<Tabs
709+
activeKey={activeTarget.key}
710+
className="mb-4"
711+
items={SERVICE_STATUS_TARGETS.map((target) => ({
712+
key: target.key,
713+
label: (
714+
<span className="inline-flex items-center gap-2">
715+
<span>{target.label}</span>
716+
<span className="hidden text-xs text-slate-400 md:inline">
717+
{target.host}
718+
</span>
719+
</span>
720+
),
721+
}))}
722+
onChange={(key) => setActiveTargetKey(key as ServiceStatusTargetKey)}
723+
/>
724+
<ServiceStatusPanel key={activeTarget.key} target={activeTarget} />
641725
</div>
642726
);
643727
};

src/pages/home.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Spin } from 'antd';
2+
import { Navigate } from 'react-router-dom';
3+
import { rootRouterPath } from '@/router';
4+
import { useUserInfo } from '@/utils/hooks';
5+
6+
export const Component = () => {
7+
const { isLoading, user } = useUserInfo();
8+
9+
if (isLoading || user === undefined) {
10+
return (
11+
<div className="page-section flex min-h-64 items-center justify-center">
12+
<Spin />
13+
</div>
14+
);
15+
}
16+
17+
return (
18+
<Navigate
19+
replace
20+
to={user?.admin ? rootRouterPath.adminServiceStatus : rootRouterPath.apps}
21+
/>
22+
);
23+
};

src/router.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { getToken } from './services/request';
55
// import './utils/notice';
66

77
export const rootRouterPath = {
8-
apps: '/',
8+
home: '/',
9+
apps: '/apps',
910
user: '/user',
1011
versions: (id: string) => `/apps/${id}`,
1112
resetPassword: (step: string) => `/reset-password/${step}`,
@@ -42,13 +43,13 @@ export const needAuthLoader = ({ request }: { request: Request }) => {
4243

4344
function resolveAuthenticatedRedirect(loginFrom?: string | null) {
4445
if (!loginFrom?.startsWith('/') || loginFrom.startsWith('//')) {
45-
return rootRouterPath.apps;
46+
return rootRouterPath.home;
4647
}
4748
if (
4849
loginFrom === rootRouterPath.login ||
4950
loginFrom.startsWith(`${rootRouterPath.login}?`)
5051
) {
51-
return rootRouterPath.apps;
52+
return rootRouterPath.home;
5253
}
5354
return loginFrom;
5455
}
@@ -72,7 +73,7 @@ export const router = createHashRouter([
7273
{
7374
index: true,
7475
loader: needAuthLoader,
75-
lazy: () => import('./pages/apps'),
76+
lazy: () => import('./pages/home'),
7677
},
7778
{
7879
path: 'apps',

src/services/api.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,14 @@ export const api = {
441441
'get',
442442
`/metrics/app?appKey=${encodeURIComponent(params.appKey)}&start=${encodeURIComponent(params.start)}&end=${encodeURIComponent(params.end)}`,
443443
),
444-
getInternalMetrics: () =>
445-
request<InternalMetricsResponse>('get', '/metrics/internal'),
444+
getInternalMetrics: (params?: {
445+
baseUrl?: string;
446+
suppressErrorToast?: boolean;
447+
}) =>
448+
request<InternalMetricsResponse>('get', '/metrics/internal', undefined, {
449+
baseUrl: params?.baseUrl,
450+
suppressErrorToast: params?.suppressErrorToast,
451+
}),
446452
// API Token
447453
createApiToken: (params: {
448454
name: string;

src/services/auth.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const mockRouterObj = {
1616

1717
mock.module('@/router', () => ({
1818
rootRouterPath: {
19-
apps: '/',
19+
apps: '/apps',
20+
home: '/',
2021
inactivated: '/inactivated',
2122
login: '/login',
2223
},

src/services/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ function getSearchParam(name: string) {
1818

1919
function resolveLoginFrom(loginFrom?: string | null) {
2020
if (!loginFrom?.startsWith('/') || loginFrom.startsWith('//')) {
21-
return rootRouterPath.apps;
21+
return rootRouterPath.home;
2222
}
2323
if (
2424
loginFrom === rootRouterPath.login ||
2525
loginFrom.startsWith(`${rootRouterPath.login}?`)
2626
) {
27-
return rootRouterPath.apps;
27+
return rootRouterPath.home;
2828
}
2929
return loginFrom;
3030
}

src/services/request.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export class RequestError extends Error {
5454
}
5555

5656
export interface RequestOptions {
57+
baseUrl?: string;
5758
suppressErrorToast?: boolean;
5859
}
5960

@@ -65,8 +66,8 @@ export default async function request<T extends Record<any, any>>(
6566
) {
6667
const headers: HeadersInit = {};
6768
const options: RequestInit = { method, headers };
68-
const baseUrl = await getBaseUrl;
69-
let url = `${baseUrl}${path}`;
69+
const baseUrl = requestOptions.baseUrl ?? (await getBaseUrl);
70+
let url = `${baseUrl.replace(/\/$/, '')}${path}`;
7071
if (_token) {
7172
headers['x-accesstoken'] = _token;
7273
}

0 commit comments

Comments
 (0)