diff --git a/package.json b/package.json index bc851bed..be352c04 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "socket.io-client": "^4.7.2", "str2int": "^1.1.0", "swagger-ui-express": "^5.0.0", + "tcp-ping": "^0.1.1", "trpc-openapi": "^1.2.0", "ts-node": "^10.9.1", "uuid": "^9.0.0", @@ -116,6 +117,7 @@ "@types/request-ip": "^0.0.38", "@types/swagger-ui-express": "^4.1.5", "@types/tar": "^6.1.5", + "@types/tcp-ping": "^0.1.5", "@vitejs/plugin-react": "^4.0.4", "autoprefixer": "^10.4.15", "cross-env": "^7.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24b551f3..1ee9924c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,6 +181,9 @@ dependencies: swagger-ui-express: specifier: ^5.0.0 version: 5.0.0(express@4.18.2) + tcp-ping: + specifier: ^0.1.1 + version: 0.1.1 trpc-openapi: specifier: ^1.2.0 version: 1.2.0(@trpc/server@10.38.4)(zod@3.22.2) @@ -279,6 +282,9 @@ devDependencies: '@types/tar': specifier: ^6.1.5 version: 6.1.5 + '@types/tcp-ping': + specifier: ^0.1.5 + version: 0.1.5 '@vitejs/plugin-react': specifier: ^4.0.4 version: 4.0.4(vite@4.4.9) @@ -2749,6 +2755,10 @@ packages: minipass: 4.2.8 dev: true + /@types/tcp-ping@0.1.5: + resolution: {integrity: sha512-79CSV6HXSi53zB7JwEpDMIPa881n7drC+Ed1JtQ5kdVUklYyG1g4GqefuUQy/AblK58Q5JAS7d9LWbdE2xiEqA==} + dev: true + /@types/triple-beam@1.3.4: resolution: {integrity: sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==} dev: false @@ -9429,6 +9439,10 @@ packages: yallist: 4.0.0 dev: true + /tcp-ping@0.1.1: + resolution: {integrity: sha512-7Ed10Ds0hYnF+O1lfiZ2iSZ1bCAj+96Madctebmq7Y1ALPWlBY4YI8C6pCL+UTlshFY5YogixKLpgDP/4BlHrw==} + dev: false + /temp-dir@1.0.0: resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} engines: {node: '>=4'} diff --git a/src/client/components/monitor/MonitorInfoEditor.tsx b/src/client/components/monitor/MonitorInfoEditor.tsx index e3f284e2..bfe5b8bd 100644 --- a/src/client/components/monitor/MonitorInfoEditor.tsx +++ b/src/client/components/monitor/MonitorInfoEditor.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import type { Monitor } from '@prisma/client'; import { Button, Form, Input, InputNumber, Select } from 'antd'; import { getMonitorProvider, monitorProviders } from './provider'; -import { useEvent } from '../../hooks/useEvent'; +import { useEvent, useEventWithLoading } from '../../hooks/useEvent'; import { NotificationPicker } from '../notification/NotificationPicker'; export type MonitorInfoEditorValues = Omit< @@ -23,7 +23,7 @@ const defaultValues: Omit = { interface MonitorInfoEditorProps { initialValues?: MonitorInfoEditorValues; - onSave: (value: MonitorInfoEditorValues) => void; + onSave: (value: MonitorInfoEditorValues) => Promise; } export const MonitorInfoEditor: React.FC = React.memo( (props) => { @@ -46,8 +46,8 @@ export const MonitorInfoEditor: React.FC = React.memo( return ; }, [provider]); - const handleSubmit = useEvent((values) => { - props.onSave({ + const [handleSubmit, isLoading] = useEventWithLoading(async (values) => { + await props.onSave({ ...values, active: true, }); @@ -96,7 +96,7 @@ export const MonitorInfoEditor: React.FC = React.memo( - diff --git a/src/client/components/monitor/provider/index.ts b/src/client/components/monitor/provider/index.ts index 05dcf2aa..5283bf53 100644 --- a/src/client/components/monitor/provider/index.ts +++ b/src/client/components/monitor/provider/index.ts @@ -5,11 +5,13 @@ import { httpProvider } from './http'; import { MonitorProvider } from './types'; import { openaiProvider } from './openai'; import { customProvider } from './custom'; +import { tcpProvider } from './tcp'; export const monitorProviders: MonitorProvider[] = [ pingProvider, // ping + tcpProvider, // tcp httpProvider, // http - openaiProvider, // http + openaiProvider, // openai customProvider, // custom node script ]; diff --git a/src/client/components/monitor/provider/tcp.tsx b/src/client/components/monitor/provider/tcp.tsx new file mode 100644 index 00000000..996e639f --- /dev/null +++ b/src/client/components/monitor/provider/tcp.tsx @@ -0,0 +1,43 @@ +import { Form, Input, InputNumber } from 'antd'; +import React from 'react'; +import { MonitorProvider } from './types'; +import { hostnameValidator, portValidator } from '../../../utils/validator'; + +export const MonitorTCP: React.FC = React.memo(() => { + return ( + <> + + + + + + + + ); +}); +MonitorTCP.displayName = 'MonitorTCP'; + +export const tcpProvider: MonitorProvider = { + label: 'TCP Port', + name: 'tcp', + link: (info) => `${info.payload.hostname}:${info.payload.port}`, + form: MonitorTCP, +}; diff --git a/src/client/hooks/useEvent.ts b/src/client/hooks/useEvent.ts index 2e1c5255..e64c789f 100644 --- a/src/client/hooks/useEvent.ts +++ b/src/client/hooks/useEvent.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef } from 'react'; +import { useMemo, useRef, useState } from 'react'; // From https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMemoizedFn/index.ts @@ -33,3 +33,23 @@ export function useEvent(fn: T) { return memoizedFn.current as T; } + +/** + * Same with useEvent but return loading state + */ +export function useEventWithLoading Promise>( + fn: T +): [T, boolean] { + const [isLoading, setIsLoading] = useState(false); + + const _fn = useEvent(async (...args: Parameters) => { + setIsLoading(true); + try { + return await fn(...args); + } finally { + setIsLoading(false); + } + }) as T; + + return [_fn as T, isLoading]; +} diff --git a/src/client/utils/validator.ts b/src/client/utils/validator.ts index a80ce7ca..895b0340 100644 --- a/src/client/utils/validator.ts +++ b/src/client/utils/validator.ts @@ -25,3 +25,12 @@ export const urlSlugValidator: Validator = (rule, value, callback) => { callback('Not valid slug'); } }; + +export const portValidator: Validator = (rule, value, callback) => { + try { + z.number().min(1).max(65535).parse(value); + callback(); + } catch (err) { + callback('Not valid port, it should be 1 ~ 65535'); + } +}; diff --git a/src/server/model/monitor/provider/index.ts b/src/server/model/monitor/provider/index.ts index 0a225dc2..f7409422 100644 --- a/src/server/model/monitor/provider/index.ts +++ b/src/server/model/monitor/provider/index.ts @@ -3,10 +3,12 @@ import { ping } from './ping'; import { openai } from './openai'; import type { MonitorProvider } from './type'; import { custom } from './custom'; +import { tcp } from './tcp'; export const monitorProviders: Record> = { ping, http, + tcp, openai, custom, }; diff --git a/src/server/model/monitor/provider/tcp.ts b/src/server/model/monitor/provider/tcp.ts new file mode 100644 index 00000000..d7584bda --- /dev/null +++ b/src/server/model/monitor/provider/tcp.ts @@ -0,0 +1,42 @@ +import { MonitorProvider } from './type'; +import tcpp from 'tcp-ping'; + +export const tcp: MonitorProvider<{ + hostname: string; + port: number; +}> = { + run: async (monitor) => { + if (typeof monitor.payload !== 'object') { + throw new Error('monitor.payload should be object'); + } + + const { hostname, port } = monitor.payload; + + const res = await pingAction(hostname, port); + + return res; + }, +}; + +function pingAction(hostname: string, port: number) { + return new Promise((resolve, reject) => { + tcpp.ping( + { + address: hostname, + port, + attempts: 1, + }, + (err, result) => { + if (err) { + reject(err); + } else { + if (result.results.length >= 1 && result.results[0].err) { + reject(result.results[0].err); + } + + resolve(Math.round(result.max)); + } + } + ); + }); +}