Fix and update (#139)

* refactor(ui): 统一组件引用类型为ComponentRef

更新所有UI组件中的forwardRef类型,从ElementRef改为ComponentRef以保持一致性
迁移postcss配置至mjs格式并更新依赖版本

* refactor: 优化表单类型定义和验证逻辑

移除自定义的 asOptionalField 工具函数,直接使用 Zod 的 optional() 方法,并明确定义表单数据类型。

* style: 更新UI主题配置和样式变量

将主题风格从default切换为new-york,并重构CSS变量使用OKLCH色彩空间。同时添加tailwindcss-animate插件支持。

* style: 统一页面头部按钮组样式

优化多个页面头部按钮组的布局样式,增加响应式设计和flex-wrap支持

* fix(server): 修复对话框交互问题并优化SWR配置

修复对话框关闭逻辑并阻止外部交互,同时禁用SWR的自动重新验证功能以提升性能。

* feat: 添加日历组件及账单相关国际化

实现基于 react-day-picker 的日历组件,并添加账单管理相关的多语言支持

* style(components): 统一按钮样式并格式化代码

为删除和禁用按钮添加text-white类名,同时调整ServerCard组件中的代码缩进格式。

* perf(build): 优化Vite打包配置与代码分割策略

调整Vite构建配置,改进第三方依赖的分组逻辑并添加UUID支持到安装命令组件

* fix: 修正页面标题翻译不一致问题

将CronPage和ServicePage的标题从"Server"分别改为"Task"和"Service",并优化NotificationGroupPage的按钮组布局。

* fix(auth): 改进登录错误处理和国际化支持

优化登录错误提示,添加多语言支持并移除控制台错误日志。同时修复头部组件透明度样式问题。

* feat: 添加服务器操作下拉菜单

为服务器卡片添加统一的下拉菜单操作入口,整合终端、配置和安装命令功能。

* feat[alert-rule]: 优化告警规则组件性能

重构告警规则组件代码结构,提升渲染效率并减少内存占用。

* docs(i18n): 新增翻译字段

为界面添加"Add"、"Delete"、"AdvancedJSON"和"Save"等关键操作的翻译字段,支持中英文双语显示。

* perf(vite): 优化分包策略以提升构建性能

重构 manualChunks 逻辑,按功能类别分组依赖项,并增加大型库的独立分包规则。

* style: 统一危险操作按钮的文字颜色

在所有确认操作的弹窗按钮中添加白色文字样式,保持视觉一致性。

* fix(components): 调整下拉菜单对齐方式

根据菜单项状态动态设置下拉菜单的对齐方向和起始位置。

* fix(types): 修复在线用户API分页类型

添加ModelOnlineUserApi接口类型,包含分页信息,并移除index.ts中重复的类型定义。

* chore: auto-fix linting and formatting issues

* feat(locales): 添加无过期相关翻译项

为英文和中文翻译文件添加"NoExpiry"、"SetNoExpiry"等无过期相关字段的翻译。

fix(components): 移除重复的图标按钮选项

从IconButton组件中删除重复的"more"图标选项。

* feat(ServerCard): 优化日期选择器并添加下拉提示

为日期选择器添加下拉布局和年份范围限制,并在公共笔记区域增加下拉项生效提示文本。

* chore: auto-fix linting and formatting issues

* style: 优化多个组件的UI交互细节

统一按钮悬停样式并简化国际化文本调用,移除冗余的单位显示和空值判断逻辑。

* refactor(ServerCard): 移除网络路由相关代码

删除 ServerCard 组件中与 plan.networkRoute 相关的字段验证和错误显示逻辑。

* chore: auto-fix linting and formatting issues

* feat(ui): 添加Switch组件并改进服务器表单交互

- 新增Radix UI Switch组件依赖及实现
- 将IPv4/IPv6输入改为开关控件,优化用户体验
- 添加"按量付费"选项和新的翻译字段
- 改进网络路由和备注输入的占位提示
- 修复暗黑模式下的按钮背景色

* style(components): 为禁止按钮添加白色文本样式

* chore: auto-fix linting and formatting issues

* fix(ServerCard): 修复日期选择器样式和滚动问题

调整日期选择器的宽度和高度限制,添加滚动容器以解决内容溢出问题

* refactor(server-config): 简化复选框checked属性的布尔转换

使用!!操作符简化controllerField.value的布尔值转换,使代码更简洁

* feat(国际化): 添加告警规则和搜索框的国际化支持

为告警规则组件添加多语言支持,包括服务器监控选项、忽略提示和示例文本。同时将搜索框的占位文本替换为国际化字段。

* chore: auto-fix linting and formatting issues

* fix(switch): 修正 Switch 组件 ref 类型定义错误

---------

Co-authored-by: Guccen <171530509+Chillln@users.noreply.github.com>
This commit is contained in:
Chillln
2025-10-02 14:23:11 +08:00
committed by GitHub
parent 875750d74e
commit bb288c554f
61 changed files with 2214 additions and 446 deletions
BIN
View File
Binary file not shown.
+3 -1
View File
@@ -10,11 +10,13 @@
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
}, },
"iconLibrary": "lucide",
"aliases": { "aliases": {
"components": "@/components", "components": "@/components",
"utils": "@/lib/utils", "utils": "@/lib/utils",
"ui": "@/components/ui", "ui": "@/components/ui",
"lib": "@/lib", "lib": "@/lib",
"hooks": "@/hooks" "hooks": "@/hooks"
} },
"registries": {}
} }
+54 -50
View File
@@ -13,68 +13,72 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.10.0", "@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.5", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.2", "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.5", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-navigation-menu": "^1.2.4", "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.5", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.5", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-switch": "^1.2.6",
"@tanstack/react-table": "^8.20.6", "@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/postcss": "^4.1.13",
"@tanstack/react-table": "^8.21.3",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.7.1",
"@xterm/addon-attach": "^0.11.0", "@xterm/addon-attach": "^0.11.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.4", "cmdk": "^1.1.1",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"framer-motion": "^11.18.2", "date-fns": "^4.1.0",
"i18next": "^24.2.2", "framer-motion": "^12.23.22",
"i18next-browser-languagedetector": "^8.0.2", "i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"jotai-zustand": "^0.6.0", "jotai-zustand": "^0.6.0",
"lucide-react": "^0.454.0", "lucide-react": "^0.544.0",
"luxon": "^3.5.0", "luxon": "^3.7.2",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.14",
"react": "^19.0.0", "react": "^19.1.1",
"react-dom": "^19.0.0", "react-day-picker": "^9.11.0",
"react-hook-form": "^7.54.2", "react-dom": "^19.1.1",
"react-i18next": "^15.4.0", "react-hook-form": "^7.63.0",
"react-router-dom": "^7.1.5", "react-i18next": "^16.0.0",
"react-virtuoso": "^4.12.3", "react-router-dom": "^7.9.3",
"sonner": "^1.7.4", "react-virtuoso": "^4.14.1",
"swr": "^2.3.0", "sonner": "^2.0.7",
"tailwind-merge": "^2.6.0", "swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"zod": "^3.24.1", "zod": "^4.1.11",
"zustand": "^5.0.3" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.36.0",
"@types/node": "^22.13.0", "@types/node": "^24.5.2",
"@types/react": "^18.3.18", "@types/react": "^19.1.15",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^5.0.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.21",
"eslint": "^9.19.0", "eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-react-refresh": "^0.4.22",
"globals": "^15.14.0", "globals": "^16.4.0",
"postcss": "^8.5.1", "postcss": "^8.5.6",
"swagger-typescript-api": "^13.0.23", "swagger-typescript-api": "^13.2.13",
"tailwindcss": "^3.4.17", "tailwindcss": "^4.1.13",
"typescript": "~5.6.3", "typescript": "~5.9.2",
"typescript-eslint": "^8.22.0", "typescript-eslint": "^8.44.1",
"vite": "^6.0.11" "vite": "^7.1.7"
} }
} }
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
}
+10 -4
View File
@@ -49,7 +49,7 @@ export function ActionButtonGroup<E, U>({
{children} {children}
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="trash" /> <IconButton variant="destructive" icon="trash" className="text-white" />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -61,7 +61,10 @@ export function ActionButtonGroup<E, U>({
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleDelete} onClick={handleDelete}
> >
{t("Confirm")} {t("Confirm")}
@@ -95,7 +98,7 @@ export function BlockButtonGroup<E, U>({
{children} {children}
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="ban" /> <IconButton variant="destructive" icon="ban" className="text-white" />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -107,7 +110,10 @@ export function BlockButtonGroup<E, U>({
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleBlock} onClick={handleBlock}
> >
{t("Confirm")} {t("Confirm")}
+457 -26
View File
@@ -32,11 +32,10 @@ import {
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { useNotification } from "@/hooks/useNotfication" import { useNotification } from "@/hooks/useNotfication"
import { conv } from "@/lib/utils" import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils"
import { ModelAlertRule } from "@/types" import { ModelAlertRule } from "@/types"
import { triggerModes } from "@/types" import { triggerModes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react" import { useEffect, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -53,16 +52,16 @@ interface AlertRuleCardProps {
const ruleSchema = z.object({ const ruleSchema = z.object({
type: z.string(), type: z.string(),
min: asOptionalField(z.number()), min: z.number().optional(),
max: asOptionalField(z.number()), max: z.number().optional(),
cycle_start: asOptionalField(z.string()), cycle_start: z.string().optional(),
cycle_interval: asOptionalField(z.number()), cycle_interval: z.number().optional(),
cycle_unit: asOptionalField(z.enum(["hour", "day", "week", "month", "year"])), cycle_unit: z.enum(["hour", "day", "week", "month", "year"]).optional(),
duration: asOptionalField(z.number()), duration: z.number().optional(),
cover: z.number().int().min(0), cover: z.number().int().min(0),
ignore: asOptionalField(z.record(z.boolean())), ignore: z.record(z.string(), z.boolean()).optional(),
next_transfer_at: asOptionalField(z.record(z.string())), next_transfer_at: z.record(z.string(), z.string()).optional(),
last_cycle_status: asOptionalField(z.boolean()), last_cycle_status: z.boolean().optional(),
}) })
const alertRuleFormSchema = z.object({ const alertRuleFormSchema = z.object({
@@ -87,13 +86,16 @@ const alertRuleFormSchema = z.object({
recover_trigger_tasks_raw: z.string(), recover_trigger_tasks_raw: z.string(),
notification_group_id: z.coerce.number().int(), notification_group_id: z.coerce.number().int(),
trigger_mode: z.coerce.number().int().min(0), trigger_mode: z.coerce.number().int().min(0),
enable: asOptionalField(z.boolean()), enable: z.boolean().optional(),
}) })
export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => { export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof alertRuleFormSchema>>({
resolver: zodResolver(alertRuleFormSchema), type AlertRuleFormData = z.infer<typeof alertRuleFormSchema>
const form = useForm({
resolver: zodResolver(alertRuleFormSchema) as any,
defaultValues: data defaultValues: data
? { ? {
...data, ...data,
@@ -119,7 +121,28 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => { // 结构化规则编辑状态:从已有数据或 rules_raw 初始化
const initialRules = (() => {
try {
if (data?.rules) return data.rules as any[]
const raw = form.getValues("rules_raw")
return raw ? JSON.parse(raw) : []
} catch {
return []
}
})()
const [rulesUI, setRulesUI] = useState<any[]>(initialRules)
// 同步到 rules_raw(提交仍走 JSON 字符串)
useEffect(() => {
try {
form.setValue("rules_raw", JSON.stringify(rulesUI), { shouldDirty: true })
} catch {
// ignore
}
}, [rulesUI])
const onSubmit = async (values: AlertRuleFormData) => {
values.rules = JSON.parse(values.rules_raw) values.rules = JSON.parse(values.rules_raw)
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number) values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number)
values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number) values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number)
@@ -161,7 +184,10 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2"> <form
onSubmit={form.handleSubmit(onSubmit as any)}
className="space-y-2 my-2"
>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
@@ -175,19 +201,424 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
</FormItem> </FormItem>
)} )}
/> />
<FormField {/* 结构化规则编辑器 */}
control={form.control}
name="rules_raw"
render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("Rules")}</FormLabel> <FormLabel>{t("Rules")}</FormLabel>
<FormControl> <div className="space-y-3">
<Textarea className="resize-y" {...field} /> {rulesUI.map((r, idx) => {
</FormControl> const isCycle =
<FormMessage /> typeof r.type === "string" &&
</FormItem> r.type.endsWith("_cycle")
const isOffline = r.type === "offline"
return (
<div
key={idx}
className="rounded-md border p-3 space-y-2"
>
{/* 类型选择 */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div>
<Label className="text-sm">
{t("Type")}
</Label>
<Select
onValueChange={(val) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
type: val,
}
// 切换类型时,若不是周期型,清理周期字段
if (!val.endsWith("_cycle")) {
delete next[idx].cycle_start
delete next[idx]
.cycle_interval
delete next[idx].cycle_unit
}
setRulesUI(next)
}}
defaultValue={r.type || ""}
>
<SelectTrigger>
<SelectValue
placeholder={t("Select")}
/>
</SelectTrigger>
<SelectContent>
{/* 资源类 */}
<SelectItem value="cpu">
cpu
</SelectItem>
<SelectItem value="gpu">
gpu
</SelectItem>
<SelectItem value="memory">
memory
</SelectItem>
<SelectItem value="swap">
swap
</SelectItem>
<SelectItem value="disk">
disk
</SelectItem>
{/* 网络类 */}
<SelectItem value="net_in_speed">
net_in_speed
</SelectItem>
<SelectItem value="net_out_speed">
net_out_speed
</SelectItem>
<SelectItem value="net_all_speed">
net_all_speed
</SelectItem>
<SelectItem value="transfer_in">
transfer_in
</SelectItem>
<SelectItem value="transfer_out">
transfer_out
</SelectItem>
<SelectItem value="transfer_all">
transfer_all
</SelectItem>
{/* 系统类 */}
<SelectItem value="offline">
offline
</SelectItem>
<SelectItem value="load1">
load1
</SelectItem>
<SelectItem value="load5">
load5
</SelectItem>
<SelectItem value="load15">
load15
</SelectItem>
<SelectItem value="process_count">
process_count
</SelectItem>
{/* 连接数 */}
<SelectItem value="tcp_conn_count">
tcp_conn_count
</SelectItem>
<SelectItem value="udp_conn_count">
udp_conn_count
</SelectItem>
{/* 温度 */}
<SelectItem value="temperature_max">
temperature_max
</SelectItem>
{/* 特殊:周期流量 */}
<SelectItem value="transfer_in_cycle">
transfer_in_cycle
</SelectItem>
<SelectItem value="transfer_out_cycle">
transfer_out_cycle
</SelectItem>
<SelectItem value="transfer_all_cycle">
transfer_all_cycle
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm">
duration
</Label>
<Input
type="number"
value={r.duration ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
duration: e.target.value
? Number(e.target.value)
: undefined,
}
setRulesUI(next)
}}
placeholder="10"
/>
</div>
</div>
{/* 阈值:offline 不需要 min/max */}
{!isOffline && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div>
<Label className="text-sm">
min
</Label>
<Input
type="number"
value={r.min ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
min: e.target.value
? Number(
e.target
.value,
)
: undefined,
}
setRulesUI(next)
}}
placeholder="0"
/>
</div>
<div>
<Label className="text-sm">
max
</Label>
<Input
type="number"
value={r.max ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
max: e.target.value
? Number(
e.target
.value,
)
: undefined,
}
setRulesUI(next)
}}
placeholder="100"
/>
</div>
</div>
)}
{/* 覆盖/忽略 */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<div>
<Label className="text-sm">cover</Label>
<Select
onValueChange={(val) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
cover: Number(val),
}
setRulesUI(next)
}}
defaultValue={(
r.cover ?? 0
).toString()}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">
0
{t(
"AlertRules.CoverAllServers",
)}
</SelectItem>
<SelectItem value="1">
1
{t(
"AlertRules.IgnoreAllSelectSpecific",
)}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm">
{t("AlertRules.IgnoreHint", {
server: t("Server"),
})}
</Label>
{/* 简化:以 JSON 对象输入 */}
<Textarea
className="resize-y"
value={(() => {
try {
return r.ignore
? JSON.stringify(
r.ignore,
)
: ""
} catch {
return ""
}
})()}
onChange={(e) => {
const next = [...rulesUI]
try {
const obj = e.target.value
? JSON.parse(
e.target.value,
)
: undefined
next[idx] = {
...next[idx],
ignore: obj,
}
} catch {
// 保持原值,避免无效 JSON 覆盖
}
setRulesUI(next)
}}
placeholder={t(
"AlertRules.IgnoreExample",
)} )}
/> />
</div>
</div>
{/* 周期型字段 */}
{isCycle && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="sm:col-span-2">
<Label className="text-sm">
cycle_start (RFC3339)
</Label>
<Input
value={r.cycle_start ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
cycle_start:
e.target.value ||
undefined,
}
setRulesUI(next)
}}
placeholder="2022-01-01T00:00:00+08:00"
/>
</div>
<div>
<Label className="text-sm">
cycle_interval
</Label>
<Input
type="number"
value={r.cycle_interval ?? ""}
onChange={(e) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
cycle_interval: e.target
.value
? Number(
e.target
.value,
)
: undefined,
}
setRulesUI(next)
}}
placeholder="1"
/>
</div>
<div>
<Label className="text-sm">
cycle_unit
</Label>
<Select
onValueChange={(val) => {
const next = [...rulesUI]
next[idx] = {
...next[idx],
cycle_unit: val,
}
setRulesUI(next)
}}
defaultValue={
r.cycle_unit || "month"
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hour">
hour
</SelectItem>
<SelectItem value="day">
day
</SelectItem>
<SelectItem value="week">
week
</SelectItem>
<SelectItem value="month">
month
</SelectItem>
<SelectItem value="year">
year
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
<div className="flex justify-between">
<Button
type="button"
variant="secondary"
onClick={() => {
const next = [...rulesUI]
next.splice(idx, 1)
setRulesUI(next)
}}
>
{t("Delete")}
</Button>
{/* 占位以对齐 */}
<span />
</div>
</div>
)
})}
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={() => {
setRulesUI([
...rulesUI,
{ type: "", cover: 0, duration: 10 },
])
}}
>
{t("Add")}
</Button>
</div>
</div>
{/* 高级:直接编辑 JSON(与结构化编辑器同步) */}
<FormLabel className="mt-3">{t("AdvancedJSON")}</FormLabel>
<FormControl>
<Textarea
className="resize-y"
value={form.watch("rules_raw")}
onChange={(e) => {
// 同步到结构化编辑器
form.setValue("rules_raw", e.target.value, {
shouldDirty: true,
})
try {
const arr = JSON.parse(e.target.value)
if (Array.isArray(arr)) setRulesUI(arr)
} catch {
// ignore invalid
}
}}
/>
</FormControl>
</FormItem>
<FormField <FormField
control={form.control} control={form.control}
name="notification_group_id" name="notification_group_id"
@@ -196,7 +627,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
<FormLabel>{t("NotifierGroup")}</FormLabel> <FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl> <FormControl>
<Combobox <Combobox
placeholder="Search..." placeholder={t("Search")}
options={ngroupList} options={ngroupList}
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value.toString()} defaultValue={field.value.toString()}
+13 -6
View File
@@ -17,13 +17,17 @@ import { IconButton } from "@/components/xui/icon-button"
import { useState } from "react" import { useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
import { Textarea } from "./ui/textarea" import { Textarea } from "./ui/textarea"
interface BatchMoveServerIconProps extends ButtonProps { interface BatchMoveServerIconProps extends ButtonProps {
serverIds: number[] serverIds: number[]
} }
export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ serverIds, ...props }) => { export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({
serverIds,
...props
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [toUserId, setToUserId] = useState<number | undefined>(undefined) const [toUserId, setToUserId] = useState<number | undefined>(undefined)
@@ -32,7 +36,7 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
try { try {
await batchMoveServer({ await batchMoveServer({
ids: serverIds, ids: serverIds,
to_user: toUserId! to_user: toUserId!,
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -69,9 +73,7 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-3 mt-4"> <div className="flex flex-col gap-3 mt-4">
<Label>{t("Servers")}</Label> <Label>{t("Servers")}</Label>
<Textarea disabled> <Textarea disabled>{serverIds.join(", ")}</Textarea>
{serverIds.join(", ")}
</Textarea>
<Label>{t("ToUser")}</Label> <Label>{t("ToUser")}</Label>
<Input <Input
type="number" type="number"
@@ -87,7 +89,12 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({ server
{t("Cancel")} {t("Cancel")}
</Button> </Button>
</DialogClose> </DialogClose>
<Button disabled={!toUserId || toUserId == 0} type="submit" className="my-2" onClick={onSubmit}> <Button
disabled={!toUserId || toUserId == 0}
type="submit"
className="my-2"
onClick={onSubmit}
>
{t("Move")} {t("Move")}
</Button> </Button>
</DialogFooter> </DialogFooter>
+18 -5
View File
@@ -63,18 +63,31 @@ const cronFormSchema = z.object({
notification_group_id: z.coerce.number().int(), notification_group_id: z.coerce.number().int(),
}) })
type CronFormData = z.infer<typeof cronFormSchema>
export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => { export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof cronFormSchema>>({ const form = useForm<CronFormData>({
resolver: zodResolver(cronFormSchema), resolver: zodResolver(cronFormSchema as any),
defaultValues: data defaultValues: data
? data ? {
task_type: data.task_type ?? 0,
name: data.name ?? "",
scheduler: data.scheduler ?? "",
command: (data as any).command ?? "",
servers: data.servers ?? [],
cover: data.cover ?? 0,
push_successful: (data as any).push_successful ?? false,
notification_group_id: data.notification_group_id ?? 0,
}
: { : {
name: "",
task_type: 0, task_type: 0,
name: "",
scheduler: "", scheduler: "",
command: "",
servers: [], servers: [],
cover: 0, cover: 0,
push_successful: false,
notification_group_id: 0, notification_group_id: 0,
}, },
resetOptions: { resetOptions: {
@@ -84,7 +97,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof cronFormSchema>) => { const onSubmit = async (values: CronFormData) => {
try { try {
data?.id ? await updateCron(data.id, values) : await createCron(values) data?.id ? await updateCron(data.id, values) : await createCron(values)
} catch (e) { } catch (e) {
+28 -5
View File
@@ -67,21 +67,44 @@ const ddnsFormSchema = z.object({
webhook_headers: asOptionalField(z.string()), webhook_headers: asOptionalField(z.string()),
}) })
type DDNSFormData = z.infer<typeof ddnsFormSchema>
export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) => { export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof ddnsFormSchema>>({ const form = useForm<DDNSFormData>({
resolver: zodResolver(ddnsFormSchema), resolver: zodResolver(ddnsFormSchema as any),
defaultValues: data defaultValues: data
? { ? {
...data, max_retries: data.max_retries ?? 3,
domains_raw: conv.arrToStr(data.domains), enable_ipv4: (data as any).enable_ipv4 ?? false,
enable_ipv6: (data as any).enable_ipv6 ?? false,
name: data.name ?? "",
provider: data.provider ?? "dummy",
domains: data.domains ?? [],
domains_raw: conv.arrToStr(data.domains ?? []),
access_id: (data as any).access_id ?? "",
access_secret: (data as any).access_secret ?? "",
webhook_url: (data as any).webhook_url ?? "",
webhook_method: (data as any).webhook_method,
webhook_request_type: (data as any).webhook_request_type,
webhook_request_body: (data as any).webhook_request_body ?? "",
webhook_headers: (data as any).webhook_headers ?? "",
} }
: { : {
max_retries: 3, max_retries: 3,
enable_ipv4: false,
enable_ipv6: false,
name: "", name: "",
provider: "dummy", provider: "dummy",
domains: [], domains: [],
domains_raw: "", domains_raw: "",
access_id: "",
access_secret: "",
webhook_url: "",
webhook_method: undefined,
webhook_request_type: undefined,
webhook_request_body: "",
webhook_headers: "",
}, },
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
@@ -90,7 +113,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof ddnsFormSchema>) => { const onSubmit = async (values: DDNSFormData) => {
try { try {
values.domains = conv.strToArr(values.domains_raw) values.domains = conv.strToArr(values.domains_raw)
data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values) data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values)
+1 -1
View File
@@ -37,7 +37,7 @@ import {
import { ColumnDef } from "@tanstack/react-table" import { ColumnDef } from "@tanstack/react-table"
import { Row, flexRender } from "@tanstack/react-table" import { Row, flexRender } from "@tanstack/react-table"
import { File, Folder } from "lucide-react" import { File, Folder } from "lucide-react"
import { HTMLAttributes, useEffect, useRef, useState } from "react" import { HTMLAttributes, JSX, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
+12 -4
View File
@@ -51,6 +51,7 @@ export function HeaderButtonGroup<E, U>({
<IconButton <IconButton
variant="destructive" variant="destructive"
icon="trash" icon="trash"
className="text-white"
onClick={() => { onClick={() => {
toast(t("Error"), { toast(t("Error"), {
description: t("Results.NoRowsAreSelected"), description: t("Results.NoRowsAreSelected"),
@@ -63,7 +64,7 @@ export function HeaderButtonGroup<E, U>({
<> <>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="trash" /> <IconButton variant="destructive" icon="trash" className="text-white" />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -75,7 +76,10 @@ export function HeaderButtonGroup<E, U>({
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleDelete} onClick={handleDelete}
> >
{t("Confirm")} {t("Confirm")}
@@ -114,6 +118,7 @@ export function HeaderBlockButtonGroup<E, U>({
<IconButton <IconButton
variant="destructive" variant="destructive"
icon="ban" icon="ban"
className="text-white"
onClick={() => { onClick={() => {
toast(t("Error"), { toast(t("Error"), {
description: t("Results.NoRowsAreSelected"), description: t("Results.NoRowsAreSelected"),
@@ -126,7 +131,7 @@ export function HeaderBlockButtonGroup<E, U>({
<> <>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<IconButton variant="destructive" icon="ban" /> <IconButton variant="destructive" icon="ban" className="text-white" />
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-lg"> <AlertDialogContent className="sm:max-w-lg">
<AlertDialogHeader> <AlertDialogHeader>
@@ -138,7 +143,10 @@ export function HeaderBlockButtonGroup<E, U>({
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>{t("Close")}</AlertDialogCancel> <AlertDialogCancel>{t("Close")}</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className={buttonVariants({
variant: "destructive",
className: "text-white",
})}
onClick={handleBlock} onClick={handleBlock}
> >
{t("Confirm")} {t("Confirm")}
+1 -1
View File
@@ -403,7 +403,7 @@ function Overview() {
{!profile && <p className="text-sm font-semibold">{t("LoginFirst")}</p>} {!profile && <p className="text-sm font-semibold">{t("LoginFirst")}</p>}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<p className="text-[13px] font-medium opacity-50">{t("CurrentTime")}</p> <p className="text-[13px] font-medium opacity-50">{t("CurrentTime")}</p>
<p className="opacity-1 text-[13px] font-medium">{timeString}</p> <p className="opacity-100 text-[13px] font-medium">{timeString}</p>
</div> </div>
</section> </section>
) )
+65 -10
View File
@@ -10,7 +10,7 @@ import useSettings from "@/hooks/useSetting"
import { copyToClipboard } from "@/lib/utils" import { copyToClipboard } from "@/lib/utils"
import { ModelProfile, ModelSetting } from "@/types" import { ModelProfile, ModelSetting } from "@/types"
import i18next from "i18next" import i18next from "i18next"
import { Check, Clipboard } from "lucide-react" import { Check, Copy, Download } from "lucide-react"
import { forwardRef, useState } from "react" import { forwardRef, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner" import { toast } from "sonner"
@@ -21,7 +21,14 @@ enum OSTypes {
Windows, Windows,
} }
export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => { type InstallCommandsMenuProps = ButtonProps & {
uuid?: string
iconOnly?: boolean
menuItem?: boolean
}
export const InstallCommandsMenu = forwardRef<HTMLButtonElement, InstallCommandsMenuProps>(
({ uuid, iconOnly = false, menuItem = false, ...props }, ref) => {
const [copy, setCopy] = useState(false) const [copy, setCopy] = useState(false)
const { data: settings } = useSettings() const { data: settings } = useSettings()
const { profile } = useAuth() const { profile } = useAuth()
@@ -34,7 +41,9 @@ export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((p
setCopy(true) setCopy(true)
if (!profile) throw new Error("Profile is not found.") if (!profile) throw new Error("Profile is not found.")
if (!settings?.config) throw new Error("Settings is not found.") if (!settings?.config) throw new Error("Settings is not found.")
await copyToClipboard(generateCommand(type, settings!.config, profile) || "") await copyToClipboard(
generateCommand(type, settings!.config, profile, uuid) || "",
)
} catch (e: Error | any) { } catch (e: Error | any) {
console.error(e) console.error(e)
toast(t("Error"), { toast(t("Error"), {
@@ -51,12 +60,43 @@ export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((p
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button {...props} ref={ref}> {menuItem ? (
{copy ? <Check /> : <Clipboard />} <button
{t("InstallCommands")} type="button"
className="flex w-full items-center text-sm px-2 py-2 hover:bg-accent hover:text-accent-foreground"
title={i18next.t("InstallCommands")}
>
{copy ? (
<Check className="h-4 w-4 mr-2" />
) : (
<Copy className="h-4 w-4 mr-2" />
)}
<span>{i18next.t("InstallCommands")}</span>
</button>
) : iconOnly ? (
<Button
ref={ref}
title={i18next.t("InstallCommands")}
size="icon"
{...props}
>
{copy ? (
<Check className="h-4 w-4" />
) : (
<Download className="h-4 w-4" />
)}
</Button> </Button>
) : (
<Button ref={ref} title={i18next.t("InstallCommands")} {...props}>
{copy ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
<span className="ml-2">{i18next.t("InstallCommands")}</span>
</Button>
)}
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent
side={menuItem ? "right" : undefined}
align={menuItem ? "start" : undefined}
>
<DropdownMenuItem <DropdownMenuItem
className="nezha-copy" className="nezha-copy"
onClick={async () => { onClick={async () => {
@@ -84,19 +124,34 @@ export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((p
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) )
}) },
)
const generateCommand = ( const generateCommand = (
type: number, type: number,
{ install_host, tls }: ModelSetting, { install_host, tls }: ModelSetting,
{ agent_secret }: ModelProfile, { agent_secret }: ModelProfile,
uuid?: string,
) => { ) => {
if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired")) if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired"))
if (!agent_secret) throw new Error(i18next.t("Results.AgentSecretRequired")) if (!agent_secret) throw new Error(i18next.t("Results.AgentSecretRequired"))
const env = `NZ_SERVER=${install_host} NZ_TLS=${tls || false} NZ_CLIENT_SECRET=${agent_secret}` const envParts = [
const env_win = `$env:NZ_SERVER=\"${install_host}\";$env:NZ_TLS=\"${tls || false}\";$env:NZ_CLIENT_SECRET=\"${agent_secret}\";` `NZ_SERVER=${install_host}`,
`NZ_TLS=${tls || false}`,
`NZ_CLIENT_SECRET=${agent_secret}`,
]
if (uuid) envParts.push(`NZ_UUID=${uuid}`)
const env = envParts.join(" ")
const envWinParts = [
`$env:NZ_SERVER=\"${install_host}\";`,
`$env:NZ_TLS=\"${tls || false}\";`,
`$env:NZ_CLIENT_SECRET=\"${agent_secret}\";`,
]
if (uuid) envWinParts.push(`$env:NZ_UUID=\"${uuid}\";`)
const env_win = envWinParts.join("")
switch (type) { switch (type) {
case OSTypes.Linux: case OSTypes.Linux:
+12 -4
View File
@@ -46,12 +46,20 @@ const natFormSchema = z.object({
domain: z.string(), domain: z.string(),
}) })
type NatFormData = z.infer<typeof natFormSchema>
export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => { export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof natFormSchema>>({ const form = useForm<NatFormData>({
resolver: zodResolver(natFormSchema), resolver: zodResolver(natFormSchema as any),
defaultValues: data defaultValues: data
? data ? {
name: data.name ?? "",
enabled: (data as any).enabled ?? false,
server_id: data.server_id ?? 0,
host: data.host ?? "",
domain: data.domain ?? "",
}
: { : {
name: "", name: "",
enabled: false, enabled: false,
@@ -66,7 +74,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof natFormSchema>) => { const onSubmit = async (values: NatFormData) => {
try { try {
data?.id ? await updateNAT(data.id, values) : await createNAT(values) data?.id ? await updateNAT(data.id, values) : await createNAT(values)
} catch (e) { } catch (e) {
+21 -5
View File
@@ -61,10 +61,21 @@ const notificationFormSchema = z.object({
export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => { export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof notificationFormSchema>>({ type notificationFormData = z.infer<typeof notificationFormSchema>
resolver: zodResolver(notificationFormSchema),
const form = useForm({
resolver: zodResolver(notificationFormSchema) as any,
defaultValues: data defaultValues: data
? data ? {
name: data.name ?? "",
url: data.url ?? "",
request_method: data.request_method ?? 1,
request_type: data.request_type ?? 1,
request_header: data.request_header ?? "",
request_body: data.request_body ?? "",
verify_tls: (data as any).verify_tls ?? false,
skip_check: (data as any).skip_check ?? false,
}
: { : {
name: "", name: "",
url: "", url: "",
@@ -72,6 +83,8 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
request_type: 1, request_type: 1,
request_header: "", request_header: "",
request_body: "", request_body: "",
verify_tls: false,
skip_check: false,
}, },
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
@@ -80,7 +93,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof notificationFormSchema>) => { const onSubmit = async (values: notificationFormData) => {
try { try {
data?.id ? await updateNotification(data.id, values) : await createNotification(values) data?.id ? await updateNotification(data.id, values) : await createNotification(values)
} catch (e) { } catch (e) {
@@ -110,7 +123,10 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
<DialogDescription /> <DialogDescription />
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2"> <form
onSubmit={form.handleSubmit(onSubmit as any)}
className="space-y-2 my-2"
>
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
+3 -3
View File
@@ -42,8 +42,8 @@ export const ProfileCard = ({ className }: { className: string }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { profile, setProfile } = useMainStore() const { profile, setProfile } = useMainStore()
const form = useForm<z.infer<typeof profileFormSchema>>({ const form = useForm({
resolver: zodResolver(profileFormSchema), resolver: zodResolver(profileFormSchema) as any,
defaultValues: { defaultValues: {
original_password: "", original_password: "",
new_password: "", new_password: "",
@@ -57,7 +57,7 @@ export const ProfileCard = ({ className }: { className: string }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof profileFormSchema>) => { const onSubmit = async (values: any) => {
try { try {
await updateProfile(values) await updateProfile(values)
} catch (e) { } catch (e) {
+19 -6
View File
@@ -27,6 +27,7 @@ import { IconButton } from "@/components/xui/icon-button"
import { asOptionalField } from "@/lib/utils" import { asOptionalField } from "@/lib/utils"
import { ModelServerTaskResponse } from "@/types" import { ModelServerTaskResponse } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { CogIcon } from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
@@ -58,7 +59,7 @@ const agentConfigSchema = z.object({
), ),
), ),
ip_report_period: asOptionalField(z.coerce.number().int().min(30)), ip_report_period: asOptionalField(z.coerce.number().int().min(30)),
nic_allowlist: asOptionalField(z.record(z.boolean())), nic_allowlist: asOptionalField(z.record(z.string(), z.boolean())),
nic_allowlist_raw: asOptionalField( nic_allowlist_raw: asOptionalField(
z.string().refine( z.string().refine(
(val) => { (val) => {
@@ -102,9 +103,10 @@ for (let i = 0; i < boolFields.length; i += 2) {
interface ServerConfigCardProps extends ButtonProps { interface ServerConfigCardProps extends ButtonProps {
sid: number sid: number
menuItem?: boolean
} }
export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => { export const ServerConfigCard = ({ sid, menuItem = false, ...props }: ServerConfigCardProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [data, setData] = useState<AgentConfig | undefined>(undefined) const [data, setData] = useState<AgentConfig | undefined>(undefined)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -129,8 +131,8 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
if (open) fetchData() if (open) fetchData()
}, [open]) }, [open])
const form = useForm<AgentConfig>({ const form = useForm({
resolver: zodResolver(agentConfigSchema), resolver: zodResolver(agentConfigSchema) as any,
defaultValues: { defaultValues: {
...data, ...data,
hard_drive_partition_allowlist_raw: JSON.stringify( hard_drive_partition_allowlist_raw: JSON.stringify(
@@ -155,7 +157,7 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
} }
}, [data, form]) }, [data, form])
const onSubmit = async (values: AgentConfig) => { const onSubmit = async (values: any) => {
let resp: ModelServerTaskResponse = {} let resp: ModelServerTaskResponse = {}
try { try {
values.nic_allowlist = values.nic_allowlist_raw values.nic_allowlist = values.nic_allowlist_raw
@@ -186,7 +188,18 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
{menuItem ? (
<button
type="button"
className="flex w-full items-center text-sm px-2 py-2 hover:bg-accent hover:text-accent-foreground"
onClick={() => setOpen(true)}
>
<CogIcon className="h-4 w-4 mr-2" />
<span>{t("Config")}</span>
</button>
) : (
<IconButton {...props} icon="cog" /> <IconButton {...props} icon="cog" />
)}
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent className="sm:max-w-xl">
{loading ? ( {loading ? (
@@ -283,7 +296,7 @@ export const ServerConfigCard = ({ sid, ...props }: ServerConfigCardProps) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
checked={ checked={
controllerField.value as boolean !!controllerField.value
} }
onCheckedChange={ onCheckedChange={
controllerField.onChange controllerField.onChange
+692 -16
View File
@@ -1,5 +1,6 @@
import { updateServer } from "@/api/server" import { updateServer } from "@/api/server"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { import {
Dialog, Dialog,
@@ -21,7 +22,16 @@ import {
} from "@/components/ui/form" } from "@/components/ui/form"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { IconButton } from "@/components/xui/icon-button" import { IconButton } from "@/components/xui/icon-button"
import { conv } from "@/lib/utils" import { conv } from "@/lib/utils"
@@ -49,7 +59,7 @@ const serverFormSchema = z.object({
enable_ddns: asOptionalField(z.boolean()), enable_ddns: asOptionalField(z.boolean()),
ddns_profiles: asOptionalField(z.array(z.number())), ddns_profiles: asOptionalField(z.array(z.number())),
ddns_profiles_raw: asOptionalField(z.string()), ddns_profiles_raw: asOptionalField(z.string()),
override_ddns_domains: asOptionalField(z.record(z.coerce.number().int(), z.array(z.string()))), override_ddns_domains: asOptionalField(z.record(z.string(), z.array(z.string()))),
override_ddns_domains_raw: asOptionalField( override_ddns_domains_raw: asOptionalField(
z.string().refine( z.string().refine(
(val) => { (val) => {
@@ -69,8 +79,8 @@ const serverFormSchema = z.object({
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => { export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof serverFormSchema>>({ const form = useForm({
resolver: zodResolver(serverFormSchema), resolver: zodResolver(serverFormSchema) as any,
defaultValues: { defaultValues: {
...data, ...data,
ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined, ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined,
@@ -85,7 +95,131 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const onSubmit = async (values: z.infer<typeof serverFormSchema>) => { type PublicNote = {
billingDataMod: {
startDate: string
endDate: string
autoRenewal: string
cycle: string
amount: string
}
planDataMod: {
bandwidth: string
trafficVol: string
trafficType: string
IPv4: string
IPv6: string
networkRoute: string
extra: string
}
}
const defaultPublicNote: PublicNote = {
billingDataMod: {
startDate: "",
endDate: "",
autoRenewal: "",
cycle: "",
amount: "",
},
planDataMod: {
bandwidth: "",
trafficVol: "",
trafficType: "",
IPv4: "0",
IPv6: "0",
networkRoute: "",
extra: "",
},
}
const parsePublicNote = (s?: string): PublicNote => {
if (!s) return defaultPublicNote
try {
const obj = JSON.parse(s)
return {
billingDataMod: {
startDate: obj?.billingDataMod?.startDate ?? "",
endDate: obj?.billingDataMod?.endDate ?? "",
autoRenewal: obj?.billingDataMod?.autoRenewal ?? "",
cycle: obj?.billingDataMod?.cycle ?? "",
amount: obj?.billingDataMod?.amount ?? "",
},
planDataMod: {
bandwidth: obj?.planDataMod?.bandwidth ?? "",
trafficVol: obj?.planDataMod?.trafficVol ?? "",
trafficType: obj?.planDataMod?.trafficType ?? "",
IPv4: obj?.planDataMod?.IPv4 === "1" ? "1" : "0",
IPv6: obj?.planDataMod?.IPv6 === "1" ? "1" : "0",
networkRoute: obj?.planDataMod?.networkRoute ?? "",
extra: obj?.planDataMod?.extra ?? "",
},
}
} catch {
return defaultPublicNote
}
}
const [publicNoteObj, setPublicNoteObj] = useState<PublicNote>(
parsePublicNote(data?.public_note),
)
const [publicNoteErrors, setPublicNoteErrors] = useState<
Partial<
Record<
| "billing.startDate"
| "billing.endDate"
| "billing.autoRenewal"
| "billing.cycle"
| "billing.amount"
| "plan.bandwidth"
| "plan.trafficVol"
| "plan.trafficType"
| "plan.IPv4"
| "plan.IPv6"
| "plan.extra",
string
>
>
>({})
const isValidISOLike = (v: string) => {
if (!v) return true
// special marker for "no expiry"
if (v === "0000-00-00T23:59:59+08:00") return true
const d = new Date(v)
return !isNaN(d.getTime())
}
const validatePublicNote = (pn: PublicNote) => {
const errs: Partial<Record<string, string>> = {}
if (pn.billingDataMod.startDate && !isValidISOLike(pn.billingDataMod.startDate)) {
errs["billing.startDate"] = t("Validation.InvalidDate")
}
if (pn.billingDataMod.endDate && !isValidISOLike(pn.billingDataMod.endDate)) {
errs["billing.endDate"] = t("Validation.InvalidDate")
}
if (pn.billingDataMod.autoRenewal && !/^(0|1)$/.test(pn.billingDataMod.autoRenewal)) {
errs["billing.autoRenewal"] = t("Validation.MustBe0Or1")
}
if (pn.billingDataMod.cycle && !/^(Day|Week|Month|Year)$/i.test(pn.billingDataMod.cycle)) {
errs["billing.cycle"] = t("Validation.MustBeDayWeekMonthYear")
}
// amount 允许任意非空字符串或空
if (pn.planDataMod.trafficType && !/^(1|2)$/.test(pn.planDataMod.trafficType)) {
errs["plan.trafficType"] = t("Validation.MustBe1Or2")
}
if (!/^(0|1)$/.test(pn.planDataMod.IPv4)) {
errs["plan.IPv4"] = t("Validation.MustBe0Or1")
}
if (!/^(0|1)$/.test(pn.planDataMod.IPv6)) {
errs["plan.IPv6"] = t("Validation.MustBe0Or1")
}
return { errors: errs, valid: Object.keys(errs).length === 0 }
}
const onSubmit = async (values: any) => {
try { try {
values.ddns_profiles = values.ddns_profiles_raw values.ddns_profiles = values.ddns_profiles_raw
? conv.strToArr(values.ddns_profiles_raw).map(Number) ? conv.strToArr(values.ddns_profiles_raw).map(Number)
@@ -93,6 +227,36 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
values.override_ddns_domains = values.override_ddns_domains_raw values.override_ddns_domains = values.override_ddns_domains_raw
? JSON.parse(values.override_ddns_domains_raw) ? JSON.parse(values.override_ddns_domains_raw)
: undefined : undefined
// validate structured fields
const { errors, valid } = validatePublicNote(publicNoteObj)
if (!valid) {
setPublicNoteErrors(errors)
toast(t("Error"), { description: t("Validation.InvalidForm") })
return
}
setPublicNoteErrors({})
// normalize datetime-local to ISO string if provided
const normalizeISO = (v: string) => {
if (!v) return v
// keep special "no expiry" value as-is
if (v === "0000-00-00T23:59:59+08:00") return v
const date = new Date(v)
return isNaN(date.getTime()) ? v : date.toISOString()
}
const pnNormalized: PublicNote = {
billingDataMod: {
...publicNoteObj.billingDataMod,
startDate: normalizeISO(publicNoteObj.billingDataMod.startDate),
endDate: normalizeISO(publicNoteObj.billingDataMod.endDate),
// keep others as-is
},
planDataMod: { ...publicNoteObj.planDataMod },
}
// serialize structured public note back to JSON string
values.public_note = JSON.stringify(pnNormalized)
await updateServer(data!.id!, values) await updateServer(data!.id!, values)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -111,7 +275,11 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
<DialogTrigger asChild> <DialogTrigger asChild>
<IconButton variant="outline" icon="edit" /> <IconButton variant="outline" icon="edit" />
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl"> <DialogContent
className="sm:max-w-xl"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3"> <ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
<div className="items-center mx-1"> <div className="items-center mx-1">
<DialogHeader> <DialogHeader>
@@ -236,19 +404,527 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField {/* Structured Public Note fields */}
control={form.control} <div className="space-y-3">
name="public_note" <div className="space-y-1">
render={({ field }) => (
<FormItem>
<FormLabel>{t("Public") + t("Note")}</FormLabel> <FormLabel>{t("Public") + t("Note")}</FormLabel>
<FormControl> <p className="text-xs text-muted-foreground">
<Textarea className="resize-y" {...field} /> {t("PublicNote.DropdownHint")}
</FormControl> </p>
<FormMessage /> </div>
</FormItem>
)} <div className="rounded-md border p-3 space-y-3">
<div className="text-sm font-medium opacity-80">
{t("PublicNote.Billing")}
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.StartDate")}
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-start text-left font-normal"
>
{publicNoteObj.billingDataMod.startDate
? new Date(
publicNoteObj.billingDataMod.startDate,
).toLocaleDateString()
: "YYYY-MM-DD"}
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[300px] max-h-[60dvh] overflow-hidden"
align="start"
>
<div className="max-h-[500px] overflow-y-auto">
<Calendar
className="w-full min-h-[320px]"
mode="single"
captionLayout="dropdown"
startMonth={new Date(2000, 0)}
endMonth={new Date(2050, 11)}
selected={
publicNoteObj.billingDataMod
.startDate
? new Date(
publicNoteObj.billingDataMod.startDate,
)
: undefined
}
onSelect={(d) => {
if (!d) return
setPublicNoteObj((prev) => {
const prevDateStr =
prev.billingDataMod
.startDate
if (prevDateStr) {
const pd = new Date(
prevDateStr,
)
// 仅在有效日期时复制时分秒
if (
!isNaN(pd.getTime())
) {
d.setHours(
pd.getHours(),
pd.getMinutes(),
pd.getSeconds(),
0,
)
}
}
return {
...prev,
billingDataMod: {
...prev.billingDataMod,
startDate:
d.toISOString(),
},
}
})
}}
autoFocus
/> />
</div>
</PopoverContent>
</Popover>
{publicNoteErrors["billing.startDate"] && (
<p className="text-xs text-destructive mt-1">
{publicNoteErrors["billing.startDate"]}
</p>
)}
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Label className="text-xs">
{t("PublicNote.EndDate")}
</Label>
<Button
type="button"
variant="outline"
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
onClick={() =>
setPublicNoteObj((prev) => ({
...prev,
billingDataMod: {
...prev.billingDataMod,
endDate:
prev.billingDataMod
.endDate ===
"0000-00-00T23:59:59+08:00"
? ""
: "0000-00-00T23:59:59+08:00",
},
}))
}
>
{publicNoteObj.billingDataMod.endDate ===
"0000-00-00T23:59:59+08:00"
? t("PublicNote.CancelNoExpiry")
: t("PublicNote.SetNoExpiry")}
</Button>
</div>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-start text-left font-normal"
>
{publicNoteObj.billingDataMod.endDate
? publicNoteObj.billingDataMod
.endDate ===
"0000-00-00T23:59:59+08:00"
? t("PublicNote.NoExpiry")
: new Date(
publicNoteObj.billingDataMod.endDate,
).toLocaleDateString()
: "YYYY-MM-DD"}
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[300px] max-h-[60dvh] overflow-hidden"
align="start"
>
<div className="max-h-[500px] overflow-y-auto">
<Calendar
className="w-full min-h-[320px]"
mode="single"
captionLayout="dropdown"
startMonth={new Date(2000, 0)}
endMonth={new Date(2050, 11)}
selected={
publicNoteObj.billingDataMod
.endDate &&
publicNoteObj.billingDataMod
.endDate !==
"0000-00-00T23:59:59+08:00"
? new Date(
publicNoteObj.billingDataMod.endDate,
)
: undefined
}
onSelect={(d) => {
if (!d) return
setPublicNoteObj((prev) => {
const prevDateStr =
prev.billingDataMod
.endDate
if (prevDateStr) {
const pd = new Date(
prevDateStr,
)
// 仅在有效日期时复制时分秒(特殊“不过期”值不会影响)
if (
!isNaN(pd.getTime())
) {
d.setHours(
pd.getHours(),
pd.getMinutes(),
pd.getSeconds(),
0,
)
}
}
return {
...prev,
billingDataMod: {
...prev.billingDataMod,
endDate:
d.toISOString(),
},
}
})
}}
autoFocus
/>
</div>
</PopoverContent>
</Popover>
{publicNoteErrors["billing.endDate"] && (
<p className="text-xs text-destructive mt-1">
{publicNoteErrors["billing.endDate"]}
</p>
)}
</div>
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.AutoRenewal")}
</Label>
<Select
onValueChange={(val) =>
setPublicNoteObj((prev) => ({
...prev,
billingDataMod: {
...prev.billingDataMod,
autoRenewal: val,
},
}))
}
defaultValue={
publicNoteObj.billingDataMod.autoRenewal ||
"0"
}
>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">
{t("PublicNote.Enabled")}
</SelectItem>
<SelectItem value="0">
{t("PublicNote.Disabled")}
</SelectItem>
</SelectContent>
</Select>
{publicNoteErrors["billing.autoRenewal"] && (
<p className="text-xs text-destructive mt-1">
{publicNoteErrors["billing.autoRenewal"]}
</p>
)}
</div>
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.Cycle")}
</Label>
<Select
onValueChange={(val) =>
setPublicNoteObj((prev) => ({
...prev,
billingDataMod: {
...prev.billingDataMod,
cycle: val,
},
}))
}
defaultValue={
publicNoteObj.billingDataMod.cycle ||
"Month"
}
>
<SelectTrigger>
<SelectValue placeholder="Select cycle" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Day">
{t("PublicNote.Day")}
</SelectItem>
<SelectItem value="Week">
{t("PublicNote.Week")}
</SelectItem>
<SelectItem value="Month">
{t("PublicNote.Month")}
</SelectItem>
<SelectItem value="Year">
{t("PublicNote.Year")}
</SelectItem>
</SelectContent>
</Select>
{publicNoteErrors["billing.cycle"] && (
<p className="text-xs text-destructive mt-1">
{publicNoteErrors["billing.cycle"]}
</p>
)}
</div>
<div className="space-y-1 sm:col-span-2">
<div className="flex items-center gap-2">
<Label className="text-xs">
{t("PublicNote.Amount")}
</Label>
<Button
type="button"
variant="outline"
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
onClick={() =>
setPublicNoteObj((prev) => ({
...prev,
billingDataMod: {
...prev.billingDataMod,
amount: "0",
},
}))
}
>
{t("PublicNote.Free")}
</Button>
<Button
type="button"
variant="outline"
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
onClick={() =>
setPublicNoteObj((prev) => ({
...prev,
billingDataMod: {
...prev.billingDataMod,
amount: "-1",
},
}))
}
>
{t("PublicNote.PayAsYouGo")}
</Button>
</div>
<Input
placeholder="200EUR"
value={publicNoteObj.billingDataMod.amount}
onChange={(e) =>
setPublicNoteObj((prev) => ({
...prev,
billingDataMod: {
...prev.billingDataMod,
amount: e.target.value,
},
}))
}
/>
</div>
</div>
</div>
<div className="rounded-md border p-3 space-y-3">
<div className="text-sm font-medium opacity-80">
{t("PublicNote.Plan")}
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.Bandwidth")}
</Label>
<Input
placeholder="30Mbps"
value={publicNoteObj.planDataMod.bandwidth}
onChange={(e) =>
setPublicNoteObj((prev) => ({
...prev,
planDataMod: {
...prev.planDataMod,
bandwidth: e.target.value,
},
}))
}
/>
</div>
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.TrafficVolume")}
</Label>
<Input
placeholder="1TB/Month"
value={publicNoteObj.planDataMod.trafficVol}
onChange={(e) =>
setPublicNoteObj((prev) => ({
...prev,
planDataMod: {
...prev.planDataMod,
trafficVol: e.target.value,
},
}))
}
/>
</div>
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.TrafficType")}
</Label>
<Select
onValueChange={(val) =>
setPublicNoteObj((prev) => ({
...prev,
planDataMod: {
...prev.planDataMod,
trafficType: val,
},
}))
}
defaultValue={
publicNoteObj.planDataMod.trafficType || "2"
}
>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">
{t("PublicNote.Inbound")}
</SelectItem>
<SelectItem value="2">
{t("PublicNote.Both")}
</SelectItem>
</SelectContent>
</Select>
{publicNoteErrors["plan.trafficType"] && (
<p className="text-xs text-destructive mt-1">
{publicNoteErrors["plan.trafficType"]}
</p>
)}
</div>
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.IPv4")}
</Label>
<div className="flex items-center gap-2 mt-2">
<span className="text-xs">
{t("PublicNote.None")}
</span>
<Switch
checked={
publicNoteObj.planDataMod.IPv4 === "1"
}
onCheckedChange={(checked) =>
setPublicNoteObj((prev) => ({
...prev,
planDataMod: {
...prev.planDataMod,
IPv4: checked ? "1" : "0",
},
}))
}
/>
<span className="text-xs">
{t("PublicNote.Has")}
</span>
</div>
{publicNoteErrors["plan.IPv4"] && (
<p className="text-xs text-destructive mt-1">
{publicNoteErrors["plan.IPv4"]}
</p>
)}
</div>
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.IPv6")}
</Label>
<div className="flex items-center gap-2 mt-2">
<span className="text-xs">
{t("PublicNote.None")}
</span>
<Switch
checked={
publicNoteObj.planDataMod.IPv6 === "1"
}
onCheckedChange={(checked) =>
setPublicNoteObj((prev) => ({
...prev,
planDataMod: {
...prev.planDataMod,
IPv6: checked ? "1" : "0",
},
}))
}
/>
<span className="text-xs">
{t("PublicNote.Has")}
</span>
</div>
{publicNoteErrors["plan.IPv6"] && (
<p className="text-xs text-destructive mt-1">
{publicNoteErrors["plan.IPv6"]}
</p>
)}
</div>
<div className="space-y-1">
<Label className="text-xs">
{t("PublicNote.NetworkRoute")}
</Label>
<Input
placeholder={t("PublicNote.CommaSeparated")}
value={publicNoteObj.planDataMod.networkRoute}
onChange={(e) =>
setPublicNoteObj((prev) => ({
...prev,
planDataMod: {
...prev.planDataMod,
networkRoute: e.target.value,
},
}))
}
/>
</div>
<div className="space-y-1 sm:col-span-2">
<Label className="text-xs">
{t("PublicNote.Extra")}
</Label>
<Input
placeholder={t("PublicNote.CommaSeparated")}
value={publicNoteObj.planDataMod.extra}
onChange={(e) =>
setPublicNoteObj((prev) => ({
...prev,
planDataMod: {
...prev.planDataMod,
extra: e.target.value,
},
}))
}
/>
</div>
</div>
</div>
</div>
<DialogFooter className="justify-end"> <DialogFooter className="justify-end">
<DialogClose asChild> <DialogClose asChild>
<Button type="button" className="my-2" variant="secondary"> <Button type="button" className="my-2" variant="secondary">
+4 -4
View File
@@ -67,7 +67,7 @@ const serviceFormSchema = z.object({
notify: asOptionalField(z.boolean()), notify: asOptionalField(z.boolean()),
recover_trigger_tasks: z.array(z.number()), recover_trigger_tasks: z.array(z.number()),
recover_trigger_tasks_raw: z.string(), recover_trigger_tasks_raw: z.string(),
skip_servers: z.record(z.boolean()), skip_servers: z.record(z.string(), z.boolean()),
skip_servers_raw: z.array(z.string()), skip_servers_raw: z.array(z.string()),
target: z.string(), target: z.string(),
type: z.coerce.number().int().min(0), type: z.coerce.number().int().min(0),
@@ -75,8 +75,8 @@ const serviceFormSchema = z.object({
export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => { export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
const { t } = useTranslation() const { t } = useTranslation()
const form = useForm<z.infer<typeof serviceFormSchema>>({ const form = useForm({
resolver: zodResolver(serviceFormSchema), resolver: zodResolver(serviceFormSchema) as any,
defaultValues: data defaultValues: data
? { ? {
...data, ...data,
@@ -107,7 +107,7 @@ 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: any) => {
values.skip_servers = conv.arrToRecord(values.skip_servers_raw) values.skip_servers = conv.arrToRecord(values.skip_servers_raw)
values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number) values.fail_trigger_tasks = conv.strToArr(values.fail_trigger_tasks_raw).map(Number)
values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number) values.recover_trigger_tasks = conv.strToArr(values.recover_trigger_tasks_raw).map(Number)
+19 -3
View File
@@ -13,7 +13,9 @@ import { AttachAddon } from "@xterm/addon-attach"
import { FitAddon } from "@xterm/addon-fit" import { FitAddon } from "@xterm/addon-fit"
import { Terminal } from "@xterm/xterm" import { Terminal } from "@xterm/xterm"
import "@xterm/xterm/css/xterm.css" import "@xterm/xterm/css/xterm.css"
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react" import { Terminal as TerminalIcon } from "lucide-react"
import { JSX, forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { toast } from "sonner" import { toast } from "sonner"
@@ -142,7 +144,7 @@ export const TerminalPage = () => {
<div className="px-8"> <div className="px-8">
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{`Terminal (${id})`}</h1> <h1 className="flex-1 text-3xl font-bold tracking-tight">{`Terminal (${id})`}</h1>
<div className="flex-2 flex ml-auto gap-2"> <div className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0">
<IconButton <IconButton
icon="expand" icon="expand"
onClick={async () => { onClick={async () => {
@@ -181,10 +183,24 @@ export const TerminalPage = () => {
) )
} }
export const TerminalButton = ({ id }: { id: number }) => { export const TerminalButton = ({ id, menuItem = false }: { id: number; menuItem?: boolean }) => {
const { t } = useTranslation()
const handleOpenNewTab = () => { const handleOpenNewTab = () => {
window.open(`/dashboard/terminal/${id}`, "_blank") window.open(`/dashboard/terminal/${id}`, "_blank")
} }
if (menuItem) {
return (
<button
type="button"
onClick={handleOpenNewTab}
className="flex w-full items-center text-sm px-2 py-2 hover:bg-accent hover:text-accent-foreground"
>
<TerminalIcon className="h-4 w-4 mr-2" />
<span>{t("Terminal")}</span>
</button>
)
}
return <IconButton variant="outline" icon="terminal" onClick={handleOpenNewTab} /> return <IconButton variant="outline" icon="terminal" onClick={handleOpenNewTab} />
} }
+6 -6
View File
@@ -10,7 +10,7 @@ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef< const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, React.ComponentRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
@@ -25,7 +25,7 @@ const AlertDialogOverlay = React.forwardRef<
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef< const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>, React.ComponentRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPortal> <AlertDialogPortal>
@@ -56,7 +56,7 @@ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDiv
AlertDialogFooter.displayName = "AlertDialogFooter" AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef< const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>, React.ComponentRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
@@ -68,7 +68,7 @@ const AlertDialogTitle = React.forwardRef<
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef< const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>, React.ComponentRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
@@ -80,7 +80,7 @@ const AlertDialogDescription = React.forwardRef<
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef< const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>, React.ComponentRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} /> <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
@@ -88,7 +88,7 @@ const AlertDialogAction = React.forwardRef<
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef< const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, React.ComponentRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
+3 -3
View File
@@ -3,7 +3,7 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react" import * as React from "react"
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ComponentRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Root <AvatarPrimitive.Root
@@ -15,7 +15,7 @@ const Avatar = React.forwardRef<
Avatar.displayName = AvatarPrimitive.Root.displayName Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef< const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ComponentRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image
@@ -27,7 +27,7 @@ const AvatarImage = React.forwardRef<
AvatarImage.displayName = AvatarPrimitive.Image.displayName AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ComponentRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
+184
View File
@@ -0,0 +1,184 @@
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
import * as React from "react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className,
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav,
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next,
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
defaultClassNames.month_caption,
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns,
),
dropdown_root: cn(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root,
),
dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label,
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday,
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
defaultClassNames.week_number_header,
),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number,
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day,
),
range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today,
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside,
),
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />
}
if (orientation === "right") {
return <ChevronRightIcon className={cn("size-4", className)} {...props} />
}
return <ChevronDownIcon className={cn("size-4", className)} {...props} />
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-[--cell-size] items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className,
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }
+1 -1
View File
@@ -4,7 +4,7 @@ import { Check } from "lucide-react"
import * as React from "react" import * as React from "react"
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, React.ComponentRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
+7 -7
View File
@@ -6,7 +6,7 @@ import { Search } from "lucide-react"
import * as React from "react" import * as React from "react"
const Command = React.forwardRef< const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>, React.ComponentRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive> React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive <CommandPrimitive
@@ -33,7 +33,7 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
} }
const CommandInput = React.forwardRef< const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper=""> <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
@@ -52,7 +52,7 @@ const CommandInput = React.forwardRef<
CommandInput.displayName = CommandPrimitive.Input.displayName CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef< const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>, React.ComponentRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
@@ -65,7 +65,7 @@ const CommandList = React.forwardRef<
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef< const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => ( >((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} /> <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
@@ -74,7 +74,7 @@ const CommandEmpty = React.forwardRef<
CommandEmpty.displayName = CommandPrimitive.Empty.displayName CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef< const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Group <CommandPrimitive.Group
@@ -90,7 +90,7 @@ const CommandGroup = React.forwardRef<
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef< const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, React.ComponentRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Separator <CommandPrimitive.Separator
@@ -102,7 +102,7 @@ const CommandSeparator = React.forwardRef<
CommandSeparator.displayName = CommandPrimitive.Separator.displayName CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef< const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Item <CommandPrimitive.Item
+4 -4
View File
@@ -12,7 +12,7 @@ const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ComponentRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
@@ -27,7 +27,7 @@ const DialogOverlay = React.forwardRef<
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ComponentRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DialogPortal> <DialogPortal>
@@ -67,7 +67,7 @@ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef< const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
@@ -79,7 +79,7 @@ const DialogTitle = React.forwardRef<
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
+4 -4
View File
@@ -17,7 +17,7 @@ const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef< const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, React.ComponentRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
@@ -29,7 +29,7 @@ const DrawerOverlay = React.forwardRef<
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef< const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, React.ComponentRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DrawerPortal> <DrawerPortal>
@@ -60,7 +60,7 @@ const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
DrawerFooter.displayName = "DrawerFooter" DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef< const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, React.ComponentRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Title <DrawerPrimitive.Title
@@ -72,7 +72,7 @@ const DrawerTitle = React.forwardRef<
DrawerTitle.displayName = DrawerPrimitive.Title.displayName DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef< const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, React.ComponentRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Description <DrawerPrimitive.Description
+8 -8
View File
@@ -16,7 +16,7 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, React.ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean
} }
@@ -37,7 +37,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, React.ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
@@ -52,7 +52,7 @@ const DropdownMenuSubContent = React.forwardRef<
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
@@ -70,7 +70,7 @@ const DropdownMenuContent = React.forwardRef<
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, React.ComponentRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean
} }
@@ -88,7 +88,7 @@ const DropdownMenuItem = React.forwardRef<
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, React.ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => ( >(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
@@ -111,7 +111,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
@@ -133,7 +133,7 @@ const DropdownMenuRadioItem = React.forwardRef<
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, React.ComponentRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean
} }
@@ -147,7 +147,7 @@ const DropdownMenuLabel = React.forwardRef<
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
+2 -2
View File
@@ -79,7 +79,7 @@ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ComponentRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField()
@@ -96,7 +96,7 @@ const FormLabel = React.forwardRef<
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef< const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>, React.ComponentRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot> React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+1 -1
View File
@@ -8,7 +8,7 @@ const labelVariants = cva(
) )
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ComponentRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} /> <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
+6 -6
View File
@@ -5,7 +5,7 @@ import { ChevronDown } from "lucide-react"
import * as React from "react" import * as React from "react"
const NavigationMenu = React.forwardRef< const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>, React.ComponentRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
@@ -20,7 +20,7 @@ const NavigationMenu = React.forwardRef<
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef< const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>, React.ComponentRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
@@ -41,7 +41,7 @@ const navigationMenuTriggerStyle = cva(
) )
const NavigationMenuTrigger = React.forwardRef< const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, React.ComponentRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger <NavigationMenuPrimitive.Trigger
@@ -59,7 +59,7 @@ const NavigationMenuTrigger = React.forwardRef<
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef< const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>, React.ComponentRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
@@ -76,7 +76,7 @@ NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef< const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, React.ComponentRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}> <div className={cn("absolute left-0 top-full flex justify-center")}>
@@ -93,7 +93,7 @@ const NavigationMenuViewport = React.forwardRef<
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef< const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, React.ComponentRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
+1 -1
View File
@@ -7,7 +7,7 @@ const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
+2 -2
View File
@@ -3,7 +3,7 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from "react" import * as React from "react"
const ScrollArea = React.forwardRef< const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>, React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
@@ -21,7 +21,7 @@ const ScrollArea = React.forwardRef<
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef< const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, React.ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => ( >(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
+7 -7
View File
@@ -10,7 +10,7 @@ const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ComponentRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
@@ -30,7 +30,7 @@ const SelectTrigger = React.forwardRef<
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
@@ -44,7 +44,7 @@ const SelectScrollUpButton = React.forwardRef<
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
@@ -58,7 +58,7 @@ const SelectScrollDownButton = React.forwardRef<
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef< const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, React.ComponentRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => ( >(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
@@ -90,7 +90,7 @@ const SelectContent = React.forwardRef<
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef< const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Label <SelectPrimitive.Label
@@ -102,7 +102,7 @@ const SelectLabel = React.forwardRef<
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef< const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item <SelectPrimitive.Item
@@ -125,7 +125,7 @@ const SelectItem = React.forwardRef<
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef< const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, React.ComponentRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Separator <SelectPrimitive.Separator
+1 -1
View File
@@ -3,7 +3,7 @@ import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react" import * as React from "react"
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ComponentRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
+26
View File
@@ -0,0 +1,26 @@
import { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import * as React from "react"
const Switch = React.forwardRef<
React.ComponentRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
+3 -3
View File
@@ -5,7 +5,7 @@ import * as React from "react"
const Tabs = TabsPrimitive.Root const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ComponentRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.List <TabsPrimitive.List
@@ -20,7 +20,7 @@ const TabsList = React.forwardRef<
TabsList.displayName = TabsPrimitive.List.displayName TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ComponentRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
@@ -35,7 +35,7 @@ const TabsTrigger = React.forwardRef<
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef< const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, React.ComponentRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Content <TabsPrimitive.Content
+5
View File
@@ -11,6 +11,7 @@ import {
FolderClosed, FolderClosed,
Menu, Menu,
Minus, Minus,
MoreHorizontal,
Play, Play,
Plus, Plus,
Terminal, Terminal,
@@ -39,6 +40,7 @@ export interface IconButtonProps extends ButtonProps {
| "cog" | "cog"
| "minus" | "minus"
| "user-pen" | "user-pen"
| "more"
} }
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => { export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -102,6 +104,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
case "user-pen": { case "user-pen": {
return <UserPen /> return <UserPen />
} }
case "more": {
return <MoreHorizontal />
}
} }
})()} })()}
</Button> </Button>
+3 -3
View File
@@ -36,7 +36,7 @@ interface SheetContentProps
} }
const SheetContent = React.forwardRef< const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>, React.ComponentRef<typeof SheetPrimitive.Content>,
SheetContentProps SheetContentProps
>(({ side = "right", className, children, setOpen, ...props }, ref) => ( >(({ side = "right", className, children, setOpen, ...props }, ref) => (
<SheetPortal> <SheetPortal>
@@ -74,7 +74,7 @@ const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElemen
SheetFooter.displayName = "SheetFooter" SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef< const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>, React.ComponentRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Title <SheetPrimitive.Title
@@ -86,7 +86,7 @@ const SheetTitle = React.forwardRef<
SheetTitle.displayName = SheetPrimitive.Title.displayName SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef< const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>, React.ComponentRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Description <SheetPrimitive.Description
+1 -1
View File
@@ -14,7 +14,7 @@ import {
getSortedRowModel, getSortedRowModel,
useReactTable, useReactTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { HTMLAttributes, forwardRef, useEffect, useRef, useState } from "react" import { HTMLAttributes, JSX, forwardRef, useEffect, useRef, useState } from "react"
import { TableVirtuoso } from "react-virtuoso" import { TableVirtuoso } from "react-virtuoso"
// Original Table is wrapped with a <div> (see https://ui.shadcn.com/docs/components/table#radix-:r24:-content-manual), // Original Table is wrapped with a <div> (see https://ui.shadcn.com/docs/components/table#radix-:r24:-content-manual),
+8 -2
View File
@@ -1,6 +1,7 @@
import { getProfile, login as loginRequest } from "@/api/user" import { getProfile, login as loginRequest } from "@/api/user"
import { AuthContextProps } from "@/types" import { AuthContextProps } from "@/types"
import { createContext, useContext, useEffect, useMemo } from "react" import { createContext, useContext, useEffect, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { toast } from "sonner" import { toast } from "sonner"
@@ -16,6 +17,7 @@ const AuthContext = createContext<AuthContextProps>({
export const AuthProvider = ({ children }: { children: React.ReactNode }) => { export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const profile = useMainStore((store) => store.profile) const profile = useMainStore((store) => store.profile)
const setProfile = useMainStore((store) => store.setProfile) const setProfile = useMainStore((store) => store.setProfile)
const { t } = useTranslation()
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
@@ -25,7 +27,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setProfile(user) setProfile(user)
} catch (error: any) { } catch (error: any) {
setProfile(undefined) setProfile(undefined)
console.error("Error fetching profile", error)
} }
})() })()
}, []) }, [])
@@ -40,7 +41,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setProfile(user) setProfile(user)
navigate("/dashboard") navigate("/dashboard")
} catch (error: any) { } catch (error: any) {
toast(error.message) const msg = error?.message
if (msg === "ApiErrorUnauthorized" || msg === "Unauthorized") {
toast(t("InvalidUsernameOrPassword"))
} else {
toast(msg || t("NetworkError"))
}
} }
} }
+107 -74
View File
@@ -1,88 +1,121 @@
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities; @plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
:root { :root {
--radius: 0.5rem; --radius: 0.625rem;
--background: 0 0% 100%; --background: oklch(1 0 0);
--foreground: 0 0% 3.9%; --foreground: oklch(0.145 0 0);
--card: 0 0% 100%; --card: oklch(1 0 0);
--card-foreground: 0 0% 3.9%; --card-foreground: oklch(0.145 0 0);
--popover: 0 0% 100%; --popover: oklch(1 0 0);
--popover-foreground: 0 0% 3.9%; --popover-foreground: oklch(0.145 0 0);
--primary: 0 0% 9%; --primary: oklch(0.205 0 0);
--primary-foreground: 0 0% 98%; --primary-foreground: oklch(0.985 0 0);
--secondary: 0 0% 96.1%; --secondary: oklch(0.97 0 0);
--secondary-foreground: 0 0% 9%; --secondary-foreground: oklch(0.205 0 0);
--muted: 0 0% 96.1%; --muted: oklch(0.97 0 0);
--muted-foreground: 0 0% 45.1%; --muted-foreground: oklch(0.556 0 0);
--accent: 0 0% 96.1%; --accent: oklch(0.97 0 0);
--accent-foreground: 0 0% 9%; --accent-foreground: oklch(0.205 0 0);
--destructive: 0 84.2% 60.2%; --destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: 0 0% 98%; --border: oklch(0.922 0 0);
--border: 0 0% 89.8%; --input: oklch(0.922 0 0);
--input: 0 0% 89.8%; --ring: oklch(0.708 0 0);
--ring: 0 0% 3.9%; --chart-1: oklch(0.646 0.222 41.116);
--chart-1: 12 76% 61%; --chart-2: oklch(0.6 0.118 184.704);
--chart-2: 173 58% 39%; --chart-3: oklch(0.398 0.07 227.392);
--chart-3: 197 37% 24%; --chart-4: oklch(0.828 0.189 84.429);
--chart-4: 43 74% 66%; --chart-5: oklch(0.769 0.188 70.08);
--chart-5: 27 87% 67%; --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
} }
.dark { .dark {
--background: 0 0% 9%; --background: oklch(0.145 0 0);
--foreground: 0 0% 98%; --foreground: oklch(0.985 0 0);
--card: 0 0% 3.9%; --card: oklch(0.205 0 0);
--card-foreground: 0 0% 98%; --card-foreground: oklch(0.985 0 0);
--popover: 0 0% 3.9%; --popover: oklch(0.205 0 0);
--popover-foreground: 0 0% 98%; --popover-foreground: oklch(0.985 0 0);
--primary: 0 0% 98%; --primary: oklch(0.922 0 0);
--primary-foreground: 0 0% 9%; --primary-foreground: oklch(0.205 0 0);
--secondary: 0 0% 14.9%; --secondary: oklch(0.269 0 0);
--secondary-foreground: 0 0% 98%; --secondary-foreground: oklch(0.985 0 0);
--muted: 0 0% 14.9%; --muted: oklch(0.269 0 0);
--muted-foreground: 0 0% 63.9%; --muted-foreground: oklch(0.708 0 0);
--accent: 0 0% 14.9%; --accent: oklch(0.269 0 0);
--accent-foreground: 0 0% 98%; --accent-foreground: oklch(0.985 0 0);
--destructive: 0 62.8% 30.6%; --destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: 0 0% 98%; --border: oklch(1 0 0 / 10%);
--border: 0 0% 14.9%; --input: oklch(1 0 0 / 15%);
--input: 0 0% 14.9%; --ring: oklch(0.556 0 0);
--ring: 0 0% 83.1%; --chart-1: oklch(0.488 0.243 264.376);
--chart-1: 220 70% 50%; --chart-2: oklch(0.696 0.17 162.48);
--chart-2: 160 60% 45%; --chart-3: oklch(0.769 0.188 70.08);
--chart-3: 30 80% 55%; --chart-4: oklch(0.627 0.265 303.9);
--chart-4: 280 65% 60%; --chart-5: oklch(0.645 0.246 16.439);
--chart-5: 340 75% 55%; --sidebar: oklch(0.205 0 0);
} --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
html,
body,
#root {
height: 100%;
}
::-webkit-scrollbar {
@apply h-2.5 w-2.5;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-full border-[1px] border-solid border-transparent bg-border bg-clip-padding;
}
+2 -2
View File
@@ -4,13 +4,13 @@ import { initReactI18next } from "react-i18next"
import deTranslation from "../locales/de/translation.json" import deTranslation from "../locales/de/translation.json"
import enTranslation from "../locales/en/translation.json" import enTranslation from "../locales/en/translation.json"
import esTranslation from "../locales/es/translation.json" import esTranslation from "../locales/es/translation.json"
import frTranslation from "../locales/fr/translation.json"
import idTranslation from "../locales/id/translation.json"
import itTranslation from "../locales/it/translation.json" import itTranslation from "../locales/it/translation.json"
import ruTranslation from "../locales/ru/translation.json" import ruTranslation from "../locales/ru/translation.json"
import taTranslation from "../locales/ta/translation.json" import taTranslation from "../locales/ta/translation.json"
import zhCNTranslation from "../locales/zh-CN/translation.json" import zhCNTranslation from "../locales/zh-CN/translation.json"
import zhTWTranslation from "../locales/zh-TW/translation.json" import zhTWTranslation from "../locales/zh-TW/translation.json"
import frTranslation from "../locales/fr/translation.json"
import idTranslation from "../locales/id/translation.json"
const resources = { const resources = {
"zh-CN": { "zh-CN": {
+57 -1
View File
@@ -7,6 +7,8 @@
}, },
"Username": "Username", "Username": "Username",
"Password": "Password", "Password": "Password",
"InvalidUsernameOrPassword": "Invalid Username Or Password",
"NetworkError": "Network Error",
"LoginFirst": "Please Log in first", "LoginFirst": "Please Log in first",
"CurrentTime": "Current Time", "CurrentTime": "Current Time",
"Results": { "Results": {
@@ -45,6 +47,8 @@
"Enable": "Enable", "Enable": "Enable",
"HideForGuest": "Hidden from Visitors", "HideForGuest": "Hidden from Visitors",
"InstallCommands": "Installation command", "InstallCommands": "Installation command",
"Terminal": "Terminal",
"Config": "Config",
"Note": "Note", "Note": "Note",
"Success": "Success", "Success": "Success",
"Done": "Finish", "Done": "Finish",
@@ -72,6 +76,10 @@
"Trigger": "On Trigger", "Trigger": "On Trigger",
"TasksToTriggerOnAlert": "Tasks to be triggered on alert", "TasksToTriggerOnAlert": "Tasks to be triggered on alert",
"TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery", "TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery",
"Add": "Add",
"Delete": "Delete",
"AdvancedJSON": "Advanced JSON",
"Save": "Save",
"Confirm": "Confirm", "Confirm": "Confirm",
"ConfirmDeletion": "Confirm Deletion?", "ConfirmDeletion": "Confirm Deletion?",
"Services": "Services", "Services": "Services",
@@ -185,5 +193,53 @@
"EditServerConfig": "Edit Server Config", "EditServerConfig": "Edit Server Config",
"Option": "Option", "Option": "Option",
"Value": "Value", "Value": "Value",
"Preview": "Preview" "Preview": "Preview",
"PublicNote": {
"Billing": "Billing",
"Plan": "Plan",
"StartDate": "Start Date",
"EndDate": "End Date",
"AutoRenewal": "Auto Renewal",
"Cycle": "Cycle",
"Amount": "Amount",
"Bandwidth": "Bandwidth",
"TrafficVolume": "Traffic Volume",
"TrafficType": "Traffic Type",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "Network Route",
"Extra": "Extra",
"Enabled": "Enabled",
"Disabled": "Disabled",
"Inbound": "Inbound",
"Both": "Both",
"Day": "Day",
"Week": "Week",
"Month": "Month",
"Year": "Year",
"NoExpiry": "No Expiry",
"SetNoExpiry": "Set No Expiry",
"CancelNoExpiry": "Cancel No Expiry",
"Free": "Free",
"PayAsYouGo": "Pay as you go",
"DropdownHint": "You may need to reselect for dropdown changes to take effect",
"CommaSeparated": "Separate multiple items with commas",
"Has": "Has",
"None": "None"
},
"Validation": {
"InvalidDate": "Invalid date",
"MustBe0Or1": "Must be 0 or 1",
"MustBeDayWeekMonthYear": "Must be Day/Week/Month/Year",
"MustBe1Or2": "Must be 1 or 2",
"DigitsOnly": "Digits only",
"InvalidForm": "Invalid form"
},
"AlertRules": {
"CoverAllServers": "Monitor all servers",
"IgnoreAllSelectSpecific": "Ignore all, select specific servers",
"IgnoreHint": "{{server}} ID: true/false",
"IgnoreExample": "e.g., {\"1\": true, \"2\": false}"
},
"Search": "Search..."
} }
+57 -1
View File
@@ -7,6 +7,8 @@
}, },
"Username": "用户名", "Username": "用户名",
"Password": "密码", "Password": "密码",
"InvalidUsernameOrPassword": "用户名或密码错误",
"NetworkError": "网络错误",
"LoginFirst": "请先登录", "LoginFirst": "请先登录",
"CurrentTime": "当前时间", "CurrentTime": "当前时间",
"Results": { "Results": {
@@ -45,6 +47,8 @@
"Enable": "启用", "Enable": "启用",
"HideForGuest": "对游客隐藏", "HideForGuest": "对游客隐藏",
"InstallCommands": "安装命令", "InstallCommands": "安装命令",
"Terminal": "终端",
"Config": "配置",
"Note": "备注", "Note": "备注",
"Success": "成功", "Success": "成功",
"Done": "完成", "Done": "完成",
@@ -72,6 +76,10 @@
"Trigger": "触发", "Trigger": "触发",
"TasksToTriggerOnAlert": "告警时要触发的任务", "TasksToTriggerOnAlert": "告警时要触发的任务",
"TasksToTriggerAfterRecovery": "恢复后要触发的任务", "TasksToTriggerAfterRecovery": "恢复后要触发的任务",
"Add": "添加",
"Delete": "删除",
"AdvancedJSON": "高级 JSON",
"Save": "保存",
"Confirm": "确认", "Confirm": "确认",
"ConfirmDeletion": "确认删除?", "ConfirmDeletion": "确认删除?",
"Services": "服务", "Services": "服务",
@@ -185,5 +193,53 @@
"OverrideDDNSDomains": "强制指定 DDNS 域名(每个配置)", "OverrideDDNSDomains": "强制指定 DDNS 域名(每个配置)",
"Value": "值", "Value": "值",
"Preview": "预览", "Preview": "预览",
"Option": "选项" "Option": "选项",
"PublicNote": {
"Billing": "账单信息",
"Plan": "套餐配置",
"StartDate": "开始时间",
"EndDate": "结束时间",
"AutoRenewal": "自动续费",
"Cycle": "周期",
"Amount": "金额",
"Bandwidth": "带宽",
"TrafficVolume": "流量配额",
"TrafficType": "流量类型",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "网络路由",
"Extra": "额外备注",
"Enabled": "启用",
"Disabled": "禁用",
"Inbound": "入站",
"Both": "双向",
"Day": "天",
"Week": "周",
"Month": "月",
"Year": "年",
"NoExpiry": "不过期",
"SetNoExpiry": "设置为不过期",
"CancelNoExpiry": "取消不过期",
"Free": "免费",
"PayAsYouGo": "按量付费",
"DropdownHint": "下拉项需要重新选择才能生效",
"CommaSeparated": "以英文逗号分隔多个",
"Has": "有",
"None": "无"
},
"Validation": {
"InvalidDate": "无效的日期格式",
"MustBe0Or1": "只能为 0 或 1",
"MustBeDayWeekMonthYear": "必须为 Day/Week/Month/Year",
"MustBe1Or2": "只能为 1 或 2",
"DigitsOnly": "仅允许数字",
"InvalidForm": "表单校验失败"
},
"AlertRules": {
"CoverAllServers": "监控所有服务器",
"IgnoreAllSelectSpecific": "忽略所有,选择特定服务器",
"IgnoreHint": "{{server}}ID: true/false",
"IgnoreExample": "例如:{\"1\": true, \"2\": false}"
},
"Search": "搜索..."
} }
+1 -1
View File
@@ -144,7 +144,7 @@ export default function AlertRulePage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" /> <NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex gap-2 ml-auto" className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteAlertRules, fn: deleteAlertRules,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+4 -4
View File
@@ -208,11 +208,11 @@ export default function CronPage() {
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
return ( return (
<div className="px-3"> <div className="px-3 max-w-7xl mx-auto">
<div className="flex mt-6 mb-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-3 mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Task")}</h1> <h1 className="text-3xl font-bold tracking-tight">{t("Task")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteCron, fn: deleteCron,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+1 -1
View File
@@ -149,7 +149,7 @@ export default function DDNSPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1> <h1 className="flex-1 text-3xl font-bold tracking-tight">{t("DDNS")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteDDNSProfiles, fn: deleteDDNSProfiles,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+1 -1
View File
@@ -132,7 +132,7 @@ export default function NATPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1> <h1 className="flex-1 text-3xl font-bold tracking-tight"> {t("NATT")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteNAT, fn: deleteNAT,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+1 -1
View File
@@ -125,7 +125,7 @@ export default function NotificationGroupPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" /> <GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex gap-2 ml-auto" className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteNotificationGroups, fn: deleteNotificationGroups,
id: selectedRows.map((r) => r.original.group.id), id: selectedRows.map((r) => r.original.group.id),
+1 -1
View File
@@ -140,7 +140,7 @@ export default function NotificationPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" /> <NotificationTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex gap-2 ml-auto" className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteNotification, fn: deleteNotification,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+2 -2
View File
@@ -40,7 +40,7 @@ export default function OnlineUserPage() {
// 计算 offset // 计算 offset
const offset = (page - 1) * pageSize const offset = (page - 1) * pageSize
const { data, mutate, error, isLoading } = useSWR<ModelOnlineUserApi>( const { data, mutate, error, isLoading } = useSWR<ModelOnlineUserApi, Error>(
`/api/v1/online-user?offset=${offset}&limit=${pageSize}`, `/api/v1/online-user?offset=${offset}&limit=${pageSize}`,
swrFetcher, swrFetcher,
) )
@@ -128,7 +128,7 @@ export default function OnlineUserPage() {
return data?.value ?? [] return data?.value ?? []
}, [data]) }, [data])
const table = useReactTable({ const table = useReactTable<ModelOnlineUser>({
data: dataCache, data: dataCache,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
+1 -1
View File
@@ -125,7 +125,7 @@ export default function ServerGroupPage() {
<div className="flex mt-6 mb-4"> <div className="flex mt-6 mb-4">
<GroupTab className="flex-1 mr-4 sm:max-w-[40%]" /> <GroupTab className="flex-1 mr-4 sm:max-w-[40%]" />
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteServerGroups, fn: deleteServerGroups,
id: selectedRows.map((r) => r.original.group.id), id: selectedRows.map((r) => r.original.group.id),
+45 -10
View File
@@ -11,6 +11,12 @@ import { ServerConfigCard } from "@/components/server-config"
import { ServerConfigCardBatch } from "@/components/server-config-batch" import { ServerConfigCardBatch } from "@/components/server-config-batch"
import { TerminalButton } from "@/components/terminal" import { TerminalButton } from "@/components/terminal"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { import {
Table, Table,
TableBody, TableBody,
@@ -31,7 +37,10 @@ import useSWR from "swr"
export default function ServerPage() { export default function ServerPage() {
const { t } = useTranslation() const { t } = useTranslation()
const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher) const { data, mutate, error, isLoading } = useSWR<Server[]>("/api/v1/server", swrFetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
})
const { serverGroups } = useServer() const { serverGroups } = useServer()
useEffect(() => { useEffect(() => {
@@ -144,9 +153,27 @@ export default function ServerPage() {
delete={{ fn: deleteServer, id: s.id, mutate: mutate }} delete={{ fn: deleteServer, id: s.id, mutate: mutate }}
> >
<> <>
<TerminalButton id={s.id} />
<ServerCard mutate={mutate} data={s} /> <ServerCard mutate={mutate} data={s} />
<ServerConfigCard sid={s.id} variant="outline" /> <DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
icon="more"
variant="outline"
aria-label="More actions"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<TerminalButton id={s.id} menuItem />
</DropdownMenuItem>
<DropdownMenuItem asChild>
<ServerConfigCard sid={s.id} variant="ghost" menuItem />
</DropdownMenuItem>
<DropdownMenuItem asChild>
<InstallCommandsMenu uuid={s.uuid} menuItem />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</> </>
</ActionButtonGroup> </ActionButtonGroup>
) )
@@ -167,11 +194,11 @@ export default function ServerPage() {
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
return ( return (
<div className="px-3"> <div className="px-3 max-w-7xl mx-auto">
<div className="flex mt-6 mb-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-3 mt-6 mb-4">
<h1 className="text-3xl font-bold tracking-tight">{t("Server")}</h1> <h1 className="text-3xl font-bold tracking-tight">{t("Server")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteServer, fn: deleteServer,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
@@ -222,8 +249,9 @@ export default function ServerPage() {
<InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" /> <InstallCommandsMenu className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-blue-700 text-white hover:bg-blue-600 dark:hover:bg-blue-800 rounded-lg" />
</HeaderButtonGroup> </HeaderButtonGroup>
</div> </div>
<Table> <div className="rounded-md border overflow-x-auto">
<TableHeader> <Table className="min-w-[960px]">
<TableHeader className="sticky top-0 bg-background z-10">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
@@ -250,10 +278,16 @@ export default function ServerPage() {
</TableRow> </TableRow>
) : table.getRowModel().rows?.length ? ( ) : table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}> <TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="text-xsm"> <TableCell key={cell.id} className="text-xsm">
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
@@ -268,5 +302,6 @@ export default function ServerPage() {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
</div>
) )
} }
+4 -4
View File
@@ -174,11 +174,11 @@ export default function ServicePage() {
const selectedRows = table.getSelectedRowModel().rows const selectedRows = table.getSelectedRowModel().rows
return ( return (
<div className="px-3"> <div className="px-3 max-w-7xl mx-auto">
<div className="flex mt-6 mb-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between w-full gap-3 mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{t("Services")}</h1> <h1 className="text-3xl font-bold tracking-tight">{t("Service")}</h1>
<HeaderButtonGroup <HeaderButtonGroup
className="flex-2 flex ml-auto gap-2" className="flex gap-2 flex-wrap shrink-0"
delete={{ delete={{
fn: deleteService, fn: deleteService,
id: selectedRows.map((r) => r.original.id), id: selectedRows.map((r) => r.original.id),
+3 -3
View File
@@ -64,8 +64,8 @@ export default function SettingsPage() {
navigate("/dashboard/settings/online-user") navigate("/dashboard/settings/online-user")
} }
const form = useForm<z.infer<typeof settingFormSchema>>({ const form = useForm({
resolver: zodResolver(settingFormSchema), resolver: zodResolver(settingFormSchema) as any,
defaultValues: config defaultValues: config
? { ? {
...config.config, ...config.config,
@@ -92,7 +92,7 @@ export default function SettingsPage() {
} }
}, [config?.config, form]) }, [config?.config, form])
const onSubmit = async (values: z.infer<typeof settingFormSchema>) => { const onSubmit = async (values: any) => {
try { try {
await updateSettings(values) await updateSettings(values)
form.reset() form.reset()
+2 -1
View File
@@ -77,7 +77,8 @@ export default function UserPage() {
{ {
header: t("LastLogin"), header: t("LastLogin"),
accessorKey: "updated_at", accessorKey: "updated_at",
accessorFn: (row) => row.updated_at ? new Date(row.updated_at).toLocaleString() : t("Never"), accessorFn: (row) =>
row.updated_at ? new Date(row.updated_at).toLocaleString() : t("Never"),
}, },
{ {
id: "actions", id: "actions",
+9
View File
@@ -431,6 +431,15 @@ export interface ModelOnlineUser {
user_id: number user_id: number
} }
export interface ModelOnlineUserApi {
value: ModelOnlineUser[]
pagination: {
total: number
offset: number
limit: number
}
}
export interface ModelPagination { export interface ModelPagination {
limit: number limit: number
offset: number offset: number
+1 -1
View File
@@ -26,7 +26,7 @@ export const AgentConfigSchema = z.object({
), ),
), ),
ip_report_period: asOptionalField(z.coerce.number().int().min(30)), ip_report_period: asOptionalField(z.coerce.number().int().min(30)),
nic_allowlist: asOptionalField(z.record(z.boolean())), nic_allowlist: asOptionalField(z.record(z.string(), z.boolean())),
nic_allowlist_raw: asOptionalField( nic_allowlist_raw: asOptionalField(
z.string().refine( z.string().refine(
(val) => { (val) => {
+83 -3
View File
@@ -24,12 +24,92 @@ export default defineConfig({
}, },
}, },
build: { build: {
cssCodeSplit: true,
sourcemap: false,
chunkSizeWarningLimit: 1500,
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks(id) { manualChunks(id: string) {
if (id.includes("node_modules")) { if (!id.includes("node_modules")) return
return id.toString().split("node_modules/")[1].split("/")[0].toString()
// 提取顶级包名,兼容 scoped packages(如 @radix-ui/react-dialog
const match = id.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/)
const pkg = match ? match[1] : null
if (!pkg) return "vendor"
// 1. 核心框架:React 及其紧密依赖(必须合并,避免运行时错误)
if (
pkg === "react" ||
pkg === "react-dom" ||
pkg === "scheduler" ||
pkg === "react-router" ||
pkg === "react-router-dom" ||
pkg === "history"
) {
return "react"
} }
// 2. UI 相关:Radix UI + shadcn 工具链
if (
pkg.startsWith("@radix-ui/") ||
pkg === "class-variance-authority" ||
pkg === "clsx" ||
pkg === "tailwind-merge"
) {
return "ui"
}
// 3. 表单与校验
if (
pkg === "react-hook-form" ||
pkg.startsWith("@hookform/") || // 匹配 @hookform/resolvers, @hookform/devtools 等
pkg === "zod"
) {
return "form"
}
// 4. 国际化
if (pkg === "i18next" || pkg === "react-i18next") {
return "i18n"
}
// 5. 数据获取
if (pkg === "swr") {
return "data"
}
// 6. 工具类库(高频、轻量、通用)—— 合并减少请求数
const utilityLibs = [
"lodash-es",
"date-fns",
"dayjs",
"axios",
"nanoid",
"uuid",
"immer",
"lodash",
]
if (utilityLibs.includes(pkg)) {
return "utils"
}
// 7. 大型独立库(如图表、富文本等)单独分包,按需加载
const largeLibs = [
"chart.js",
"recharts",
"echarts",
"quill",
"draft-js",
"monaco-editor",
"@monaco-editor/react",
]
if (largeLibs.includes(pkg)) {
return `lib-${pkg.replace(/@/g, "").replace(/\//g, "-")}`
}
// 8. 其他第三方库:按顶级包名分组,但限制数量(避免太多小 chunk)
// 如果你项目依赖很多,可考虑合并为 "vendor-others"
return "vendor"
}, },
}, },
}, },