Skip to content

Commit b474400

Browse files
authored
implement setting page (#12)
1 parent 34b7a67 commit b474400

21 files changed

+941
-36
lines changed

src/api/settings.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ModelSettingForm, ModelConfig } from "@/types"
2+
import { fetcher, FetcherMethod } from "./api"
3+
4+
export const updateSettings = async (data: ModelSettingForm): Promise<void> => {
5+
return fetcher<void>(FetcherMethod.PATCH, `/api/v1/setting`, data);
6+
}
7+
8+
export const getSettings = async (): Promise<ModelConfig> => {
9+
return fetcher<ModelConfig>(FetcherMethod.GET, '/api/v1/setting', null);
10+
}

src/api/user.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import { ModelUser } from "@/types"
1+
import { ModelProfile, ModelUserForm } from "@/types"
22
import { fetcher, FetcherMethod } from "./api"
33

4-
export const getProfile = async (): Promise<ModelUser> => {
5-
return fetcher<ModelUser>(FetcherMethod.GET, '/api/v1/profile', null);
4+
export const getProfile = async (): Promise<ModelProfile> => {
5+
return fetcher<ModelProfile>(FetcherMethod.GET, '/api/v1/profile', null);
66
}
77

88
export const login = async (username: string, password: string): Promise<any> => {
99
return fetcher<any>(FetcherMethod.POST, '/api/v1/login', { username, password });
1010
}
11+
12+
export const createUser = async (data: ModelUserForm): Promise<number> => {
13+
return fetcher<number>(FetcherMethod.POST, '/api/v1/user', data);
14+
}
15+
16+
export const deleteUser = async (id: number[]): Promise<void> => {
17+
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/user', id);
18+
}

src/api/waf.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ModelWAF } from "@/types"
2+
import { fetcher, FetcherMethod } from "./api"
3+
4+
export const deleteWAF = async (ip: string[]): Promise<void> => {
5+
return fetcher<void>(FetcherMethod.POST, '/api/v1/batch-delete/waf', ip);
6+
}
7+
8+
export const getWAFList = async (): Promise<ModelWAF[]> => {
9+
return fetcher<ModelWAF[]>(FetcherMethod.GET, '/api/v1/waf', null);
10+
}

src/components/action-button-group.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import {
1313
import { KeyedMutator } from "swr";
1414
import { buttonVariants } from "@/components/ui/button"
1515

16-
interface ButtonGroupProps<T> {
16+
interface ButtonGroupProps<E, U> {
1717
className?: string;
1818
children: React.ReactNode;
19-
delete: { fn: (id: number[]) => Promise<void>, id: number, mutate: KeyedMutator<T> };
19+
delete: { fn: (id: E[]) => Promise<void>, id: E, mutate: KeyedMutator<U> };
2020
}
2121

22-
export function ActionButtonGroup<T>({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps<T>) {
22+
export function ActionButtonGroup<E, U>({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps<E, U>) {
2323
const handleDelete = async () => {
2424
await fn([id]);
2525
await mutate();

src/components/header-button-group.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import {
1414
import { KeyedMutator } from "swr";
1515
import { toast } from "sonner"
1616

17-
interface ButtonGroupProps<T> {
17+
interface ButtonGroupProps<E, U> {
1818
className?: string;
1919
children?: React.ReactNode;
20-
delete: { fn: (id: number[]) => Promise<void>, id: number[], mutate: KeyedMutator<T> };
20+
delete: { fn: (id: E[]) => Promise<void>, id: E[], mutate: KeyedMutator<U> };
2121
}
2222

23-
export function HeaderButtonGroup<T>({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps<T>) {
23+
export function HeaderButtonGroup<E, U>({ className, children, delete: { fn, id, mutate } }: ButtonGroupProps<E, U>) {
2424
const handleDelete = async () => {
2525
await fn(id);
2626
await mutate();

src/components/header.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { Card } from "./ui/card";
1010
import { useMainStore } from "@/hooks/useMainStore";
1111
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
1212
import { NzNavigationMenuLink } from "./xui/navigation-menu";
13-
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./ui/dropdown-menu";
14-
import { User, LogOut } from "lucide-react";
13+
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from "./ui/dropdown-menu";
14+
import { LogOut, Settings } from "lucide-react";
1515
import { useAuth } from "@/hooks/useAuth";
1616
import { Link, useLocation } from "react-router-dom";
1717
import { useMediaQuery } from "@/hooks/useMediaQuery";
@@ -47,6 +47,7 @@ export default function Header() {
4747
const isDesktop = useMediaQuery("(min-width: 890px)")
4848

4949
const [open, setOpen] = useState(false)
50+
const [dropdownOpen, setDropdownOpen] = useState(false);
5051

5152
return (
5253
isDesktop ? (
@@ -105,7 +106,7 @@ export default function Header() {
105106
<ModeToggle />
106107
{
107108
profile && <>
108-
<DropdownMenu>
109+
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
109110
<DropdownMenuTrigger asChild>
110111
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
111112
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} />
@@ -116,14 +117,16 @@ export default function Header() {
116117
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
117118
<DropdownMenuSeparator />
118119
<DropdownMenuGroup>
119-
<DropdownMenuItem>
120-
<User />
121-
<span>Profile</span>
122-
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
120+
<DropdownMenuItem onClick={() => { setDropdownOpen(false) }}>
121+
<Link to="/dashboard/settings" className="flex items-center gap-2 w-full">
122+
<Settings />
123+
Settings
124+
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
125+
</Link>
123126
</DropdownMenuItem>
124127
</DropdownMenuGroup>
125128
<DropdownMenuSeparator />
126-
<DropdownMenuItem onClick={logout}>
129+
<DropdownMenuItem onClick={logout} className="cursor-pointer">
127130
<LogOut />
128131
<span>Log out</span>
129132
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
@@ -171,13 +174,13 @@ export default function Header() {
171174
}
172175
</div>
173176
<Card className="mx-2 my-2 flex justify-center items-center hover:bg-accent transition duration-200">
174-
<Link className="inline-flex w-full items-center px-4 py-2" to="/dashboard"><img className="h-7 mr-1" src='/dashboard/logo.svg' /> NEZHA</Link>
177+
<Link className="inline-flex w-full items-center px-4 py-2" to={profile ? "/dashboard" : '#'}><img className="h-7 mr-1" src='/dashboard/logo.svg' /> NEZHA</Link>
175178
</Card>
176179
<div className="ml-auto flex items-center gap-1">
177180
<ModeToggle />
178181
{
179182
profile && <>
180-
<DropdownMenu>
183+
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
181184
<DropdownMenuTrigger asChild>
182185
<Avatar className="ml-1 h-8 w-8 cursor-pointer border-foreground border-[1px]">
183186
<AvatarImage src={'https://api.dicebear.com/7.x/notionists/svg?seed=' + profile.username} alt={profile.username} />
@@ -188,14 +191,16 @@ export default function Header() {
188191
<DropdownMenuLabel>{profile.username}</DropdownMenuLabel>
189192
<DropdownMenuSeparator />
190193
<DropdownMenuGroup>
191-
<DropdownMenuItem>
192-
<User />
193-
<span>Profile</span>
194-
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
194+
<DropdownMenuItem onClick={() => { setDropdownOpen(false) }}>
195+
<Link to="/dashboard/settings" className="flex items-center gap-2 w-full">
196+
<Settings />
197+
Settings
198+
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
199+
</Link>
195200
</DropdownMenuItem>
196201
</DropdownMenuGroup>
197202
<DropdownMenuSeparator />
198-
<DropdownMenuItem onClick={logout}>
203+
<DropdownMenuItem onClick={logout} className="cursor-pointer">
199204
<LogOut />
200205
<span>Log out</span>
201206
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>

src/components/service.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
7777
resolver: zodResolver(serviceFormSchema),
7878
defaultValues: data ? {
7979
...data,
80-
skip_servers_raw: conv.recordToStrArr(data.skip_servers),
80+
skip_servers_raw: conv.recordToStrArr(data.skip_servers ? data.skip_servers : {}),
8181
} : {
8282
type: 1,
8383
cover: 0,

src/components/settings-tab.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {
2+
Tabs,
3+
TabsList,
4+
TabsTrigger,
5+
} from "@/components/ui/tabs"
6+
import { Link, useLocation } from "react-router-dom"
7+
8+
export const SettingsTab = ({ className }: { className?: string }) => {
9+
const location = useLocation();
10+
11+
return (
12+
<Tabs defaultValue={location.pathname} className={className}>
13+
<TabsList className="grid w-full grid-cols-3">
14+
<TabsTrigger value="/dashboard/settings" asChild>
15+
<Link to="/dashboard/settings">Config</Link>
16+
</TabsTrigger>
17+
<TabsTrigger value="/dashboard/settings/user" asChild>
18+
<Link to="/dashboard/settings/user">User</Link>
19+
</TabsTrigger>
20+
<TabsTrigger value="/dashboard/settings/waf" asChild>
21+
<Link to="/dashboard/settings/waf">WAF</Link>
22+
</TabsTrigger>
23+
</TabsList>
24+
</Tabs>
25+
)
26+
}

src/components/user.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { Button } from "@/components/ui/button"
2+
import {
3+
Dialog,
4+
DialogClose,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogTrigger,
11+
} from "@/components/ui/dialog"
12+
import { Input } from "@/components/ui/input"
13+
import {
14+
Form,
15+
FormControl,
16+
FormField,
17+
FormItem,
18+
FormLabel,
19+
FormMessage,
20+
} from "@/components/ui/form"
21+
import { ScrollArea } from "@/components/ui/scroll-area"
22+
import { useForm } from "react-hook-form"
23+
import { z } from "zod"
24+
import { zodResolver } from "@hookform/resolvers/zod"
25+
import { ModelUser } from "@/types"
26+
import { useState } from "react"
27+
import { KeyedMutator } from "swr"
28+
import { IconButton } from "@/components/xui/icon-button"
29+
import { createUser } from "@/api/user"
30+
31+
interface UserCardProps {
32+
mutate: KeyedMutator<ModelUser[]>;
33+
}
34+
35+
const userFormSchema = z.object({
36+
username: z.string().min(1),
37+
password: z.string().min(8).max(72),
38+
});
39+
40+
export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
41+
const form = useForm<z.infer<typeof userFormSchema>>({
42+
resolver: zodResolver(userFormSchema),
43+
defaultValues: {
44+
username: "",
45+
password: "",
46+
},
47+
resetOptions: {
48+
keepDefaultValues: false,
49+
}
50+
})
51+
52+
const [open, setOpen] = useState(false);
53+
54+
const onSubmit = async (values: z.infer<typeof userFormSchema>) => {
55+
await createUser(values);
56+
setOpen(false);
57+
await mutate();
58+
form.reset();
59+
}
60+
61+
return (
62+
<Dialog open={open} onOpenChange={setOpen}>
63+
<DialogTrigger asChild>
64+
<IconButton icon="plus" />
65+
</DialogTrigger>
66+
<DialogContent className="sm:max-w-xl">
67+
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
68+
<div className="items-center mx-1">
69+
<DialogHeader>
70+
<DialogTitle>New User</DialogTitle>
71+
<DialogDescription />
72+
</DialogHeader>
73+
<Form {...form}>
74+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
75+
<FormField
76+
control={form.control}
77+
name="username"
78+
render={({ field }) => (
79+
<FormItem>
80+
<FormLabel>Username</FormLabel>
81+
<FormControl>
82+
<Input
83+
{...field}
84+
/>
85+
</FormControl>
86+
<FormMessage />
87+
</FormItem>
88+
)}
89+
/>
90+
<FormField
91+
control={form.control}
92+
name="password"
93+
render={({ field }) => (
94+
<FormItem>
95+
<FormLabel>Password</FormLabel>
96+
<FormControl>
97+
<Input
98+
{...field}
99+
/>
100+
</FormControl>
101+
<FormMessage />
102+
</FormItem>
103+
)}
104+
/>
105+
<DialogFooter className="justify-end">
106+
<DialogClose asChild>
107+
<Button type="button" className="my-2" variant="secondary">
108+
Close
109+
</Button>
110+
</DialogClose>
111+
<Button type="submit" className="my-2">Submit</Button>
112+
</DialogFooter>
113+
</form>
114+
</Form>
115+
</div>
116+
</ScrollArea>
117+
</DialogContent>
118+
</Dialog>
119+
)
120+
}

src/hooks/useNotfication.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useNotificationStore } from "./useNotificationStore"
33
import { getNotificationGroups } from "@/api/notification-group"
44
import { getNotification } from "@/api/notification"
55
import { NotificationContextProps } from "@/types"
6+
import { useLocation } from "react-router-dom"
67

78
const NotificationContext = createContext<NotificationContextProps>({});
89

@@ -19,6 +20,8 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({ chil
1920
const notifiers = useNotificationStore(store => store.notifiers);
2021
const setNotifier = useNotificationStore(store => store.setNotifier);
2122

23+
const location = useLocation();
24+
2225
useEffect(() => {
2326
if (withNotifierGroup)
2427
(async () => {
@@ -39,7 +42,7 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({ chil
3942
setNotifier(undefined);
4043
}
4144
})();
42-
}, [])
45+
}, [location.pathname])
4346

4447
const value: NotificationContextProps = useMemo(() => ({
4548
notifiers: notifiers,

0 commit comments

Comments
 (0)