diff --git a/src/api/user.ts b/src/api/user.ts index d8cafc1..d2cb0e2 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,12 +1,12 @@ -import { ModelProfile, ModelUserForm } from "@/types" +import { ModelProfile, ModelUserForm, ModelProfileForm } from "@/types" import { fetcher, FetcherMethod } from "./api" export const getProfile = async (): Promise => { return fetcher(FetcherMethod.GET, '/api/v1/profile', null); } -export const login = async (username: string, password: string): Promise => { - return fetcher(FetcherMethod.POST, '/api/v1/login', { username, password }); +export const login = async (username: string, password: string): Promise => { + return fetcher(FetcherMethod.POST, '/api/v1/login', { username, password }); } export const createUser = async (data: ModelUserForm): Promise => { @@ -16,3 +16,7 @@ export const createUser = async (data: ModelUserForm): Promise => { export const deleteUser = async (id: number[]): Promise => { return fetcher(FetcherMethod.POST, '/api/v1/batch-delete/user', id); } + +export const updateProfile = async (data: ModelProfileForm): Promise => { + return fetcher(FetcherMethod.POST, '/api/v1/profile', data); +} diff --git a/src/components/alert-rule.tsx b/src/components/alert-rule.tsx index 9330f46..bf76c8d 100644 --- a/src/components/alert-rule.tsx +++ b/src/components/alert-rule.tsx @@ -75,12 +75,8 @@ const alertRuleFormSchema = z.object({ message: 'Invalid JSON string', }), rules: z.array(ruleSchema), - fail_trigger_tasks: z.array(z.string()).transform((v => { - return v.filter(Boolean).map(Number); - })), - recover_trigger_tasks: z.array(z.string()).transform((v => { - return v.filter(Boolean).map(Number); - })), + fail_trigger_tasks: z.array(z.number()), + recover_trigger_tasks: z.array(z.number()), notification_group_id: z.coerce.number().int(), trigger_mode: z.coerce.number().int().min(0), enable: asOptionalField(z.boolean()), @@ -225,7 +221,7 @@ export const AlertRuleCard: React.FC = ({ data, mutate }) => {...field} value={conv.arrToStr(field.value ?? [])} onChange={e => { - const arr = conv.strToArr(e.target.value); + const arr = conv.strToArr(e.target.value).map(Number); field.onChange(arr); }} /> @@ -246,7 +242,7 @@ export const AlertRuleCard: React.FC = ({ data, mutate }) => {...field} value={conv.arrToStr(field.value ?? [])} onChange={e => { - const arr = conv.strToArr(e.target.value); + const arr = conv.strToArr(e.target.value).map(Number); field.onChange(arr); }} /> diff --git a/src/components/cron.tsx b/src/components/cron.tsx index 931adb0..7b5d644 100644 --- a/src/components/cron.tsx +++ b/src/components/cron.tsx @@ -52,9 +52,7 @@ const cronFormSchema = z.object({ name: z.string().min(1), scheduler: z.string(), command: asOptionalField(z.string()), - servers: z.array(z.string()).transform((v => { - return v.filter(Boolean).map(Number); - })), + servers: z.array(z.number()), cover: z.coerce.number().int(), push_successful: asOptionalField(z.boolean()), notification_group_id: z.coerce.number().int(), @@ -217,7 +215,10 @@ export const CronCard: React.FC = ({ data, mutate }) => { { + const arr = e.map(Number); + field.onChange(arr); + }} defaultValue={field.value?.map(String)} /> diff --git a/src/components/header.tsx b/src/components/header.tsx index f2cda57..06c44d2 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -11,7 +11,7 @@ import { useMainStore } from "@/hooks/useMainStore"; import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; import { NzNavigationMenuLink } from "./xui/navigation-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger } from "./ui/dropdown-menu"; -import { LogOut, Settings } from "lucide-react"; +import { LogOut, Settings, User2 } from "lucide-react"; import { useAuth } from "@/hooks/useAuth"; import { Link, useLocation } from "react-router-dom"; import { useMediaQuery } from "@/hooks/useMediaQuery"; @@ -117,11 +117,18 @@ export default function Header() { {profile.username} + { setDropdownOpen(false) }}> + + + Profile + ⇧⌘P + + { setDropdownOpen(false) }}> Settings - ⇧⌘P + ⇧⌘S @@ -191,11 +198,18 @@ export default function Header() { {profile.username} + { setDropdownOpen(false) }}> + + + Profile + ⇧⌘P + + { setDropdownOpen(false) }}> Settings - ⇧⌘P + ⇧⌘S diff --git a/src/components/notification-group.tsx b/src/components/notification-group.tsx index 562f458..ebda677 100644 --- a/src/components/notification-group.tsx +++ b/src/components/notification-group.tsx @@ -37,9 +37,7 @@ interface NotificationGroupCardProps { const notificationGroupFormSchema = z.object({ name: z.string().min(1), - notifications: z.array(z.string()).transform((v => { - return v.filter(Boolean).map(Number); - })), + notifications: z.array(z.number()), }); export const NotificationGroupCard: React.FC = ({ data, mutate }) => { @@ -115,7 +113,10 @@ export const NotificationGroupCard: React.FC = ({ da Notifiers { + const arr = e.map(Number); + field.onChange(arr); + }} defaultValue={field.value?.map(String)} /> diff --git a/src/components/profile.tsx b/src/components/profile.tsx new file mode 100644 index 0000000..cabd24a --- /dev/null +++ b/src/components/profile.tsx @@ -0,0 +1,127 @@ +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { ScrollArea } from "@/components/ui/scroll-area" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { getProfile, updateProfile } from "@/api/user" +import { useState } from "react" +import { useMainStore } from "@/hooks/useMainStore" +import { toast } from "sonner" + +const profileFormSchema = z.object({ + original_password: z.string().min(5).max(72), + new_password: z.string().min(8).max(72), +}); + +export const ProfileCard = ({ className }: { className: string }) => { + const form = useForm>({ + resolver: zodResolver(profileFormSchema), + defaultValues: { + original_password: '', + new_password: '', + }, + resetOptions: { + keepDefaultValues: false, + } + }) + + const { setProfile } = useMainStore(); + const [open, setOpen] = useState(false); + + const onSubmit = async (values: z.infer) => { + try { + await updateProfile(values); + } catch (e) { + toast("Update failed", { + description: `${e}`, + }) + return; + } + const profile = await getProfile(); + setProfile(profile); + setOpen(false); + form.reset(); + } + + return ( + + + + + + +
+ + Update Server + + +
+ + ( + + Original Password + + + + + + )} + /> + ( + + New Password + + + + + + )} + /> + + + + + + + + + +
+
+
+
+ ) +} diff --git a/src/components/server-group.tsx b/src/components/server-group.tsx index c20d1b8..df16032 100644 --- a/src/components/server-group.tsx +++ b/src/components/server-group.tsx @@ -37,9 +37,7 @@ interface ServerGroupCardProps { const serverGroupFormSchema = z.object({ name: z.string().min(1), - servers: z.array(z.string()).transform((v => { - return v.filter(Boolean).map(Number); - })), + servers: z.array(z.number()), }); export const ServerGroupCard: React.FC = ({ data, mutate }) => { @@ -116,7 +114,10 @@ export const ServerGroupCard: React.FC = ({ data, mutate } { + const arr = e.map(Number); + field.onChange(arr); + }} defaultValue={field.value?.map(String)} /> diff --git a/src/components/server.tsx b/src/components/server.tsx index 771786b..4cfbfea 100644 --- a/src/components/server.tsx +++ b/src/components/server.tsx @@ -45,9 +45,7 @@ const serverFormSchema = z.object({ display_index: z.number().int(), hide_for_guest: asOptionalField(z.boolean()), enable_ddns: asOptionalField(z.boolean()), - ddns_profiles: z.array(z.string()).transform((v => { - return v.filter(Boolean).map(Number); - })), + ddns_profiles: asOptionalField(z.array(z.number())), }); export const ServerCard: React.FC = ({ data, mutate }) => { @@ -77,7 +75,7 @@ export const ServerCard: React.FC = ({ data, mutate }) => {
- New Service + Update Server
@@ -125,9 +123,10 @@ export const ServerCard: React.FC = ({ data, mutate }) => { { - const arr = conv.strToArr(e.target.value); + console.log(field.value) + const arr = conv.strToArr(e.target.value).map(Number); field.onChange(arr); }} /> diff --git a/src/components/service.tsx b/src/components/service.tsx index f3775ca..a862c86 100644 --- a/src/components/service.tsx +++ b/src/components/service.tsx @@ -54,21 +54,17 @@ const serviceFormSchema = z.object({ duration: z.coerce.number().int().min(30), enable_show_in_service: asOptionalField(z.boolean()), enable_trigger_task: asOptionalField(z.boolean()), - fail_trigger_tasks: z.array(z.string()).transform((v => { - return v.filter(Boolean).map(Number); - })), + fail_trigger_tasks: z.array(z.number()), latency_notify: asOptionalField(z.boolean()), max_latency: z.coerce.number().int().min(0), min_latency: z.coerce.number().int().min(0), name: z.string().min(1), notification_group_id: z.coerce.number().int(), notify: asOptionalField(z.boolean()), - recover_trigger_tasks: z.array(z.string()).transform((v => { - return v.filter(Boolean).map(Number); - })), + recover_trigger_tasks: z.array(z.number()), skip_servers: z.record(z.boolean()), skip_servers_raw: z.array(z.string()), - target: z.string().url(), + target: z.string(), type: z.coerce.number().int().min(0), }); @@ -385,7 +381,7 @@ export const ServiceCard: React.FC = ({ data, mutate }) => { {...field} value={conv.arrToStr(field.value ?? [])} onChange={e => { - const arr = conv.strToArr(e.target.value); + const arr = conv.strToArr(e.target.value).map(Number); field.onChange(arr); }} /> @@ -406,7 +402,7 @@ export const ServiceCard: React.FC = ({ data, mutate }) => { {...field} value={conv.arrToStr(field.value ?? [])} onChange={e => { - const arr = conv.strToArr(e.target.value); + const arr = conv.strToArr(e.target.value).map(Number); field.onChange(arr); }} /> diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b92c1da..10a957e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -34,7 +34,7 @@ export const conv = { return arr.join(','); }, strToArr: (str: string) => { - return str.split(','); + return str.split(',').filter(Boolean) || []; }, recordToArr: (rec: Record) => { const arr: T[] = []; diff --git a/src/main.tsx b/src/main.tsx index c596307..8e2f72a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -27,6 +27,7 @@ import AlertRulePage from './routes/alert-rule'; import SettingsPage from './routes/settings'; import UserPage from './routes/user'; import WAFPage from './routes/waf'; +import ProfilePage from './routes/profile'; const router = createBrowserRouter([ { @@ -90,6 +91,10 @@ const router = createBrowserRouter([ path: "/dashboard/terminal/:id", element: , }, + { + path: "/dashboard/profile", + element: , + }, { path: "/dashboard/settings", element: , diff --git a/src/routes/profile.tsx b/src/routes/profile.tsx new file mode 100644 index 0000000..44586df --- /dev/null +++ b/src/routes/profile.tsx @@ -0,0 +1,65 @@ +import { useMainStore } from "@/hooks/useMainStore" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { useMediaQuery } from "@/hooks/useMediaQuery"; +import { Server, Boxes } from "lucide-react"; +import { useServer } from "@/hooks/useServer"; +import { ProfileCard } from "@/components/profile"; + +export default function ProfilePage() { + const { profile } = useMainStore(); + const { servers, serverGroups } = useServer(); + const isDesktop = useMediaQuery("(min-width: 890px)") + + return ( + profile && ( +
+
+ + + {profile.username} + +
+

{profile.username}

+

IP: {profile.login_ip || 'Unknown'}

+
+ {isDesktop && + + } +
+ {!isDesktop && + + } +
+
+ + + + Servers + + + + {servers?.length || 0} + + + + + + Server Groups + + + + {serverGroups?.length || 0} + + +
+
+
+ ) + ); +} diff --git a/src/types/api.ts b/src/types/api.ts index 87f936d..792c236 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -423,6 +423,11 @@ export interface ModelProfile { username: string; } +export interface ModelProfileForm { + new_password: string; + original_password: string; +} + export interface ModelRule { /** 覆盖范围 RuleCoverAll/IgnoreAll */ cover: number; @@ -482,7 +487,7 @@ export interface ModelServer { export interface ModelServerForm { /** DDNS配置 */ - ddns_profiles: number[]; + ddns_profiles?: number[]; /** * 展示排序,越大越靠前 * @default 0 @@ -607,6 +612,7 @@ export interface ModelSettingForm { } export interface ModelStreamServer { + country_code: string; /** 展示排序,越大越靠前 */ display_index: number; host: ModelHost;