Add Status Header & Modify Styles

This commit is contained in:
tommy 2024-10-29 20:57:09 -04:00 committed by moonrailgun
parent 59b874644f
commit 6312ec6eed
3 changed files with 199 additions and 13 deletions

View File

@ -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 (
<StatusItemMonitor
key={item.key}
workspaceId={props.workspaceId}
monitorId={item.id}
showCurrent={item.showCurrent ?? false}
/>
<>
<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)
)}
>

View 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;

View File

@ -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>