Optimize loading and style (#2)

This commit is contained in:
Weilong Huang
2024-11-25 06:40:28 +01:00
committed by GitHub
parent bcd5e721c2
commit 5a874d4930
14 changed files with 1246 additions and 1267 deletions

View File

@@ -23,6 +23,7 @@ export default tseslint.config(
'warn', 'warn',
{ allowConstantExport: true }, { allowConstantExport: true },
], ],
"indent": ['error', 4],
}, },
}, },
) )

View File

@@ -1,8 +1,8 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { import {
createBrowserRouter, createBrowserRouter,
RouterProvider, RouterProvider,
} from "react-router-dom"; } from "react-router-dom";
import './index.css' import './index.css'
@@ -29,85 +29,85 @@ import UserPage from './routes/user';
import WAFPage from './routes/waf'; import WAFPage from './routes/waf';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/dashboard",
element: <AuthProvider><ProtectedRoute><Root /></ProtectedRoute></AuthProvider>,
errorElement: <ErrorPage />,
children: [
{
path: "/dashboard/login",
element: <LoginPage />,
},
{
path: "/dashboard", path: "/dashboard",
element: <ServerProvider withServerGroup><ServerPage /></ServerProvider>, element: <AuthProvider><ProtectedRoute><Root /></ProtectedRoute></AuthProvider>,
}, errorElement: <ErrorPage />,
{ children: [
path: "/dashboard/service", {
element: ( path: "/dashboard/login",
<ServerProvider withServer> element: <LoginPage />,
<NotificationProvider withNotifierGroup> },
<ServicePage /> {
</NotificationProvider> path: "/dashboard",
</ServerProvider> element: <ServerProvider withServerGroup><ServerPage /></ServerProvider>,
), },
}, {
{ path: "/dashboard/service",
path: "/dashboard/cron", element: (
element: ( <ServerProvider withServer>
<ServerProvider withServer> <NotificationProvider withNotifierGroup>
<NotificationProvider withNotifierGroup> <ServicePage />
<CronPage /> </NotificationProvider>
</NotificationProvider> </ServerProvider>
</ServerProvider> ),
), },
}, {
{ path: "/dashboard/cron",
path: "/dashboard/notification", element: (
element: <NotificationProvider withNotifierGroup><NotificationPage /></NotificationProvider>, <ServerProvider withServer>
}, <NotificationProvider withNotifierGroup>
{ <CronPage />
path: "/dashboard/alert-rule", </NotificationProvider>
element: <NotificationProvider withNotifierGroup><AlertRulePage /></NotificationProvider>, </ServerProvider>
}, ),
{ },
path: "/dashboard/ddns", {
element: <DDNSPage />, path: "/dashboard/notification",
}, element: <NotificationProvider withNotifierGroup><NotificationPage /></NotificationProvider>,
{ },
path: "/dashboard/nat", {
element: <NATPage />, path: "/dashboard/alert-rule",
}, element: <NotificationProvider withNotifierGroup><AlertRulePage /></NotificationProvider>,
{ },
path: "/dashboard/server-group", {
element: <ServerProvider withServer><ServerGroupPage /></ServerProvider>, path: "/dashboard/ddns",
}, element: <DDNSPage />,
{ },
path: "/dashboard/notification-group", {
element: <NotificationProvider withNotifier><NotificationGroupPage /></NotificationProvider>, path: "/dashboard/nat",
}, element: <NATPage />,
{ },
path: "/dashboard/terminal/:id", {
element: <TerminalPage />, path: "/dashboard/server-group",
}, element: <ServerProvider withServer><ServerGroupPage /></ServerProvider>,
{ },
path: "/dashboard/settings", {
element: <SettingsPage />, path: "/dashboard/notification-group",
}, element: <NotificationProvider withNotifier><NotificationGroupPage /></NotificationProvider>,
{ },
path: "/dashboard/settings/user", {
element: <UserPage />, path: "/dashboard/terminal/:id",
}, element: <TerminalPage />,
{ },
path: "/dashboard/settings/waf", {
element: <WAFPage />, path: "/dashboard/settings",
}, element: <SettingsPage />,
] },
}, {
path: "/dashboard/settings/user",
element: <UserPage />,
},
{
path: "/dashboard/settings/waf",
element: <WAFPage />,
},
]
},
]); ]);
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<RouterProvider router={router} /> <RouterProvider router={router} />
</StrictMode>, </StrictMode>,
) )

View File

@@ -1,27 +1,36 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" Table,
import useSWR from "swr" TableBody,
import { useEffect } from "react" TableCell,
import { ActionButtonGroup } from "@/components/action-button-group" TableHead,
import { HeaderButtonGroup } from "@/components/header-button-group" TableHeader,
import { Skeleton } from "@/components/ui/skeleton" TableRow,
import { toast } from "sonner" } from "@/components/ui/table";
import { ModelAlertRule, triggerModes } from "@/types" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { deleteAlertRules } from "@/api/alert-rule" import useSWR from "swr";
import { NotificationTab } from "@/components/notification-tab" import { useEffect } from "react";
import { AlertRuleCard } from "@/components/alert-rule" import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelAlertRule, triggerModes } from "@/types";
import { deleteAlertRules } from "@/api/alert-rule";
import { NotificationTab } from "@/components/notification-tab";
import { AlertRuleCard } from "@/components/alert-rule";
export default function AlertRulePage() { export default function AlertRulePage() {
const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>("/api/v1/alert-rule", swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>(
"/api/v1/alert-rule",
swrFetcher
);
useEffect(() => { useEffect(() => {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelAlertRule>[] = [ const columns: ColumnDef<ModelAlertRule>[] = [
{ {
@@ -30,7 +39,7 @@ export default function AlertRulePage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -49,74 +58,73 @@ export default function AlertRulePage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => row.id, accessorFn: (row) => row.id,
}, },
{ {
header: "Name", header: "Name",
accessorKey: "name", accessorKey: "name",
accessorFn: row => row.name, accessorFn: (row) => row.name,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
<div className="max-w-32 whitespace-normal break-words"> },
{s.name}
</div>
)
}
}, },
{ {
header: "Notifier Group", header: "Notifier Group",
accessorKey: "ngroup", accessorKey: "ngroup",
accessorFn: row => row.notification_group_id, accessorFn: (row) => row.notification_group_id,
}, },
{ {
header: "Trigger Mode", header: "Trigger Mode",
accessorKey: "trigger Mode", accessorKey: "trigger Mode",
accessorFn: row => triggerModes[row.trigger_mode] || '', accessorFn: (row) => triggerModes[row.trigger_mode] || "",
}, },
{ {
header: "Rules", header: "Rules",
accessorKey: "rules", accessorKey: "rules",
accessorFn: row => JSON.stringify(row.rules), accessorFn: (row) => JSON.stringify(row.rules),
}, },
{ {
header: "Tasks to trigger on alert", header: "Tasks to trigger on alert",
accessorKey: "failTriggerTasks", accessorKey: "failTriggerTasks",
accessorFn: row => row.fail_trigger_tasks, accessorFn: (row) => row.fail_trigger_tasks,
}, },
{ {
header: "Tasks to trigger after recovery", header: "Tasks to trigger after recovery",
accessorKey: "recoverTriggerTasks", accessorKey: "recoverTriggerTasks",
accessorFn: row => row.recover_trigger_tasks, accessorFn: (row) => row.recover_trigger_tasks,
}, },
{ {
header: "Enable", header: "Enable",
accessorKey: "enable", accessorKey: "enable",
accessorFn: row => row.enable, accessorFn: (row) => row.enable,
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ <ActionButtonGroup
fn: deleteAlertRules, className="flex gap-2"
id: s.id, delete={{
mutate: mutate, fn: deleteAlertRules,
}}> id: s.id,
mutate: mutate,
}}
>
<AlertRuleCard mutate={mutate} data={s} /> <AlertRuleCard mutate={mutate} data={s} />
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
@@ -124,64 +132,60 @@ export default function AlertRulePage() {
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" /> <NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{ <HeaderButtonGroup
fn: deleteAlertRules, className="flex-2 flex gap-2 ml-auto"
id: selectedRows.map(r => r.original.id), delete={{
mutate: mutate, fn: deleteAlertRules,
}}> id: selectedRows.map((r) => r.original.id),
mutate: mutate,
}}
>
<AlertRuleCard mutate={mutate} /> <AlertRuleCard mutate={mutate} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div> </div>
) );
} }

View File

@@ -1,28 +1,34 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ModelCron } from "@/types" Table,
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" TableBody,
import useSWR from "swr" TableCell,
import { useEffect } from "react" TableHead,
import { ActionButtonGroup } from "@/components/action-button-group" TableHeader,
import { HeaderButtonGroup } from "@/components/header-button-group" TableRow,
import { Skeleton } from "@/components/ui/skeleton" } from "@/components/ui/table";
import { toast } from "sonner" import { ModelCron } from "@/types";
import { deleteCron, runCron } from "@/api/cron" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { CronCard } from "@/components/cron" import useSWR from "swr";
import { cronTypes } from "@/types" import { useEffect } from "react";
import { IconButton } from "@/components/xui/icon-button" import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { deleteCron, runCron } from "@/api/cron";
import { CronCard } from "@/components/cron";
import { cronTypes } from "@/types";
import { IconButton } from "@/components/xui/icon-button";
export default function CronPage() { export default function CronPage() {
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>('/api/v1/cron', swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher);
useEffect(() => { useEffect(() => {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelCron>[] = [ const columns: ColumnDef<ModelCron>[] = [
{ {
@@ -31,7 +37,7 @@ export default function CronPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -50,205 +56,193 @@ export default function CronPage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => row.id, accessorFn: (row) => row.id,
}, },
{ {
header: "Name", header: "Name",
accessorKey: "name", accessorKey: "name",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
<div className="max-w-32 whitespace-normal break-words"> },
{s.name}
</div>
)
}
}, },
{ {
header: "Task Type", header: "Task Type",
accessorKey: "taskType", accessorKey: "taskType",
accessorFn: row => cronTypes[row.task_type] || '', accessorFn: (row) => cronTypes[row.task_type] || "",
}, },
{ {
header: "Cron Expression", header: "Cron Expression",
accessorKey: "scheduler", accessorKey: "scheduler",
accessorFn: row => row.scheduler, accessorFn: (row) => row.scheduler,
}, },
{ {
header: "Command", header: "Command",
accessorKey: "command", accessorKey: "command",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>;
<div className="max-w-48 whitespace-normal break-words"> },
{s.command}
</div>
)
}
}, },
{ {
header: "Notifier Group", header: "Notifier Group",
accessorKey: "ngroup", accessorKey: "ngroup",
accessorFn: row => row.notification_group_id, accessorFn: (row) => row.notification_group_id,
}, },
{ {
header: "Send Success Notification", header: "Send Success Notification",
accessorKey: "pushSuccessful", accessorKey: "pushSuccessful",
accessorFn: row => row.push_successful ?? false, accessorFn: (row) => row.push_successful ?? false,
}, },
{ {
header: "Coverage", header: "Coverage",
accessorKey: "cover", accessorKey: "cover",
accessorFn: row => row.cover, accessorFn: (row) => row.cover,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return (
<div className="max-w-48 whitespace-normal break-words"> <div className="max-w-48 whitespace-normal break-words">
{(() => { {(() => {
switch (s.cover) { switch (s.cover) {
case 0: { case 0: {
return <span>Ignore All</span> return <span>Ignore All</span>;
} }
case 1: { case 1: {
return <span>Cover All</span> return <span>Cover All</span>;
} }
case 2: { case 2: {
return <span>On alert</span> return <span>On alert</span>;
} }
} }
})()} })()}
</div> </div>
) );
} },
}, },
{ {
header: "Specific Servers", header: "Specific Servers",
accessorKey: "servers", accessorKey: "servers",
accessorFn: row => row.servers, accessorFn: (row) => row.servers,
}, },
{ {
header: "Last Execution", header: "Last Execution",
accessorKey: "lastExecution", accessorKey: "lastExecution",
accessorFn: row => row.last_executed_at, accessorFn: (row) => row.last_executed_at,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-24 whitespace-normal break-words">{s.last_executed_at}</div>;
<div className="max-w-24 whitespace-normal break-words"> },
{s.last_executed_at}
</div>
)
}
}, },
{ {
header: "Last Result", header: "Last Result",
accessorKey: "lastResult", accessorKey: "lastResult",
accessorFn: row => row.last_result ?? false, accessorFn: (row) => row.last_result ?? false,
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteCron, id: s.id, mutate: mutate }}> <ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteCron, id: s.id, mutate: mutate }}
>
<> <>
<IconButton variant="outline" icon="play" onClick={ <IconButton
async () => { variant="outline"
icon="play"
onClick={async () => {
try { try {
await runCron(s.id); await runCron(s.id);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
toast("Error executing task", { toast("Error executing task", {
description: "Please see the console for details.", description: "Please see the console for details.",
}) });
await mutate(); await mutate();
return; return;
} }
toast("Success", { toast("Success", {
description: "The task triggered successfully.", description: "The task triggered successfully.",
}) });
await mutate(); await mutate();
}} /> }}
/>
<CronCard mutate={mutate} data={s} /> <CronCard mutate={mutate} data={s} />
</> </>
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
return ( return (
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> <h1 className="flex-1 text-3xl font-bold tracking-tight">Task</h1>
Task <HeaderButtonGroup
</h1> className="flex-2 flex ml-auto gap-2"
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{ delete={{
fn: deleteCron, fn: deleteCron,
id: selectedRows.map(r => r.original.id), id: selectedRows.map((r) => r.original.id),
mutate: mutate, mutate: mutate,
}}> }}
>
<CronCard mutate={mutate} /> <CronCard mutate={mutate} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm"> </div>
{flexRender(cell.column.columnDef.cell, cell.getContext())} );
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div >
)
} }

View File

@@ -1,19 +1,25 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { DDNSCard } from "@/components/ddns" import { DDNSCard } from "@/components/ddns";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ModelDDNSProfile } from "@/types" Table,
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" TableBody,
import useSWR from "swr" TableCell,
import { useEffect, useState } from "react" TableHead,
import { ActionButtonGroup } from "@/components/action-button-group" TableHeader,
import { HeaderButtonGroup } from "@/components/header-button-group" TableRow,
import { Skeleton } from "@/components/ui/skeleton" } from "@/components/ui/table";
import { toast } from "sonner" import { ModelDDNSProfile } from "@/types";
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useState } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns";
export default function DDNSPage() { export default function DDNSPage() {
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>('/api/v1/ddns', swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>("/api/v1/ddns", swrFetcher);
const [providers, setProviders] = useState<string[]>([]); const [providers, setProviders] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
@@ -28,8 +34,8 @@ export default function DDNSPage() {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelDDNSProfile>[] = [ const columns: ColumnDef<ModelDDNSProfile>[] = [
{ {
@@ -38,7 +44,7 @@ export default function DDNSPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -57,140 +63,129 @@ export default function DDNSPage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => row.id, accessorFn: (row) => row.id,
}, },
{ {
header: "Name", header: "Name",
accessorKey: "name", accessorKey: "name",
accessorFn: row => row.name, accessorFn: (row) => row.name,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
<div className="max-w-24 whitespace-normal break-words"> },
{s.name}
</div>
)
}
}, },
{ {
header: "IPv4 Enabled", header: "IPv4 Enabled",
accessorKey: "enableIPv4", accessorKey: "enableIPv4",
accessorFn: row => row.enable_ipv4 ?? false, accessorFn: (row) => row.enable_ipv4 ?? false,
}, },
{ {
header: "IPv6 Enabled", header: "IPv6 Enabled",
accessorKey: "enableIPv6", accessorKey: "enableIPv6",
accessorFn: row => row.enable_ipv6 ?? false, accessorFn: (row) => row.enable_ipv6 ?? false,
}, },
{ {
header: "DDNS Provider", header: "DDNS Provider",
accessorKey: "provider", accessorKey: "provider",
accessorFn: row => row.provider, accessorFn: (row) => row.provider,
}, },
{ {
header: "Domains", header: "Domains",
accessorKey: "domains", accessorKey: "domains",
accessorFn: row => row.domains, accessorFn: (row) => row.domains,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>;
<div className="max-w-24 whitespace-normal break-words"> },
{s.domains}
</div>
)
}
}, },
{ {
header: "Maximum retry attempts", header: "Maximum retry attempts",
accessorKey: "maxRetries", accessorKey: "maxRetries",
accessorFn: row => row.max_retries, accessorFn: (row) => row.max_retries,
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteDDNSProfiles, id: s.id, mutate: mutate }}> <ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteDDNSProfiles, id: s.id, mutate: mutate }}
>
<DDNSCard mutate={mutate} data={s} providers={providers} /> <DDNSCard mutate={mutate} data={s} providers={providers} />
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
return ( return (
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> <h1 className="flex-1 text-3xl font-bold tracking-tight">Dynamic DNS</h1>
Dynamic DNS <HeaderButtonGroup
</h1> className="flex-2 flex ml-auto gap-2"
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{ delete={{
fn: deleteDDNSProfiles, fn: deleteDDNSProfiles,
id: selectedRows.map(r => r.original.id), id: selectedRows.map((r) => r.original.id),
mutate: mutate, mutate: mutate,
}}> }}
>
<DDNSCard mutate={mutate} providers={providers} /> <DDNSCard mutate={mutate} providers={providers} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm"> </div>
{flexRender(cell.column.columnDef.cell, cell.getContext())} );
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div >
)
} }

View File

@@ -1,26 +1,32 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { NATCard } from "@/components/nat" import { NATCard } from "@/components/nat";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ModelNAT } from "@/types" Table,
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" TableBody,
import useSWR from "swr" TableCell,
import { useEffect } from "react" TableHead,
import { ActionButtonGroup } from "@/components/action-button-group" TableHeader,
import { HeaderButtonGroup } from "@/components/header-button-group" TableRow,
import { Skeleton } from "@/components/ui/skeleton" } from "@/components/ui/table";
import { toast } from "sonner" import { ModelNAT } from "@/types";
import { deleteNAT } from "@/api/nat" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { deleteNAT } from "@/api/nat";
export default function NATPage() { export default function NATPage() {
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>('/api/v1/nat', swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher);
useEffect(() => { useEffect(() => {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelNAT>[] = [ const columns: ColumnDef<ModelNAT>[] = [
{ {
@@ -29,7 +35,7 @@ export default function NATPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -48,138 +54,123 @@ export default function NATPage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => row.id, accessorFn: (row) => row.id,
}, },
{ {
header: "Name", header: "Name",
accessorKey: "name", accessorKey: "name",
accessorFn: row => row.name, accessorFn: (row) => row.name,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
<div className="max-w-32 whitespace-normal break-words"> },
{s.name}
</div>
)
}
}, },
{ {
header: "Server ID", header: "Server ID",
accessorKey: "serverID", accessorKey: "serverID",
accessorFn: row => row.server_id, accessorFn: (row) => row.server_id,
}, },
{ {
header: "Local service", header: "Local service",
accessorKey: "host", accessorKey: "host",
accessorFn: row => row.host, accessorFn: (row) => row.host,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>;
<div className="max-w-32 whitespace-normal break-words"> },
{s.host}
</div>
)
}
}, },
{ {
header: "Bind hostname", header: "Bind hostname",
accessorKey: "domain", accessorKey: "domain",
accessorFn: row => row.domain, accessorFn: (row) => row.domain,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>;
<div className="max-w-32 whitespace-normal break-words"> },
{s.domain}
</div>
)
}
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteNAT, id: s.id, mutate: mutate }}> <ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteNAT, id: s.id, mutate: mutate }}
>
<NATCard mutate={mutate} data={s} /> <NATCard mutate={mutate} data={s} />
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
return ( return (
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> <h1 className="flex-1 text-3xl font-bold tracking-tight">NAT Traversal</h1>
NAT Traversal <HeaderButtonGroup
</h1> className="flex-2 flex ml-auto gap-2"
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{ delete={{
fn: deleteNAT, fn: deleteNAT,
id: selectedRows.map(r => r.original.id), id: selectedRows.map((r) => r.original.id),
mutate: mutate, mutate: mutate,
}}> }}
>
<NATCard mutate={mutate} /> <NATCard mutate={mutate} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm"> </div>
{flexRender(cell.column.columnDef.cell, cell.getContext())} );
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div >
)
} }

View File

@@ -1,27 +1,36 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" Table,
import useSWR from "swr" TableBody,
import { useEffect } from "react" TableCell,
import { ActionButtonGroup } from "@/components/action-button-group" TableHead,
import { HeaderButtonGroup } from "@/components/header-button-group" TableHeader,
import { Skeleton } from "@/components/ui/skeleton" TableRow,
import { toast } from "sonner" } from "@/components/ui/table";
import { ModelNotificationGroupResponseItem } from "@/types" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { deleteNotificationGroups } from "@/api/notification-group" import useSWR from "swr";
import { GroupTab } from "@/components/group-tab" import { useEffect } from "react";
import { NotificationGroupCard } from "@/components/notification-group" import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelNotificationGroupResponseItem } from "@/types";
import { deleteNotificationGroups } from "@/api/notification-group";
import { GroupTab } from "@/components/group-tab";
import { NotificationGroupCard } from "@/components/notification-group";
export default function NotificationGroupPage() { export default function NotificationGroupPage() {
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>("/api/v1/notification-group", swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
"/api/v1/notification-group",
swrFetcher
);
useEffect(() => { useEffect(() => {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [ const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [
{ {
@@ -30,7 +39,7 @@ export default function NotificationGroupPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -49,49 +58,48 @@ export default function NotificationGroupPage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => row.group.id, accessorFn: (row) => row.group.id,
}, },
{ {
header: "Name", header: "Name",
accessorKey: "name", accessorKey: "name",
accessorFn: row => row.group.name, accessorFn: (row) => row.group.name,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>;
<div className="max-w-48 whitespace-normal break-words"> },
{s.group.name}
</div>
)
}
}, },
{ {
header: "Notifiers (ID)", header: "Notifiers (ID)",
accessorKey: "notifications", accessorKey: "notifications",
accessorFn: row => row.notifications, accessorFn: (row) => row.notifications,
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ <ActionButtonGroup
fn: deleteNotificationGroups, className="flex gap-2"
id: s.group.id, delete={{
mutate: mutate, fn: deleteNotificationGroups,
}}> id: s.group.id,
mutate: mutate,
}}
>
<NotificationGroupCard mutate={mutate} data={s} /> <NotificationGroupCard mutate={mutate} data={s} />
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
@@ -99,64 +107,60 @@ export default function NotificationGroupPage() {
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" /> <GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{ <HeaderButtonGroup
fn: deleteNotificationGroups, className="flex-2 flex gap-2 ml-auto"
id: selectedRows.map(r => r.original.group.id), delete={{
mutate: mutate fn: deleteNotificationGroups,
}}> id: selectedRows.map((r) => r.original.group.id),
mutate: mutate,
}}
>
<NotificationGroupCard mutate={mutate} /> <NotificationGroupCard mutate={mutate} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div> </div>
) );
} }

View File

@@ -1,29 +1,38 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" Table,
import useSWR from "swr" TableBody,
import { useEffect } from "react" TableCell,
import { ActionButtonGroup } from "@/components/action-button-group" TableHead,
import { HeaderButtonGroup } from "@/components/header-button-group" TableHeader,
import { Skeleton } from "@/components/ui/skeleton" TableRow,
import { toast } from "sonner" } from "@/components/ui/table";
import { ModelNotification } from "@/types" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { deleteNotification } from "@/api/notification" import useSWR from "swr";
import { NotificationTab } from "@/components/notification-tab" import { useEffect } from "react";
import { NotifierCard } from "@/components/notifier" import { ActionButtonGroup } from "@/components/action-button-group";
import { useNotification } from "@/hooks/useNotfication" import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelNotification } from "@/types";
import { deleteNotification } from "@/api/notification";
import { NotificationTab } from "@/components/notification-tab";
import { NotifierCard } from "@/components/notifier";
import { useNotification } from "@/hooks/useNotfication";
export default function NotificationPage() { export default function NotificationPage() {
const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>("/api/v1/notification", swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>(
"/api/v1/notification",
swrFetcher
);
const { notifierGroup } = useNotification(); const { notifierGroup } = useNotification();
useEffect(() => { useEffect(() => {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelNotification>[] = [ const columns: ColumnDef<ModelNotification>[] = [
{ {
@@ -32,7 +41,7 @@ export default function NotificationPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -51,71 +60,68 @@ export default function NotificationPage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => row.id, accessorFn: (row) => row.id,
}, },
{ {
header: "Name", header: "Name",
accessorKey: "name", accessorKey: "name",
accessorFn: row => row.name, accessorFn: (row) => row.name,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
<div className="max-w-32 whitespace-normal break-words"> },
{s.name}
</div>
)
}
}, },
{ {
header: "Groups", header: "Groups",
accessorKey: "groups", accessorKey: "groups",
accessorFn: row => { accessorFn: (row) => {
return notifierGroup?.filter(ng => ng.notifications?.includes(row.id)) return (
.map(ng => ng.group.id) notifierGroup
|| []; ?.filter((ng) => ng.notifications?.includes(row.id))
.map((ng) => ng.group.id) || []
);
}, },
}, },
{ {
header: "URL", header: "URL",
accessorKey: "url", accessorKey: "url",
accessorFn: row => row.url, accessorFn: (row) => row.url,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-64 whitespace-normal break-words">{s.url}</div>;
<div className="max-w-64 whitespace-normal break-words"> },
{s.url}
</div>
)
}
}, },
{ {
header: "Verify TLS", header: "Verify TLS",
accessorKey: "verify_tls", accessorKey: "verify_tls",
accessorFn: row => row.verify_tls, accessorFn: (row) => row.verify_tls,
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ <ActionButtonGroup
fn: deleteNotification, className="flex gap-2"
id: s.id, delete={{
mutate: mutate, fn: deleteNotification,
}}> id: s.id,
mutate: mutate,
}}
>
<NotifierCard mutate={mutate} data={s} /> <NotifierCard mutate={mutate} data={s} />
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
@@ -123,64 +129,60 @@ export default function NotificationPage() {
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" /> <NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{ <HeaderButtonGroup
fn: deleteNotification, className="flex-2 flex gap-2 ml-auto"
id: selectedRows.map(r => r.original.id), delete={{
mutate: mutate fn: deleteNotification,
}}> id: selectedRows.map((r) => r.original.id),
mutate: mutate,
}}
>
<NotifierCard mutate={mutate} /> <NotifierCard mutate={mutate} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div> </div>
) );
} }

View File

@@ -1,27 +1,36 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" Table,
import useSWR from "swr" TableBody,
import { useEffect } from "react" TableCell,
import { ActionButtonGroup } from "@/components/action-button-group" TableHead,
import { HeaderButtonGroup } from "@/components/header-button-group" TableHeader,
import { Skeleton } from "@/components/ui/skeleton" TableRow,
import { toast } from "sonner" } from "@/components/ui/table";
import { ModelServerGroupResponseItem } from "@/types" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { deleteServerGroups } from "@/api/server-group" import useSWR from "swr";
import { GroupTab } from "@/components/group-tab" import { useEffect } from "react";
import { ServerGroupCard } from "@/components/server-group" import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelServerGroupResponseItem } from "@/types";
import { deleteServerGroups } from "@/api/server-group";
import { GroupTab } from "@/components/group-tab";
import { ServerGroupCard } from "@/components/server-group";
export default function ServerGroupPage() { export default function ServerGroupPage() {
const { data, mutate, error, isLoading } = useSWR<ModelServerGroupResponseItem[]>("/api/v1/server-group", swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelServerGroupResponseItem[]>(
"/api/v1/server-group",
swrFetcher
);
useEffect(() => { useEffect(() => {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelServerGroupResponseItem>[] = [ const columns: ColumnDef<ModelServerGroupResponseItem>[] = [
{ {
@@ -30,7 +39,7 @@ export default function ServerGroupPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -49,49 +58,48 @@ export default function ServerGroupPage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => row.group.id, accessorFn: (row) => row.group.id,
}, },
{ {
header: "Name", header: "Name",
accessorKey: "name", accessorKey: "name",
accessorFn: row => row.group.name, accessorFn: (row) => row.group.name,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>;
<div className="max-w-48 whitespace-normal break-words"> },
{s.group.name}
</div>
)
}
}, },
{ {
header: "Servers (ID)", header: "Servers (ID)",
accessorKey: "servers", accessorKey: "servers",
accessorFn: row => row.servers, accessorFn: (row) => row.servers,
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ <ActionButtonGroup
fn: deleteServerGroups, className="flex gap-2"
id: s.group.id, delete={{
mutate: mutate, fn: deleteServerGroups,
}}> id: s.group.id,
mutate: mutate,
}}
>
<ServerGroupCard mutate={mutate} data={s} /> <ServerGroupCard mutate={mutate} data={s} />
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
@@ -99,64 +107,59 @@ export default function ServerGroupPage() {
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" /> <GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{ <HeaderButtonGroup
fn: deleteServerGroups, className="flex-2 flex ml-auto gap-2"
id: selectedRows.map(r => r.original.group.id), delete={{
mutate: mutate fn: deleteServerGroups,
}}> id: selectedRows.map((r) => r.original.group.id),
mutate: mutate,
}}
>
<ServerGroupCard mutate={mutate} /> <ServerGroupCard mutate={mutate} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? ( <Table>
<div className="flex flex-col items-center space-y-4"> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {headerGroup.headers.map((header) => {
</div> return (
) : ( <TableHead key={header.id} className="text-sm">
<Table> {header.isPlaceholder
<TableHeader> ? null
{table.getHeaderGroups().map((headerGroup) => ( : flexRender(header.column.columnDef.header, header.getContext())}
<TableRow key={headerGroup.id}> </TableHead>
{headerGroup.headers.map((header) => { );
return ( })}
<TableHead key={header.id} className="text-sm"> </TableRow>
{header.isPlaceholder ))}
? null </TableHeader>
: flexRender( <TableBody>
header.column.columnDef.header, {isLoading ? (
header.getContext() <TableRow>
)} <TableCell colSpan={columns.length} className="h-24 text-center">
</TableHead> Loading ...
) </TableCell>
})} </TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div> </div>
) );
} }

View File

@@ -1,33 +1,39 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ModelServer as Server, ModelForceUpdateResponse } from "@/types" Table,
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" TableBody,
import useSWR from "swr" TableCell,
import { HeaderButtonGroup } from "@/components/header-button-group" TableHead,
import { deleteServer, forceUpdateServer } from "@/api/server" TableHeader,
import { ServerCard } from "@/components/server" TableRow,
import { Skeleton } from "@/components/ui/skeleton" } from "@/components/ui/table";
import { ActionButtonGroup } from "@/components/action-button-group" import { ModelServer as Server, ModelForceUpdateResponse } from "@/types";
import { useEffect } from "react" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { toast } from "sonner" import useSWR from "swr";
import { IconButton } from "@/components/xui/icon-button" import { HeaderButtonGroup } from "@/components/header-button-group";
import { InstallCommandsMenu } from "@/components/install-commands" import { deleteServer, forceUpdateServer } from "@/api/server";
import { NoteMenu } from "@/components/note-menu" import { ServerCard } from "@/components/server";
import { TerminalButton } from "@/components/terminal" import { ActionButtonGroup } from "@/components/action-button-group";
import { useServer } from "@/hooks/useServer" import { useEffect } from "react";
import { joinIP } from "@/lib/utils" import { toast } from "sonner";
import { IconButton } from "@/components/xui/icon-button";
import { InstallCommandsMenu } from "@/components/install-commands";
import { NoteMenu } from "@/components/note-menu";
import { TerminalButton } from "@/components/terminal";
import { useServer } from "@/hooks/useServer";
import { joinIP } from "@/lib/utils";
export default function ServerPage() { export default function ServerPage() {
const { data, mutate, error, isLoading } = useSWR<Server[]>('/api/v1/server', swrFetcher); const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher);
const { serverGroups } = useServer(); const { serverGroups } = useServer();
useEffect(() => { useEffect(() => {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<Server>[] = [ const columns: ColumnDef<Server>[] = [
{ {
@@ -36,7 +42,7 @@ export default function ServerPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -55,28 +61,24 @@ export default function ServerPage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => `${row.id}(${row.display_index})`, accessorFn: (row) => `${row.id}(${row.display_index})`,
}, },
{ {
header: "Name", header: "Name",
accessorKey: "name", accessorKey: "name",
accessorFn: row => row.name, accessorFn: (row) => row.name,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
<div className="max-w-24 whitespace-normal break-words"> },
{s.name}
</div>
)
}
}, },
{ {
header: "Groups", header: "Groups",
accessorKey: "groups", accessorKey: "groups",
accessorFn: row => { accessorFn: (row) => {
return serverGroups?.filter(sg => sg.servers?.includes(row.id)) return (
.map(sg => sg.group.id) serverGroups?.filter((sg) => sg.servers?.includes(row.id)).map((sg) => sg.group.id) || []
|| []; );
}, },
}, },
{ {
@@ -84,27 +86,23 @@ export default function ServerPage() {
header: "IP", header: "IP",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-24 whitespace-normal break-words">{joinIP(s.geoip?.ip)}</div>;
<div className="max-w-24 whitespace-normal break-words"> },
{joinIP(s.geoip?.ip)}
</div>
)
}
}, },
{ {
header: "Version", header: "Version",
accessorKey: "host.version", accessorKey: "host.version",
accessorFn: row => row.host.version || "Unknown", accessorFn: (row) => row.host.version || "Unknown",
}, },
{ {
header: "Enable DDNS", header: "Enable DDNS",
accessorKey: "enableDDNS", accessorKey: "enableDDNS",
accessorFn: row => row.enable_ddns ?? false, accessorFn: (row) => row.enable_ddns ?? false,
}, },
{ {
header: "Hide from Guest", header: "Hide from Guest",
accessorKey: "hideForGuest", accessorKey: "hideForGuest",
accessorFn: row => row.hide_for_guest ?? false, accessorFn: (row) => row.hide_for_guest ?? false,
}, },
{ {
id: "installCommands", id: "installCommands",
@@ -125,42 +123,47 @@ export default function ServerPage() {
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteServer, id: s.id, mutate: mutate }}> <ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteServer, id: s.id, mutate: mutate }}
>
<> <>
<TerminalButton id={s.id} /> <TerminalButton id={s.id} />
<ServerCard mutate={mutate} data={s} /> <ServerCard mutate={mutate} data={s} />
</> </>
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
return ( return (
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight"> <h1 className="text-3xl font-bold tracking-tight">Server</h1>
Server <HeaderButtonGroup
</h1> className="flex-2 flex ml-auto gap-2"
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{ delete={{
fn: deleteServer, fn: deleteServer,
id: selectedRows.map(r => r.original.id), id: selectedRows.map((r) => r.original.id),
mutate: mutate, mutate: mutate,
}}> }}
<IconButton icon="update" onClick={ >
async () => { <IconButton
const id = selectedRows.map(r => r.original.id); icon="update"
onClick={async () => {
const id = selectedRows.map((r) => r.original.id);
if (id.length < 1) { if (id.length < 1) {
toast("Error", { toast("Error", {
description: "No rows are selected." description: "No rows are selected.",
}); });
return; return;
} }
@@ -172,69 +175,63 @@ export default function ServerPage() {
console.error(e); console.error(e);
toast("Error executing task", { toast("Error executing task", {
description: "Please see the console for details.", description: "Please see the console for details.",
}) });
return; return;
} }
toast("Task executed successfully", { toast("Task executed successfully", {
description: `Result (Server ID): description: `Result (Server ID):
${resp.success?.length ? `Success: ${resp.success.join(",")}, ` : ''} ${resp.success?.length ? `Success: ${resp.success.join(",")}, ` : ""}
${resp.failure?.length ? `Failure: ${resp.failure.join(",")}, ` : ''} ${resp.failure?.length ? `Failure: ${resp.failure.join(",")}, ` : ""}
${resp.offline?.length ? `Offline: ${resp.offline.join(",")}` : ''} ${resp.offline?.length ? `Offline: ${resp.offline.join(",")}` : ""}
` `,
}) });
}} /> }}
/>
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div> </div>
) );
} }

View File

@@ -1,28 +1,37 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { ServiceCard } from "@/components/service" import { ServiceCard } from "@/components/service";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ModelServiceResponse, ModelServiceResponseItem as Service } from "@/types" Table,
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" TableBody,
import useSWR from "swr" TableCell,
import { conv } from "@/lib/utils" TableHead,
import { useEffect, useMemo } from "react" TableHeader,
import { serviceTypes } from "@/types" TableRow,
import { ActionButtonGroup } from "@/components/action-button-group" } from "@/components/ui/table";
import { deleteService } from "@/api/service" import { ModelServiceResponse, ModelServiceResponseItem as Service } from "@/types";
import { HeaderButtonGroup } from "@/components/header-button-group" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { Skeleton } from "@/components/ui/skeleton" import useSWR from "swr";
import { toast } from "sonner" 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 { toast } from "sonner";
export default function ServicePage() { export default function ServicePage() {
const { data, mutate, error, isLoading } = useSWR<ModelServiceResponse>('/api/v1/service', swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelServiceResponse>(
"/api/v1/service",
swrFetcher
);
useEffect(() => { useEffect(() => {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<Service>[] = [ const columns: ColumnDef<Service>[] = [
{ {
@@ -31,7 +40,7 @@ export default function ServicePage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -50,105 +59,100 @@ export default function ServicePage() {
{ {
header: "ID", header: "ID",
accessorKey: "service.id", accessorKey: "service.id",
accessorFn: row => row.service.id, accessorFn: (row) => row.service.id,
}, },
{ {
header: "Name", header: "Name",
accessorFn: row => row.service.name, accessorFn: (row) => row.service.name,
accessorKey: "service.name", accessorKey: "service.name",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-24 whitespace-normal break-words">{s.service.name}</div>;
<div className="max-w-24 whitespace-normal break-words"> },
{s.service.name}
</div>
)
}
}, },
{ {
header: "Target", header: "Target",
accessorFn: row => row.service.target, accessorFn: (row) => row.service.target,
accessorKey: "service.target", accessorKey: "service.target",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
return ( return <div className="max-w-24 whitespace-normal break-words">{s.service.target}</div>;
<div className="max-w-24 whitespace-normal break-words"> },
{s.service.target}
</div>
)
}
}, },
{ {
header: "Coverage", header: "Coverage",
accessorKey: "service.cover", accessorKey: "service.cover",
accessorFn: row => row.service.cover, accessorFn: (row) => row.service.cover,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original.service; const s = row.original.service;
return ( return (
<div className="max-w-48 whitespace-normal break-words"> <div className="max-w-48 whitespace-normal break-words">
{(() => { {(() => {
switch (s.cover) { switch (s.cover) {
case 0: { case 0: {
return <span>Cover All</span> return <span>Cover All</span>;
} }
case 1: { case 1: {
return <span>Ignore All</span> return <span>Ignore All</span>;
} }
} }
})()} })()}
</div> </div>
) );
} },
}, },
{ {
header: "Specific Servers", header: "Specific Servers",
accessorKey: "service.skipServers", accessorKey: "service.skipServers",
accessorFn: row => Object.keys(row.service.skip_servers ?? {}), accessorFn: (row) => Object.keys(row.service.skip_servers ?? {}),
}, },
{ {
header: "Type", header: "Type",
accessorKey: "service.type", accessorKey: "service.type",
accessorFn: row => row.service.type, accessorFn: (row) => row.service.type,
cell: ({ row }) => serviceTypes[row.original.service.type] || '', cell: ({ row }) => serviceTypes[row.original.service.type] || "",
}, },
{ {
header: "Interval", header: "Interval",
accessorKey: "service.duration", accessorKey: "service.duration",
accessorFn: row => row.service.duration, accessorFn: (row) => row.service.duration,
}, },
{ {
header: "Notifier Group ID", header: "Notifier Group ID",
accessorKey: "service.ngroup", accessorKey: "service.ngroup",
accessorFn: row => row.service.notification_group_id, accessorFn: (row) => row.service.notification_group_id,
}, },
{ {
header: "On Trigger", header: "On Trigger",
accessorKey: "service.triggerTask", accessorKey: "service.triggerTask",
accessorFn: row => row.service.enable_trigger_task ?? false, accessorFn: (row) => row.service.enable_trigger_task ?? false,
}, },
{ {
header: "Tasks to trigger on alert", header: "Tasks to trigger on alert",
accessorKey: "service.failTriggerTasks", accessorKey: "service.failTriggerTasks",
accessorFn: row => row.service.fail_trigger_tasks, accessorFn: (row) => row.service.fail_trigger_tasks,
}, },
{ {
header: "Tasks to trigger after recovery", header: "Tasks to trigger after recovery",
accessorKey: "service.recoverTriggerTasks", accessorKey: "service.recoverTriggerTasks",
accessorFn: row => row.service.recover_trigger_tasks, accessorFn: (row) => row.service.recover_trigger_tasks,
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ fn: deleteService, id: s.service.id, mutate: mutate }}> <ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteService, id: s.service.id, mutate: mutate }}
>
<ServiceCard mutate={mutate} data={s.service} /> <ServiceCard mutate={mutate} data={s.service} />
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const dataArr = useMemo(() => { const dataArr = useMemo(() => {
return conv.recordToArr(data?.services ?? {}); return conv.recordToArr(data?.services ?? {});
@@ -158,74 +162,68 @@ export default function ServicePage() {
data: dataArr, data: dataArr,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
return ( return (
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> <h1 className="flex-1 text-3xl font-bold tracking-tight">Service</h1>
Service <HeaderButtonGroup
</h1> className="flex-2 flex ml-auto gap-2"
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{ delete={{
fn: deleteService, fn: deleteService,
id: selectedRows.map(r => r.original.service.id), id: selectedRows.map((r) => r.original.service.id),
mutate: mutate, mutate: mutate,
}}> }}
>
<ServiceCard mutate={mutate} /> <ServiceCard mutate={mutate} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm"> </div>
{flexRender(cell.column.columnDef.cell, cell.getContext())} );
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div >
)
} }

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react";
import { toast } from "sonner" import { toast } from "sonner";
import { ModelConfig, settingCoverageTypes, nezhaLang } from "@/types" import { ModelConfig, settingCoverageTypes, nezhaLang } from "@/types";
import { SettingsTab } from "@/components/settings-tab" import { SettingsTab } from "@/components/settings-tab";
import { z } from "zod" import { z } from "zod";
import { asOptionalField } from "@/lib/utils" import { asOptionalField } from "@/lib/utils";
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod";
import { import {
Form, Form,
FormControl, FormControl,
@@ -13,24 +13,21 @@ import {
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form" } from "@/components/ui/form";
import { getSettings, updateSettings } from "@/api/settings" import { getSettings, updateSettings } from "@/api/settings";
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { import { Card, CardContent } from "@/components/ui/card";
Card,
CardContent,
} from "@/components/ui/card"
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select";
const settingFormSchema = z.object({ const settingFormSchema = z.object({
custom_nameservers: asOptionalField(z.string()), custom_nameservers: asOptionalField(z.string()),
@@ -56,8 +53,8 @@ export default function SettingsPage() {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -67,21 +64,23 @@ export default function SettingsPage() {
} catch (e) { } catch (e) {
if (e instanceof Error) setError(e); if (e instanceof Error) setError(e);
} }
})() })();
}, []) }, []);
const form = useForm<z.infer<typeof settingFormSchema>>({ const form = useForm<z.infer<typeof settingFormSchema>>({
resolver: zodResolver(settingFormSchema), resolver: zodResolver(settingFormSchema),
defaultValues: config ? config : { defaultValues: config
ip_change_notification_group_id: 0, ? config
cover: 1, : {
site_name: "", ip_change_notification_group_id: 0,
language: "", cover: 1,
}, site_name: "",
language: "",
},
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
} },
}) });
useEffect(() => { useEffect(() => {
if (config) { if (config) {
@@ -101,9 +100,9 @@ export default function SettingsPage() {
} finally { } finally {
toast("Success", { toast("Success", {
description: "Config updated successfully.", description: "Config updated successfully.",
}) });
} }
} };
return ( return (
<div className="px-8"> <div className="px-8">
@@ -118,9 +117,7 @@ export default function SettingsPage() {
<FormItem> <FormItem>
<FormLabel>Site Name</FormLabel> <FormLabel>Site Name</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -141,7 +138,9 @@ export default function SettingsPage() {
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(nezhaLang).map(([k, v]) => ( {Object.entries(nezhaLang).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>
{v}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -157,10 +156,7 @@ export default function SettingsPage() {
<FormItem> <FormItem>
<FormLabel>Custom Codes (Style and Script)</FormLabel> <FormLabel>Custom Codes (Style and Script)</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea className="resize-y min-h-48" {...field} />
className="resize-y min-h-48"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -173,10 +169,7 @@ export default function SettingsPage() {
<FormItem> <FormItem>
<FormLabel>Custom Codes for Dashboard</FormLabel> <FormLabel>Custom Codes for Dashboard</FormLabel>
<FormControl> <FormControl>
<Textarea <Textarea className="resize-y min-h-48" {...field} />
className="resize-y min-h-48"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -189,9 +182,7 @@ export default function SettingsPage() {
<FormItem> <FormItem>
<FormLabel>Dashboard Server Domain/IP without CDN</FormLabel> <FormLabel>Dashboard Server Domain/IP without CDN</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -202,11 +193,11 @@ export default function SettingsPage() {
name="custom_nameservers" name="custom_nameservers"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Custom Public DNS Nameservers for DDNS (separate with comma)</FormLabel> <FormLabel>
Custom Public DNS Nameservers for DDNS (separate with comma)
</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -219,10 +210,7 @@ export default function SettingsPage() {
<FormItem> <FormItem>
<FormLabel>Real IP Header</FormLabel> <FormLabel>Real IP Header</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="NZ::Use-Peer-IP" {...field} />
placeholder="NZ::Use-Peer-IP"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -247,7 +235,9 @@ export default function SettingsPage() {
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{Object.entries(settingCoverageTypes).map(([k, v]) => ( {Object.entries(settingCoverageTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem> <SelectItem key={k} value={k}>
{v}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -262,10 +252,7 @@ export default function SettingsPage() {
<FormItem> <FormItem>
<FormLabel>Specific Servers (separate with comma)</FormLabel> <FormLabel>Specific Servers (separate with comma)</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="1,2,3" {...field} />
placeholder="1,2,3"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -278,10 +265,7 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2"> <FormItem className="flex items-center space-x-2">
<FormControl> <FormControl>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox checked={field.value} onCheckedChange={field.onChange} />
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Enable</Label> <Label className="text-sm">Enable</Label>
</div> </div>
</FormControl> </FormControl>
@@ -300,11 +284,10 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2"> <FormItem className="flex items-center space-x-2">
<FormControl> <FormControl>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox checked={field.value} onCheckedChange={field.onChange} />
checked={field.value} <Label className="text-sm">
onCheckedChange={field.onChange} Show Full IP Address in Notification Messages
/> </Label>
<Label className="text-sm">Show Full IP Address in Notification Messages</Label>
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -316,5 +299,5 @@ export default function SettingsPage() {
</Form> </Form>
</div> </div>
</div> </div>
) );
} }

View File

@@ -1,17 +1,23 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" Table,
import useSWR from "swr" TableBody,
import { useEffect } from "react" TableCell,
import { ActionButtonGroup } from "@/components/action-button-group" TableHead,
import { HeaderButtonGroup } from "@/components/header-button-group" TableHeader,
import { Skeleton } from "@/components/ui/skeleton" TableRow,
import { toast } from "sonner" } from "@/components/ui/table";
import { ModelUser } from "@/types" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { deleteUser } from "@/api/user" import useSWR from "swr";
import { SettingsTab } from "@/components/settings-tab" import { useEffect } from "react";
import { UserCard } from "@/components/user" import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelUser } from "@/types";
import { deleteUser } from "@/api/user";
import { SettingsTab } from "@/components/settings-tab";
import { UserCard } from "@/components/user";
export default function UserPage() { export default function UserPage() {
const { data, mutate, error, isLoading } = useSWR<ModelUser[]>("/api/v1/user", swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelUser[]>("/api/v1/user", swrFetcher);
@@ -20,8 +26,8 @@ export default function UserPage() {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelUser>[] = [ const columns: ColumnDef<ModelUser>[] = [
{ {
@@ -30,7 +36,7 @@ export default function UserPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -49,36 +55,39 @@ export default function UserPage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: row => row.id, accessorFn: (row) => row.id,
}, },
{ {
header: "Username", header: "Username",
accessorKey: "username", accessorKey: "username",
accessorFn: row => row.username, accessorFn: (row) => row.username,
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ <ActionButtonGroup
fn: deleteUser, className="flex gap-2"
id: s.id, delete={{
mutate: mutate, fn: deleteUser,
}}> id: s.id,
mutate: mutate,
}}
>
<></> <></>
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
@@ -86,64 +95,60 @@ export default function UserPage() {
<div className="px-8"> <div className="px-8">
<SettingsTab className="mt-6 w-full" /> <SettingsTab className="mt-6 w-full" />
<div className="flex mt-4 mb-4"> <div className="flex mt-4 mb-4">
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{ <HeaderButtonGroup
fn: deleteUser, className="flex-2 flex gap-2 ml-auto"
id: selectedRows.map(r => r.original.id), delete={{
mutate: mutate, fn: deleteUser,
}}> id: selectedRows.map((r) => r.original.id),
mutate: mutate,
}}
>
<UserCard mutate={mutate} /> <UserCard mutate={mutate} />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4"> <Table>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
</div> {headerGroup.headers.map((header) => {
) : ( return (
<Table> <TableHead key={header.id} className="text-sm">
<TableHeader> {header.isPlaceholder
{table.getHeaderGroups().map((headerGroup) => ( ? null
<TableRow key={headerGroup.id}> : flexRender(header.column.columnDef.header, header.getContext())}
{headerGroup.headers.map((header) => { </TableHead>
return ( );
<TableHead key={header.id} className="text-sm"> })}
{header.isPlaceholder </TableRow>
? null ))}
: flexRender( </TableHeader>
header.column.columnDef.header, <TableBody>
header.getContext() {isLoading ? (
)} <TableRow>
</TableHead> <TableCell colSpan={columns.length} className="h-24 text-center">
) Loading ...
})} </TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div> </div>
) );
} }

View File

@@ -1,17 +1,23 @@
import { swrFetcher } from "@/api/api" import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import {
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table" Table,
import useSWR from "swr" TableBody,
import { useEffect } from "react" TableCell,
import { ActionButtonGroup } from "@/components/action-button-group" TableHead,
import { HeaderButtonGroup } from "@/components/header-button-group" TableHeader,
import { Skeleton } from "@/components/ui/skeleton" TableRow,
import { toast } from "sonner" } from "@/components/ui/table";
import { ModelWAF, wafBlockReasons } from "@/types" import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import { deleteWAF } from "@/api/waf" import useSWR from "swr";
import { ip16Str } from "@/lib/utils" import { useEffect } from "react";
import { SettingsTab } from "@/components/settings-tab" import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelWAF, wafBlockReasons } from "@/types";
import { deleteWAF } from "@/api/waf";
import { ip16Str } from "@/lib/utils";
import { SettingsTab } from "@/components/settings-tab";
export default function WAFPage() { export default function WAFPage() {
const { data, mutate, error, isLoading } = useSWR<ModelWAF[]>("/api/v1/waf", swrFetcher); const { data, mutate, error, isLoading } = useSWR<ModelWAF[]>("/api/v1/waf", swrFetcher);
@@ -20,8 +26,8 @@ export default function WAFPage() {
if (error) if (error)
toast("Error", { toast("Error", {
description: `Error fetching resource: ${error.message}.`, description: `Error fetching resource: ${error.message}.`,
}) });
}, [error]) }, [error]);
const columns: ColumnDef<ModelWAF>[] = [ const columns: ColumnDef<ModelWAF>[] = [
{ {
@@ -30,7 +36,7 @@ export default function WAFPage() {
<Checkbox <Checkbox
checked={ checked={
table.getIsAllPageRowsSelected() || table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate") (table.getIsSomePageRowsSelected() && "indeterminate")
} }
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all" aria-label="Select all"
@@ -49,54 +55,55 @@ export default function WAFPage() {
{ {
header: "IP", header: "IP",
accessorKey: "ip", accessorKey: "ip",
accessorFn: row => ip16Str(row.ip ?? []), accessorFn: (row) => ip16Str(row.ip ?? []),
}, },
{ {
header: "Count", header: "Count",
accessorKey: "count", accessorKey: "count",
accessorFn: row => row.count, accessorFn: (row) => row.count,
}, },
{ {
header: "Last Block Reason", header: "Last Block Reason",
accessorKey: "lastBlockReason", accessorKey: "lastBlockReason",
accessorFn: row => row.last_block_reason, accessorFn: (row) => row.last_block_reason,
cell: ({ row }) => ( cell: ({ row }) => <span>{wafBlockReasons[row.original.last_block_reason] || ""}</span>,
<span>{wafBlockReasons[row.original.last_block_reason] || ''}</span>
)
}, },
{ {
header: "Last Block Time", header: "Last Block Time",
accessorKey: "lastBlockTime", accessorKey: "lastBlockTime",
accessorFn: row => row.last_block_timestamp, accessorFn: (row) => row.last_block_timestamp,
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original; const s = row.original;
const date = new Date(s.last_block_timestamp || 0); const date = new Date(s.last_block_timestamp || 0);
return <span>{date.toISOString()}</span> return <span>{date.toISOString()}</span>;
} },
}, },
{ {
id: "actions", id: "actions",
header: "Actions", header: "Actions",
cell: ({ row }) => { cell: ({ row }) => {
const s = row.original const s = row.original;
return ( return (
<ActionButtonGroup className="flex gap-2" delete={{ <ActionButtonGroup
fn: deleteWAF, className="flex gap-2"
id: ip16Str(s.ip ?? []), delete={{
mutate: mutate, fn: deleteWAF,
}}> id: ip16Str(s.ip ?? []),
mutate: mutate,
}}
>
<></> <></>
</ActionButtonGroup> </ActionButtonGroup>
) );
}, },
}, },
] ];
const table = useReactTable({ const table = useReactTable({
data: data ?? [], data: data ?? [],
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) });
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
@@ -104,64 +111,59 @@ export default function WAFPage() {
<div className="px-8"> <div className="px-8">
<SettingsTab className="mt-6 w-full" /> <SettingsTab className="mt-6 w-full" />
<div className="flex mt-4 mb-4"> <div className="flex mt-4 mb-4">
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{ <HeaderButtonGroup
fn: deleteWAF, className="flex-2 flex gap-2 ml-auto"
id: selectedRows.map(r => ip16Str(r.original.ip ?? [])), delete={{
mutate: mutate, fn: deleteWAF,
}}> id: selectedRows.map((r) => ip16Str(r.original.ip ?? [])),
mutate: mutate,
}}
>
<></> <></>
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
{isLoading ? ( <Table>
<div className="flex flex-col items-center space-y-4"> <TableHeader>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {table.getHeaderGroups().map((headerGroup) => (
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> <TableRow key={headerGroup.id}>
<Skeleton className="h-[60px] w-[100%] rounded-lg" /> {headerGroup.headers.map((header) => {
</div> return (
) : ( <TableHead key={header.id} className="text-sm">
<Table> {header.isPlaceholder
<TableHeader> ? null
{table.getHeaderGroups().map((headerGroup) => ( : flexRender(header.column.columnDef.header, header.getContext())}
<TableRow key={headerGroup.id}> </TableHead>
{headerGroup.headers.map((header) => { );
return ( })}
<TableHead key={header.id} className="text-sm"> </TableRow>
{header.isPlaceholder ))}
? null </TableHeader>
: flexRender( <TableBody>
header.column.columnDef.header, {isLoading ? (
header.getContext() <TableRow>
)} <TableCell colSpan={columns.length} className="h-24 text-center">
</TableHead> Loading ...
) </TableCell>
})} </TableRow>
) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell colSpan={columns.length} className="h-24 text-center">
table.getRowModel().rows.map((row) => ( No results.
<TableRow </TableCell>
key={row.id} </TableRow>
data-state={row.getIsSelected() && "selected"} )}
> </TableBody>
{row.getVisibleCells().map((cell) => ( </Table>
<TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div> </div>
) );
} }