feat: add monitor detail page

This commit is contained in:
moonrailgun 2023-10-10 00:09:39 +08:00
parent 5581d39930
commit 9ea006b955
8 changed files with 270 additions and 11 deletions

View File

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import { MonitorInfo } from '../../../../../types';
import { MonitorPing } from './ping'; import { MonitorPing } from './ping';
interface MonitorProvider { interface MonitorProvider {
label: string; label: string;
name: string; name: string;
link: (info: MonitorInfo) => React.ReactNode;
form: React.ComponentType; form: React.ComponentType;
} }
@ -11,6 +13,16 @@ export const monitorProviders: MonitorProvider[] = [
{ {
label: 'Ping', label: 'Ping',
name: 'ping', name: 'ping',
link: (info) => String(info.payload.hostname),
form: MonitorPing, 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);
}

View File

@ -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<MonitorInfoProps> = React.memo((props) => {
const workspaceId = useCurrentWorkspaceId();
const { monitorId } = props;
const { data: monitorInfo, isLoading } = trpc.monitor.get.useQuery({
workspaceId,
id: monitorId,
});
if (isLoading) {
return <Loading />;
}
if (!monitorInfo) {
return <NotFoundTip />;
}
return (
<div>
<Space className="w-full" direction="vertical">
<div className="text-2xl">{monitorInfo.name}</div>
<div>
<Tag color="cyan">{monitorInfo.type}</Tag>
<span>{getMonitorLink(monitorInfo as any as MonitorInfoType)}</span>
</div>
<div>
Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '}
days
</div>
<Card>
<MonitorDataChart monitorId={monitorId} />
</Card>
</Space>
</div>
);
});
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<AreaConfig>(() => {
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 (
<div>
<div className="mb-4 text-right">
<Select
className="w-20 text-center"
size="small"
value={rangeType}
onChange={(val) => setRangeType(val)}
>
<Select.Option value="recent">Recent</Select.Option>
<Select.Option value="3h">3h</Select.Option>
<Select.Option value="6h">6h</Select.Option>
<Select.Option value="24h">24h</Select.Option>
<Select.Option value="1w">1w</Select.Option>
</Select>
</div>
<Area {...config} />
</div>
);
}
);
MonitorDataChart.displayName = 'MonitorDataChart';

View File

@ -1,18 +1,34 @@
import { Card } from 'antd';
import clsx from 'clsx'; import clsx from 'clsx';
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { trpc } from '../../../api/trpc'; import { useLocation, useNavigate, useParams } from 'react-router';
import { useCurrentWorkspaceId } from '../../../store/user'; import { trpc } from '../../api/trpc';
import { HealthBar } from '../../HealthBar'; import { useCurrentWorkspaceId } from '../../store/user';
import { NoWorkspaceTip } from '../../NoWorkspaceTip'; import { HealthBar } from '../HealthBar';
import { NoWorkspaceTip } from '../NoWorkspaceTip';
export const MonitorList: React.FC = React.memo(() => { export const MonitorList: React.FC = React.memo(() => {
const currentWorkspaceId = useCurrentWorkspaceId()!; const currentWorkspaceId = useCurrentWorkspaceId()!;
const { data: monitors = [] } = trpc.monitor.all.useQuery({ const { data: monitors = [] } = trpc.monitor.all.useQuery({
workspaceId: currentWorkspaceId, 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<string | null>( const [selectedMonitorId, setSelectedMonitorId] = useState<string | null>(
null initMonitorId
); );
if (!currentWorkspaceId) { if (!currentWorkspaceId) {
@ -30,7 +46,10 @@ export const MonitorList: React.FC = React.memo(() => {
? 'bg-green-500 bg-opacity-20' ? 'bg-green-500 bg-opacity-20'
: 'bg-green-500 bg-opacity-0 hover:bg-opacity-10' : 'bg-green-500 bg-opacity-0 hover:bg-opacity-10'
)} )}
onClick={() => setSelectedMonitorId(monitor.id)} onClick={() => {
navigate(`/monitor/${monitor.id}`);
setSelectedMonitorId(monitor.id);
}}
> >
<div> <div>
<span <span

View File

@ -1,6 +1,19 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router';
import { ErrorTip } from '../../components/ErrorTip';
import { MonitorInfo } from '../../components/monitor/MonitorInfo';
export const MonitorDetail: React.FC = React.memo(() => { export const MonitorDetail: React.FC = React.memo(() => {
return <div>Detail</div>; const { monitorId } = useParams();
if (!monitorId) {
return <ErrorTip />;
}
return (
<div className="px-2">
<MonitorInfo monitorId={monitorId} />
</div>
);
}); });
MonitorDetail.displayName = 'MonitorDetail'; MonitorDetail.displayName = 'MonitorDetail';

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Route, Routes, useNavigate } from 'react-router'; 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 { MonitorAdd } from './Add';
import { MonitorDetail } from './Detail'; import { MonitorDetail } from './Detail';
import { MonitorEdit } from './Edit'; import { MonitorEdit } from './Edit';

View File

@ -2,6 +2,7 @@ import { router, workspaceOwnerProcedure, workspaceProcedure } from '../trpc';
import { prisma } from '../../model/_client'; import { prisma } from '../../model/_client';
import { z } from 'zod'; import { z } from 'zod';
import { monitorManager } from '../../model/monitor'; import { monitorManager } from '../../model/monitor';
import { MonitorInfo } from '../../../types';
export const monitorRouter = router({ export const monitorRouter = router({
all: workspaceProcedure.query(async ({ input }) => { all: workspaceProcedure.query(async ({ input }) => {
@ -12,7 +13,7 @@ export const monitorRouter = router({
}, },
}); });
return monitors; return monitors as MonitorInfo[];
}), }),
get: workspaceProcedure get: workspaceProcedure
.input( .input(
@ -29,7 +30,7 @@ export const monitorRouter = router({
}, },
}); });
return monitor; return monitor as MonitorInfo;
}), }),
upsert: workspaceOwnerProcedure upsert: workspaceOwnerProcedure
.input( .input(
@ -57,4 +58,32 @@ export const monitorRouter = router({
return monitor; 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,
},
});
}),
}); });

View File

@ -1,2 +1,3 @@
export * from './server'; export * from './server';
export * from './monitor';
export * from './utils'; export * from './utils';

9
src/types/monitor.ts Normal file
View File

@ -0,0 +1,9 @@
import type { Monitor } from '@prisma/client';
import { ExactType } from './utils';
export type MonitorInfo = ExactType<
Monitor,
{
payload: Record<string, any>;
}
>;