mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-05 13:10:08 +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",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zustand": "^5.0.1"
|
"zustand": "^5.0.1"
|
||||||
},
|
},
|
||||||
@@ -6355,6 +6356,19 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.10",
|
"version": "5.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "^1.1.1",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zustand": "^5.0.1"
|
"zustand": "^5.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import { asOptionalField } from "@/lib/utils"
|
|||||||
import { IconButton } from "@/components/xui/icon-button"
|
import { IconButton } from "@/components/xui/icon-button"
|
||||||
import { triggerModes } from "@/types"
|
import { triggerModes } from "@/types"
|
||||||
import { Textarea } from "./ui/textarea"
|
import { Textarea } from "./ui/textarea"
|
||||||
|
import { useNotification } from "@/hooks/useNotfication"
|
||||||
|
import { Combobox } from "./ui/combobox"
|
||||||
|
|
||||||
interface AlertRuleCardProps {
|
interface AlertRuleCardProps {
|
||||||
data?: ModelAlertRule;
|
data?: ModelAlertRule;
|
||||||
@@ -108,12 +110,19 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
|||||||
|
|
||||||
const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => {
|
const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => {
|
||||||
values.rules = JSON.parse(values.rules_raw);
|
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);
|
setOpen(false);
|
||||||
await mutate();
|
await mutate();
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { notifierGroup } = useNotification();
|
||||||
|
const ngroupList = notifierGroup?.map(ng => ({
|
||||||
|
value: `${ng.group.id}`,
|
||||||
|
label: ng.group.name,
|
||||||
|
})) || [{ value: "", label: "" }];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -169,12 +178,13 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
|||||||
name="notification_group_id"
|
name="notification_group_id"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Notifier Group ID</FormLabel>
|
<FormLabel>Notifier Group</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Combobox
|
||||||
type="number"
|
placeholder="Search..."
|
||||||
placeholder="0"
|
options={ngroupList}
|
||||||
{...field}
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value.toString()}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ import { createCron, updateCron } from "@/api/cron"
|
|||||||
import { asOptionalField } from "@/lib/utils"
|
import { asOptionalField } from "@/lib/utils"
|
||||||
import { cronTypes, cronCoverageTypes } from "@/types"
|
import { cronTypes, cronCoverageTypes } from "@/types"
|
||||||
import { Textarea } from "./ui/textarea"
|
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 {
|
interface CronCardProps {
|
||||||
data?: ModelCron;
|
data?: ModelCron;
|
||||||
@@ -82,6 +85,18 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
|||||||
form.reset();
|
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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -200,14 +215,10 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Specific Servers (Separate with comma)</FormLabel>
|
<FormLabel>Specific Servers (Separate with comma)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<MultiSelect
|
||||||
placeholder="1,2,3"
|
options={serverList}
|
||||||
{...field}
|
onValueChange={field.onChange}
|
||||||
value={conv.arrToStr(field.value ?? [])}
|
defaultValue={field.value?.map(String)}
|
||||||
onChange={e => {
|
|
||||||
const arr = conv.strToArr(e.target.value);
|
|
||||||
field.onChange(arr);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -221,10 +232,11 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Notifier Group ID</FormLabel>
|
<FormLabel>Notifier Group ID</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Combobox
|
||||||
type="number"
|
placeholder="Search..."
|
||||||
placeholder="0"
|
options={ngroupList}
|
||||||
{...field}
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value.toString()}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -14,94 +14,198 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem,
|
|||||||
import { User, LogOut } from "lucide-react";
|
import { User, LogOut } from "lucide-react";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
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() {
|
export default function Header() {
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
const profile = useMainStore(store => store.profile);
|
const profile = useMainStore(store => store.profile);
|
||||||
|
|
||||||
const location = useLocation();
|
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">
|
const [open, setOpen] = useState(false)
|
||||||
<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>
|
|
||||||
|
|
||||||
{
|
return (
|
||||||
profile && <>
|
isDesktop ? (
|
||||||
<NavigationMenuItem>
|
<header className="h-16 flex items-center border-b-2 px-4 overflow-x-auto">
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard"} className={navigationMenuTriggerStyle()}>
|
<NavigationMenu className="sm:max-w-full">
|
||||||
<Link to="/dashboard">Server</Link>
|
<NavigationMenuList>
|
||||||
</NzNavigationMenuLink>
|
<Card className="mr-1">
|
||||||
</NavigationMenuItem>
|
<NavigationMenuLink asChild className={navigationMenuTriggerStyle() + ' !text-foreground'}>
|
||||||
<NavigationMenuItem>
|
<Link to="/dashboard"><img className="h-7 mr-1" src='/dashboard/logo.svg' /> 哪吒监控</Link>
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/service"} className={navigationMenuTriggerStyle()}>
|
</NavigationMenuLink>
|
||||||
<Link to="/dashboard/service">Service</Link>
|
</Card>
|
||||||
</NzNavigationMenuLink>
|
|
||||||
</NavigationMenuItem>
|
{
|
||||||
<NavigationMenuItem>
|
profile && (
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/cron"} className={navigationMenuTriggerStyle()}>
|
<>
|
||||||
<Link to="/dashboard/cron">Task</Link>
|
<NavigationMenuItem>
|
||||||
</NzNavigationMenuLink>
|
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard"} className={navigationMenuTriggerStyle()}>
|
||||||
</NavigationMenuItem>
|
<Link to="/dashboard">Server</Link>
|
||||||
<NavigationMenuItem>
|
</NzNavigationMenuLink>
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/notification" || location.pathname === "/dashboard/alert-rule"} className={navigationMenuTriggerStyle()}>
|
</NavigationMenuItem>
|
||||||
<Link to="/dashboard/notification">Notification</Link>
|
<NavigationMenuItem>
|
||||||
</NzNavigationMenuLink>
|
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/service"} className={navigationMenuTriggerStyle()}>
|
||||||
</NavigationMenuItem>
|
<Link to="/dashboard/service">Service</Link>
|
||||||
<NavigationMenuItem>
|
</NzNavigationMenuLink>
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/ddns"} className={navigationMenuTriggerStyle()}>
|
</NavigationMenuItem>
|
||||||
<Link to="/dashboard/ddns">Dynamic DNS</Link>
|
<NavigationMenuItem>
|
||||||
</NzNavigationMenuLink>
|
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/cron"} className={navigationMenuTriggerStyle()}>
|
||||||
</NavigationMenuItem>
|
<Link to="/dashboard/cron">Task</Link>
|
||||||
<NavigationMenuItem>
|
</NzNavigationMenuLink>
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/nat"} className={navigationMenuTriggerStyle()}>
|
</NavigationMenuItem>
|
||||||
<Link to="/dashboard/nat">NAT Traversal</Link>
|
<NavigationMenuItem>
|
||||||
</NzNavigationMenuLink>
|
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/notification" || location.pathname === "/dashboard/alert-rule"} className={navigationMenuTriggerStyle()}>
|
||||||
</NavigationMenuItem>
|
<Link to="/dashboard/notification">Notification</Link>
|
||||||
<NavigationMenuItem>
|
</NzNavigationMenuLink>
|
||||||
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/server-group" || location.pathname === "/dashboard/notification-group"} className={navigationMenuTriggerStyle()}>
|
</NavigationMenuItem>
|
||||||
<Link to="/dashboard/server-group">Group</Link>
|
<NavigationMenuItem>
|
||||||
</NzNavigationMenuLink>
|
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/ddns"} className={navigationMenuTriggerStyle()}>
|
||||||
</NavigationMenuItem>
|
<Link to="/dashboard/ddns">Dynamic DNS</Link>
|
||||||
</>
|
</NzNavigationMenuLink>
|
||||||
}
|
</NavigationMenuItem>
|
||||||
</NavigationMenuList>
|
<NavigationMenuItem>
|
||||||
<div className="ml-auto flex items-center">
|
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/nat"} className={navigationMenuTriggerStyle()}>
|
||||||
<ModeToggle />
|
<Link to="/dashboard/nat">NAT Traversal</Link>
|
||||||
{
|
</NzNavigationMenuLink>
|
||||||
profile && <>
|
</NavigationMenuItem>
|
||||||
<DropdownMenu>
|
<NavigationMenuItem>
|
||||||
<DropdownMenuTrigger asChild>
|
<NzNavigationMenuLink asChild active={location.pathname === "/dashboard/server-group" || location.pathname === "/dashboard/notification-group"} className={navigationMenuTriggerStyle()}>
|
||||||
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
|
<Link to="/dashboard/server-group">Group</Link>
|
||||||
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} />
|
</NzNavigationMenuLink>
|
||||||
<AvatarFallback>{profile.username}</AvatarFallback>
|
</NavigationMenuItem>
|
||||||
</Avatar>
|
</>
|
||||||
</DropdownMenuTrigger>
|
)
|
||||||
<DropdownMenuContent className="w-56">
|
}
|
||||||
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
|
</NavigationMenuList>
|
||||||
<DropdownMenuSeparator />
|
<div className="ml-auto flex items-center gap-1">
|
||||||
<DropdownMenuGroup>
|
<ModeToggle />
|
||||||
<DropdownMenuItem>
|
{
|
||||||
<User />
|
profile && <>
|
||||||
<span>Profile</span>
|
<DropdownMenu>
|
||||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
<DropdownMenuTrigger asChild>
|
||||||
</DropdownMenuItem>
|
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
|
||||||
</DropdownMenuGroup>
|
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} />
|
||||||
<DropdownMenuSeparator />
|
<AvatarFallback>{profile.username}</AvatarFallback>
|
||||||
<DropdownMenuItem onClick={logout}>
|
</Avatar>
|
||||||
<LogOut />
|
</DropdownMenuTrigger>
|
||||||
<span>Log out</span>
|
<DropdownMenuContent className="w-56">
|
||||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
|
||||||
</DropdownMenuItem>
|
<DropdownMenuSeparator />
|
||||||
</DropdownMenuContent>
|
<DropdownMenuGroup>
|
||||||
</DropdownMenu>
|
<DropdownMenuItem>
|
||||||
</>
|
<User />
|
||||||
}
|
<span>Profile</span>
|
||||||
</div>
|
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||||
</NavigationMenu>
|
</DropdownMenuItem>
|
||||||
</header>
|
</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 { asOptionalField } from "@/lib/utils"
|
||||||
import { IconButton } from "@/components/xui/icon-button"
|
import { IconButton } from "@/components/xui/icon-button"
|
||||||
import { serviceTypes, serviceCoverageTypes } from "@/types"
|
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 {
|
interface ServiceCardProps {
|
||||||
data?: ModelService;
|
data?: ModelService;
|
||||||
@@ -63,6 +67,7 @@ const serviceFormSchema = z.object({
|
|||||||
return v.filter(Boolean).map(Number);
|
return v.filter(Boolean).map(Number);
|
||||||
})),
|
})),
|
||||||
skip_servers: z.record(z.boolean()),
|
skip_servers: z.record(z.boolean()),
|
||||||
|
skip_servers_raw: z.array(z.string()),
|
||||||
target: z.string().url(),
|
target: z.string().url(),
|
||||||
type: z.coerce.number().int().min(0),
|
type: z.coerce.number().int().min(0),
|
||||||
});
|
});
|
||||||
@@ -70,7 +75,10 @@ const serviceFormSchema = z.object({
|
|||||||
export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||||
const form = useForm<z.infer<typeof serviceFormSchema>>({
|
const form = useForm<z.infer<typeof serviceFormSchema>>({
|
||||||
resolver: zodResolver(serviceFormSchema),
|
resolver: zodResolver(serviceFormSchema),
|
||||||
defaultValues: data ? data : {
|
defaultValues: data ? {
|
||||||
|
...data,
|
||||||
|
skip_servers_raw: conv.recordToStrArr(data.skip_servers),
|
||||||
|
} : {
|
||||||
type: 1,
|
type: 1,
|
||||||
cover: 0,
|
cover: 0,
|
||||||
name: "",
|
name: "",
|
||||||
@@ -82,6 +90,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
|||||||
fail_trigger_tasks: [],
|
fail_trigger_tasks: [],
|
||||||
recover_trigger_tasks: [],
|
recover_trigger_tasks: [],
|
||||||
skip_servers: {},
|
skip_servers: {},
|
||||||
|
skip_servers_raw: [],
|
||||||
},
|
},
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
keepDefaultValues: false,
|
keepDefaultValues: false,
|
||||||
@@ -91,12 +100,26 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const onSubmit = async (values: z.infer<typeof serviceFormSchema>) => {
|
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);
|
setOpen(false);
|
||||||
await mutate();
|
await mutate();
|
||||||
form.reset();
|
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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -229,19 +252,15 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
|||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="skip_servers"
|
name="skip_servers_raw"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Specific Servers (separate with comma)</FormLabel>
|
<FormLabel>Specific Servers</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<MultiSelect
|
||||||
placeholder="1,2,3"
|
options={serverList}
|
||||||
{...field}
|
onValueChange={field.onChange}
|
||||||
value={conv.recordToStr(field.value ?? {})}
|
defaultValue={field.value}
|
||||||
onChange={e => {
|
|
||||||
const rec = conv.strToRecord(e.target.value);
|
|
||||||
field.onChange(rec);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -253,12 +272,13 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
|||||||
name="notification_group_id"
|
name="notification_group_id"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Notifier Group ID</FormLabel>
|
<FormLabel>Notifier Group</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Combobox
|
||||||
type="number"
|
placeholder="Search..."
|
||||||
placeholder="1"
|
options={ngroupList}
|
||||||
{...field}
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value.toString()}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<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 () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const n = await getNotification();
|
const n = await getNotification();
|
||||||
setNotifier(n);
|
const nData = n.map(({ id, name }) => ({ id, name }));
|
||||||
|
setNotifier(nData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setNotifier(undefined);
|
setNotifier(undefined);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const ServerProvider: React.FC<ServerProviderProps> = ({ children, withSe
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const s = await getServers();
|
const s = await getServers();
|
||||||
setServer(s);
|
const serverData = s.map(({ id, name }) => ({ id, name }));
|
||||||
|
setServer(serverData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setServer(undefined);
|
setServer(undefined);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,3 +74,16 @@ body,
|
|||||||
#root {
|
#root {
|
||||||
height: 100%;
|
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;
|
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) => {
|
export const sleep = (ms: number) => {
|
||||||
|
|||||||
18
src/main.tsx
18
src/main.tsx
@@ -41,11 +41,23 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/dashboard/service",
|
path: "/dashboard/service",
|
||||||
element: <ServicePage />,
|
element: (
|
||||||
|
<ServerProvider withServer>
|
||||||
|
<NotificationProvider withNotifierGroup>
|
||||||
|
<ServicePage />
|
||||||
|
</NotificationProvider>
|
||||||
|
</ServerProvider>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/dashboard/cron",
|
path: "/dashboard/cron",
|
||||||
element: <CronPage />,
|
element: (
|
||||||
|
<ServerProvider withServer>
|
||||||
|
<NotificationProvider withNotifierGroup>
|
||||||
|
<CronPage />
|
||||||
|
</NotificationProvider>
|
||||||
|
</ServerProvider>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/dashboard/notification",
|
path: "/dashboard/notification",
|
||||||
@@ -53,7 +65,7 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/dashboard/alert-rule",
|
path: "/dashboard/alert-rule",
|
||||||
element: <AlertRulePage />,
|
element: <NotificationProvider withNotifierGroup><AlertRulePage /></NotificationProvider>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/dashboard/ddns",
|
path: "/dashboard/ddns",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default function AlertRulePage() {
|
|||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
|
accessorFn: row => row.name,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -121,8 +122,8 @@ export default function AlertRulePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-8">
|
<div className="px-8">
|
||||||
<div className="flex mt-6 mb-4 gap-[60%]">
|
<div className="flex mt-6 mb-4">
|
||||||
<NotificationTab className="flex-1" />
|
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||||
fn: deleteAlertRules,
|
fn: deleteAlertRules,
|
||||||
id: selectedRows.map(r => r.original.id),
|
id: selectedRows.map(r => r.original.id),
|
||||||
|
|||||||
@@ -99,19 +99,27 @@ export default function CronPage() {
|
|||||||
{
|
{
|
||||||
header: "Coverage",
|
header: "Coverage",
|
||||||
accessorKey: "cover",
|
accessorKey: "cover",
|
||||||
accessorFn: row => {
|
accessorFn: row => row.cover,
|
||||||
switch (row.cover) {
|
cell: ({ row }) => {
|
||||||
case 0: {
|
const s = row.original;
|
||||||
return "Ignore All"
|
return (
|
||||||
}
|
<div className="max-w-48 whitespace-normal break-words">
|
||||||
case 1: {
|
{(() => {
|
||||||
return "Cover All"
|
switch (s.cover) {
|
||||||
}
|
case 0: {
|
||||||
case 2: {
|
return <span>Ignore All</span>
|
||||||
return "On alert"
|
}
|
||||||
}
|
case 1: {
|
||||||
}
|
return <span>Cover All</span>
|
||||||
},
|
}
|
||||||
|
case 2: {
|
||||||
|
return <span>On alert</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Specific Servers",
|
header: "Specific Servers",
|
||||||
@@ -121,7 +129,15 @@ export default function CronPage() {
|
|||||||
{
|
{
|
||||||
header: "Last Execution",
|
header: "Last Execution",
|
||||||
accessorKey: "lastExecution",
|
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",
|
header: "Last Result",
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export default function DDNSPage() {
|
|||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
|
accessorFn: row => row.name,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -89,6 +90,7 @@ export default function DDNSPage() {
|
|||||||
{
|
{
|
||||||
header: "Domains",
|
header: "Domains",
|
||||||
accessorKey: "domains",
|
accessorKey: "domains",
|
||||||
|
accessorFn: row => row.domains,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="shadcn" {...field} />
|
<Input placeholder="shadcn" autoComplete="username" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
This is your public display name.
|
This is your public display name.
|
||||||
@@ -66,7 +66,7 @@ export default () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="shadcn" {...field} />
|
<Input type="password" placeholder="shadcn" autoComplete="current-password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
This is your public display name.
|
This is your public display name.
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default function NATPage() {
|
|||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
|
accessorFn: row => row.name,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -70,6 +71,7 @@ export default function NATPage() {
|
|||||||
{
|
{
|
||||||
header: "Local service",
|
header: "Local service",
|
||||||
accessorKey: "host",
|
accessorKey: "host",
|
||||||
|
accessorFn: row => row.host,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -82,6 +84,7 @@ export default function NATPage() {
|
|||||||
{
|
{
|
||||||
header: "Bind hostname",
|
header: "Bind hostname",
|
||||||
accessorKey: "domain",
|
accessorKey: "domain",
|
||||||
|
accessorFn: row => row.domain,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default function NotificationGroupPage() {
|
|||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
|
accessorFn: row => row.group.name,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -96,8 +97,8 @@ export default function NotificationGroupPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-8">
|
<div className="px-8">
|
||||||
<div className="flex mt-6 mb-4 gap-[60%]">
|
<div className="flex mt-6 mb-4">
|
||||||
<GroupTab className="flex-1" />
|
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||||
fn: deleteNotificationGroups,
|
fn: deleteNotificationGroups,
|
||||||
id: selectedRows.map(r => r.original.group.id),
|
id: selectedRows.map(r => r.original.group.id),
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export default function NotificationPage() {
|
|||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
|
accessorFn: row => row.name,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -77,6 +78,7 @@ export default function NotificationPage() {
|
|||||||
{
|
{
|
||||||
header: "URL",
|
header: "URL",
|
||||||
accessorKey: "url",
|
accessorKey: "url",
|
||||||
|
accessorFn: row => row.url,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -119,8 +121,8 @@ export default function NotificationPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-8">
|
<div className="px-8">
|
||||||
<div className="flex mt-6 mb-4 gap-[60%]">
|
<div className="flex mt-6 mb-4">
|
||||||
<NotificationTab className="flex-1" />
|
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||||
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
<HeaderButtonGroup className="flex-2 flex gap-2 ml-auto" delete={{
|
||||||
fn: deleteNotification,
|
fn: deleteNotification,
|
||||||
id: selectedRows.map(r => r.original.id),
|
id: selectedRows.map(r => r.original.id),
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default function ServerGroupPage() {
|
|||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
|
accessorFn: row => row.group.name,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -96,8 +97,8 @@ export default function ServerGroupPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-8">
|
<div className="px-8">
|
||||||
<div className="flex mt-6 mb-4 gap-[60%]">
|
<div className="flex mt-6 mb-4">
|
||||||
<GroupTab className="flex-1" />
|
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
|
||||||
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
<HeaderButtonGroup className="flex-2 flex ml-auto gap-2" delete={{
|
||||||
fn: deleteServerGroups,
|
fn: deleteServerGroups,
|
||||||
id: selectedRows.map(r => r.original.group.id),
|
id: selectedRows.map(r => r.original.group.id),
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export default function ServerPage() {
|
|||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
|
accessorFn: row => row.name,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
@@ -81,6 +82,7 @@ export default function ServerPage() {
|
|||||||
id: "ip",
|
id: "ip",
|
||||||
header: "IP",
|
header: "IP",
|
||||||
accessorKey: "host.ip",
|
accessorKey: "host.ip",
|
||||||
|
accessorFn: row => row.host?.ip,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default function ServicePage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Name",
|
header: "Name",
|
||||||
|
accessorFn: row => row.service.name,
|
||||||
accessorKey: "service.name",
|
accessorKey: "service.name",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
@@ -66,6 +67,7 @@ export default function ServicePage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Target",
|
header: "Target",
|
||||||
|
accessorFn: row => row.service.target,
|
||||||
accessorKey: "service.target",
|
accessorKey: "service.target",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original;
|
const s = row.original;
|
||||||
@@ -79,15 +81,23 @@ export default function ServicePage() {
|
|||||||
{
|
{
|
||||||
header: "Coverage",
|
header: "Coverage",
|
||||||
accessorKey: "service.cover",
|
accessorKey: "service.cover",
|
||||||
accessorFn: row => {
|
accessorFn: row => row.service.cover,
|
||||||
switch (row.service.cover) {
|
cell: ({ row }) => {
|
||||||
case 0: {
|
const s = row.original.service;
|
||||||
return "Cover All"
|
return (
|
||||||
}
|
<div className="max-w-48 whitespace-normal break-words">
|
||||||
case 1: {
|
{(() => {
|
||||||
return "Ignore All"
|
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",
|
header: "Type",
|
||||||
accessorKey: "service.type",
|
accessorKey: "service.type",
|
||||||
accessorFn: row => serviceTypes[row.service.type] || '',
|
accessorFn: row => row.service.type,
|
||||||
|
cell: ({ row }) => serviceTypes[row.original.service.type] || '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Interval",
|
header: "Interval",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ModelNotification, ModelNotificationGroupResponseItem } from "@/types";
|
import { NotificationIdentifierType, ModelNotificationGroupResponseItem } from "@/types";
|
||||||
|
|
||||||
export interface NotificationContextProps {
|
export interface NotificationContextProps {
|
||||||
notifiers?: ModelNotification[];
|
notifiers?: NotificationIdentifierType[];
|
||||||
notifierGroup?: ModelNotificationGroupResponseItem[];
|
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 {
|
export interface NotificationStore {
|
||||||
notifiers?: ModelNotification[];
|
notifiers?: NotificationIdentifierType[];
|
||||||
notifierGroup?: ModelNotificationGroupResponseItem[];
|
notifierGroup?: ModelNotificationGroupResponseItem[];
|
||||||
setNotifier: (notifiers?: ModelNotification[]) => void;
|
setNotifier: (notifiers?: NotificationIdentifierType[]) => void;
|
||||||
setNotifierGroup: (notifierGroup?: ModelNotificationGroupResponseItem[]) => void;
|
setNotifierGroup: (notifierGroup?: ModelNotificationGroupResponseItem[]) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ModelServerGroupResponseItem, ModelServer } from "@/types";
|
import { ModelServerGroupResponseItem, ServerIdentifierType } from "@/types";
|
||||||
|
|
||||||
export interface ServerContextProps {
|
export interface ServerContextProps {
|
||||||
servers?: ModelServer[];
|
servers?: ServerIdentifierType[];
|
||||||
serverGroups?: ModelServerGroupResponseItem[];
|
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 {
|
export interface ServerStore {
|
||||||
server?: ModelServer[];
|
server?: ServerIdentifierType[];
|
||||||
serverGroup?: ModelServerGroupResponseItem[];
|
serverGroup?: ModelServerGroupResponseItem[];
|
||||||
setServer: (server?: ModelServer[]) => void;
|
setServer: (server?: ServerIdentifierType[]) => void;
|
||||||
setServerGroup: (serverGroup?: ModelServerGroupResponseItem[]) => void;
|
setServerGroup: (serverGroup?: ModelServerGroupResponseItem[]) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user