mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-05 13:10:08 +00:00
Optimize loading and style (#2)
This commit is contained in:
@@ -23,6 +23,7 @@ export default tseslint.config(
|
|||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
|
"indent": ['error', 4],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
158
src/main.tsx
158
src/main.tsx
@@ -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>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 >
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 >
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 >
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 >
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user