feat: add MonitorHealthBar and data subscribe
This commit is contained in:
parent
8aaf3c22b7
commit
f53fba35f4
@ -3,18 +3,35 @@ import React from 'react';
|
||||
|
||||
type HealthStatus = 'health' | 'error' | 'warning' | 'none';
|
||||
|
||||
interface HealthBarProps {
|
||||
beats: { title?: string; status: HealthStatus }[];
|
||||
export interface HealthBarBeat {
|
||||
title?: string;
|
||||
status: HealthStatus;
|
||||
}
|
||||
|
||||
export interface HealthBarProps {
|
||||
size?: 'small' | 'large';
|
||||
beats: HealthBarBeat[];
|
||||
}
|
||||
export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
|
||||
const size = props.size ?? 'small';
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<div
|
||||
className={clsx('flex', {
|
||||
'gap-[3px]': size === 'small',
|
||||
'gap-1': size === 'large',
|
||||
})}
|
||||
>
|
||||
{props.beats.map((beat, i) => (
|
||||
<div
|
||||
key={i}
|
||||
title={beat.title}
|
||||
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-red-600': beat.status === 'error',
|
||||
|
90
src/client/components/monitor/MonitorHealthBar.tsx
Normal file
90
src/client/components/monitor/MonitorHealthBar.tsx
Normal 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';
|
@ -8,6 +8,7 @@ import { getMonitorLink } from '../modals/monitor/provider';
|
||||
import { NotFoundTip } from '../NotFoundTip';
|
||||
import { MonitorInfo as MonitorInfoType } from '../../../types';
|
||||
import { Area, AreaConfig } from '@ant-design/charts';
|
||||
import { MonitorHealthBar } from './MonitorHealthBar';
|
||||
|
||||
interface MonitorInfoProps {
|
||||
monitorId: string;
|
||||
@ -32,17 +33,32 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
|
||||
return (
|
||||
<div>
|
||||
<Space className="w-full" direction="vertical">
|
||||
<div className="flex justify-between">
|
||||
<Space direction="vertical">
|
||||
<div className="text-2xl">{monitorInfo.name}</div>
|
||||
|
||||
<div>
|
||||
<Tag color="cyan">{monitorInfo.type}</Tag>
|
||||
<span>{getMonitorLink(monitorInfo as any as MonitorInfoType)}</span>
|
||||
<span>
|
||||
{getMonitorLink(monitorInfo as any as MonitorInfoType)}
|
||||
</span>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
<div>
|
||||
<div className="text-black text-opacity-75">
|
||||
Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '}
|
||||
days
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<MonitorHealthBar
|
||||
monitorId={monitorId}
|
||||
count={40}
|
||||
size="large"
|
||||
showCurrentStatus={true}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<MonitorDataChart monitorId={monitorId} />
|
||||
|
@ -6,6 +6,7 @@ import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { HealthBar } from '../HealthBar';
|
||||
import { NoWorkspaceTip } from '../NoWorkspaceTip';
|
||||
import { MonitorHealthBar } from './MonitorHealthBar';
|
||||
|
||||
export const MonitorList: React.FC = React.memo(() => {
|
||||
const currentWorkspaceId = useCurrentWorkspaceId()!;
|
||||
@ -76,11 +77,7 @@ export const MonitorList: React.FC = React.memo(() => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<HealthBar
|
||||
beats={Array.from({ length: 13 }).map(() => ({
|
||||
status: 'health',
|
||||
}))}
|
||||
/>
|
||||
<MonitorHealthBar monitorId={monitor.id} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Monitor } from '@prisma/client';
|
||||
import { createSubscribeInitializer, subscribeEventBus } from '../../ws/shared';
|
||||
import { prisma } from '../_client';
|
||||
import { monitorProviders } from './provider';
|
||||
|
||||
@ -80,6 +81,9 @@ class MonitorManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class which actually run monitor data collect
|
||||
*/
|
||||
class MonitorRunner {
|
||||
isStopped = false;
|
||||
timer: NodeJS.Timeout | null = null;
|
||||
@ -91,7 +95,7 @@ class MonitorRunner {
|
||||
*/
|
||||
async startMonitor() {
|
||||
const monitor = this.monitor;
|
||||
const { type, interval } = monitor;
|
||||
const { type, interval, workspaceId } = monitor;
|
||||
|
||||
const provider = monitorProviders[type];
|
||||
if (!provider) {
|
||||
@ -139,18 +143,20 @@ class MonitorRunner {
|
||||
}
|
||||
|
||||
// insert into data
|
||||
await prisma.monitorData.create({
|
||||
const data = await prisma.monitorData.create({
|
||||
data: {
|
||||
monitorId: monitor.id,
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
subscribeEventBus.emit('onMonitorReceiveNewData', workspaceId, data);
|
||||
|
||||
// Run next loop
|
||||
nextAction();
|
||||
}
|
||||
|
||||
nextAction();
|
||||
run();
|
||||
|
||||
console.log(`Start monitor ${monitor.name}(${monitor.id})`);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export const monitorRouter = router({
|
||||
get: workspaceProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
id: z.string().cuid2(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
@ -35,7 +35,7 @@ export const monitorRouter = router({
|
||||
upsert: workspaceOwnerProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
id: z.string().cuid2().optional(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
active: z.boolean().default(true),
|
||||
@ -61,7 +61,7 @@ export const monitorRouter = router({
|
||||
data: workspaceOwnerProcedure
|
||||
.input(
|
||||
z.object({
|
||||
monitorId: z.string(),
|
||||
monitorId: z.string().cuid2(),
|
||||
startAt: 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,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { MonitorData } from '@prisma/client';
|
||||
import { EventEmitter } from 'eventemitter-strict';
|
||||
import { Socket } from 'socket.io';
|
||||
import { ServerStatusInfo } from '../../types';
|
||||
import { MaybePromise, ServerStatusInfo } from '../../types';
|
||||
|
||||
type SubscribeEventFn<T> = (workspaceId: string, eventData: T) => void;
|
||||
|
||||
export interface SubscribeEventMap {
|
||||
onServerStatusUpdate: SubscribeEventFn<Record<string, ServerStatusInfo>>;
|
||||
onMonitorReceiveNewData: SubscribeEventFn<MonitorData>;
|
||||
}
|
||||
|
||||
type SocketEventFn<T, U = unknown> = (
|
||||
@ -31,9 +33,7 @@ type SubscribeInitializerFn<
|
||||
> = (
|
||||
workspaceId: string,
|
||||
socket: Socket
|
||||
) =>
|
||||
| Parameters<SubscribeEventMap[T]>[1]
|
||||
| Promise<Parameters<SubscribeEventMap[T]>[1]>;
|
||||
) => MaybePromise<Parameters<SubscribeEventMap[T]>[1]> | MaybePromise<void>;
|
||||
const subscribeInitializerList: [
|
||||
keyof SubscribeEventMap,
|
||||
SubscribeInitializerFn
|
||||
@ -46,7 +46,7 @@ socketEventBus.on('$subscribe', (eventData, socket, callback) => {
|
||||
const { name } = eventData;
|
||||
|
||||
const cursor = i++;
|
||||
const fn: SubscribeEventMap[typeof name] = (workspaceId, data) => {
|
||||
const fn: SubscribeEventFn<unknown> = (workspaceId, data) => {
|
||||
if (workspaceId === '*' || _workspaceId === workspaceId) {
|
||||
socket.emit(`${name}#${cursor}`, data);
|
||||
}
|
||||
@ -58,7 +58,10 @@ socketEventBus.on('$subscribe', (eventData, socket, callback) => {
|
||||
|
||||
subscribeInitializerList.forEach(async ([_name, initializer]) => {
|
||||
if (_name === name) {
|
||||
socket.emit(`${name}#${cursor}`, await initializer(_workspaceId, socket));
|
||||
const res = await initializer(_workspaceId, socket);
|
||||
if (res) {
|
||||
socket.emit(`${name}#${cursor}`, res);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
export * from './server';
|
||||
export * from './monitor';
|
||||
export * from './utils';
|
||||
|
||||
export type { MaybePromise } from '@trpc/server';
|
||||
|
Loading…
Reference in New Issue
Block a user