feat: add monitor detail page
This commit is contained in:
parent
5581d39930
commit
9ea006b955
@ -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);
|
||||||
|
}
|
||||||
|
176
src/client/components/monitor/MonitorInfo.tsx
Normal file
176
src/client/components/monitor/MonitorInfo.tsx
Normal 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';
|
@ -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
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
@ -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
9
src/types/monitor.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { Monitor } from '@prisma/client';
|
||||||
|
import { ExactType } from './utils';
|
||||||
|
|
||||||
|
export type MonitorInfo = ExactType<
|
||||||
|
Monitor,
|
||||||
|
{
|
||||||
|
payload: Record<string, any>;
|
||||||
|
}
|
||||||
|
>;
|
Loading…
Reference in New Issue
Block a user