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 React, { useMemo, useReducer } from 'react';
|
||||||
import { bodySchema } from './schema';
|
import { bodySchema } from './schema';
|
||||||
import { Empty } from 'antd';
|
import { Empty } from 'antd';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useTranslation } from '@i18next-toolkit/react';
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
import { cn } from '@/utils/style';
|
import { cn } from '@/utils/style';
|
||||||
import {
|
import {
|
||||||
@ -36,12 +37,13 @@ export const StatusPageBody: React.FC<StatusPageBodyProps> = React.memo(
|
|||||||
}, [info.body]);
|
}, [info.body]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="rounded-lg border border-gray-200/80 dark:border-gray-700/25">
|
||||||
{body.groups.map((group) => (
|
{body.groups.map((group) => (
|
||||||
<div key={group.key} className="mb-6">
|
<div key={group.key} className="m-4 rounded-lg bg-neutral-500/15">
|
||||||
<div className="mb-2 text-lg font-semibold">{group.title}</div>
|
<div className="ml-4 pl-2.5 pt-2.5 text-lg font-semibold">
|
||||||
|
{group.title}
|
||||||
<div className="flex flex-col gap-4 rounded-md border border-gray-200 p-2.5 dark:border-gray-700">
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 rounded-md p-2.5">
|
||||||
{group.children.length === 0 && (
|
{group.children.length === 0 && (
|
||||||
<Empty description={t('No any monitor has been set')} />
|
<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) => {
|
{group.children.map((item) => {
|
||||||
if (item.type === 'monitor') {
|
if (item.type === 'monitor') {
|
||||||
return (
|
return (
|
||||||
<StatusItemMonitor
|
<>
|
||||||
key={item.key}
|
<Separator />
|
||||||
workspaceId={props.workspaceId}
|
<StatusItemMonitor
|
||||||
monitorId={item.id}
|
key={item.key}
|
||||||
showCurrent={item.showCurrent ?? false}
|
workspaceId={props.workspaceId}
|
||||||
/>
|
monitorId={item.id}
|
||||||
|
showCurrent={item.showCurrent ?? false}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +126,7 @@ export const StatusItemMonitor: React.FC<{
|
|||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
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)
|
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 { useRequest } from '../../../hooks/useRequest';
|
||||||
import { ColorSchemeSwitcher } from '../../ColorSchemeSwitcher';
|
import { ColorSchemeSwitcher } from '../../ColorSchemeSwitcher';
|
||||||
import { StatusPageServices } from './Services';
|
import { StatusPageServices } from './Services';
|
||||||
|
import { StatusPageHeader } from './StatusHeader';
|
||||||
import { useTranslation } from '@i18next-toolkit/react';
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
import { Link, useNavigate } from '@tanstack/react-router';
|
import { Link, useNavigate } from '@tanstack/react-router';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
@ -146,8 +147,14 @@ export const MonitorStatusPage: React.FC<MonitorStatusPageProps> = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{info && (
|
||||||
|
<div className="my-6">
|
||||||
|
<StatusPageHeader info={info} workspaceId={info.workspaceId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Desc */}
|
{/* Desc */}
|
||||||
<div className="mb-4">
|
<div className="mb-6 text-center">
|
||||||
<MarkdownViewer value={info?.description ?? ''} />
|
<MarkdownViewer value={info?.description ?? ''} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user