From 6e3f8887921cbfa151f155742054af4cd98b1c3f Mon Sep 17 00:00:00 2001 From: UUBulb <35923940+uubulb@users.noreply.github.com> Date: Sun, 17 Nov 2024 10:05:20 +0800 Subject: [PATCH] further implementing service page (#3) --- package-lock.json | 32 ++ package.json | 1 + src/api/service.ts | 4 +- src/components/action-button-group.tsx | 50 +++ src/components/header-button-group.tsx | 64 ++++ src/components/header.tsx | 14 +- src/components/service.tsx | 512 +++++++++++++++++-------- src/components/ui/checkbox.tsx | 2 +- src/components/ui/scroll-area.tsx | 46 +++ src/components/ui/skeleton.tsx | 15 + src/components/xui/icon-buttons.tsx | 27 ++ src/hooks/useAuth.tsx | 5 +- src/lib/utils.ts | 38 ++ src/routes/root.tsx | 2 +- src/routes/server.tsx | 2 +- src/routes/service.tsx | 206 +++++++--- src/types/api.ts | 125 +++--- src/types/index.ts | 3 +- src/types/service.ts | 10 + tailwind.config.js | 111 +++--- 20 files changed, 936 insertions(+), 333 deletions(-) create mode 100644 src/components/action-button-group.tsx create mode 100644 src/components/header-button-group.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/skeleton.tsx create mode 100644 src/components/xui/icon-buttons.tsx create mode 100644 src/types/service.ts diff --git a/package-lock.json b/package-lock.json index e4d5dc2..058dc1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-table": "^8.20.5", @@ -1695,6 +1696,37 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.1.tgz", + "integrity": "sha512-FnM1fHfCtEZ1JkyfH/1oMiTcFBQvHKl4vD9WnpwkLgtF+UmnXMCad6ECPTaAjcDjam+ndOEJWgHyKDGNteWSHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.2.tgz", diff --git a/package.json b/package.json index 513cb0c..ed93608 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-table": "^8.20.5", diff --git a/src/api/service.ts b/src/api/service.ts index 1b917d3..e96280f 100644 --- a/src/api/service.ts +++ b/src/api/service.ts @@ -2,11 +2,11 @@ import { ModelServiceForm } from "@/types" import { fetcher, FetcherMethod } from "./api" export const createService = async (data: ModelServiceForm): Promise => { - return fetcher(FetcherMethod.POST, '/api/v1/profile', data) + return fetcher(FetcherMethod.POST, '/api/v1/service', data) } export const updateService = async (id: number, data: ModelServiceForm): Promise => { - return fetcher(FetcherMethod.PATCH, `/api/v1/profile/${id}`, data) + return fetcher(FetcherMethod.PATCH, `/api/v1/service/${id}`, data) } export const deleteService = async (id: number[]): Promise => { diff --git a/src/components/action-button-group.tsx b/src/components/action-button-group.tsx new file mode 100644 index 0000000..208519a --- /dev/null +++ b/src/components/action-button-group.tsx @@ -0,0 +1,50 @@ +import { Button } from "@/components/ui/button"; +import { TrashButton } from "@/components/xui/icon-buttons"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, +} from "@/components/ui/dialog" +import { KeyedMutator } from "swr"; + +interface ButtonGroupProps { + className?: string; + children: React.ReactNode; + delete: { fn: (id: number[]) => Promise, id: number, mutate: KeyedMutator }; +} + +export function ActionButtonGroup({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps) { + const handleDelete = async () => { + await fn([id]); + await mutate(); + } + + return ( +
+ {children} + + + + + + + Confirm Deletion? + + This operation is unrecoverable! + + + + + + + + + +
+ ) +} diff --git a/src/components/header-button-group.tsx b/src/components/header-button-group.tsx new file mode 100644 index 0000000..8534b47 --- /dev/null +++ b/src/components/header-button-group.tsx @@ -0,0 +1,64 @@ +import { Button } from "@/components/ui/button"; +import { TrashButton } from "@/components/xui/icon-buttons"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, +} from "@/components/ui/dialog" +import { KeyedMutator } from "swr"; +import { toast } from "sonner" + +interface ButtonGroupProps { + className?: string; + children: React.ReactNode; + delete: { fn: (id: number[]) => Promise, id: number[], mutate: KeyedMutator }; +} + +export function HeaderButtonGroup({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps) { + const handleDelete = async () => { + await fn(id); + await mutate(); + } + + return ( +
+ {id.length < 1 ? ( + <> + { + toast("Error", { + description: "No rows are selected." + }); + }} /> + {children} + + ) : ( + <> + + + + + + + Confirm Deletion? + + This operation is unrecoverable! + + + + + + + + + + {children} + + )} +
+ ) +} diff --git a/src/components/header.tsx b/src/components/header.tsx index 6a276d3..5fb9d8a 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -13,12 +13,14 @@ import { NzNavigationMenuLink } from "./xui/navigation-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./ui/dropdown-menu"; import { User, LogOut } from "lucide-react"; import { useAuth } from "@/hooks/useAuth"; -import { Link } from "react-router-dom"; - +import { Link, useLocation } from "react-router-dom"; export default function Header() { - const { logout } = useAuth() - const profile = useMainStore(store => store.profile) + const { logout } = useAuth(); + const profile = useMainStore(store => store.profile); + + const location = useLocation(); + return
@@ -31,12 +33,12 @@ export default function Header() { { profile && <> - + Server - + Service diff --git a/src/components/service.tsx b/src/components/service.tsx index b6d1344..f74afc8 100644 --- a/src/components/service.tsx +++ b/src/components/service.tsx @@ -1,9 +1,9 @@ -import { Plus } from "lucide-react" import { Button } from "@/components/ui/button" import { Dialog, DialogClose, DialogContent, + DialogDescription, DialogFooter, DialogHeader, DialogTitle, @@ -25,184 +25,388 @@ import { FormLabel, FormMessage, } from "@/components/ui/form" +import { ScrollArea } from "@/components/ui/scroll-area" import { useForm } from "react-hook-form" import { z } from "zod" import { zodResolver } from "@hookform/resolvers/zod" -import { ModelService } from "@/types" +import { ModelService, ModelServiceResponse } from "@/types" import { createService, updateService } from "@/api/service" -import { Checkbox } from "./ui/checkbox" -import { Label } from "./ui/label" +import { Checkbox } from "@/components/ui/checkbox" +import { Label } from "@/components/ui/label" +import { conv } from "@/lib/utils" +import { useState } from "react" +import { KeyedMutator } from "swr" +import { asOptionalField } from "@/lib/utils" +import { EditButton, PlusButton } from "@/components/xui/icon-buttons" +import { serviceTypes, serviceCoverageTypes } from "@/types" interface ServiceCardProps { - className?: string; data?: ModelService; + mutate: KeyedMutator; } const serviceFormSchema = z.object({ - cover: z.number(), - duration: z.number().min(30), - enable_show_in_service: z.boolean().default(false), - enable_trigger_task: z.boolean().default(false), - fail_trigger_tasks: z.array(z.number()), - latency_notify: z.boolean(), - max_latency: z.number(), - min_latency: z.number(), - name: z.string(), - notification_group_id: z.number(), - notify: z.boolean(), - recover_trigger_tasks: z.array(z.number()), + cover: z.coerce.number().min(0), + duration: z.coerce.number().min(30), + enable_show_in_service: asOptionalField(z.boolean()), + enable_trigger_task: asOptionalField(z.boolean()), + fail_trigger_tasks: z.array(z.string()).transform((v => { + return v.filter(Boolean).map(Number); + })), + latency_notify: asOptionalField(z.boolean()), + max_latency: z.coerce.number().min(0), + min_latency: z.coerce.number().min(0), + name: z.string().min(1), + notification_group_id: z.coerce.number(), + notify: asOptionalField(z.boolean()), + recover_trigger_tasks: z.array(z.string()).transform((v => { + return v.filter(Boolean).map(Number); + })), skip_servers: z.record(z.boolean()), - target: z.string(), - type: z.number(), + target: z.string().url(), + type: z.coerce.number().min(0), }); -const serviceTypes = { - 1: "HTTP GET (Certificate expiration and changes)", - 2: "ICMP Ping", - 3: "TCPing", -} - -const serviceCoverageTypes = { - 0: "All excludes specific servers", - 1: "Only specific servers", -} - -export const ServiceCard: React.FC = ({ className, data }) => { +export const ServiceCard: React.FC = ({ data, mutate }) => { const form = useForm>({ resolver: zodResolver(serviceFormSchema), - defaultValues: data, + defaultValues: data ? data : { + type: 1, + cover: 0, + name: "", + target: "", + max_latency: 0.0, + min_latency: 0.0, + duration: 30, + notification_group_id: 0, + fail_trigger_tasks: [], + recover_trigger_tasks: [], + skip_servers: {}, + }, + resetOptions: { + keepDefaultValues: false, + } }) - const onSubmit = (values: z.infer) => { - data?.id ? updateService(data.id, values) - : createService(values); + const [open, setOpen] = useState(false); + + const onSubmit = async (values: z.infer) => { + data?.id ? await updateService(data.id, values) : await createService(values); + setOpen(false); + await mutate(); + form.reset(); } return ( - + - + {data + ? + + : + + } - - - New Service - -
-
- - ( - - Service Name - - - - - - )} - /> - ( - - Target - - - - - - )} - /> - ( - - Type - - - - )} - /> - ( - - -
- - -
-
- -
- )} - /> - ( - - Interval - - - - - - )} - /> - ( - - Coverage - - - - )} - /> - - -
- - - - - + + + )} + /> + ( + + Target + + + + + + )} + /> + ( + + Type + + + + )} + /> + ( + + +
+ + +
+
+ +
+ )} + /> + ( + + Interval (s) + + + + + + )} + /> + ( + + Coverage + + + + )} + /> + ( + + Specific Servers (separate with comma) + + { + const rec = conv.strToRecord(e.target.value); + field.onChange(rec); + }} + /> + + + + )} + /> + ( + + Notification Group ID + + + + + + )} + /> + ( + + +
+ + +
+
+ +
+ )} + /> + ( + + Maximum Latency Time (ms) + + + + + + )} + /> + ( + + Minimum Latency Time (ms) + + + + + + )} + /> + ( + + +
+ + +
+
+ +
+ )} + /> + ( + + +
+ + +
+
+ +
+ )} + /> + ( + + Tasks to trigger on an alarm (Separate with comma) + + { + const arr = conv.strToArr(e.target.value); + field.onChange(arr); + }} + /> + + + + )} + /> + ( + + Tasks to trigger after recovery (Separate with comma) + + { + const arr = conv.strToArr(e.target.value); + field.onChange(arr); + }} + /> + + + + )} + /> + + + + + + + + + +
) diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index ddbdd01..93cba99 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -11,7 +11,7 @@ const Checkbox = React.forwardRef< , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..01b8b6d --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/src/components/xui/icon-buttons.tsx b/src/components/xui/icon-buttons.tsx new file mode 100644 index 0000000..8add626 --- /dev/null +++ b/src/components/xui/icon-buttons.tsx @@ -0,0 +1,27 @@ +import { Plus, Edit2, Trash2 } from "lucide-react" +import { Button, ButtonProps } from "@/components/ui/button" +import { forwardRef } from "react"; + +export const EditButton = forwardRef((props, ref) => { + return ( + + ); +}); + +export const TrashButton = forwardRef((props, ref) => { + return ( + + ); +}); + +export const PlusButton = forwardRef((props, ref) => { + return ( + + ); +}); diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 1653b51..54bef76 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -32,8 +32,9 @@ export const AuthProvider = ({ children }: { const login = async (username: string, password: string) => { try { - await loginRequest(username, password) - setProfile({ username: username }); + await loginRequest(username, password); + const user = await getProfile(); + setProfile(user); navigate("/dashboard"); } catch (error: any) { toast(error.message); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391..924c466 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,44 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" +import { z } from "zod" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +const emptyStringToUndefined = z.literal('').transform(() => undefined); + +export function asOptionalField(schema: T) { + return schema.optional().or(emptyStringToUndefined); +} + +export const conv = { + recordToStr: (rec: Record) => { + const arr: string[] = []; + for (const key in rec) { + arr.push(key); + } + + return arr.join(','); + }, + strToRecord: (str: string) => { + const arr = str.split(','); + return arr.reduce((acc, num) => { + acc[num] = true; + return acc; + }, {} as Record); + }, + arrToStr: (arr: T[]) => { + return arr.join(','); + }, + strToArr: (str: string) => { + return str.split(','); + }, + recordToArr: (rec: Record) => { + const arr: T[] = []; + for (const val of Object.values(rec)) { + arr.push(val); + } + return arr; + }, +} diff --git a/src/routes/root.tsx b/src/routes/root.tsx index 391a72f..1dbaaa8 100644 --- a/src/routes/root.tsx +++ b/src/routes/root.tsx @@ -8,7 +8,7 @@ import { Toaster } from "@/components/ui/sonner"; export default function Root() { return ( - +
diff --git a/src/routes/server.tsx b/src/routes/server.tsx index 760ce95..19c285f 100644 --- a/src/routes/server.tsx +++ b/src/routes/server.tsx @@ -113,4 +113,4 @@ export default function ServerPage() {
-} \ No newline at end of file +} diff --git a/src/routes/service.tsx b/src/routes/service.tsx index a209979..06dbbad 100644 --- a/src/routes/service.tsx +++ b/src/routes/service.tsx @@ -2,11 +2,28 @@ import { swrFetcher } from "@/api/api" import { Checkbox } from "@/components/ui/checkbox" import { ServiceCard } from "@/components/service" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" -import { ModelService as Service } from "@/types" +import { ModelServiceResponse, ModelServiceResponseItem as Service } from "@/types" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" import useSWR from "swr" +import { conv } from "@/lib/utils" +import { useEffect, useMemo } from "react" +import { serviceTypes } from "@/types" +import { ActionButtonGroup } from "@/components/action-button-group" +import { deleteService } from "@/api/service" +import { HeaderButtonGroup } from "@/components/header-button-group" +import { Skeleton } from "@/components/ui/skeleton" +import { toast } from "sonner" export default function ServicePage() { + const { data, mutate, error, isLoading } = useSWR('/api/v1/service', swrFetcher) + + useEffect(() => { + if (error) + toast("Error", { + description: "Error fetching resource.", + }) + }, [error]) + const columns: ColumnDef[] = [ { id: "select", @@ -32,18 +49,67 @@ export default function ServicePage() { }, { header: "ID", - accessorKey: "id", - accessorFn: (row) => row.id, + accessorKey: "service.id", + accessorFn: row => row.service.id, }, { header: "Name", - accessorKey: "name", - accessorFn: (row) => row.name, + accessorKey: "service.name", + accessorFn: row => row.service.name, + }, + { + header: "Target", + accessorKey: "service.target", + accessorFn: row => row.service.target, + }, + { + header: "Coverage", + accessorKey: "service.cover", + accessorFn: row => { + switch (row.service.cover) { + case 0: { + return "Cover All" + } + case 1: { + return "Ignore All" + } + } + } + }, + { + header: "Specific Servers", + accessorKey: "service.skipServers", + accessorFn: row => Object.keys(row.service.skip_servers ?? {}), }, { header: "Type", accessorKey: "service.type", - accessorFn: (row) => row.type, + accessorFn: row => serviceTypes[row.service.type] || '', + }, + { + header: "Interval", + accessorKey: "service.duration", + accessorFn: row => row.service.duration, + }, + { + header: "Notification Group ID", + accessorKey: "service.ngroup", + accessorFn: row => row.service.notification_group_id, + }, + { + header: "Enable Trigger Task", + accessorKey: "service.triggerTask", + accessorFn: row => row.service.enable_trigger_task ?? false, + }, + { + header: "Tasks to trigger on an alarm", + accessorKey: "service.failTriggerTasks", + accessorFn: row => row.service.fail_trigger_tasks, + }, + { + header: "Tasks to trigger after recovery", + accessorKey: "service.recoverTriggerTasks", + accessorFn: row => row.service.recover_trigger_tasks, }, { id: "actions", @@ -51,68 +117,90 @@ export default function ServicePage() { cell: ({ row }) => { const s = row.original return ( - <>{s.id} + + + ) }, }, ] - const { data, error, isLoading } = useSWR('/api/v1/service', swrFetcher) + const dataArr = useMemo(() => { + return conv.recordToArr(data?.services ?? {}); + }, [data?.services]); const table = useReactTable({ - data: data ?? [], + data: dataArr, columns, getCoreRowModel: getCoreRowModel(), }) - return
-
-

- Service -

- -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} + const selectedRows = table.getSelectedRowModel().rows; + + return ( +
+
+

+ Service +

+ r.original.service.id), + mutate: mutate, + }}> + + +
+ {isLoading ? ( +
+ + + +
+ ) : ( +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
-} \ No newline at end of file + + )} + + + )} +
+ ) +} diff --git a/src/types/api.ts b/src/types/api.ts index 5994152..fe28ee0 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -142,14 +142,16 @@ export interface ModelAlertRule { } export interface ModelAlertRuleForm { - enable: boolean; + enable?: boolean; /** 失败时触发的任务id */ fail_trigger_tasks: number[]; + /** @minLength 1 */ name: string; notification_group_id: number; /** 恢复时触发的任务id */ recover_trigger_tasks: number[]; rules: ModelRule[]; + /** @default 0 */ trigger_mode: number; } @@ -219,43 +221,52 @@ export interface ModelCron { } export interface ModelCronForm { - command: string; + command?: string; + /** @default 0 */ cover: number; id: number; + /** @minLength 1 */ name: string; notification_group_id: number; - push_successful: boolean; + push_successful?: boolean; scheduler: string; servers: number[]; - /** 0:计划任务 1:触发任务 */ + /** + * 0:计划任务 1:触发任务 + * @default 0 + */ task_type: number; } export interface ModelCycleTransferStats { - from?: string; - max?: number; - min?: number; - name?: string; - nextUpdate?: Record; - serverName?: Record; - to?: string; - transfer?: Record; + from: string; + max: number; + min: number; + name: string; + next_update: Record; + server_name: Record; + to: string; + transfer: Record; } export interface ModelDDNSForm { - access_id: string; - access_secret: string; + access_id?: string; + access_secret?: string; domains: string[]; - enable_ipv4: boolean; - enable_ipv6: boolean; + enable_ipv4?: boolean; + enable_ipv6?: boolean; + /** @default 3 */ max_retries: number; + /** @minLength 1 */ name: string; provider: string; - webhook_headers: string; - webhook_method: number; - webhook_request_body: string; - webhook_request_type: number; - webhook_url: string; + webhook_headers?: string; + /** @default 1 */ + webhook_method?: number; + webhook_request_body?: string; + /** @default 1 */ + webhook_request_type?: number; + webhook_url?: string; } export interface ModelDDNSProfile { @@ -328,16 +339,17 @@ export interface ModelNAT { created_at: string; deleted_at: GormDeletedAt; domain: string; - host?: string; + host: string; id: number; - name?: string; - serverID?: number; + name: string; + server_id: number; updated_at: string; } export interface ModelNATForm { domain: string; host: string; + /** @minLength 1 */ name: string; server_id: number; } @@ -357,14 +369,15 @@ export interface ModelNotification { } export interface ModelNotificationForm { + /** @minLength 1 */ name: string; request_body: string; request_header: string; request_method: number; request_type: number; - skip_check: boolean; + skip_check?: boolean; url: string; - verify_tls: boolean; + verify_tls?: boolean; } export interface ModelNotificationGroup { @@ -376,6 +389,7 @@ export interface ModelNotificationGroup { } export interface ModelNotificationGroupForm { + /** @minLength 1 */ name: string; notifications: number[]; } @@ -442,17 +456,20 @@ export interface ModelServer { export interface ModelServerForm { /** DDNS配置 */ ddns_profiles: number[]; - /** 展示排序,越大越靠前 */ + /** + * 展示排序,越大越靠前 + * @default 0 + */ display_index: number; /** 启用DDNS */ - enable_ddns: boolean; + enable_ddns?: boolean; /** 对游客隐藏 */ - hide_for_guest: boolean; + hide_for_guest?: boolean; name: string; /** 管理员可见备注 */ - note: string; + note?: string; /** 公开备注 */ - public_note: string; + public_note?: string; } export interface ModelServerGroup { @@ -464,6 +481,7 @@ export interface ModelServerGroup { } export interface ModelServerGroupForm { + /** @minLength 1 */ name: string; servers: number[]; } @@ -501,15 +519,18 @@ export interface ModelService { export interface ModelServiceForm { cover: number; duration: number; - enable_show_in_service: boolean; - enable_trigger_task: boolean; + enable_show_in_service?: boolean; + enable_trigger_task?: boolean; fail_trigger_tasks: number[]; - latency_notify: boolean; + latency_notify?: boolean; + /** @default 0 */ max_latency: number; + /** @default 0 */ min_latency: number; + /** @minLength 1 */ name: string; notification_group_id: number; - notify: boolean; + notify?: boolean; recover_trigger_tasks: number[]; skip_servers: Record; target: string; @@ -526,30 +547,30 @@ export interface ModelServiceInfos { } export interface ModelServiceResponse { - cycleTransferStats?: Record; - services?: Record; + cycle_transfer_stats: Record; + services: Record; } export interface ModelServiceResponseItem { - currentDown?: number; - currentUp?: number; - delay?: number[]; - down?: number[]; - service?: ModelService; - totalDown?: number; - totalUp?: number; - up?: number[]; + current_down: number; + current_up: number; + delay: number[]; + down: number[]; + service: ModelService; + total_down: number; + total_up: number; + up: number[]; } export interface ModelSettingForm { cover: number; - custom_code: string; - custom_code_dashboard: string; - custom_nameservers: string; - enable_ip_change_notification: boolean; - enable_plain_ip_in_notification: boolean; - ignored_ip_notification: string; - install_host: string; + custom_code?: string; + custom_code_dashboard?: string; + custom_nameservers?: string; + enable_ip_change_notification?: boolean; + enable_plain_ip_in_notification?: boolean; + ignored_ip_notification?: string; + install_host?: string; /** IP变更提醒的通知组 */ ip_change_notification_group_id: number; language: string; diff --git a/src/types/index.ts b/src/types/index.ts index ae27896..7defd11 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ export * from './mainStore'; export * from './authContext'; -export * from './api'; \ No newline at end of file +export * from './api'; +export * from './service'; diff --git a/src/types/service.ts b/src/types/service.ts new file mode 100644 index 0000000..7be6e8c --- /dev/null +++ b/src/types/service.ts @@ -0,0 +1,10 @@ +export const serviceTypes: Record = { + 1: "HTTP GET", + 2: "ICMP Ping", + 3: "TCPing", +} + +export const serviceCoverageTypes: Record = { + 0: "All excludes specific servers", + 1: "Only specific servers", +} diff --git a/tailwind.config.js b/tailwind.config.js index c73f0d9..28bdada 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,57 +1,60 @@ /** @type {import('tailwindcss').Config} */ export default { - darkMode: ["class"], - content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], - theme: { - extend: { - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - }, - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - } - } - } - }, - plugins: [require("tailwindcss-animate")], + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + }, + }, + fontSize: { + xsm: ['0.825rem', '1.25rem'], + }, + } + }, + plugins: [require("tailwindcss-animate")], }