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';
|
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',
|
||||||
|
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 { 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="text-2xl">{monitorInfo.name}</div>
|
<div className="flex justify-between">
|
||||||
|
<Space direction="vertical">
|
||||||
|
<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>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<div className="text-black text-opacity-75">
|
||||||
|
Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '}
|
||||||
|
days
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<Card>
|
||||||
Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '}
|
<MonitorHealthBar
|
||||||
days
|
monitorId={monitorId}
|
||||||
</div>
|
count={40}
|
||||||
|
size="large"
|
||||||
|
showCurrentStatus={true}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<MonitorDataChart monitorId={monitorId} />
|
<MonitorDataChart monitorId={monitorId} />
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
@ -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})`);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user