feat: add MonitorHealthBar and data subscribe

This commit is contained in:
moonrailgun 2023-10-10 19:47:05 +08:00
parent 8aaf3c22b7
commit f53fba35f4
8 changed files with 177 additions and 29 deletions

View File

@ -3,18 +3,35 @@ import React from 'react';
type HealthStatus = 'health' | 'error' | 'warning' | 'none'; type HealthStatus = 'health' | 'error' | 'warning' | 'none';
interface HealthBarProps { export interface HealthBarBeat {
beats: { title?: string; status: HealthStatus }[]; title?: string;
status: HealthStatus;
}
export interface HealthBarProps {
size?: 'small' | 'large';
beats: HealthBarBeat[];
} }
export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => { export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
const size = props.size ?? 'small';
return ( return (
<div className="flex"> <div
className={clsx('flex', {
'gap-[3px]': size === 'small',
'gap-1': size === 'large',
})}
>
{props.beats.map((beat, i) => ( {props.beats.map((beat, i) => (
<div <div
key={i} key={i}
title={beat.title} title={beat.title}
className={clsx( className={clsx(
'rounded-full w-1 h-4 m-0.5 hover:scale-150 transition-transform', 'rounded-full hover:scale-150 transition-transform',
{
'w-[5px] h-4': size === 'small',
'w-2 h-8': size === 'large',
},
{ {
'bg-green-500': beat.status === 'health', 'bg-green-500': beat.status === 'health',
'bg-red-600': beat.status === 'error', 'bg-red-600': beat.status === 'error',

View File

@ -0,0 +1,90 @@
import React, { useMemo } from 'react';
import { useSocketSubscribeList } from '../../api/socketio';
import { takeRight, last } from 'lodash-es';
import { HealthBar, HealthBarBeat, HealthBarProps } from '../HealthBar';
import dayjs from 'dayjs';
import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user';
interface MonitorHealthBarProps {
monitorId: string;
count?: number;
size?: HealthBarProps['size'];
showCurrentStatus?: boolean;
}
export const MonitorHealthBar: React.FC<MonitorHealthBarProps> = React.memo(
(props) => {
const { monitorId, size, count = 20, showCurrentStatus = false } = props;
const workspaceId = useCurrentWorkspaceId();
const { data: recent = [] } = trpc.monitor.recentData.useQuery({
workspaceId,
monitorId,
take: count,
});
const newDataList = useSocketSubscribeList('onMonitorReceiveNewData', {
filter: (data) => {
return data.monitorId === props.monitorId;
},
});
const items = useMemo(() => {
return takeRight(
[
...Array.from({ length: count }).map(() => null),
...recent,
...takeRight(newDataList, count),
],
count
);
}, [newDataList, recent, count]);
const beats = items.map((item): HealthBarBeat => {
if (!item) {
return {
status: 'none',
};
}
const title = `${dayjs(item.createdAt).format('YYYY-MM-DD HH:mm')} | ${
item.value
}ms`;
if (item.value < 0) {
return {
title,
status: 'error',
};
}
return {
title,
status: 'health',
};
});
return (
<div className="flex justify-between items-center">
<HealthBar size={size} beats={beats} />
{showCurrentStatus && (
<>
{last(beats)?.status === 'health' ? (
<div className="bg-green-500 text-white px-4 py-1 rounded-full text-lg font-bold">
UP
</div>
) : last(beats)?.status === 'error' ? (
<div className="bg-red-600 text-white px-4 py-1 rounded-full text-lg font-bold">
DOWN
</div>
) : (
<div className="bg-gray-400 text-white px-4 py-1 rounded-full text-lg font-bold">
NONE
</div>
)}
</>
)}
</div>
);
}
);
MonitorHealthBar.displayName = 'MonitorHealthBar';

View File

@ -8,6 +8,7 @@ import { getMonitorLink } from '../modals/monitor/provider';
import { NotFoundTip } from '../NotFoundTip'; import { NotFoundTip } from '../NotFoundTip';
import { MonitorInfo as MonitorInfoType } from '../../../types'; import { MonitorInfo as MonitorInfoType } from '../../../types';
import { Area, AreaConfig } from '@ant-design/charts'; import { Area, AreaConfig } from '@ant-design/charts';
import { MonitorHealthBar } from './MonitorHealthBar';
interface MonitorInfoProps { interface MonitorInfoProps {
monitorId: string; monitorId: string;
@ -32,17 +33,32 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
return ( return (
<div> <div>
<Space className="w-full" direction="vertical"> <Space className="w-full" direction="vertical">
<div className="flex justify-between">
<Space direction="vertical">
<div className="text-2xl">{monitorInfo.name}</div> <div className="text-2xl">{monitorInfo.name}</div>
<div> <div>
<Tag color="cyan">{monitorInfo.type}</Tag> <Tag color="cyan">{monitorInfo.type}</Tag>
<span>{getMonitorLink(monitorInfo as any as MonitorInfoType)}</span> <span>
{getMonitorLink(monitorInfo as any as MonitorInfoType)}
</span>
</div> </div>
</Space>
<div> <div className="text-black text-opacity-75">
Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '} Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '}
days days
</div> </div>
</div>
<Card>
<MonitorHealthBar
monitorId={monitorId}
count={40}
size="large"
showCurrentStatus={true}
/>
</Card>
<Card> <Card>
<MonitorDataChart monitorId={monitorId} /> <MonitorDataChart monitorId={monitorId} />

View File

@ -6,6 +6,7 @@ import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user'; import { useCurrentWorkspaceId } from '../../store/user';
import { HealthBar } from '../HealthBar'; import { HealthBar } from '../HealthBar';
import { NoWorkspaceTip } from '../NoWorkspaceTip'; import { NoWorkspaceTip } from '../NoWorkspaceTip';
import { MonitorHealthBar } from './MonitorHealthBar';
export const MonitorList: React.FC = React.memo(() => { export const MonitorList: React.FC = React.memo(() => {
const currentWorkspaceId = useCurrentWorkspaceId()!; const currentWorkspaceId = useCurrentWorkspaceId()!;
@ -76,11 +77,7 @@ export const MonitorList: React.FC = React.memo(() => {
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<HealthBar <MonitorHealthBar monitorId={monitor.id} />
beats={Array.from({ length: 13 }).map(() => ({
status: 'health',
}))}
/>
</div> </div>
</div> </div>
))} ))}

View File

@ -1,4 +1,5 @@
import { Monitor } from '@prisma/client'; import { Monitor } from '@prisma/client';
import { createSubscribeInitializer, subscribeEventBus } from '../../ws/shared';
import { prisma } from '../_client'; import { prisma } from '../_client';
import { monitorProviders } from './provider'; import { monitorProviders } from './provider';
@ -80,6 +81,9 @@ class MonitorManager {
} }
} }
/**
* Class which actually run monitor data collect
*/
class MonitorRunner { class MonitorRunner {
isStopped = false; isStopped = false;
timer: NodeJS.Timeout | null = null; timer: NodeJS.Timeout | null = null;
@ -91,7 +95,7 @@ class MonitorRunner {
*/ */
async startMonitor() { async startMonitor() {
const monitor = this.monitor; const monitor = this.monitor;
const { type, interval } = monitor; const { type, interval, workspaceId } = monitor;
const provider = monitorProviders[type]; const provider = monitorProviders[type];
if (!provider) { if (!provider) {
@ -139,18 +143,20 @@ class MonitorRunner {
} }
// insert into data // insert into data
await prisma.monitorData.create({ const data = await prisma.monitorData.create({
data: { data: {
monitorId: monitor.id, monitorId: monitor.id,
value, value,
}, },
}); });
subscribeEventBus.emit('onMonitorReceiveNewData', workspaceId, data);
// Run next loop // Run next loop
nextAction(); nextAction();
} }
nextAction(); run();
console.log(`Start monitor ${monitor.name}(${monitor.id})`); console.log(`Start monitor ${monitor.name}(${monitor.id})`);
} }

View File

@ -18,7 +18,7 @@ export const monitorRouter = router({
get: workspaceProcedure get: workspaceProcedure
.input( .input(
z.object({ z.object({
id: z.string(), id: z.string().cuid2(),
}) })
) )
.query(async ({ input }) => { .query(async ({ input }) => {
@ -35,7 +35,7 @@ export const monitorRouter = router({
upsert: workspaceOwnerProcedure upsert: workspaceOwnerProcedure
.input( .input(
z.object({ z.object({
id: z.string().optional(), id: z.string().cuid2().optional(),
name: z.string(), name: z.string(),
type: z.string(), type: z.string(),
active: z.boolean().default(true), active: z.boolean().default(true),
@ -61,7 +61,7 @@ export const monitorRouter = router({
data: workspaceOwnerProcedure data: workspaceOwnerProcedure
.input( .input(
z.object({ z.object({
monitorId: z.string(), monitorId: z.string().cuid2(),
startAt: z.number(), startAt: z.number(),
endAt: z.number(), endAt: z.number(),
}) })
@ -86,4 +86,21 @@ export const monitorRouter = router({
}, },
}); });
}), }),
recentData: workspaceOwnerProcedure
.input(
z.object({
monitorId: z.string().cuid2(),
take: z.number(),
})
)
.query(async ({ input }) => {
const { monitorId, take } = input;
return prisma.monitorData.findMany({
where: {
monitorId,
},
take: -take,
});
}),
}); });

View File

@ -1,11 +1,13 @@
import { MonitorData } from '@prisma/client';
import { EventEmitter } from 'eventemitter-strict'; import { EventEmitter } from 'eventemitter-strict';
import { Socket } from 'socket.io'; import { Socket } from 'socket.io';
import { ServerStatusInfo } from '../../types'; import { MaybePromise, ServerStatusInfo } from '../../types';
type SubscribeEventFn<T> = (workspaceId: string, eventData: T) => void; type SubscribeEventFn<T> = (workspaceId: string, eventData: T) => void;
export interface SubscribeEventMap { export interface SubscribeEventMap {
onServerStatusUpdate: SubscribeEventFn<Record<string, ServerStatusInfo>>; onServerStatusUpdate: SubscribeEventFn<Record<string, ServerStatusInfo>>;
onMonitorReceiveNewData: SubscribeEventFn<MonitorData>;
} }
type SocketEventFn<T, U = unknown> = ( type SocketEventFn<T, U = unknown> = (
@ -31,9 +33,7 @@ type SubscribeInitializerFn<
> = ( > = (
workspaceId: string, workspaceId: string,
socket: Socket socket: Socket
) => ) => MaybePromise<Parameters<SubscribeEventMap[T]>[1]> | MaybePromise<void>;
| Parameters<SubscribeEventMap[T]>[1]
| Promise<Parameters<SubscribeEventMap[T]>[1]>;
const subscribeInitializerList: [ const subscribeInitializerList: [
keyof SubscribeEventMap, keyof SubscribeEventMap,
SubscribeInitializerFn SubscribeInitializerFn
@ -46,7 +46,7 @@ socketEventBus.on('$subscribe', (eventData, socket, callback) => {
const { name } = eventData; const { name } = eventData;
const cursor = i++; const cursor = i++;
const fn: SubscribeEventMap[typeof name] = (workspaceId, data) => { const fn: SubscribeEventFn<unknown> = (workspaceId, data) => {
if (workspaceId === '*' || _workspaceId === workspaceId) { if (workspaceId === '*' || _workspaceId === workspaceId) {
socket.emit(`${name}#${cursor}`, data); socket.emit(`${name}#${cursor}`, data);
} }
@ -58,7 +58,10 @@ socketEventBus.on('$subscribe', (eventData, socket, callback) => {
subscribeInitializerList.forEach(async ([_name, initializer]) => { subscribeInitializerList.forEach(async ([_name, initializer]) => {
if (_name === name) { if (_name === name) {
socket.emit(`${name}#${cursor}`, await initializer(_workspaceId, socket)); const res = await initializer(_workspaceId, socket);
if (res) {
socket.emit(`${name}#${cursor}`, res);
}
} }
}); });

View File

@ -1,3 +1,5 @@
export * from './server'; export * from './server';
export * from './monitor'; export * from './monitor';
export * from './utils'; export * from './utils';
export type { MaybePromise } from '@trpc/server';