diff --git a/src/client/components/modals/monitor/provider/index.ts b/src/client/components/modals/monitor/provider/index.ts index a2cf80f..88ad54f 100644 --- a/src/client/components/modals/monitor/provider/index.ts +++ b/src/client/components/modals/monitor/provider/index.ts @@ -1,9 +1,11 @@ import React from 'react'; +import { MonitorInfo } from '../../../../../types'; import { MonitorPing } from './ping'; interface MonitorProvider { label: string; name: string; + link: (info: MonitorInfo) => React.ReactNode; form: React.ComponentType; } @@ -11,6 +13,16 @@ export const monitorProviders: MonitorProvider[] = [ { label: 'Ping', name: 'ping', + link: (info) => String(info.payload.hostname), form: MonitorPing, }, ]; + +export function getMonitorLink(info: MonitorInfo): React.ReactNode { + const provider = monitorProviders.find((m) => m.name === info.type); + if (!provider) { + return null; + } + + return provider.link(info); +} diff --git a/src/client/components/monitor/MonitorInfo.tsx b/src/client/components/monitor/MonitorInfo.tsx new file mode 100644 index 0000000..d49b7f9 --- /dev/null +++ b/src/client/components/monitor/MonitorInfo.tsx @@ -0,0 +1,176 @@ +import { Card, Select, Space, Tag } from 'antd'; +import dayjs, { Dayjs } from 'dayjs'; +import React, { useMemo, useState } from 'react'; +import { trpc } from '../../api/trpc'; +import { useCurrentWorkspaceId } from '../../store/user'; +import { Loading } from '../Loading'; +import { getMonitorLink } from '../modals/monitor/provider'; +import { NotFoundTip } from '../NotFoundTip'; +import { MonitorInfo as MonitorInfoType } from '../../../types'; +import { Area, AreaConfig } from '@ant-design/charts'; + +interface MonitorInfoProps { + monitorId: string; +} +export const MonitorInfo: React.FC = React.memo((props) => { + const workspaceId = useCurrentWorkspaceId(); + const { monitorId } = props; + + const { data: monitorInfo, isLoading } = trpc.monitor.get.useQuery({ + workspaceId, + id: monitorId, + }); + + if (isLoading) { + return ; + } + + if (!monitorInfo) { + return ; + } + + return ( +
+ +
{monitorInfo.name}
+ +
+ {monitorInfo.type} + {getMonitorLink(monitorInfo as any as MonitorInfoType)} +
+ +
+ Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '} + days +
+ + + + +
+
+ ); +}); +MonitorInfo.displayName = 'MonitorInfo'; + +const MonitorDataChart: React.FC<{ monitorId: string }> = React.memo( + (props) => { + const workspaceId = useCurrentWorkspaceId(); + const { monitorId } = props; + const [rangeType, setRangeType] = useState('recent'); + + const range = useMemo((): [Dayjs, Dayjs] => { + if (rangeType === '3h') { + return [dayjs().subtract(3, 'hour'), dayjs()]; + } + if (rangeType === '6h') { + return [dayjs().subtract(6, 'hour'), dayjs()]; + } + if (rangeType === '24h') { + return [dayjs().subtract(24, 'hour'), dayjs()]; + } + if (rangeType === '1w') { + return [dayjs().subtract(1, 'week'), dayjs()]; + } + + return [dayjs().subtract(0.5, 'hour'), dayjs()]; + }, [rangeType]); + + const { data: _data = [] } = trpc.monitor.data.useQuery({ + workspaceId, + monitorId, + startAt: range[0].valueOf(), + endAt: range[1].valueOf(), + }); + + const { data, annotations } = useMemo(() => { + const annotations: AreaConfig['annotations'] = []; + let start: number | null = null; + const data = _data.map((d, i, arr) => { + const value = d.value > 0 ? d.value : null; + const time = dayjs(d.createdAt).valueOf(); + + if (!value && !start && arr[i - 1]) { + start = dayjs(arr[i - 1]['createdAt']).valueOf(); + } else if (value && start) { + annotations.push({ + type: 'region', + start: [start, 'min'], + end: [time, 'max'], + style: { + fill: 'red', + fillOpacity: 0.25, + }, + }); + start = null; + } + + return { + value, + time, + }; + }); + + return { data, annotations }; + }, [_data]); + + const config = useMemo(() => { + return { + data, + height: 200, + xField: 'time', + yField: 'value', + smooth: true, + meta: { + time: { + formatter(value) { + return dayjs(value).format( + rangeType === '1w' ? 'MM-DD HH:mm' : 'HH:mm' + ); + }, + }, + }, + color: 'rgb(34 197 94 / 0.8)', + areaStyle: () => { + return { + fill: 'l(270) 0:rgb(34 197 94 / 0.2) 0.5:rgb(34 197 94 / 0.5) 1:rgb(34 197 94 / 0.8)', + }; + }, + annotations, + tooltip: { + title: (title, datum) => { + return dayjs(datum.time).format('YYYY-MM-DD HH:mm'); + }, + formatter(datum) { + return { + name: 'usage', + value: datum.value ? datum.value + 'ms' : 'null', + }; + }, + }, + }; + }, [data, rangeType]); + + return ( +
+
+ +
+ + +
+ ); + } +); +MonitorDataChart.displayName = 'MonitorDataChart'; diff --git a/src/client/components/modals/monitor/MonitorList.tsx b/src/client/components/monitor/MonitorList.tsx similarity index 68% rename from src/client/components/modals/monitor/MonitorList.tsx rename to src/client/components/monitor/MonitorList.tsx index 8bb560e..77b2feb 100644 --- a/src/client/components/modals/monitor/MonitorList.tsx +++ b/src/client/components/monitor/MonitorList.tsx @@ -1,18 +1,34 @@ +import { Card } from 'antd'; import clsx from 'clsx'; -import React, { useState } from 'react'; -import { trpc } from '../../../api/trpc'; -import { useCurrentWorkspaceId } from '../../../store/user'; -import { HealthBar } from '../../HealthBar'; -import { NoWorkspaceTip } from '../../NoWorkspaceTip'; +import React, { useMemo, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router'; +import { trpc } from '../../api/trpc'; +import { useCurrentWorkspaceId } from '../../store/user'; +import { HealthBar } from '../HealthBar'; +import { NoWorkspaceTip } from '../NoWorkspaceTip'; export const MonitorList: React.FC = React.memo(() => { const currentWorkspaceId = useCurrentWorkspaceId()!; const { data: monitors = [] } = trpc.monitor.all.useQuery({ workspaceId: currentWorkspaceId, }); + const navigate = useNavigate(); + const initMonitorId = useMemo(() => { + const pathname = window.location.pathname; + const re = /^\/monitor\/([^\/]+?)$/; + if (re.test(pathname)) { + const id = pathname.match(re)?.[1]; + + if (typeof id === 'string') { + return id; + } + } + + return null; + }, []); const [selectedMonitorId, setSelectedMonitorId] = useState( - null + initMonitorId ); if (!currentWorkspaceId) { @@ -30,7 +46,10 @@ export const MonitorList: React.FC = React.memo(() => { ? 'bg-green-500 bg-opacity-20' : 'bg-green-500 bg-opacity-0 hover:bg-opacity-10' )} - onClick={() => setSelectedMonitorId(monitor.id)} + onClick={() => { + navigate(`/monitor/${monitor.id}`); + setSelectedMonitorId(monitor.id); + }} >
{ - return
Detail
; + const { monitorId } = useParams(); + + if (!monitorId) { + return ; + } + + return ( +
+ +
+ ); }); MonitorDetail.displayName = 'MonitorDetail'; diff --git a/src/client/pages/Monitor/index.tsx b/src/client/pages/Monitor/index.tsx index 7223d34..4c7dadb 100644 --- a/src/client/pages/Monitor/index.tsx +++ b/src/client/pages/Monitor/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Route, Routes, useNavigate } from 'react-router'; -import { MonitorList } from '../../components/modals/monitor/MonitorList'; +import { MonitorList } from '../../components/monitor/MonitorList'; import { MonitorAdd } from './Add'; import { MonitorDetail } from './Detail'; import { MonitorEdit } from './Edit'; diff --git a/src/server/trpc/routers/monitor.ts b/src/server/trpc/routers/monitor.ts index 1415cae..55769f2 100644 --- a/src/server/trpc/routers/monitor.ts +++ b/src/server/trpc/routers/monitor.ts @@ -2,6 +2,7 @@ import { router, workspaceOwnerProcedure, workspaceProcedure } from '../trpc'; import { prisma } from '../../model/_client'; import { z } from 'zod'; import { monitorManager } from '../../model/monitor'; +import { MonitorInfo } from '../../../types'; export const monitorRouter = router({ all: workspaceProcedure.query(async ({ input }) => { @@ -12,7 +13,7 @@ export const monitorRouter = router({ }, }); - return monitors; + return monitors as MonitorInfo[]; }), get: workspaceProcedure .input( @@ -29,7 +30,7 @@ export const monitorRouter = router({ }, }); - return monitor; + return monitor as MonitorInfo; }), upsert: workspaceOwnerProcedure .input( @@ -57,4 +58,32 @@ export const monitorRouter = router({ return monitor; }), + data: workspaceOwnerProcedure + .input( + z.object({ + monitorId: z.string(), + startAt: z.number(), + endAt: z.number(), + }) + ) + .query(async ({ input }) => { + const { monitorId, workspaceId, startAt, endAt } = input; + + return prisma.monitorData.findMany({ + where: { + monitor: { + id: monitorId, + workspaceId, + }, + createdAt: { + gte: new Date(startAt), + lte: new Date(endAt), + }, + }, + select: { + value: true, + createdAt: true, + }, + }); + }), }); diff --git a/src/types/index.ts b/src/types/index.ts index 42b042d..6438088 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,3 @@ export * from './server'; +export * from './monitor'; export * from './utils'; diff --git a/src/types/monitor.ts b/src/types/monitor.ts new file mode 100644 index 0000000..12cd2d4 --- /dev/null +++ b/src/types/monitor.ts @@ -0,0 +1,9 @@ +import type { Monitor } from '@prisma/client'; +import { ExactType } from './utils'; + +export type MonitorInfo = ExactType< + Monitor, + { + payload: Record; + } +>;