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',
{ allowConstantExport: true },
],
"indent": ['error', 4],
},
},
)

View File

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

View File

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

View File

@@ -1,28 +1,34 @@
import { swrFetcher } from "@/api/api"
import { Checkbox } from "@/components/ui/checkbox"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ModelCron } from "@/types"
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 { Skeleton } from "@/components/ui/skeleton"
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"
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelCron } from "@/types";
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 { 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() {
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>('/api/v1/cron', swrFetcher);
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher);
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
})
}, [error])
});
}, [error]);
const columns: ColumnDef<ModelCron>[] = [
{
@@ -31,7 +37,7 @@ export default function CronPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -50,205 +56,193 @@ export default function CronPage() {
{
header: "ID",
accessorKey: "id",
accessorFn: row => row.id,
accessorFn: (row) => row.id,
},
{
header: "Name",
accessorKey: "name",
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-32 whitespace-normal break-words">
{s.name}
</div>
)
}
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
},
},
{
header: "Task Type",
accessorKey: "taskType",
accessorFn: row => cronTypes[row.task_type] || '',
accessorFn: (row) => cronTypes[row.task_type] || "",
},
{
header: "Cron Expression",
accessorKey: "scheduler",
accessorFn: row => row.scheduler,
accessorFn: (row) => row.scheduler,
},
{
header: "Command",
accessorKey: "command",
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-48 whitespace-normal break-words">
{s.command}
</div>
)
}
return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>;
},
},
{
header: "Notifier Group",
accessorKey: "ngroup",
accessorFn: row => row.notification_group_id,
accessorFn: (row) => row.notification_group_id,
},
{
header: "Send Success Notification",
accessorKey: "pushSuccessful",
accessorFn: row => row.push_successful ?? false,
accessorFn: (row) => row.push_successful ?? false,
},
{
header: "Coverage",
accessorKey: "cover",
accessorFn: row => row.cover,
accessorFn: (row) => row.cover,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-48 whitespace-normal break-words">
{(() => {
switch (s.cover) {
case 0: {
return <span>Ignore All</span>
}
case 1: {
return <span>Cover All</span>
}
case 2: {
return <span>On alert</span>
}
case 0: {
return <span>Ignore All</span>;
}
case 1: {
return <span>Cover All</span>;
}
case 2: {
return <span>On alert</span>;
}
}
})()}
</div>
)
}
);
},
},
{
header: "Specific Servers",
accessorKey: "servers",
accessorFn: row => row.servers,
accessorFn: (row) => row.servers,
},
{
header: "Last Execution",
accessorKey: "lastExecution",
accessorFn: row => row.last_executed_at,
accessorFn: (row) => row.last_executed_at,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-24 whitespace-normal break-words">
{s.last_executed_at}
</div>
)
}
return <div className="max-w-24 whitespace-normal break-words">{s.last_executed_at}</div>;
},
},
{
header: "Last Result",
accessorKey: "lastResult",
accessorFn: row => row.last_result ?? false,
accessorFn: (row) => row.last_result ?? false,
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const s = row.original
const s = row.original;
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={
async () => {
<IconButton
variant="outline"
icon="play"
onClick={async () => {
try {
await runCron(s.id);
} catch (e) {
console.error(e);
toast("Error executing task", {
description: "Please see the console for details.",
})
});
await mutate();
return;
}
toast("Success", {
description: "The task triggered successfully.",
})
});
await mutate();
}} />
}}
/>
<CronCard mutate={mutate} data={s} />
</>
</ActionButtonGroup>
)
);
},
},
]
];
const table = useReactTable({
data: data ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
})
});
const selectedRows = table.getSelectedRowModel().rows;
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">
Task
</h1>
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
fn: deleteCron,
id: selectedRows.map(r => r.original.id),
mutate: mutate,
}}>
<h1 className="flex-1 text-3xl font-bold tracking-tight">Task</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
fn: deleteCron,
id: selectedRows.map((r) => r.original.id),
mutate: mutate,
}}
>
<CronCard mutate={mutate} />
</HeaderButtonGroup>
</div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4">
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<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>
))}
</TableHeader>
<TableBody>
{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>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div >
)
))
) : (
<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 { Checkbox } from "@/components/ui/checkbox"
import { DDNSCard } from "@/components/ddns"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ModelDDNSProfile } from "@/types"
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 { Skeleton } from "@/components/ui/skeleton"
import { toast } from "sonner"
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns"
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { DDNSCard } from "@/components/ddns";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelDDNSProfile } from "@/types";
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() {
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[]>([]);
useEffect(() => {
@@ -28,8 +34,8 @@ export default function DDNSPage() {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
})
}, [error])
});
}, [error]);
const columns: ColumnDef<ModelDDNSProfile>[] = [
{
@@ -38,7 +44,7 @@ export default function DDNSPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -57,140 +63,129 @@ export default function DDNSPage() {
{
header: "ID",
accessorKey: "id",
accessorFn: row => row.id,
accessorFn: (row) => row.id,
},
{
header: "Name",
accessorKey: "name",
accessorFn: row => row.name,
accessorFn: (row) => row.name,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-24 whitespace-normal break-words">
{s.name}
</div>
)
}
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
},
},
{
header: "IPv4 Enabled",
accessorKey: "enableIPv4",
accessorFn: row => row.enable_ipv4 ?? false,
accessorFn: (row) => row.enable_ipv4 ?? false,
},
{
header: "IPv6 Enabled",
accessorKey: "enableIPv6",
accessorFn: row => row.enable_ipv6 ?? false,
accessorFn: (row) => row.enable_ipv6 ?? false,
},
{
header: "DDNS Provider",
accessorKey: "provider",
accessorFn: row => row.provider,
accessorFn: (row) => row.provider,
},
{
header: "Domains",
accessorKey: "domains",
accessorFn: row => row.domains,
accessorFn: (row) => row.domains,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-24 whitespace-normal break-words">
{s.domains}
</div>
)
}
return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>;
},
},
{
header: "Maximum retry attempts",
accessorKey: "maxRetries",
accessorFn: row => row.max_retries,
accessorFn: (row) => row.max_retries,
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const s = row.original
const s = row.original;
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} />
</ActionButtonGroup>
)
);
},
},
]
];
const table = useReactTable({
data: data ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
})
});
const selectedRows = table.getSelectedRowModel().rows;
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">
Dynamic DNS
</h1>
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
fn: deleteDDNSProfiles,
id: selectedRows.map(r => r.original.id),
mutate: mutate,
}}>
<h1 className="flex-1 text-3xl font-bold tracking-tight">Dynamic DNS</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
fn: deleteDDNSProfiles,
id: selectedRows.map((r) => r.original.id),
mutate: mutate,
}}
>
<DDNSCard mutate={mutate} providers={providers} />
</HeaderButtonGroup>
</div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4">
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<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>
))}
</TableHeader>
<TableBody>
{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>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div >
)
))
) : (
<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 { Checkbox } from "@/components/ui/checkbox"
import { NATCard } from "@/components/nat"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ModelNAT } from "@/types"
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 { Skeleton } from "@/components/ui/skeleton"
import { toast } from "sonner"
import { deleteNAT } from "@/api/nat"
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { NATCard } from "@/components/nat";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelNAT } from "@/types";
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() {
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>('/api/v1/nat', swrFetcher);
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher);
useEffect(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
})
}, [error])
});
}, [error]);
const columns: ColumnDef<ModelNAT>[] = [
{
@@ -29,7 +35,7 @@ export default function NATPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -48,138 +54,123 @@ export default function NATPage() {
{
header: "ID",
accessorKey: "id",
accessorFn: row => row.id,
accessorFn: (row) => row.id,
},
{
header: "Name",
accessorKey: "name",
accessorFn: row => row.name,
accessorFn: (row) => row.name,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-32 whitespace-normal break-words">
{s.name}
</div>
)
}
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
},
},
{
header: "Server ID",
accessorKey: "serverID",
accessorFn: row => row.server_id,
accessorFn: (row) => row.server_id,
},
{
header: "Local service",
accessorKey: "host",
accessorFn: row => row.host,
accessorFn: (row) => row.host,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-32 whitespace-normal break-words">
{s.host}
</div>
)
}
return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>;
},
},
{
header: "Bind hostname",
accessorKey: "domain",
accessorFn: row => row.domain,
accessorFn: (row) => row.domain,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-32 whitespace-normal break-words">
{s.domain}
</div>
)
}
return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>;
},
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const s = row.original
const s = row.original;
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} />
</ActionButtonGroup>
)
);
},
},
]
];
const table = useReactTable({
data: data ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
})
});
const selectedRows = table.getSelectedRowModel().rows;
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">
NAT Traversal
</h1>
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
fn: deleteNAT,
id: selectedRows.map(r => r.original.id),
mutate: mutate,
}}>
<h1 className="flex-1 text-3xl font-bold tracking-tight">NAT Traversal</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
fn: deleteNAT,
id: selectedRows.map((r) => r.original.id),
mutate: mutate,
}}
>
<NATCard mutate={mutate} />
</HeaderButtonGroup>
</div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4">
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<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>
))}
</TableHeader>
<TableBody>
{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>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div >
)
))
) : (
<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 { Checkbox } from "@/components/ui/checkbox"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
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 { Skeleton } from "@/components/ui/skeleton"
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"
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
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 { 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() {
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(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
})
}, [error])
});
}, [error]);
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [
{
@@ -30,7 +39,7 @@ export default function NotificationGroupPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -49,49 +58,48 @@ export default function NotificationGroupPage() {
{
header: "ID",
accessorKey: "id",
accessorFn: row => row.group.id,
accessorFn: (row) => row.group.id,
},
{
header: "Name",
accessorKey: "name",
accessorFn: row => row.group.name,
accessorFn: (row) => row.group.name,
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-48 whitespace-normal break-words">
{s.group.name}
</div>
)
}
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>;
},
},
{
header: "Notifiers (ID)",
accessorKey: "notifications",
accessorFn: row => row.notifications,
accessorFn: (row) => row.notifications,
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const s = row.original
const s = row.original;
return (
<ActionButtonGroup className="flex gap-2" delete={{
fn: deleteNotificationGroups,
id: s.group.id,
mutate: mutate,
}}>
<ActionButtonGroup
className="flex gap-2"
delete={{
fn: deleteNotificationGroups,
id: s.group.id,
mutate: mutate,
}}
>
<NotificationGroupCard mutate={mutate} data={s} />
</ActionButtonGroup>
)
);
},
},
]
];
const table = useReactTable({
data: data ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
})
});
const selectedRows = table.getSelectedRowModel().rows;
@@ -99,64 +107,60 @@ export default function NotificationGroupPage() {
<div className="px-8">
<div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
fn: deleteNotificationGroups,
id: selectedRows.map(r => r.original.group.id),
mutate: mutate
}}>
<HeaderButtonGroup
className="flex-2 flex gap-2 ml-auto"
delete={{
fn: deleteNotificationGroups,
id: selectedRows.map((r) => r.original.group.id),
mutate: mutate,
}}
>
<NotificationGroupCard mutate={mutate} />
</HeaderButtonGroup>
</div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4">
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<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>
))}
</TableHeader>
<TableBody>
{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>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
);
}

View File

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

View File

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

View File

@@ -1,28 +1,37 @@
import { swrFetcher } from "@/api/api"
import { Checkbox } from "@/components/ui/checkbox"
import { ServiceCard } from "@/components/service"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ModelServiceResponse, ModelServiceResponseItem as Service } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import useSWR from "swr"
import { conv } from "@/lib/utils"
import { useEffect, useMemo } from "react"
import { serviceTypes } from "@/types"
import { ActionButtonGroup } from "@/components/action-button-group"
import { deleteService } from "@/api/service"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { Skeleton } from "@/components/ui/skeleton"
import { toast } from "sonner"
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { ServiceCard } from "@/components/service";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelServiceResponse, ModelServiceResponseItem as Service } from "@/types";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { conv } from "@/lib/utils";
import { useEffect, useMemo } from "react";
import { serviceTypes } from "@/types";
import { ActionButtonGroup } from "@/components/action-button-group";
import { deleteService } from "@/api/service";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
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(() => {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
})
}, [error])
});
}, [error]);
const columns: ColumnDef<Service>[] = [
{
@@ -31,7 +40,7 @@ export default function ServicePage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -50,105 +59,100 @@ export default function ServicePage() {
{
header: "ID",
accessorKey: "service.id",
accessorFn: row => row.service.id,
accessorFn: (row) => row.service.id,
},
{
header: "Name",
accessorFn: row => row.service.name,
accessorFn: (row) => row.service.name,
accessorKey: "service.name",
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-24 whitespace-normal break-words">
{s.service.name}
</div>
)
}
return <div className="max-w-24 whitespace-normal break-words">{s.service.name}</div>;
},
},
{
header: "Target",
accessorFn: row => row.service.target,
accessorFn: (row) => row.service.target,
accessorKey: "service.target",
cell: ({ row }) => {
const s = row.original;
return (
<div className="max-w-24 whitespace-normal break-words">
{s.service.target}
</div>
)
}
return <div className="max-w-24 whitespace-normal break-words">{s.service.target}</div>;
},
},
{
header: "Coverage",
accessorKey: "service.cover",
accessorFn: row => row.service.cover,
accessorFn: (row) => row.service.cover,
cell: ({ row }) => {
const s = row.original.service;
return (
<div className="max-w-48 whitespace-normal break-words">
{(() => {
switch (s.cover) {
case 0: {
return <span>Cover All</span>
}
case 1: {
return <span>Ignore All</span>
}
case 0: {
return <span>Cover All</span>;
}
case 1: {
return <span>Ignore All</span>;
}
}
})()}
</div>
)
}
);
},
},
{
header: "Specific Servers",
accessorKey: "service.skipServers",
accessorFn: row => Object.keys(row.service.skip_servers ?? {}),
accessorFn: (row) => Object.keys(row.service.skip_servers ?? {}),
},
{
header: "Type",
accessorKey: "service.type",
accessorFn: row => row.service.type,
cell: ({ row }) => serviceTypes[row.original.service.type] || '',
accessorFn: (row) => row.service.type,
cell: ({ row }) => serviceTypes[row.original.service.type] || "",
},
{
header: "Interval",
accessorKey: "service.duration",
accessorFn: row => row.service.duration,
accessorFn: (row) => row.service.duration,
},
{
header: "Notifier Group ID",
accessorKey: "service.ngroup",
accessorFn: row => row.service.notification_group_id,
accessorFn: (row) => row.service.notification_group_id,
},
{
header: "On Trigger",
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",
accessorKey: "service.failTriggerTasks",
accessorFn: row => row.service.fail_trigger_tasks,
accessorFn: (row) => row.service.fail_trigger_tasks,
},
{
header: "Tasks to trigger after recovery",
accessorKey: "service.recoverTriggerTasks",
accessorFn: row => row.service.recover_trigger_tasks,
accessorFn: (row) => row.service.recover_trigger_tasks,
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const s = row.original
const s = row.original;
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} />
</ActionButtonGroup>
)
);
},
},
]
];
const dataArr = useMemo(() => {
return conv.recordToArr(data?.services ?? {});
@@ -158,74 +162,68 @@ export default function ServicePage() {
data: dataArr,
columns,
getCoreRowModel: getCoreRowModel(),
})
});
const selectedRows = table.getSelectedRowModel().rows;
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">
Service
</h1>
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
fn: deleteService,
id: selectedRows.map(r => r.original.service.id),
mutate: mutate,
}}>
<h1 className="flex-1 text-3xl font-bold tracking-tight">Service</h1>
<HeaderButtonGroup
className="flex-2 flex ml-auto gap-2"
delete={{
fn: deleteService,
id: selectedRows.map((r) => r.original.service.id),
mutate: mutate,
}}
>
<ServiceCard mutate={mutate} />
</HeaderButtonGroup>
</div>
{isLoading ? (
<div className="flex flex-col items-center space-y-4">
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
<Skeleton className="h-[60px] w-[100%] rounded-lg" />
</div>
) : (
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<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>
))}
</TableHeader>
<TableBody>
{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>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div >
)
))
) : (
<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 { toast } from "sonner"
import { ModelConfig, settingCoverageTypes, nezhaLang } from "@/types"
import { SettingsTab } from "@/components/settings-tab"
import { z } from "zod"
import { asOptionalField } from "@/lib/utils"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { ModelConfig, settingCoverageTypes, nezhaLang } from "@/types";
import { SettingsTab } from "@/components/settings-tab";
import { z } from "zod";
import { asOptionalField } from "@/lib/utils";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
@@ -13,24 +13,21 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { getSettings, updateSettings } from "@/api/settings"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
} from "@/components/ui/card"
} from "@/components/ui/form";
import { getSettings, updateSettings } from "@/api/settings";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
} from "@/components/ui/select";
const settingFormSchema = z.object({
custom_nameservers: asOptionalField(z.string()),
@@ -56,8 +53,8 @@ export default function SettingsPage() {
if (error)
toast("Error", {
description: `Error fetching resource: ${error.message}.`,
})
}, [error])
});
}, [error]);
useEffect(() => {
(async () => {
@@ -67,21 +64,23 @@ export default function SettingsPage() {
} catch (e) {
if (e instanceof Error) setError(e);
}
})()
}, [])
})();
}, []);
const form = useForm<z.infer<typeof settingFormSchema>>({
resolver: zodResolver(settingFormSchema),
defaultValues: config ? config : {
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
},
defaultValues: config
? config
: {
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
},
resetOptions: {
keepDefaultValues: false,
}
})
},
});
useEffect(() => {
if (config) {
@@ -101,9 +100,9 @@ export default function SettingsPage() {
} finally {
toast("Success", {
description: "Config updated successfully.",
})
});
}
}
};
return (
<div className="px-8">
@@ -118,9 +117,7 @@ export default function SettingsPage() {
<FormItem>
<FormLabel>Site Name</FormLabel>
<FormControl>
<Input
{...field}
/>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -141,7 +138,9 @@ export default function SettingsPage() {
</FormControl>
<SelectContent>
{Object.entries(nezhaLang).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem>
<SelectItem key={k} value={k}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
@@ -157,10 +156,7 @@ export default function SettingsPage() {
<FormItem>
<FormLabel>Custom Codes (Style and Script)</FormLabel>
<FormControl>
<Textarea
className="resize-y min-h-48"
{...field}
/>
<Textarea className="resize-y min-h-48" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -173,10 +169,7 @@ export default function SettingsPage() {
<FormItem>
<FormLabel>Custom Codes for Dashboard</FormLabel>
<FormControl>
<Textarea
className="resize-y min-h-48"
{...field}
/>
<Textarea className="resize-y min-h-48" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -189,9 +182,7 @@ export default function SettingsPage() {
<FormItem>
<FormLabel>Dashboard Server Domain/IP without CDN</FormLabel>
<FormControl>
<Input
{...field}
/>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -202,11 +193,11 @@ export default function SettingsPage() {
name="custom_nameservers"
render={({ field }) => (
<FormItem>
<FormLabel>Custom Public DNS Nameservers for DDNS (separate with comma)</FormLabel>
<FormLabel>
Custom Public DNS Nameservers for DDNS (separate with comma)
</FormLabel>
<FormControl>
<Input
{...field}
/>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -219,10 +210,7 @@ export default function SettingsPage() {
<FormItem>
<FormLabel>Real IP Header</FormLabel>
<FormControl>
<Input
placeholder="NZ::Use-Peer-IP"
{...field}
/>
<Input placeholder="NZ::Use-Peer-IP" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -247,7 +235,9 @@ export default function SettingsPage() {
</FormControl>
<SelectContent>
{Object.entries(settingCoverageTypes).map(([k, v]) => (
<SelectItem key={k} value={k}>{v}</SelectItem>
<SelectItem key={k} value={k}>
{v}
</SelectItem>
))}
</SelectContent>
</Select>
@@ -262,10 +252,7 @@ export default function SettingsPage() {
<FormItem>
<FormLabel>Specific Servers (separate with comma)</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
{...field}
/>
<Input placeholder="1,2,3" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -278,10 +265,7 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Label className="text-sm">Enable</Label>
</div>
</FormControl>
@@ -300,11 +284,10 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">Show Full IP Address in Notification Messages</Label>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Label className="text-sm">
Show Full IP Address in Notification Messages
</Label>
</div>
</FormControl>
<FormMessage />
@@ -316,5 +299,5 @@ export default function SettingsPage() {
</Form>
</div>
</div>
)
);
}

View File

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

View File

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