Dashboard Redesign (#48)

* feat: add user_template setting

* style: header

* style: page padding

* style: header

* feat: header now time

* style: login page

* feat: nav indicator

* style: button inset shadow

* style: footer text size

* feat: header show login_ip

* fix: error toast

* fix: frontend_templates setting

* fix: lint

* feat: pr auto format

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
This commit is contained in:
仓鼠
2024-12-13 23:51:33 +08:00
committed by GitHub
parent b04ef1bb72
commit 8c8d3e3057
132 changed files with 13242 additions and 12878 deletions
+41 -35
View File
@@ -1,5 +1,10 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { deleteAlertRules } from "@/api/alert-rule"
import { swrFetcher } from "@/api/api"
import { ActionButtonGroup } from "@/components/action-button-group"
import { AlertRuleCard } from "@/components/alert-rule"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { NotificationTab } from "@/components/notification-tab"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -7,35 +12,29 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelAlertRule, triggerModes } from "@/types";
import { deleteAlertRules } from "@/api/alert-rule";
import { NotificationTab } from "@/components/notification-tab";
import { AlertRuleCard } from "@/components/alert-rule";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { ModelAlertRule, triggerModes } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function AlertRulePage() {
const { t } = useTranslation();
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelAlertRule[]>(
"/api/v1/alert-rule",
swrFetcher
);
swrFetcher,
)
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
}, [error])
const columns: ColumnDef<ModelAlertRule>[] = [
{
@@ -70,8 +69,8 @@ export default function AlertRulePage() {
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
const s = row.original
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
},
},
{
@@ -87,8 +86,12 @@ export default function AlertRulePage() {
{
header: t("Rules"),
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-48 whitespace-normal break-words">{JSON.stringify(s.rules)}</div>;
const s = row.original
return (
<div className="max-w-48 whitespace-normal break-words">
{JSON.stringify(s.rules)}
</div>
)
},
},
{
@@ -110,7 +113,7 @@ export default function AlertRulePage() {
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -122,25 +125,25 @@ export default function AlertRulePage() {
>
<AlertRuleCard mutate={mutate} data={s} />
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup
@@ -164,9 +167,12 @@ export default function AlertRulePage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -198,5 +204,5 @@ export default function AlertRulePage() {
</TableBody>
</Table>
</div>
);
)
}
+66 -58
View File
@@ -1,5 +1,9 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { swrFetcher } from "@/api/api"
import { deleteCron, runCron } from "@/api/cron"
import { ActionButtonGroup } from "@/components/action-button-group"
import { CronCard } from "@/components/cron"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -7,32 +11,29 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelCron } from "@/types";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { deleteCron, runCron } from "@/api/cron";
import { CronCard } from "@/components/cron";
import { cronTypes } from "@/types";
import { IconButton } from "@/components/xui/icon-button";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { IconButton } from "@/components/xui/icon-button"
import { ModelCron } from "@/types"
import { cronTypes } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function CronPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher);
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelCron[]>("/api/v1/cron", swrFetcher)
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelCron>[] = [
{
@@ -41,7 +42,7 @@ export default function CronPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -66,8 +67,8 @@ export default function CronPage() {
header: t("Name"),
accessorKey: "name",
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
const s = row.original
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
},
},
{
@@ -84,8 +85,8 @@ export default function CronPage() {
header: t("Command"),
accessorKey: "command",
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>;
const s = row.original
return <div className="max-w-48 whitespace-normal break-words">{s.command}</div>
},
},
{
@@ -103,24 +104,24 @@ export default function CronPage() {
accessorKey: "cover",
accessorFn: (row) => row.cover,
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<div className="max-w-48 whitespace-normal break-words">
{(() => {
switch (s.cover) {
case 0: {
return <span>Ignore All</span>;
}
case 1: {
return <span>Cover All</span>;
}
case 2: {
return <span>On alert</span>;
}
case 0: {
return <span>Ignore All</span>
}
case 1: {
return <span>Cover All</span>
}
case 2: {
return <span>On alert</span>
}
}
})()}
</div>
);
)
},
},
{
@@ -133,8 +134,12 @@ export default function CronPage() {
accessorKey: "lastExecution",
accessorFn: (row) => row.last_executed_at,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-24 whitespace-normal break-words">{s.last_executed_at}</div>;
const s = row.original
return (
<div className="max-w-24 whitespace-normal break-words">
{s.last_executed_at}
</div>
)
},
},
{
@@ -146,7 +151,7 @@ export default function CronPage() {
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -158,43 +163,43 @@ export default function CronPage() {
icon="play"
onClick={async () => {
try {
await runCron(s.id);
await runCron(s.id)
} catch (e) {
console.error(e);
console.error(e)
toast(t("Error"), {
description: t("Results.UnExpectedError"),
});
await mutate();
return;
})
await mutate()
return
}
toast(t("Success"), {
description: t("Results.TaskTriggeredSuccessfully"),
});
await mutate();
})
await mutate()
}}
/>
<CronCard mutate={mutate} data={s} />
</>
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Task")}</h1>
<HeaderButtonGroup
@@ -218,9 +223,12 @@ export default function CronPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -252,5 +260,5 @@ export default function CronPage() {
</TableBody>
</Table>
</div>
);
)
}
+55 -44
View File
@@ -1,6 +1,9 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { DDNSCard } from "@/components/ddns";
import { swrFetcher } from "@/api/api"
import { deleteDDNSProfiles, getDDNSProviders } from "@/api/ddns"
import { ActionButtonGroup } from "@/components/action-button-group"
import { DDNSCard } from "@/components/ddns"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -8,38 +11,39 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelDDNSProfile } from "@/types";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useState, useMemo } 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";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { ModelDDNSProfile } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function DDNSPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>("/api/v1/ddns", swrFetcher);
const [providers, setProviders] = useState<string[]>([]);
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelDDNSProfile[]>(
"/api/v1/ddns",
swrFetcher,
)
const [providers, setProviders] = useState<string[]>([])
useEffect(() => {
const fetchProviders = async () => {
const fetchedProviders = await getDDNSProviders();
setProviders(fetchedProviders);
};
fetchProviders();
}, []);
const fetchedProviders = await getDDNSProviders()
setProviders(fetchedProviders)
}
fetchProviders()
}, [])
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelDDNSProfile>[] = [
{
@@ -48,7 +52,7 @@ export default function DDNSPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -74,8 +78,8 @@ export default function DDNSPage() {
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
const s = row.original
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>
},
},
{
@@ -94,12 +98,12 @@ export default function DDNSPage() {
accessorFn: (row) => row.provider,
},
{
header: t('Domains'),
header: t("Domains"),
accessorKey: "domains",
accessorFn: (row) => row.domains,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>;
const s = row.original
return <div className="max-w-24 whitespace-normal break-words">{s.domains}</div>
},
},
{
@@ -111,33 +115,37 @@ export default function DDNSPage() {
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
delete={{ fn: deleteDDNSProfiles, id: s.id, mutate: mutate }}
delete={{
fn: deleteDDNSProfiles,
id: s.id,
mutate: mutate,
}}
>
<DDNSCard mutate={mutate} data={s} providers={providers} />
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1>
<HeaderButtonGroup
@@ -161,9 +169,12 @@ export default function DDNSPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -195,5 +206,5 @@ export default function DDNSPage() {
</TableBody>
</Table>
</div>
);
)
}
+14 -12
View File
@@ -1,7 +1,3 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
@@ -13,9 +9,11 @@ import {
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { useAuth } from "@/hooks/useAuth"
import { zodResolver } from "@hookform/resolvers/zod"
import i18next from "i18next"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import i18next from "i18next";
import { z } from "zod"
const formSchema = z.object({
username: z.string().min(2, {
@@ -23,10 +21,9 @@ const formSchema = z.object({
}),
password: z.string().min(1, {
message: i18next.t("Results.PasswordRequired"),
})
}),
})
function Login() {
const { login } = useAuth()
@@ -42,10 +39,10 @@ function Login() {
login(values.username, values.password)
}
const { t } = useTranslation();
const { t } = useTranslation()
return (
<div className="my-8 max-w-xl m-auto">
<div className="mt-28 max-w-sm m-auto">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
@@ -68,7 +65,12 @@ function Login() {
<FormItem>
<FormLabel>{t("Password")}</FormLabel>
<FormControl>
<Input type="password" placeholder="admin" autoComplete="current-password" {...field} />
<Input
type="password"
placeholder="admin"
autoComplete="current-password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -81,4 +83,4 @@ function Login() {
)
}
export default Login;
export default Login
+43 -39
View File
@@ -1,6 +1,9 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { NATCard } from "@/components/nat";
import { swrFetcher } from "@/api/api"
import { deleteNAT } from "@/api/nat"
import { ActionButtonGroup } from "@/components/action-button-group"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { NATCard } from "@/components/nat"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -8,29 +11,27 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelNAT } from "@/types";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useMemo } 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";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { ModelNAT } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function NATPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher);
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelNAT[]>("/api/v1/nat", swrFetcher)
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelNAT>[] = [
{
@@ -39,7 +40,7 @@ export default function NATPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -65,12 +66,12 @@ export default function NATPage() {
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
const s = row.original
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
},
},
{
header: t("Server")+" ID",
header: t("Server") + " ID",
accessorKey: "serverID",
accessorFn: (row) => row.server_id,
},
@@ -79,8 +80,8 @@ export default function NATPage() {
accessorKey: "host",
accessorFn: (row) => row.host,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>;
const s = row.original
return <div className="max-w-32 whitespace-normal break-words">{s.host}</div>
},
},
{
@@ -88,15 +89,15 @@ export default function NATPage() {
accessorKey: "domain",
accessorFn: (row) => row.domain,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>;
const s = row.original
return <div className="max-w-32 whitespace-normal break-words">{s.domain}</div>
},
},
{
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -104,25 +105,25 @@ export default function NATPage() {
>
<NATCard mutate={mutate} data={s} />
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1>
<HeaderButtonGroup
@@ -146,9 +147,12 @@ export default function NATPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -180,5 +184,5 @@ export default function NATPage() {
</TableBody>
</Table>
</div>
);
)
}
+41 -37
View File
@@ -1,5 +1,10 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { swrFetcher } from "@/api/api"
import { deleteNotificationGroups } from "@/api/notification-group"
import { ActionButtonGroup } from "@/components/action-button-group"
import { GroupTab } from "@/components/group-tab"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { NotificationGroupCard } from "@/components/notification-group"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -7,34 +12,30 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelNotificationGroupResponseItem } from "@/types";
import { deleteNotificationGroups } from "@/api/notification-group";
import { GroupTab } from "@/components/group-tab";
import { NotificationGroupCard } from "@/components/notification-group";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { ModelNotificationGroupResponseItem } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function NotificationGroupPage() {
const { t } = useTranslation();
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelNotificationGroupResponseItem[]>(
"/api/v1/notification-group",
swrFetcher
);
swrFetcher,
)
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelNotificationGroupResponseItem>[] = [
{
@@ -43,7 +44,7 @@ export default function NotificationGroupPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -69,12 +70,12 @@ export default function NotificationGroupPage() {
accessorKey: "name",
accessorFn: (row) => row.group.name,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>;
const s = row.original
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>
},
},
{
header: t("Notifier")+"(ID)",
header: t("Notifier") + "(ID)",
accessorKey: "notifications",
accessorFn: (row) => row.notifications,
},
@@ -82,7 +83,7 @@ export default function NotificationGroupPage() {
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -94,25 +95,25 @@ export default function NotificationGroupPage() {
>
<NotificationGroupCard mutate={mutate} data={s} />
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup
@@ -136,9 +137,12 @@ export default function NotificationGroupPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -170,5 +174,5 @@ export default function NotificationGroupPage() {
</TableBody>
</Table>
</div>
);
)
}
+45 -42
View File
@@ -1,5 +1,10 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { swrFetcher } from "@/api/api"
import { deleteNotification } from "@/api/notification"
import { ActionButtonGroup } from "@/components/action-button-group"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { NotificationTab } from "@/components/notification-tab"
import { NotifierCard } from "@/components/notifier"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -7,37 +12,32 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelNotification } from "@/types";
import { deleteNotification } from "@/api/notification";
import { NotificationTab } from "@/components/notification-tab";
import { NotifierCard } from "@/components/notifier";
import { useNotification } from "@/hooks/useNotfication";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { useNotification } from "@/hooks/useNotfication"
import { ModelNotification } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function NotificationPage() {
const { t } = useTranslation();
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelNotification[]>(
"/api/v1/notification",
swrFetcher
);
const { notifierGroup } = useNotification();
swrFetcher,
)
const { notifierGroup } = useNotification()
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelNotification>[] = [
{
@@ -46,7 +46,7 @@ export default function NotificationPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -72,8 +72,8 @@ export default function NotificationPage() {
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>;
const s = row.original
return <div className="max-w-32 whitespace-normal break-words">{s.name}</div>
},
},
{
@@ -84,7 +84,7 @@ export default function NotificationPage() {
notifierGroup
?.filter((ng) => ng.notifications?.includes(row.id))
.map((ng) => ng.group.id) || []
);
)
},
},
{
@@ -92,8 +92,8 @@ export default function NotificationPage() {
accessorKey: "url",
accessorFn: (row) => row.url,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-64 whitespace-normal break-words">{s.url}</div>;
const s = row.original
return <div className="max-w-64 whitespace-normal break-words">{s.url}</div>
},
},
{
@@ -105,7 +105,7 @@ export default function NotificationPage() {
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -117,25 +117,25 @@ export default function NotificationPage() {
>
<NotifierCard mutate={mutate} data={s} />
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup
@@ -159,9 +159,12 @@ export default function NotificationPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -193,5 +196,5 @@ export default function NotificationPage() {
</TableBody>
</Table>
</div>
);
)
}
+28 -23
View File
@@ -1,40 +1,45 @@
import { ProfileCard } from "@/components/profile"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { useMainStore } from "@/hooks/useMainStore"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { useMediaQuery } from "@/hooks/useMediaQuery";
import { Server, Boxes } from "lucide-react";
import { useServer } from "@/hooks/useServer";
import { ProfileCard } from "@/components/profile";
import { useMediaQuery } from "@/hooks/useMediaQuery"
import { useServer } from "@/hooks/useServer"
import { Boxes, Server } from "lucide-react"
export default function ProfilePage() {
const { profile } = useMainStore();
const { servers, serverGroups } = useServer();
const { profile } = useMainStore()
const { servers, serverGroups } = useServer()
const isDesktop = useMediaQuery("(min-width: 890px)")
return (
profile && (
<div className={`flex p-8 gap-4 ${isDesktop ? 'ml-6' : 'flex-col'}`}>
<div className={`flex ${isDesktop ? 'flex-col mr-6' : 'gap-4 w-full items-center'}`}>
<Avatar className={`${isDesktop ? 'h-[300px] w-[300px]' : 'h-[150px] w-[150px]'} border-foreground border-[1px]`}>
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} />
<div className={`flex p-8 gap-4 ${isDesktop ? "ml-6" : "flex-col"}`}>
<div
className={`flex ${isDesktop ? "flex-col mr-6" : "gap-4 w-full items-center"}`}
>
<Avatar
className={`${isDesktop ? "h-[300px] w-[300px]" : "h-[150px] w-[150px]"} border-foreground border-[1px]`}
>
<AvatarImage
src={
"https://api.dicebear.com/7.x/notionists/svg?seed=" +
profile.username
}
alt={profile.username}
/>
<AvatarFallback>{profile.username}</AvatarFallback>
</Avatar>
<div>
<p className="justify-center text-3xl font-semibold">{profile.username}</p>
<p className="text-gray-400">IP: {profile.login_ip || 'Unknown'}</p>
<p className="text-gray-400">IP: {profile.login_ip || "Unknown"}</p>
</div>
{isDesktop &&
{isDesktop && (
<ProfileCard className="flex mt-4 justify-center items-center max-w-[300px] rounded-lg" />
}
)}
</div>
{!isDesktop &&
{!isDesktop && (
<ProfileCard className="flex justify-center items-center max-w-full rounded-lg" />
}
)}
<div className="w-full">
<div className="flex flex-col gap-4">
<Card className="w-full">
@@ -61,5 +66,5 @@ export default function ProfilePage() {
</div>
</div>
)
);
)
}
+13 -10
View File
@@ -1,16 +1,19 @@
import { Navigate } from "react-router-dom";
import { useAuth } from "@/hooks/useAuth";
import { useAuth } from "@/hooks/useAuth"
import { Navigate } from "react-router-dom"
export const ProtectedRoute = ({ children }: {
children: React.ReactNode;
}) => {
const { profile } = useAuth();
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const { profile } = useAuth()
if (!profile && window.location.pathname !== "/dashboard/login") {
return <><Navigate to="/dashboard/login" />{children}</>;
return (
<>
<Navigate to="/dashboard/login" />
{children}
</>
)
}
return children;
};
return children
}
export default ProtectedRoute;
export default ProtectedRoute
+19 -20
View File
@@ -1,34 +1,33 @@
import { Outlet } from "react-router-dom";
import { useEffect } from "react";
import { Card } from "@/components/ui/card";
import { ThemeProvider } from "@/components/theme-provider";
import Header from "@/components/header";
import { Toaster } from "@/components/ui/sonner";
import { useTranslation } from "react-i18next";
import useSetting from "@/hooks/useSetting";
import Header from "@/components/header"
import { ThemeProvider } from "@/components/theme-provider"
import { Toaster } from "@/components/ui/sonner"
import useSetting from "@/hooks/useSetting"
import { useEffect } from "react"
import { useTranslation } from "react-i18next"
import { Outlet } from "react-router-dom"
export default function Root() {
const { t } = useTranslation();
const settings = useSetting();
const { t } = useTranslation()
const settings = useSetting()
useEffect(() => {
document.title = settings?.site_name || "哪吒监控 Nezha Monitoring";
}, [settings]);
document.title = settings?.site_name || "哪吒监控 Nezha Monitoring"
}, [settings])
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<Card className="text-sm max-w-7xl mx-auto mt-5 min-h-[90%] flex flex-col justify-between">
<section className="text-sm mx-auto h-full flex flex-col justify-between">
<div>
<Header />
<Outlet />
<div className="max-w-5xl mx-auto">
<Outlet />
</div>
</div>
<footer className="mx-5 pb-5 text-foreground/60 font-thin text-center">
&copy; 2019-2024 {t('nezha')} {settings?.version}
<footer className="mx-5 pb-5 text-foreground/50 font-light text-xs text-center">
&copy; 2019-2024 {t("nezha")} {settings?.version}
</footer>
</Card>
</section>
<Toaster />
</ThemeProvider>
);
)
}
+41 -37
View File
@@ -1,5 +1,10 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { swrFetcher } from "@/api/api"
import { deleteServerGroups } from "@/api/server-group"
import { ActionButtonGroup } from "@/components/action-button-group"
import { GroupTab } from "@/components/group-tab"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { ServerGroupCard } from "@/components/server-group"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -7,34 +12,30 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelServerGroupResponseItem } from "@/types";
import { deleteServerGroups } from "@/api/server-group";
import { GroupTab } from "@/components/group-tab";
import { ServerGroupCard } from "@/components/server-group";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { ModelServerGroupResponseItem } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function ServerGroupPage() {
const { t } = useTranslation();
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelServerGroupResponseItem[]>(
"/api/v1/server-group",
swrFetcher
);
swrFetcher,
)
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelServerGroupResponseItem>[] = [
{
@@ -43,7 +44,7 @@ export default function ServerGroupPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -69,12 +70,12 @@ export default function ServerGroupPage() {
accessorKey: "name",
accessorFn: (row) => row.group.name,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>;
const s = row.original
return <div className="max-w-48 whitespace-normal break-words">{s.group.name}</div>
},
},
{
header: t("Server")+"(ID)",
header: t("Server") + "(ID)",
accessorKey: "servers",
accessorFn: (row) => row.servers,
},
@@ -82,7 +83,7 @@ export default function ServerGroupPage() {
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -94,25 +95,25 @@ export default function ServerGroupPage() {
>
<ServerGroupCard mutate={mutate} data={s} />
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup
@@ -135,9 +136,12 @@ export default function ServerGroupPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -169,5 +173,5 @@ export default function ServerGroupPage() {
</TableBody>
</Table>
</div>
);
)
}
+73 -58
View File
@@ -1,5 +1,12 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { swrFetcher } from "@/api/api"
import { deleteServer, forceUpdateServer } from "@/api/server"
import { ActionButtonGroup } from "@/components/action-button-group"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { InstallCommandsMenu } from "@/components/install-commands"
import { NoteMenu } from "@/components/note-menu"
import { ServerCard } from "@/components/server"
import { TerminalButton } from "@/components/terminal"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -7,37 +14,29 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelServer as Server, ModelForceUpdateResponse } from "@/types";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { deleteServer, forceUpdateServer } from "@/api/server";
import { ServerCard } from "@/components/server";
import { ActionButtonGroup } from "@/components/action-button-group";
import { useEffect, useMemo } from "react";
import { toast } from "sonner";
import { IconButton } from "@/components/xui/icon-button";
import { InstallCommandsMenu } from "@/components/install-commands";
import { NoteMenu } from "@/components/note-menu";
import { TerminalButton } from "@/components/terminal";
import { useServer } from "@/hooks/useServer";
import { joinIP } from "@/lib/utils";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { IconButton } from "@/components/xui/icon-button"
import { useServer } from "@/hooks/useServer"
import { joinIP } from "@/lib/utils"
import { ModelForceUpdateResponse, ModelServer as Server } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function ServerPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher);
const { serverGroups } = useServer();
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher)
const { serverGroups } = useServer()
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
}, [error])
const columns: ColumnDef<Server>[] = [
{
@@ -72,8 +71,8 @@ export default function ServerPage() {
accessorKey: "name",
accessorFn: (row) => row.name,
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
const s = row.original
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>
},
},
{
@@ -81,16 +80,22 @@ export default function ServerPage() {
accessorKey: "groups",
accessorFn: (row) => {
return (
serverGroups?.filter((sg) => sg.servers?.includes(row.id)).map((sg) => sg.group.id) || []
);
serverGroups
?.filter((sg) => sg.servers?.includes(row.id))
.map((sg) => sg.group.id) || []
)
},
},
{
id: "ip",
header: "IP",
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-24 whitespace-normal break-words">{joinIP(s.geoip?.ip)}</div>;
const s = row.original
return (
<div className="max-w-24 whitespace-normal break-words">
{joinIP(s.geoip?.ip)}
</div>
)
},
},
{
@@ -112,15 +117,15 @@ export default function ServerPage() {
id: "note",
header: t("Note"),
cell: ({ row }) => {
const s = row.original;
return <NoteMenu note={{ private: s.note, public: s.public_note }} />;
const s = row.original
return <NoteMenu note={{ private: s.note, public: s.public_note }} />
},
},
{
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -131,25 +136,25 @@ export default function ServerPage() {
<ServerCard mutate={mutate} data={s} />
</>
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight">{t("Server")}</h1>
<HeaderButtonGroup
@@ -163,33 +168,40 @@ export default function ServerPage() {
<IconButton
icon="update"
onClick={async () => {
const id = selectedRows.map((r) => r.original.id);
const id = selectedRows.map((r) => r.original.id)
if (id.length < 1) {
toast(t("Error"), {
description: t("Results.SelectAtLeastOneServer"),
});
return;
})
return
}
let resp: ModelForceUpdateResponse = {};
let resp: ModelForceUpdateResponse = {}
try {
resp = await forceUpdateServer(id);
resp = await forceUpdateServer(id)
} catch (e) {
console.error(e);
console.error(e)
toast(t("Error"), {
description: t("Results.UnExpectedError"),
});
return;
})
return
}
toast(t("Done"), {
description: t("Results.ForceUpdate")
+ (resp.success?.length ? t(`Success`) + ` [${resp.success.join(",")}]` : "")
+ (resp.failure?.length ? t(`Failure`) + ` [${resp.failure.join(",")}]` : "")
+ (resp.offline?.length ? t(`Offline`) + ` [${resp.offline.join(",")}]` : "")
});
description:
t("Results.ForceUpdate") +
(resp.success?.length
? t(`Success`) + ` [${resp.success.join(",")}]`
: "") +
(resp.failure?.length
? t(`Failure`) + ` [${resp.failure.join(",")}]`
: "") +
(resp.offline?.length
? t(`Offline`) + ` [${resp.offline.join(",")}]`
: ""),
})
}}
/>
<InstallCommandsMenu className="bg-blue-700" />
<InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" />
</HeaderButtonGroup>
</div>
<Table>
@@ -201,9 +213,12 @@ export default function ServerPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -235,5 +250,5 @@ export default function ServerPage() {
</TableBody>
</Table>
</div>
);
)
}
+50 -45
View File
@@ -1,6 +1,9 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { ServiceCard } from "@/components/service";
import { swrFetcher } from "@/api/api"
import { deleteService } from "@/api/service"
import { ActionButtonGroup } from "@/components/action-button-group"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { ServiceCard } from "@/components/service"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -8,33 +11,28 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ModelService as Service } from "@/types";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
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";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { ModelService as Service } from "@/types"
import { serviceTypes } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function ServicePage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<Service[]>(
"/api/v1/service/list",
swrFetcher
);
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<Service[]>("/api/v1/service/list", swrFetcher)
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
}, [error])
const columns: ColumnDef<Service>[] = [
{
@@ -69,8 +67,8 @@ export default function ServicePage() {
accessorFn: (row) => row.name,
accessorKey: "name",
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>;
const s = row.original
return <div className="max-w-24 whitespace-normal break-words">{s.name}</div>
},
},
{
@@ -78,8 +76,8 @@ export default function ServicePage() {
accessorFn: (row) => row.target,
accessorKey: "target",
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-24 whitespace-normal break-words">{s.target}</div>;
const s = row.original
return <div className="max-w-24 whitespace-normal break-words">{s.target}</div>
},
},
{
@@ -87,29 +85,33 @@ export default function ServicePage() {
accessorKey: "cover",
accessorFn: (row) => row.cover,
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<div className="max-w-48 whitespace-normal break-words">
{(() => {
switch (s.cover) {
case 0: {
return <span>{t("CoverAll")}</span>;
return <span>{t("CoverAll")}</span>
}
case 1: {
return <span>{t("IgnoreAll")}</span>;
return <span>{t("IgnoreAll")}</span>
}
}
})()}
</div>
);
)
},
},
{
header: t("SpecificServers"),
cell: ({ row }) => {
const s = row.original;
return <div className="max-w-32 whitespace-normal break-words">{Object.keys(s.skip_servers ?? {}).join(',')}</div>;
}
const s = row.original
return (
<div className="max-w-32 whitespace-normal break-words">
{Object.keys(s.skip_servers ?? {}).join(",")}
</div>
)
},
},
{
header: t("Type"),
@@ -146,7 +148,7 @@ export default function ServicePage() {
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -154,25 +156,25 @@ export default function ServicePage() {
>
<ServiceCard mutate={mutate} data={s} />
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Services")}</h1>
<HeaderButtonGroup
@@ -196,9 +198,12 @@ export default function ServicePage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -230,5 +235,5 @@ export default function ServicePage() {
</TableBody>
</Table>
</div>
);
)
}
+230 -87
View File
@@ -1,11 +1,8 @@
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { ModelSettingResponse, settingCoverageTypes, nezhaLang } from "@/types";
import { SettingsTab } from "@/components/settings-tab";
import { z } from "zod";
import { asOptionalField } from "@/lib/utils";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { getSettings, updateSettings } from "@/api/settings"
import { SettingsTab } from "@/components/settings-tab"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
Form,
FormControl,
@@ -13,23 +10,25 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { getSettings, updateSettings } from "@/api/settings";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useTranslation } from "react-i18next";
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { asOptionalField } from "@/lib/utils"
import { ModelSettingResponse, nezhaLang, settingCoverageTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { z } from "zod"
const settingFormSchema = z.object({
dns_servers: asOptionalField(z.string()),
@@ -47,77 +46,82 @@ const settingFormSchema = z.object({
tls: asOptionalField(z.boolean()),
enable_ip_change_notification: asOptionalField(z.boolean()),
enable_plain_ip_in_notification: asOptionalField(z.boolean()),
});
})
export default function SettingsPage() {
const { t, i18n } = useTranslation();
const [config, setConfig] = useState<ModelSettingResponse>();
const [error, setError] = useState<Error>();
const { t, i18n } = useTranslation()
const [config, setConfig] = useState<ModelSettingResponse>()
const [error, setError] = useState<Error>()
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
}, [error])
useEffect(() => {
(async () => {
;(async () => {
try {
const c = await getSettings();
setConfig(c);
const c = await getSettings()
setConfig(c)
} catch (e) {
if (e instanceof Error) setError(e);
if (e instanceof Error) setError(e)
}
})();
}, []);
})()
}, [])
const form = useForm<z.infer<typeof settingFormSchema>>({
resolver: zodResolver(settingFormSchema),
defaultValues: config
? {
...config,
site_name: config.site_name || "",
user_template: config.user_template || Object.keys(config.frontend_templates || {})[0] || "user-dist",
}
...config,
site_name: config.site_name || "",
user_template:
config.user_template ||
Object.keys(config.frontend_templates.filter((t) => !t.is_admin) || {})[0] ||
"user-dist",
}
: {
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
},
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
},
resetOptions: {
keepDefaultValues: false,
},
});
})
useEffect(() => {
if (config) {
form.reset(config);
form.reset(config)
}
}, [config, form]);
}, [config, form])
const onSubmit = async (values: z.infer<typeof settingFormSchema>) => {
try {
await updateSettings(values);
const newConfig = await getSettings();
setConfig(newConfig);
form.reset();
await updateSettings(values)
const newConfig = await getSettings()
setConfig(newConfig)
form.reset()
} catch (e) {
if (e instanceof Error) setError(e);
return;
if (e instanceof Error) setError(e)
return
} finally {
if (values.language != i18n.language) {
i18n.changeLanguage(values.language);
i18n.changeLanguage(values.language)
}
toast(t("Success"));
toast(t("Success"))
}
};
}
return (
<div className="px-8">
<div className="px-3">
<SettingsTab className="mt-6 mb-4 w-full" />
<div>
<Form {...form}>
@@ -171,9 +175,11 @@ export default function SettingsPage() {
<Select
value={field.value}
onValueChange={(value) => {
const template = config?.frontend_templates?.find(t => t.path === value);
const template = config?.frontend_templates?.find(
(t) => t.path === value,
)
if (template) {
form.setValue("user_template", template.path ?? '');
form.setValue("user_template", template.path)
}
}}
>
@@ -183,13 +189,22 @@ export default function SettingsPage() {
</SelectTrigger>
</FormControl>
<SelectContent>
{(config?.frontend_templates?.filter(t => !t.is_admin) || []).map((template) => (
{(
config?.frontend_templates.filter(
(t) => !t.is_admin,
) || []
).map((template) => (
<div key={template.path}>
<SelectItem value={template.path!}>
<SelectItem value={template.path}>
<div className="flex flex-col items-start gap-1">
<div className="font-medium">{template.name}</div>
<div className="font-medium">
{template.name}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{t("Author")}: {template.author}</span>
<span>
{t("Author")}:{" "}
{template.author}
</span>
{!template.is_official ? (
<span className="px-1.5 py-0.5 rounded-md bg-red-100 text-red-800 text-xs">
{t("Community")}
@@ -218,10 +233,102 @@ export default function SettingsPage() {
</Select>
</FormControl>
<FormMessage />
{!config?.frontend_templates?.find(t => t.path === field.value)?.is_official && (
{!config?.frontend_templates?.find(
(t) => t.path === field.value,
)?.is_official && (
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">{t("CommunityThemeWarning")}</div>
<div className="text-yellow-700 dark:text-yellow-200">{t("CommunityThemeDescription")}</div>
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
</div>
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
</div>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="user_template"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Theme")}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(value) => {
const template = config?.frontend_templates?.find(
(t) => t.path === value,
)
if (template) {
form.setValue(
"user_template",
template.path ?? "",
)
}
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("SelectTheme")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
config?.frontend_templates?.filter(
(t) => !t.is_admin,
) || []
).map((template) => (
<div key={template.path}>
<SelectItem value={template.path!}>
<div className="flex flex-col items-start gap-1">
<div className="font-medium">
{template.name}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{t("Author")}:{" "}
{template.author}
</span>
{!template.is_official ? (
<span className="px-1.5 py-0.5 rounded-md bg-red-100 text-red-800 text-xs">
{t("Community")}
</span>
) : (
<span className="px-1.5 py-0.5 rounded-md bg-blue-100 text-blue-800 text-xs">
{t("Official")}
</span>
)}
</div>
</div>
</SelectItem>
<div className="px-8 py-1">
<a
href={template.repository}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
>
{template.repository}
</a>
</div>
</div>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
{!config?.frontend_templates?.find(
(t) => t.path === field.value,
)?.is_official && (
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
</div>
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
</div>
)}
</FormItem>
@@ -273,10 +380,11 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Label className="text-sm">
{t("ConfigTLS")}
</Label>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">{t("ConfigTLS")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -289,7 +397,9 @@ export default function SettingsPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t("CustomPublicDNSNameserversforDDNS") + " " + t("SeparateWithComma")}
{t("CustomPublicDNSNameserversforDDNS") +
" " +
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input {...field} />
@@ -306,16 +416,28 @@ export default function SettingsPage() {
<FormLabel>{t("RealIPHeader")}</FormLabel>
<FormControl>
<div className="flex items-center">
<Input disabled={field.value == 'NZ::Use-Peer-IP'} className="w-1/2" placeholder="CF-Connecting-IP" {...field} />
<Checkbox checked={field.value == 'NZ::Use-Peer-IP'} className="ml-2" onCheckedChange={(checked) => {
if (checked) {
field.disabled = true;
form.setValue("real_ip_header", "NZ::Use-Peer-IP");
} else {
field.disabled = false;
form.setValue("real_ip_header", "");
}
}} />
<Input
disabled={field.value == "NZ::Use-Peer-IP"}
className="w-1/2"
placeholder="CF-Connecting-IP"
{...field}
/>
<Checkbox
checked={field.value == "NZ::Use-Peer-IP"}
className="ml-2"
onCheckedChange={(checked) => {
if (checked) {
field.disabled = true
form.setValue(
"real_ip_header",
"NZ::Use-Peer-IP",
)
} else {
field.disabled = false
form.setValue("real_ip_header", "")
}
}}
/>
<FormLabel className="font-normal ml-2">
{t("UseDirectConnectingIP")}
</FormLabel>
@@ -336,14 +458,19 @@ export default function SettingsPage() {
render={({ field }) => (
<FormItem>
<FormLabel>{t("Coverage")}</FormLabel>
<Select onValueChange={field.onChange} value={`${field.value}`}>
<Select
onValueChange={field.onChange}
value={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(settingCoverageTypes).map(([k, v]) => (
{Object.entries(
settingCoverageTypes,
).map(([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
@@ -359,7 +486,11 @@ export default function SettingsPage() {
name="ignored_ip_notification"
render={({ field }) => (
<FormItem>
<FormLabel>{t("SpecificServers") + " " + t("SeparateWithComma")}</FormLabel>
<FormLabel>
{t("SpecificServers") +
" " +
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input placeholder="1,2,3" {...field} />
</FormControl>
@@ -374,7 +505,11 @@ export default function SettingsPage() {
<FormItem>
<FormLabel>{t("NotifierGroupID")}</FormLabel>
<FormControl>
<Input placeholder="0" type="number" {...field} />
<Input
placeholder="0"
type="number"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -387,8 +522,13 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Label className="text-sm">{t("Enable")}</Label>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("Enable")}
</Label>
</div>
</FormControl>
<FormMessage />
@@ -406,7 +546,10 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("FullIPNotification")}
</Label>
@@ -421,5 +564,5 @@ export default function SettingsPage() {
</Form>
</div>
</div>
);
)
}
+37 -33
View File
@@ -1,5 +1,9 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { swrFetcher } from "@/api/api"
import { deleteUser } from "@/api/user"
import { ActionButtonGroup } from "@/components/action-button-group"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { SettingsTab } from "@/components/settings-tab"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -7,31 +11,28 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelUser } from "@/types";
import { deleteUser } from "@/api/user";
import { SettingsTab } from "@/components/settings-tab";
import { UserCard } from "@/components/user";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { UserCard } from "@/components/user"
import { ModelUser } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function UserPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelUser[]>("/api/v1/user", swrFetcher);
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelUser[]>("/api/v1/user", swrFetcher)
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.UnExpectedError", { error: error.message }),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
description: t("Results.UnExpectedError", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelUser>[] = [
{
@@ -40,7 +41,7 @@ export default function UserPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -70,7 +71,7 @@ export default function UserPage() {
id: "actions",
header: t("Actions"),
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -82,25 +83,25 @@ export default function UserPage() {
>
<></>
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<SettingsTab className="mt-6 w-full" />
<div className="flex mt-4 mb-4">
<HeaderButtonGroup
@@ -124,9 +125,12 @@ export default function UserPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -158,5 +162,5 @@ export default function UserPage() {
</TableBody>
</Table>
</div>
);
)
}
+37 -35
View File
@@ -1,5 +1,9 @@
import { swrFetcher } from "@/api/api";
import { Checkbox } from "@/components/ui/checkbox";
import { swrFetcher } from "@/api/api"
import { deleteWAF } from "@/api/waf"
import { ActionButtonGroup } from "@/components/action-button-group"
import { HeaderButtonGroup } from "@/components/header-button-group"
import { SettingsTab } from "@/components/settings-tab"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
@@ -7,31 +11,26 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { ActionButtonGroup } from "@/components/action-button-group";
import { HeaderButtonGroup } from "@/components/header-button-group";
import { toast } from "sonner";
import { ModelWAFApiMock, wafBlockReasons } from "@/types";
import { deleteWAF } from "@/api/waf";
import { ip16Str } from "@/lib/utils";
import { SettingsTab } from "@/components/settings-tab";
import { useTranslation } from "react-i18next";
} from "@/components/ui/table"
import { ip16Str } from "@/lib/utils"
import { ModelWAFApiMock, wafBlockReasons } from "@/types"
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
import { useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import useSWR from "swr"
export default function WAFPage() {
const { t } = useTranslation();
const { data, mutate, error, isLoading } = useSWR<ModelWAFApiMock[]>("/api/v1/waf", swrFetcher);
const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<ModelWAFApiMock[]>("/api/v1/waf", swrFetcher)
useEffect(() => {
if (error)
toast(t("Error"), {
description: t(`Error fetching resource: ${error.message}.`),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const columns: ColumnDef<ModelWAFApiMock>[] = [
{
@@ -40,7 +39,7 @@ export default function WAFPage() {
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
@@ -77,16 +76,16 @@ export default function WAFPage() {
accessorKey: "lastBlockTime",
accessorFn: (row) => row.last_block_timestamp,
cell: ({ row }) => {
const s = row.original;
const date = new Date((s.last_block_timestamp || 0)*1000);
return <span>{date.toISOString()}</span>;
const s = row.original
const date = new Date((s.last_block_timestamp || 0) * 1000)
return <span>{date.toISOString()}</span>
},
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const s = row.original;
const s = row.original
return (
<ActionButtonGroup
className="flex gap-2"
@@ -98,25 +97,25 @@ export default function WAFPage() {
>
<></>
</ActionButtonGroup>
);
)
},
},
];
]
const dataCache = useMemo(() => {
return data ?? [];
}, [data]);
return data ?? []
}, [data])
const table = useReactTable({
data: dataCache,
columns,
getCoreRowModel: getCoreRowModel(),
});
})
const selectedRows = table.getSelectedRowModel().rows;
const selectedRows = table.getSelectedRowModel().rows
return (
<div className="px-8">
<div className="px-3">
<SettingsTab className="mt-6 w-full" />
<div className="flex mt-4 mb-4">
<HeaderButtonGroup
@@ -139,9 +138,12 @@ export default function WAFPage() {
<TableHead key={header.id} className="text-sm">
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
)
})}
</TableRow>
))}
@@ -173,5 +175,5 @@ export default function WAFPage() {
</TableBody>
</Table>
</div>
);
)
}