mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-04 04:30:06 +00:00
feat: responsive header, search boxes (#10)
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -42,6 +42,7 @@
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.1",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^5.0.1"
|
||||
},
|
||||
@@ -6355,6 +6356,19 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vaul": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz",
|
||||
"integrity": "sha512-+ejzF6ffQKPcfgS7uOrGn017g39F8SO4yLPXbBhpC7a0H+oPqPna8f1BUfXaz8eU4+pxbQcmjxW+jWBSbxjaFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.10",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.1",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^5.0.1"
|
||||
},
|
||||
|
||||
@@ -40,6 +40,8 @@ import { asOptionalField } from "@/lib/utils"
|
||||
import { IconButton } from "@/components/xui/icon-button"
|
||||
import { triggerModes } from "@/types"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
import { useNotification } from "@/hooks/useNotfication"
|
||||
import { Combobox } from "./ui/combobox"
|
||||
|
||||
interface AlertRuleCardProps {
|
||||
data?: ModelAlertRule;
|
||||
@@ -108,12 +110,19 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => {
|
||||
values.rules = JSON.parse(values.rules_raw);
|
||||
data?.id ? await updateAlertRule(data.id, values) : await createAlertRule(values);
|
||||
const { rules_raw, ...requiredFields } = values;
|
||||
data?.id ? await updateAlertRule(data.id, requiredFields) : await createAlertRule(requiredFields);
|
||||
setOpen(false);
|
||||
await mutate();
|
||||
form.reset();
|
||||
}
|
||||
|
||||
const { notifierGroup } = useNotification();
|
||||
const ngroupList = notifierGroup?.map(ng => ({
|
||||
value: `${ng.group.id}`,
|
||||
label: ng.group.name,
|
||||
})) || [{ value: "", label: "" }];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -169,12 +178,13 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
name="notification_group_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notifier Group ID</FormLabel>
|
||||
<FormLabel>Notifier Group</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
{...field}
|
||||
<Combobox
|
||||
placeholder="Search..."
|
||||
options={ngroupList}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -37,7 +37,10 @@ import { createCron, updateCron } from "@/api/cron"
|
||||
import { asOptionalField } from "@/lib/utils"
|
||||
import { cronTypes, cronCoverageTypes } from "@/types"
|
||||
import { Textarea } from "./ui/textarea"
|
||||
import { conv } from "@/lib/utils"
|
||||
import { useServer } from "@/hooks/useServer"
|
||||
import { useNotification } from "@/hooks/useNotfication"
|
||||
import { MultiSelect } from "./xui/multi-select"
|
||||
import { Combobox } from "./ui/combobox"
|
||||
|
||||
interface CronCardProps {
|
||||
data?: ModelCron;
|
||||
@@ -82,6 +85,18 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
const { servers } = useServer();
|
||||
const serverList = servers?.map(s => ({
|
||||
value: `${s.id}`,
|
||||
label: s.name,
|
||||
})) || [{ value: "", label: "" }];
|
||||
|
||||
const { notifierGroup } = useNotification();
|
||||
const ngroupList = notifierGroup?.map(ng => ({
|
||||
value: `${ng.group.id}`,
|
||||
label: ng.group.name,
|
||||
})) || [{ value: "", label: "" }];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -200,14 +215,10 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
||||
<FormItem>
|
||||
<FormLabel>Specific Servers (Separate with comma)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1,2,3"
|
||||
{...field}
|
||||
value={conv.arrToStr(field.value ?? [])}
|
||||
onChange={e => {
|
||||
const arr = conv.strToArr(e.target.value);
|
||||
field.onChange(arr);
|
||||
}}
|
||||
<MultiSelect
|
||||
options={serverList}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value?.map(String)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -221,10 +232,11 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
||||
<FormItem>
|
||||
<FormLabel>Notifier Group ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
{...field}
|
||||
<Combobox
|
||||
placeholder="Search..."
|
||||
options={ngroupList}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -14,94 +14,198 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem,
|
||||
import { User, LogOut } from "lucide-react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer"
|
||||
import { Button } from "./ui/button";
|
||||
import { IconButton } from "./xui/icon-button";
|
||||
import { useState } from "react";
|
||||
|
||||
const pages = [
|
||||
{ href: "/dashboard", label: "Server" },
|
||||
{ href: "/dashboard/service", label: "Service" },
|
||||
{ href: "/dashboard/cron", label: "Task" },
|
||||
{ href: "/dashboard/notification", label: "Notification" },
|
||||
{ href: "/dashboard/ddns", label: "Dynamic DNS" },
|
||||
{ href: "/dashboard/nat", label: "NAT Traversal" },
|
||||
{ href: "/dashboard/server-group", label: "Group" },
|
||||
]
|
||||
|
||||
export default function Header() {
|
||||
const { logout } = useAuth();
|
||||
const profile = useMainStore(store => store.profile);
|
||||
|
||||
const location = useLocation();
|
||||
const isDesktop = useMediaQuery("(min-width: 890px)")
|
||||
|
||||
return <header className="h-16 flex items-center border-b-2 px-4 overflow-x-auto">
|
||||
<NavigationMenu className="sm:max-w-full">
|
||||
<NavigationMenuList>
|
||||
<Card>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle() + ' !text-foreground'}>
|
||||
<Link to="/dashboard"><img className="h-7 mr-1" src='/dashboard/logo.svg' /> 哪吒监控</Link>
|
||||
</NavigationMenuLink>
|
||||
</Card>
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
{
|
||||
profile && <>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard">Server</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/service"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/service">Service</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/cron"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/cron">Task</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/notification" || location.pathname === "/dashboard/alert-rule"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/notification">Notification</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/ddns"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/ddns">Dynamic DNS</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/nat"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/nat">NAT Traversal</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/server-group" || location.pathname === "/dashboard/notification-group"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/server-group">Group</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</>
|
||||
}
|
||||
</NavigationMenuList>
|
||||
<div className="ml-auto flex items-center">
|
||||
<ModeToggle />
|
||||
{
|
||||
profile && <>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="ml-1 h-8 w-8 cursor-pointer 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>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<User />
|
||||
<span>Profile</span>
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={logout}>
|
||||
<LogOut />
|
||||
<span>Log out</span>
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</NavigationMenu>
|
||||
</header>
|
||||
return (
|
||||
isDesktop ? (
|
||||
<header className="h-16 flex items-center border-b-2 px-4 overflow-x-auto">
|
||||
<NavigationMenu className="sm:max-w-full">
|
||||
<NavigationMenuList>
|
||||
<Card className="mr-1">
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle() + ' !text-foreground'}>
|
||||
<Link to="/dashboard"><img className="h-7 mr-1" src='/dashboard/logo.svg' /> 哪吒监控</Link>
|
||||
</NavigationMenuLink>
|
||||
</Card>
|
||||
|
||||
{
|
||||
profile && (
|
||||
<>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard">Server</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/service"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/service">Service</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/cron"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/cron">Task</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/notification" || location.pathname === "/dashboard/alert-rule"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/notification">Notification</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/ddns"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/ddns">Dynamic DNS</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/nat"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/nat">NAT Traversal</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/server-group" || location.pathname === "/dashboard/notification-group"} className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/dashboard/server-group">Group</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</NavigationMenuList>
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<ModeToggle />
|
||||
{
|
||||
profile && <>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="ml-1 h-8 w-8 cursor-pointer 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>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<User />
|
||||
<span>Profile</span>
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={logout}>
|
||||
<LogOut />
|
||||
<span>Log out</span>
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</NavigationMenu>
|
||||
</header>
|
||||
)
|
||||
: (
|
||||
<header className="flex border-b-2 px-4 h-16">
|
||||
<div className="flex max-w-max flex-1 items-center justify-center gap-2">
|
||||
{profile &&
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger aria-label="Toggle Menu" asChild>
|
||||
<IconButton icon="menu" variant="ghost" />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader className="text-left">
|
||||
<DrawerTitle>Navigate to</DrawerTitle>
|
||||
<DrawerDescription>Select a page to navigate to.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className="grid gap-1 px-4">
|
||||
{pages.slice(0).map((item, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
to={item.href ? item.href : "#"}
|
||||
className="py-1 text-sm"
|
||||
onClick={() => { setOpen(false) }}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
}
|
||||
</div>
|
||||
<Card className="mx-2 my-2 flex justify-center items-center hover:bg-accent transition duration-200">
|
||||
<Link className="inline-flex w-full items-center px-4 py-2" to="/dashboard"><img className="h-7 mr-1" src='/dashboard/logo.svg' /> 哪吒监控</Link>
|
||||
</Card>
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<ModeToggle />
|
||||
{
|
||||
profile && <>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="ml-1 h-8 w-8 cursor-pointer 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>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<User />
|
||||
<span>Profile</span>
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={logout}>
|
||||
<LogOut />
|
||||
<span>Log out</span>
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ import { KeyedMutator } from "swr"
|
||||
import { asOptionalField } from "@/lib/utils"
|
||||
import { IconButton } from "@/components/xui/icon-button"
|
||||
import { serviceTypes, serviceCoverageTypes } from "@/types"
|
||||
import { MultiSelect } from "./xui/multi-select"
|
||||
import { Combobox } from "./ui/combobox"
|
||||
import { useServer } from "@/hooks/useServer"
|
||||
import { useNotification } from "@/hooks/useNotfication"
|
||||
|
||||
interface ServiceCardProps {
|
||||
data?: ModelService;
|
||||
@@ -63,6 +67,7 @@ const serviceFormSchema = z.object({
|
||||
return v.filter(Boolean).map(Number);
|
||||
})),
|
||||
skip_servers: z.record(z.boolean()),
|
||||
skip_servers_raw: z.array(z.string()),
|
||||
target: z.string().url(),
|
||||
type: z.coerce.number().int().min(0),
|
||||
});
|
||||
@@ -70,7 +75,10 @@ const serviceFormSchema = z.object({
|
||||
export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
const form = useForm<z.infer<typeof serviceFormSchema>>({
|
||||
resolver: zodResolver(serviceFormSchema),
|
||||
defaultValues: data ? data : {
|
||||
defaultValues: data ? {
|
||||
...data,
|
||||
skip_servers_raw: conv.recordToStrArr(data.skip_servers),
|
||||
} : {
|
||||
type: 1,
|
||||
cover: 0,
|
||||
name: "",
|
||||
@@ -82,6 +90,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
fail_trigger_tasks: [],
|
||||
recover_trigger_tasks: [],
|
||||
skip_servers: {},
|
||||
skip_servers_raw: [],
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
@@ -91,12 +100,26 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof serviceFormSchema>) => {
|
||||
data?.id ? await updateService(data.id, values) : await createService(values);
|
||||
values.skip_servers = conv.arrToRecord(values.skip_servers_raw);
|
||||
const { skip_servers_raw, ...requiredFields } = values;
|
||||
data?.id ? await updateService(data.id, requiredFields) : await createService(requiredFields);
|
||||
setOpen(false);
|
||||
await mutate();
|
||||
form.reset();
|
||||
}
|
||||
|
||||
const { servers } = useServer();
|
||||
const serverList = servers?.map(s => ({
|
||||
value: `${s.id}`,
|
||||
label: s.name,
|
||||
})) || [{ value: "", label: "" }];
|
||||
|
||||
const { notifierGroup } = useNotification();
|
||||
const ngroupList = notifierGroup?.map(ng => ({
|
||||
value: `${ng.group.id}`,
|
||||
label: ng.group.name,
|
||||
})) || [{ value: "", label: "" }];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -229,19 +252,15 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="skip_servers"
|
||||
name="skip_servers_raw"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Specific Servers (separate with comma)</FormLabel>
|
||||
<FormLabel>Specific Servers</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1,2,3"
|
||||
{...field}
|
||||
value={conv.recordToStr(field.value ?? {})}
|
||||
onChange={e => {
|
||||
const rec = conv.strToRecord(e.target.value);
|
||||
field.onChange(rec);
|
||||
}}
|
||||
<MultiSelect
|
||||
options={serverList}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -253,12 +272,13 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
name="notification_group_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Notifier Group ID</FormLabel>
|
||||
<FormLabel>Notifier Group</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="1"
|
||||
{...field}
|
||||
<Combobox
|
||||
placeholder="Search..."
|
||||
options={ngroupList}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value.toString()}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
118
src/components/ui/combobox.tsx
Normal file
118
src/components/ui/combobox.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Check, ChevronDown } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
|
||||
interface ComboboxProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
options: {
|
||||
label: string,
|
||||
value: string,
|
||||
}[];
|
||||
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
className?: string;
|
||||
onValueChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const Combobox = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ComboboxProps
|
||||
>(({
|
||||
options,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
className,
|
||||
onValueChange,
|
||||
...props
|
||||
}, ref) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState(defaultValue)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
role="combobox"
|
||||
variant="outline"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
"flex w-full justify-between hover:bg-inherit",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{value
|
||||
? (() => {
|
||||
const val = options.find((option) => option.value === value)?.label
|
||||
return (
|
||||
val ? (
|
||||
<div>{val}</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">{placeholder}</div>
|
||||
)
|
||||
)
|
||||
})()
|
||||
: <div className="text-muted-foreground">{placeholder}</div>}
|
||||
<ChevronDown className="ml-auto opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Command
|
||||
filter={(value, search, keywords = []) => {
|
||||
const extendValue = value + " " + keywords.join(" ");
|
||||
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}}
|
||||
>
|
||||
<CommandInput placeholder={placeholder} className="h-9" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No result found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
keywords={[option.label]}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue)
|
||||
onValueChange(currentValue === value ? "" : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"justify-start",
|
||||
value === option.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span>{option.label}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
});
|
||||
116
src/components/ui/drawer.tsx
Normal file
116
src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
19
src/hooks/useMediaQuery.tsx
Normal file
19
src/hooks/useMediaQuery.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
export function useMediaQuery(query: string) {
|
||||
const [value, setValue] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
function onChange(event: MediaQueryListEvent) {
|
||||
setValue(event.matches)
|
||||
}
|
||||
|
||||
const result = matchMedia(query)
|
||||
result.addEventListener("change", onChange)
|
||||
setValue(result.matches)
|
||||
|
||||
return () => result.removeEventListener("change", onChange)
|
||||
}, [query])
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -33,7 +33,8 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({ chil
|
||||
(async () => {
|
||||
try {
|
||||
const n = await getNotification();
|
||||
setNotifier(n);
|
||||
const nData = n.map(({ id, name }) => ({ id, name }));
|
||||
setNotifier(nData);
|
||||
} catch (error) {
|
||||
setNotifier(undefined);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ export const ServerProvider: React.FC<ServerProviderProps> = ({ children, withSe
|
||||
(async () => {
|
||||
try {
|
||||
const s = await getServers();
|
||||
setServer(s);
|
||||
const serverData = s.map(({ id, name }) => ({ id, name }));
|
||||
setServer(serverData);
|
||||
} catch (error) {
|
||||
setServer(undefined);
|
||||
}
|
||||
|
||||
@@ -74,3 +74,16 @@ body,
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2.5 h-2.5;
|
||||
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-transparent
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply rounded-full bg-border border-[1px] border-transparent border-solid bg-clip-padding;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,20 @@ export const conv = {
|
||||
}
|
||||
return arr;
|
||||
},
|
||||
recordToStrArr: <T>(rec: Record<string, T>) => {
|
||||
const arr: string[] = [];
|
||||
for (const val of Object.keys(rec)) {
|
||||
arr.push(val);
|
||||
}
|
||||
return arr;
|
||||
},
|
||||
arrToRecord: (arr: string[]) => {
|
||||
const rec: Record<string, boolean> = {};
|
||||
for (const val of arr) {
|
||||
rec[val] = true;
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
}
|
||||
|
||||
export const sleep = (ms: number) => {
|
||||
|
||||
18
src/main.tsx
18
src/main.tsx
@@ -41,11 +41,23 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "/dashboard/service",
|
||||
element: <ServicePage />,
|
||||
element: (
|
||||
<ServerProvider withServer>
|
||||
<NotificationProvider withNotifierGroup>
|
||||
<ServicePage />
|
||||
</NotificationProvider>
|
||||
</ServerProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/cron",
|
||||
element: <CronPage />,
|
||||
element: (
|
||||
<ServerProvider withServer>
|
||||
<NotificationProvider withNotifierGroup>
|
||||
<CronPage />
|
||||
</NotificationProvider>
|
||||
</ServerProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/dashboard/notification",
|
||||
@@ -53,7 +65,7 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "/dashboard/alert-rule",
|
||||
element: <AlertRulePage />,
|
||||
element: <NotificationProvider withNotifierGroup><AlertRulePage /></NotificationProvider>,
|
||||
},
|
||||
{
|
||||
path: "/dashboard/ddns",
|
||||
|
||||
@@ -54,6 +54,7 @@ export default function AlertRulePage() {
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -121,8 +122,8 @@ export default function AlertRulePage() {
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4 gap-[60%]">
|
||||
<NotificationTab className="flex-1" />
|
||||
<div className="flex mt-6 mb-4">
|
||||
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||
fn: deleteAlertRules,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
|
||||
@@ -99,19 +99,27 @@ export default function CronPage() {
|
||||
{
|
||||
header: "Coverage",
|
||||
accessorKey: "cover",
|
||||
accessorFn: row => {
|
||||
switch (row.cover) {
|
||||
case 0: {
|
||||
return "Ignore All"
|
||||
}
|
||||
case 1: {
|
||||
return "Cover All"
|
||||
}
|
||||
case 2: {
|
||||
return "On alert"
|
||||
}
|
||||
}
|
||||
},
|
||||
accessorFn: row => row.cover,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{(() => {
|
||||
switch (s.cover) {
|
||||
case 0: {
|
||||
return <span>Ignore All</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>Cover All</span>
|
||||
}
|
||||
case 2: {
|
||||
return <span>On alert</span>
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
header: "Specific Servers",
|
||||
@@ -121,7 +129,15 @@ export default function CronPage() {
|
||||
{
|
||||
header: "Last Execution",
|
||||
accessorKey: "lastExecution",
|
||||
accessorFn: row => row.last_executed_at
|
||||
accessorFn: row => row.last_executed_at,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
<div className="max-w-24 whitespace-normal break-words">
|
||||
{s.last_executed_at}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
header: "Last Result",
|
||||
|
||||
@@ -62,6 +62,7 @@ export default function DDNSPage() {
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -89,6 +90,7 @@ export default function DDNSPage() {
|
||||
{
|
||||
header: "Domains",
|
||||
accessorKey: "domains",
|
||||
accessorFn: row => row.domains,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
|
||||
@@ -50,7 +50,7 @@ export default () => {
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="shadcn" {...field} />
|
||||
<Input placeholder="shadcn" autoComplete="username" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
@@ -66,7 +66,7 @@ export default () => {
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="shadcn" {...field} />
|
||||
<Input type="password" placeholder="shadcn" autoComplete="current-password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
|
||||
@@ -53,6 +53,7 @@ export default function NATPage() {
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -70,6 +71,7 @@ export default function NATPage() {
|
||||
{
|
||||
header: "Local service",
|
||||
accessorKey: "host",
|
||||
accessorFn: row => row.host,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -82,6 +84,7 @@ export default function NATPage() {
|
||||
{
|
||||
header: "Bind hostname",
|
||||
accessorKey: "domain",
|
||||
accessorFn: row => row.domain,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
|
||||
@@ -54,6 +54,7 @@ export default function NotificationGroupPage() {
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.group.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -96,8 +97,8 @@ export default function NotificationGroupPage() {
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4 gap-[60%]">
|
||||
<GroupTab className="flex-1" />
|
||||
<div className="flex mt-6 mb-4">
|
||||
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||
fn: deleteNotificationGroups,
|
||||
id: selectedRows.map(r => r.original.group.id),
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function NotificationPage() {
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -77,6 +78,7 @@ export default function NotificationPage() {
|
||||
{
|
||||
header: "URL",
|
||||
accessorKey: "url",
|
||||
accessorFn: row => row.url,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -119,8 +121,8 @@ export default function NotificationPage() {
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4 gap-[60%]">
|
||||
<NotificationTab className="flex-1" />
|
||||
<div className="flex mt-6 mb-4">
|
||||
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||
fn: deleteNotification,
|
||||
id: selectedRows.map(r => r.original.id),
|
||||
|
||||
@@ -54,6 +54,7 @@ export default function ServerGroupPage() {
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.group.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -96,8 +97,8 @@ export default function ServerGroupPage() {
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
<div className="flex mt-6 mb-4 gap-[60%]">
|
||||
<GroupTab className="flex-1" />
|
||||
<div className="flex mt-6 mb-4">
|
||||
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
||||
fn: deleteServerGroups,
|
||||
id: selectedRows.map(r => r.original.group.id),
|
||||
|
||||
@@ -59,6 +59,7 @@ export default function ServerPage() {
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
accessorFn: row => row.name,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
@@ -81,6 +82,7 @@ export default function ServerPage() {
|
||||
id: "ip",
|
||||
header: "IP",
|
||||
accessorKey: "host.ip",
|
||||
accessorFn: row => row.host?.ip,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
return (
|
||||
|
||||
@@ -54,6 +54,7 @@ export default function ServicePage() {
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
accessorFn: row => row.service.name,
|
||||
accessorKey: "service.name",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
@@ -66,6 +67,7 @@ export default function ServicePage() {
|
||||
},
|
||||
{
|
||||
header: "Target",
|
||||
accessorFn: row => row.service.target,
|
||||
accessorKey: "service.target",
|
||||
cell: ({ row }) => {
|
||||
const s = row.original;
|
||||
@@ -79,15 +81,23 @@ export default function ServicePage() {
|
||||
{
|
||||
header: "Coverage",
|
||||
accessorKey: "service.cover",
|
||||
accessorFn: row => {
|
||||
switch (row.service.cover) {
|
||||
case 0: {
|
||||
return "Cover All"
|
||||
}
|
||||
case 1: {
|
||||
return "Ignore All"
|
||||
}
|
||||
}
|
||||
accessorFn: row => row.service.cover,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original.service;
|
||||
return (
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{(() => {
|
||||
switch (s.cover) {
|
||||
case 0: {
|
||||
return <span>Cover All</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>Ignore All</span>
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -98,7 +108,8 @@ export default function ServicePage() {
|
||||
{
|
||||
header: "Type",
|
||||
accessorKey: "service.type",
|
||||
accessorFn: row => serviceTypes[row.service.type] || '',
|
||||
accessorFn: row => row.service.type,
|
||||
cell: ({ row }) => serviceTypes[row.original.service.type] || '',
|
||||
},
|
||||
{
|
||||
header: "Interval",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ModelNotification, ModelNotificationGroupResponseItem } from "@/types";
|
||||
import { NotificationIdentifierType, ModelNotificationGroupResponseItem } from "@/types";
|
||||
|
||||
export interface NotificationContextProps {
|
||||
notifiers?: ModelNotification[];
|
||||
notifiers?: NotificationIdentifierType[];
|
||||
notifierGroup?: ModelNotificationGroupResponseItem[];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { ModelNotification, ModelNotificationGroupResponseItem } from "@/types";
|
||||
import { ModelNotificationGroupResponseItem } from "@/types";
|
||||
|
||||
export interface NotificationIdentifierType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface NotificationStore {
|
||||
notifiers?: ModelNotification[];
|
||||
notifiers?: NotificationIdentifierType[];
|
||||
notifierGroup?: ModelNotificationGroupResponseItem[];
|
||||
setNotifier: (notifiers?: ModelNotification[]) => void;
|
||||
setNotifier: (notifiers?: NotificationIdentifierType[]) => void;
|
||||
setNotifierGroup: (notifierGroup?: ModelNotificationGroupResponseItem[]) => void;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ModelServerGroupResponseItem, ModelServer } from "@/types";
|
||||
import { ModelServerGroupResponseItem, ServerIdentifierType } from "@/types";
|
||||
|
||||
export interface ServerContextProps {
|
||||
servers?: ModelServer[];
|
||||
servers?: ServerIdentifierType[];
|
||||
serverGroups?: ModelServerGroupResponseItem[];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { ModelServer, ModelServerGroupResponseItem } from "@/types";
|
||||
import { ModelServerGroupResponseItem } from "@/types";
|
||||
|
||||
export interface ServerIdentifierType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ServerStore {
|
||||
server?: ModelServer[];
|
||||
server?: ServerIdentifierType[];
|
||||
serverGroup?: ModelServerGroupResponseItem[];
|
||||
setServer: (server?: ModelServer[]) => void;
|
||||
setServer: (server?: ServerIdentifierType[]) => void;
|
||||
setServerGroup: (serverGroup?: ModelServerGroupResponseItem[]) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user