Add Status Header & Modify Styles
This commit is contained in:
parent
59b874644f
commit
6312ec6eed
@ -2,6 +2,7 @@ import { AppRouterOutput, trpc } from '@/api/trpc';
|
||||
import React, { useMemo, useReducer } from 'react';
|
||||
import { bodySchema } from './schema';
|
||||
import { Empty } from 'antd';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { cn } from '@/utils/style';
|
||||
import {
|
||||
@ -36,12 +37,13 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
|
||||
}, [info.body]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg border border-gray-200/80 dark:border-gray-700/25">
|
||||
{body.groups.map((group) => (
|
||||
<div key={group.key} className="mb-6">
|
||||
<div className="mb-2 text-lg font-semibold">{group.title}</div>
|
||||
|
||||
<div className="flex flex-col gap-4 rounded-md border border-gray-200 p-2.5 dark:border-gray-700">
|
||||
<div key={group.key} className="m-4 rounded-lg bg-neutral-500/15">
|
||||
<div className="ml-4 pl-2.5 pt-2.5 text-lg font-semibold">
|
||||
{group.title}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 rounded-md p-2.5">
|
||||
{group.children.length === 0 && (
|
||||
<Empty description={t('No any monitor has been set')} />
|
||||
)}
|
||||
@ -49,12 +51,15 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
|
||||
{group.children.map((item) => {
|
||||
if (item.type === 'monitor') {
|
||||
return (
|
||||
<>
|
||||
<Separator />
|
||||
<StatusItemMonitor
|
||||
key={item.key}
|
||||
workspaceId={props.workspaceId}
|
||||
monitorId={item.id}
|
||||
showCurrent={item.showCurrent ?? false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -121,7 +126,7 @@ export const StatusItemMonitor: React.FC<{
|
||||
<div>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block min-w-[62px] rounded-full p-0.5 text-center text-white',
|
||||
'text-bold text-bold inline-block min-w-[62px] rounded-lg p-0.5 text-center font-semibold',
|
||||
getStatusBgColorClassName(summaryStatus)
|
||||
)}
|
||||
>
|
||||
|
174
src/client/components/monitor/StatusPage/StatusHeader.tsx
Normal file
174
src/client/components/monitor/StatusPage/StatusHeader.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { cn } from '@/utils/style';
|
||||
import { bodySchema } from './schema';
|
||||
import { LuCheckCircle2, LuCircleSlash, LuAlertCircle } from 'react-icons/lu';
|
||||
import { trpc } from '../../../api/trpc';
|
||||
import { getMonitorProvider, getProviderDisplay } from '../provider';
|
||||
import { takeRight, last } from 'lodash-es';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const StatusPageHeader = ({ info, workspaceId }) => {
|
||||
const body = useMemo(() => {
|
||||
const res = bodySchema.safeParse(info.body);
|
||||
return res.success ? res.data : { groups: [] };
|
||||
}, [info.body]);
|
||||
const monitorContexts = useMemo(() => {
|
||||
const contexts = [];
|
||||
body.groups.forEach((group) => {
|
||||
group.children.forEach((item) => {
|
||||
if (item.type === 'monitor') {
|
||||
contexts.push({
|
||||
id: item.id,
|
||||
groupId: group.id,
|
||||
groupName: group.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
if (Array.isArray(info.monitorList)) {
|
||||
info.monitorList.forEach((monitor) => {
|
||||
contexts.push({
|
||||
id: monitor.id,
|
||||
groupId: 'deprecated',
|
||||
groupName: 'Legacy Monitors',
|
||||
});
|
||||
});
|
||||
}
|
||||
return contexts;
|
||||
}, [body, info.monitorList]);
|
||||
|
||||
const recentDataQueries = monitorContexts.map((context) => {
|
||||
const { data: recentData = [] } = trpc.monitor.recentData.useQuery({
|
||||
workspaceId,
|
||||
monitorId: context.id,
|
||||
take: 1,
|
||||
});
|
||||
|
||||
const items = useMemo(() => {
|
||||
return takeRight(
|
||||
[...Array.from({ length: 1 }).map(() => null), ...recentData],
|
||||
1
|
||||
);
|
||||
}, [recentData]);
|
||||
|
||||
const provider = useMemo(
|
||||
() => getMonitorProvider(context.id),
|
||||
[context.id]
|
||||
);
|
||||
|
||||
const latestStatus = useMemo(() => {
|
||||
const latestItem = last(items);
|
||||
if (!latestItem) return 'none';
|
||||
|
||||
const { value, createdAt } = latestItem;
|
||||
const { text } = getProviderDisplay(value, provider);
|
||||
const title = `${dayjs(createdAt).format('YYYY-MM-DD HH:mm')} | ${text}`;
|
||||
return value < 0
|
||||
? { status: 'error', title }
|
||||
: { status: 'health', title };
|
||||
}, [items, provider]);
|
||||
|
||||
return {
|
||||
id: context.id,
|
||||
status: latestStatus.status,
|
||||
timestamp: last(items)?.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
const { overallStatus, servicesCount, lastChecked } = useMemo(() => {
|
||||
let totalCount = 0;
|
||||
let errorCount = 0;
|
||||
let latestTimestamp = null;
|
||||
|
||||
recentDataQueries.forEach((query) => {
|
||||
if (!query) return;
|
||||
|
||||
totalCount += 1;
|
||||
|
||||
if (query.status != 'health') {
|
||||
errorCount += 1;
|
||||
}
|
||||
|
||||
if (
|
||||
!latestTimestamp ||
|
||||
(query.timestamp && query.timestamp > latestTimestamp)
|
||||
) {
|
||||
latestTimestamp = query.timestamp;
|
||||
}
|
||||
});
|
||||
|
||||
let status = 'unknown';
|
||||
let uprate = ((totalCount - errorCount) / totalCount) * 100;
|
||||
|
||||
if (uprate > 90) {
|
||||
status = 'operational';
|
||||
} else if (uprate > 50) {
|
||||
status = 'degraded';
|
||||
} else if (uprate > 0) {
|
||||
status = 'offline';
|
||||
}
|
||||
|
||||
return {
|
||||
overallStatus: status,
|
||||
servicesCount: totalCount,
|
||||
lastChecked: latestTimestamp,
|
||||
};
|
||||
}, [recentDataQueries]);
|
||||
|
||||
const statusConfig = {
|
||||
operational: {
|
||||
text: 'All Systems Operational',
|
||||
icon: LuCheckCircle2,
|
||||
iconColor: 'text-green-500',
|
||||
},
|
||||
degraded: {
|
||||
text: 'Partial System Outage',
|
||||
icon: LuAlertCircle,
|
||||
iconColor: 'text-yellow-500',
|
||||
},
|
||||
offline: {
|
||||
text: 'Major System Outage',
|
||||
icon: LuCircleSlash,
|
||||
iconColor: 'text-red-500',
|
||||
},
|
||||
unknown: {
|
||||
text: 'Status Unknown',
|
||||
icon: LuAlertCircle,
|
||||
iconColor: 'text-gray-500',
|
||||
},
|
||||
};
|
||||
|
||||
const config = statusConfig[overallStatus];
|
||||
const StatusIcon = config.icon;
|
||||
|
||||
const formatDate = (date) => {
|
||||
const options = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'UTC',
|
||||
hour12: true,
|
||||
};
|
||||
const formatted = new Date(date).toLocaleString('en-US', options);
|
||||
const [monthDay, time] = formatted.split(',');
|
||||
return `Last updated on ${monthDay} at${time} UTC`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<StatusIcon
|
||||
className={cn('h-12 w-12', config.iconColor)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h1 className="pb-2 pt-4 text-4xl font-bold">{config.text}</h1>
|
||||
{lastChecked && (
|
||||
<p className="text-md text-gray-600 dark:text-gray-400">
|
||||
{formatDate(lastChecked)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusPageHeader;
|
@ -9,6 +9,7 @@ import clsx from 'clsx';
|
||||
import { useRequest } from '../../../hooks/useRequest';
|
||||
import { ColorSchemeSwitcher } from '../../ColorSchemeSwitcher';
|
||||
import { StatusPageServices } from './Services';
|
||||
import { StatusPageHeader } from './StatusHeader';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import { Helmet } from 'react-helmet';
|
||||
@ -146,8 +147,14 @@ export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{info && (
|
||||
<div className="my-6">
|
||||
<StatusPageHeader info={info} workspaceId={info.workspaceId} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desc */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-6 text-center">
|
||||
<MarkdownViewer value={info?.description ?? ''} />
|
||||
</div>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user