Skip to content

Commit 5fe86ab

Browse files
committed
feat: Make the status page a little prettier
https://harperdb.atlassian.net/browse/STUDIO-404
1 parent ecb1665 commit 5fe86ab

File tree

7 files changed

+161
-40
lines changed

7 files changed

+161
-40
lines changed

src/features/instance/operations/queries/getStatus.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,41 @@
11
import { InstanceClientIdConfig } from '@/config/instanceClientConfig';
22
import { queryOptions } from '@tanstack/react-query';
33

4-
interface CoreStatus {
5-
id: string;
6-
status: string;
4+
interface SystemStatus {
5+
id: 'availability' | 'maintenance' | 'primary' | string;
6+
status: 'Available' | 'Unavailable' | string;
77
__updatedtime__: number;
88
__createdtime__: number;
99
}
1010

11-
interface AvailabilityStatus extends CoreStatus {
12-
id: 'availability',
13-
status: 'Available' | 'Unavailable',
11+
const enum ComponentStatusName {
12+
'hdb.http' = 'hdb.http',
13+
'hdb.authentication' = 'hdb.authentication',
14+
'hdb.replication' = 'hdb.replication',
15+
'hdb.logging' = 'hdb.logging',
16+
'hdb.mqtt' = 'hdb.mqtt',
17+
'hdb.operationsApi' = 'hdb.operationsApi',
18+
'status-check.rest' = 'status-check.rest',
19+
'status-check.jsResource' = 'status-check.jsResource',
1420
}
1521

16-
interface MaintenanceStatus extends CoreStatus {
17-
id: 'maintenance',
22+
interface ComponentStatus {
23+
name: ComponentStatusName | string,
24+
componentName: ComponentStatusName | string,
25+
status: 'healthy' | string,
26+
lastChecked: {
27+
workers: Record<string, number>,
28+
main: number
29+
}
1830
}
1931

20-
interface PrimaryStatus extends CoreStatus {
21-
id: 'primary',
22-
}
32+
interface StatusResponse {
33+
systemStatus: SystemStatus[];
34+
restartRequired: boolean;
35+
componentStatus: ComponentStatus[];
2336

24-
type StatusResponse = Array<AvailabilityStatus | MaintenanceStatus | PrimaryStatus>;
37+
[key: string]: unknown;
38+
}
2539

2640
export function getStatusQueryOptions({ entityId, instanceClient }: InstanceClientIdConfig) {
2741
return queryOptions({

src/features/instance/operations/queries/getSystemInformation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ interface SystemInformationResponse {
9292
stats: Array<Record<string, unknown>>;
9393
connections: Array<Record<string, unknown>>;
9494
};
95+
96+
[key: string]: unknown;
9597
}
9698

9799
export function getSystemInformationQueryOptions({ entityId, instanceClient }: InstanceClientIdConfig) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TextLoadingSkeleton } from '@/components/TextLoadingSkeleton';
2+
import { useInstanceClientIdParams } from '@/config/useInstanceClient';
3+
import { getStatusQueryOptions } from '@/features/instance/operations/queries/getStatus';
4+
import { Status } from '@/features/instance/status/Status';
5+
import { useSuspenseQuery } from '@tanstack/react-query';
6+
import { Suspense } from 'react';
7+
8+
export function CloudStatus() {
9+
const instanceParams = useInstanceClientIdParams();
10+
const { data } = useSuspenseQuery(getStatusQueryOptions(instanceParams));
11+
12+
return (
13+
<Suspense fallback={<TextLoadingSkeleton />}>
14+
<Status data={data} />
15+
</Suspense>
16+
);
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TextLoadingSkeleton } from '@/components/TextLoadingSkeleton';
2+
import { useInstanceClientIdParams } from '@/config/useInstanceClient';
3+
import { getSystemInformationQueryOptions } from '@/features/instance/operations/queries/getSystemInformation';
4+
import { Status } from '@/features/instance/status/Status';
5+
import { useSuspenseQuery } from '@tanstack/react-query';
6+
import { Suspense } from 'react';
7+
8+
export function LocalStatus() {
9+
const instanceParams = useInstanceClientIdParams();
10+
const { data } = useSuspenseQuery(getSystemInformationQueryOptions(instanceParams));
11+
12+
return (
13+
<Suspense fallback={<TextLoadingSkeleton />}>
14+
<Status data={data} />
15+
</Suspense>
16+
);
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { crawlData, hasTitle } from '@/features/instance/status/crawlData';
2+
import { cn } from '@/lib/cn';
3+
import { useMemo } from 'react';
4+
5+
export function Status({ data }: { data: Record<string, unknown> }) {
6+
const items = useMemo(() => crawlData(data), [data]);
7+
return <div className="max-w-96 grid mb-12">
8+
{items.map((item, index) =>
9+
hasTitle(item) ? (
10+
<div key={index} className={cn('font-semibold text-xl', index !== 0 && 'mt-4')} style={{ paddingLeft: item.depth * 12 + 'px' }}>{item.title}</div>
11+
) : (<>
12+
<div key={index} style={{ paddingLeft: item.depth * 12 + 'px' }}>
13+
<span className="text-muted-foreground">{item.name}: </span>
14+
{item.value}
15+
</div>
16+
</>),
17+
)}
18+
</div>;
19+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { humanFileSize } from '@/lib/humanFileSize';
2+
import { translateSecondsToAgo } from '@/lib/translateSecondsToAgo';
3+
4+
const startOf2025 = new Date(2025, 0).getTime();
5+
const oneDayInMs = 24 * 60 * 60 * 1000;
6+
7+
interface TitleItem {
8+
title: string;
9+
depth: number;
10+
}
11+
12+
interface NameValuePairItem {
13+
name: string;
14+
value: string;
15+
depth: number;
16+
}
17+
18+
type ItemForDisplay = TitleItem | NameValuePairItem;
19+
20+
export function crawlData(data: Record<string, unknown>): ItemForDisplay[] {
21+
const sections: ItemForDisplay[] = [];
22+
for (const key in data) {
23+
const value = data[key];
24+
sections.push(...parseValue(key, value, 0));
25+
}
26+
return sections;
27+
}
28+
29+
export function hasTitle(item: ItemForDisplay): item is TitleItem {
30+
return !!(item as TitleItem).title;
31+
}
32+
33+
function parseValue(name: string, value: unknown, depth: number, parentName?: string): ItemForDisplay[] {
34+
if (value && Array.isArray(value)) {
35+
const array = value;
36+
return value.map((item, index) =>
37+
parseValue(
38+
array.length > 1 ? name + ' ' + (index + 1) : name,
39+
item,
40+
depth + 1,
41+
name,
42+
)).flat(1);
43+
}
44+
if (isObject(value)) {
45+
const obj = value;
46+
return [
47+
{ title: name, depth },
48+
...Object.keys(value).map(subKey => parseValue(
49+
String(subKey),
50+
obj[subKey],
51+
depth + 1,
52+
name,
53+
)).flat(1),
54+
];
55+
}
56+
if (name === '__updatedtime__' || name === '__createdtime__') {
57+
name = name.replace(/_/g, '').replace('time', '');
58+
}
59+
if (typeof value === 'number') {
60+
if (value > startOf2025 && value < Date.now() + oneDayInMs) {
61+
const elapsed = Date.now() - value;
62+
value = translateSecondsToAgo(elapsed, value);
63+
} else if (parentName === 'memory') {
64+
value = humanFileSize(value);
65+
} else if (!name.startsWith('raw') && name.toLowerCase().includes('load')) {
66+
value = Math.round(value * 10) / 10 + '%';
67+
}
68+
} else if (typeof value === 'boolean') {
69+
value = value ? 'Yes' : 'No';
70+
}
71+
return [
72+
{ name, value: String(value), depth },
73+
];
74+
}
75+
76+
function isObject(value: unknown): value is Record<string, unknown> {
77+
return !!value && typeof value === 'object';
78+
}
Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,7 @@
1-
import { TextLoadingSkeleton } from '@/components/TextLoadingSkeleton';
21
import { isLocalStudio } from '@/config/constants';
3-
import { useInstanceClientIdParams } from '@/config/useInstanceClient';
4-
import { getStatusQueryOptions } from '@/features/instance/operations/queries/getStatus';
5-
import { getSystemInformationQueryOptions } from '@/features/instance/operations/queries/getSystemInformation';
6-
import { useSuspenseQuery } from '@tanstack/react-query';
7-
import { Suspense } from 'react';
2+
import { CloudStatus } from '@/features/instance/status/CloudStatus';
3+
import { LocalStatus } from '@/features/instance/status/LocalStatus';
84

95
export function StatusIndex() {
106
return isLocalStudio ? (<LocalStatus />) : (<CloudStatus />);
117
}
12-
13-
function LocalStatus() {
14-
const instanceParams = useInstanceClientIdParams();
15-
const { data } = useSuspenseQuery(getSystemInformationQueryOptions(instanceParams));
16-
17-
return (<div className="grid grid-cols-1 gap-4 md:grid-cols-12 min-h-[calc(100vh-theme(spacing.36))]">
18-
<Suspense fallback={<TextLoadingSkeleton />}>
19-
<pre>{JSON.stringify(data, null, '\t')}</pre>
20-
</Suspense>
21-
</div>);
22-
}
23-
24-
function CloudStatus() {
25-
const instanceParams = useInstanceClientIdParams();
26-
const { data } = useSuspenseQuery(getStatusQueryOptions(instanceParams));
27-
28-
return (<div className="grid grid-cols-1 gap-4 md:grid-cols-12 min-h-[calc(100vh-theme(spacing.36))]">
29-
<Suspense fallback={<TextLoadingSkeleton />}>
30-
<pre>{JSON.stringify(data, null, '\t')}</pre>
31-
</Suspense>
32-
</div>);
33-
}

0 commit comments

Comments
 (0)