Merge upstream/main

This commit is contained in:
2026-04-16 11:52:58 +08:00
80 changed files with 9318 additions and 773 deletions
+1
View File
@@ -35,6 +35,7 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: dist.zip files: dist.zip
generate_release_notes: true
- name: Changelog - name: Changelog
run: bun x changelogithub run: bun x changelogithub
+2
View File
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
bun.lock
pnpm-lock.yaml
+1
View File
@@ -0,0 +1 @@
src/main.tsx
+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": {}
} }
+6055
View File
File diff suppressed because it is too large Load Diff
+53 -50
View File
@@ -13,69 +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-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-table": "^8.20.6", "@tailwindcss/postcss": "^4.1.14",
"@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.3",
"i18next-browser-languagedetector": "^8.2.0",
"jotai-zustand": "^0.6.0", "jotai-zustand": "^0.6.0",
"lucide-react": "^0.454.0", "lucide-react": "^0.545.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.2.0",
"react-dom": "^19.0.0", "react-day-picker": "^9.11.1",
"react-hook-form": "^7.54.2", "react-dom": "^19.2.0",
"react-i18next": "^15.4.0", "react-hook-form": "^7.71.1",
"react-router-dom": "^7.1.5", "react-i18next": "^16.0.0",
"react-virtuoso": "^4.12.3", "react-router-dom": "^7.9.4",
"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.12",
"zustand": "^5.0.3" "zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.19.0", "@eslint/js": "^9.37.0",
"@types/node": "^22.13.0", "@types/node": "^24.7.0",
"@types/react": "^18.3.18", "@types/react": "^19.2.2",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^19.2.1",
"@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.37.0",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-react-refresh": "^0.4.23",
"globals": "^15.14.0", "globals": "^16.4.0",
"postcss": "^8.5.1", "postcss": "8.4.24",
"swagger-typescript-api": "^13.0.23", "swagger-typescript-api": "^13.2.15",
"tailwindcss": "^3.4.17", "tailwindcss": "3.4.19",
"typescript": "~5.6.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.22.0", "typescript-eslint": "^8.46.0",
"vite": "^6.0.11" "vite": "^7.1.9"
} }
} }
+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
@@ -423,7 +423,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) {
+44 -5
View File
@@ -57,14 +57,27 @@ const notificationFormSchema = z.object({
request_body: z.string(), request_body: z.string(),
verify_tls: asOptionalField(z.boolean()), verify_tls: asOptionalField(z.boolean()),
skip_check: asOptionalField(z.boolean()), skip_check: asOptionalField(z.boolean()),
format_metric_units: asOptionalField(z.boolean()),
}) })
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,
format_metric_units: (data as any).format_metric_units ?? false,
}
: { : {
name: "", name: "",
url: "", url: "",
@@ -72,6 +85,9 @@ 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,
format_metric_units: false,
}, },
resetOptions: { resetOptions: {
keepDefaultValues: false, keepDefaultValues: false,
@@ -80,7 +96,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 +126,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"
@@ -267,6 +286,26 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="format_metric_units"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("FormatMetricUnits")}
</Label>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<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">
+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
+735 -19
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,13 +22,34 @@ 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 {
type PublicNote,
PublicNoteSchema,
applyPublicNoteDate,
applyPublicNotePatch,
detectPublicNoteMode,
normalizeISO,
parsePublicNote,
toggleEndNoExpiry,
validatePublicNote,
} from "@/lib/public-note"
import { conv } from "@/lib/utils" import { conv } from "@/lib/utils"
import { asOptionalField } from "@/lib/utils" import { asOptionalField } from "@/lib/utils"
import { ModelServer } from "@/types" import { ModelServer } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { HelpCircle } from "lucide-react"
import { useState } from "react" import { 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"
@@ -43,13 +65,29 @@ interface ServerCardProps {
const serverFormSchema = z.object({ const serverFormSchema = z.object({
name: z.string().min(1), name: z.string().min(1),
note: asOptionalField(z.string()), note: asOptionalField(z.string()),
public_note: asOptionalField(z.string()), public_note: asOptionalField(
z.string().refine(
(val) => {
const s = (val ?? "").trim()
if (s.length === 0) return true
try {
const obj = JSON.parse(s)
if (typeof obj !== "object" || obj === null) return true
return PublicNoteSchema.safeParse(obj).success
} catch {
// skip check if not JSON
return true
}
},
{ message: "Invalid Public Note JSON" },
),
),
display_index: z.coerce.number().int(), display_index: z.coerce.number().int(),
hide_for_guest: asOptionalField(z.boolean()), hide_for_guest: asOptionalField(z.boolean()),
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 +107,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 +123,47 @@ 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>) => { 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 [publicNoteMode, setPublicNoteMode] = useState<"structured" | "raw">(
detectPublicNoteMode(data?.public_note),
)
const [publicNoteRaw, setPublicNoteRaw] = useState<string>(data?.public_note ?? "")
const patchPublicNote = (path: string, value: string | undefined) => {
setPublicNoteObj((prev) => applyPublicNotePatch(prev, path, value))
}
const patchPublicNoteDate = (
path: "billingDataMod.startDate" | "billingDataMod.endDate",
d: Date,
) => {
setPublicNoteObj((prev) => applyPublicNoteDate(prev, path, d))
}
const toggleEndNoExpiryLocal = () => {
setPublicNoteObj((prev) => toggleEndNoExpiry(prev))
}
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 +171,37 @@ 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
if (publicNoteMode === "raw") {
const raw = (publicNoteRaw ?? "").trim()
if (raw.length === 0) {
values.public_note = undefined
} else {
values.public_note = raw
}
} else {
const { errors, valid } = validatePublicNote(publicNoteObj)
if (!valid) {
setPublicNoteErrors(errors)
toast(t("Error"), { description: t("Validation.InvalidForm") })
return
}
setPublicNoteErrors({})
const bd = publicNoteObj.billingDataMod
const pd = publicNoteObj.planDataMod
const pnNormalized: PublicNote = {
billingDataMod: bd && {
...bd,
startDate: normalizeISO(bd.startDate),
endDate: normalizeISO(bd.endDate),
},
planDataMod: pd,
}
const jsonStr = JSON.stringify(pnNormalized)
values.public_note = jsonStr.length > 2 ? jsonStr : undefined
}
await updateServer(data!.id!, values) await updateServer(data!.id!, values)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -106,12 +215,35 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
form.reset() form.reset()
} }
const handleOpenChange = (v: boolean) => {
if (v) {
form.reset({
...data,
ddns_profiles_raw: data.ddns_profiles
? conv.arrToStr(data.ddns_profiles)
: undefined,
override_ddns_domains_raw: data.override_ddns_domains
? JSON.stringify(data.override_ddns_domains)
: undefined,
})
setPublicNoteObj(parsePublicNote(data?.public_note))
setPublicNoteRaw(data?.public_note ?? "")
setPublicNoteMode(detectPublicNoteMode(data?.public_note))
setPublicNoteErrors({})
}
setOpen(v)
}
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={handleOpenChange}>
<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 +368,603 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField {/* Public Note controls (optional + dual mode) */}
control={form.control} <div className="space-y-3">
name="public_note" <div className="space-y-1">
render={({ field }) => ( <div className="flex items-center justify-between">
<FormItem> <div className="flex items-center gap-2">
<FormLabel>{t("Public") + t("Note")}</FormLabel> <FormLabel>{t("PublicNote.Label")}</FormLabel>
<FormControl> <a
<Textarea className="resize-y" {...field} /> href="https://nezha.wiki/guide/servers.html#%E5%85%AC%E5%BC%80%E5%A4%87%E6%B3%A8%E8%AE%BE%E7%BD%AE"
</FormControl> target="_blank"
<FormMessage /> rel="noopener noreferrer"
</FormItem> className="inline-flex items-center text-muted-foreground hover:text-foreground"
)} >
<HelpCircle className="h-4 w-4" />
</a>
</div>
</div>
</div>
{/* Toggle: when disabled, hide edit controls and submit an empty value */}
<div className="flex items-center gap-4">
{/* Mode switch: Raw text / Custom fields */}
<div className="flex items-center gap-2">
{/* Show 'structured' first, then 'raw' */}
<Button
type="button"
variant={
publicNoteMode === "structured"
? "default"
: "outline"
}
className="text-xs h-7"
onClick={() => {
setPublicNoteMode("structured")
setPublicNoteObj(parsePublicNote(publicNoteRaw))
}}
>
{t("PublicNote.CustomFields")}
</Button>
<Button
type="button"
variant={
publicNoteMode === "raw" ? "default" : "outline"
}
className="text-xs h-7"
onClick={() => setPublicNoteMode("raw")}
>
{t("PublicNote.RawText")}
</Button>
</div>
</div>
{/* Raw text mode: shown by default; submission uses this string */}
{publicNoteMode === "raw" && (
<div>
<Textarea
className="resize-y"
value={publicNoteRaw}
onChange={(e) => setPublicNoteRaw(e.target.value)}
rows={10}
/> />
</div>
)}
{/* Custom fields mode: keep structured editing; serialize to string on submit */}
{publicNoteMode === "structured" && (
<>
<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>
{/* Add 'Clear' button to allow removing the date */}
<Button
type="button"
variant="outline"
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700 ml-2"
onClick={() =>
patchPublicNote(
"billingDataMod.startDate",
undefined,
)
}
>
{t("PublicNote.ClearDate") ?? "Clear"}
</Button>
<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
patchPublicNoteDate(
"billingDataMod.startDate",
d,
)
}}
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={toggleEndNoExpiryLocal}
>
{publicNoteObj.billingDataMod
?.endDate ===
"0000-00-00T23:59:59+08:00"
? t("PublicNote.CancelNoExpiry")
: t("PublicNote.SetNoExpiry")}
</Button>
{/* Add 'Clear' button to allow removing the date */}
<Button
type="button"
variant="outline"
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
onClick={() =>
patchPublicNote(
"billingDataMod.endDate",
undefined,
)
}
>
{t("PublicNote.ClearDate") ??
"Clear"}
</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 as string,
).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 as string,
)
: undefined
}
onSelect={(d) => {
if (!d) return
patchPublicNoteDate(
"billingDataMod.endDate",
d,
)
}}
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">
<div className="flex items-center gap-2">
<Label className="text-xs">
{t("PublicNote.AutoRenewal")}
</Label>
</div>
<div className="flex items-center gap-2 mt-3">
<span className="text-xs">
{t("PublicNote.Disabled")}
</span>
<Switch
checked={
publicNoteObj.billingDataMod
?.autoRenewal === "1"
}
onCheckedChange={(checked) =>
patchPublicNote(
"billingDataMod.autoRenewal",
checked ? "1" : undefined,
)
}
/>
<span className="text-xs">
{t("PublicNote.Enabled")}
</span>
</div>
{publicNoteErrors[
"billing.autoRenewal"
] && (
<p className="text-xs text-destructive mt-1">
{
publicNoteErrors[
"billing.autoRenewal"
]
}
</p>
)}
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Label className="text-xs">
{t("PublicNote.Cycle")}
</Label>
<Button
type="button"
variant="outline"
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
onClick={() =>
patchPublicNote(
"billingDataMod.cycle",
undefined,
)
}
>
{t("PublicNote.Clear") ?? "Clear"}
</Button>
</div>
<Select
onValueChange={(val) =>
patchPublicNote(
"billingDataMod.cycle",
val,
)
}
value={
publicNoteObj.billingDataMod?.cycle
}
>
<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={() =>
patchPublicNote(
"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={() =>
patchPublicNote(
"billingDataMod.amount",
"-1",
)
}
>
{t("PublicNote.PayAsYouGo")}
</Button>
</div>
<Input
placeholder="200EUR"
value={
publicNoteObj.billingDataMod?.amount
}
onChange={(e) =>
patchPublicNote(
"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) =>
patchPublicNote(
"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) =>
patchPublicNote(
"planDataMod.trafficVol",
e.target.value,
)
}
/>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Label className="text-xs">
{t("PublicNote.TrafficType")}
</Label>
<Button
type="button"
variant="outline"
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
onClick={() =>
patchPublicNote(
"planDataMod.trafficType",
undefined,
)
}
>
{t("PublicNote.Clear") ?? "Clear"}
</Button>
</div>
<Select
onValueChange={(val) =>
patchPublicNote(
"planDataMod.trafficType",
val,
)
}
value={
publicNoteObj.planDataMod
?.trafficType ?? ""
}
>
<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) =>
patchPublicNote(
"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) =>
patchPublicNote(
"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) =>
patchPublicNote(
"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) =>
patchPublicNote(
"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">
+19 -4
View File
@@ -54,6 +54,7 @@ interface ServiceCardProps {
const serviceFormSchema = z.object({ const serviceFormSchema = z.object({
cover: z.coerce.number().int().min(0), cover: z.coerce.number().int().min(0),
display_index: z.coerce.number().int(),
duration: z.coerce.number().int().min(30), duration: z.coerce.number().int().min(30),
enable_show_in_service: asOptionalField(z.boolean()), enable_show_in_service: asOptionalField(z.boolean()),
enable_trigger_task: asOptionalField(z.boolean()), enable_trigger_task: asOptionalField(z.boolean()),
@@ -67,7 +68,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 +76,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,
@@ -87,6 +88,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
: { : {
type: 1, type: 1,
cover: 0, cover: 0,
display_index: 0,
name: "", name: "",
target: "", target: "",
max_latency: 0.0, max_latency: 0.0,
@@ -107,7 +109,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)
@@ -172,6 +174,19 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="display_index"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Weight")}</FormLabel>
<FormControl>
<Input type="number" placeholder="0" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="target" name="target"
+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} />
} }
+22 -22
View File
@@ -1,7 +1,7 @@
import { buttonVariants } from "@/components/ui/button" import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const AlertDialog = AlertDialogPrimitive.Root const AlertDialog = AlertDialogPrimitive.Root
@@ -9,9 +9,9 @@ const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef< const AlertDialogOverlay = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>, ComponentRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
className={cn( className={cn(
@@ -24,9 +24,9 @@ const AlertDialogOverlay = React.forwardRef<
)) ))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef< const AlertDialogContent = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>, ComponentRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay /> <AlertDialogOverlay />
@@ -42,12 +42,12 @@ const AlertDialogContent = React.forwardRef<
)) ))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const AlertDialogHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} /> <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
) )
AlertDialogHeader.displayName = "AlertDialogHeader" AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const AlertDialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} {...props}
@@ -55,9 +55,9 @@ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDiv
) )
AlertDialogFooter.displayName = "AlertDialogFooter" AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef< const AlertDialogTitle = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>, ComponentRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
ref={ref} ref={ref}
@@ -65,11 +65,11 @@ const AlertDialogTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName AlertDialogTitle.displayName = "AlertDialogTitle"
const AlertDialogDescription = React.forwardRef< const AlertDialogDescription = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>, ComponentRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
ref={ref} ref={ref}
@@ -79,17 +79,17 @@ const AlertDialogDescription = React.forwardRef<
)) ))
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef< const AlertDialogAction = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>, ComponentRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> 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} />
)) ))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef< const AlertDialogCancel = forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>, ComponentRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
ref={ref} ref={ref}
+10 -10
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from "@radix-ui/react-avatar"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Avatar = React.forwardRef< const Avatar = forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, ComponentRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Root <AvatarPrimitive.Root
ref={ref} ref={ref}
@@ -14,9 +14,9 @@ const Avatar = React.forwardRef<
)) ))
Avatar.displayName = AvatarPrimitive.Root.displayName Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef< const AvatarImage = forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, ComponentRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Image <AvatarPrimitive.Image
ref={ref} ref={ref}
@@ -26,9 +26,9 @@ const AvatarImage = React.forwardRef<
)) ))
AvatarImage.displayName = AvatarPrimitive.Image.displayName AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef< const AvatarFallback = forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, ComponentRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
+2 -2
View File
@@ -1,6 +1,6 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react" import { HTMLAttributes } from "react"
const badgeVariants = cva( const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -23,7 +23,7 @@ const badgeVariants = cva(
) )
export interface BadgeProps export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>, extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {} VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) { function Badge({ className, variant, ...props }: BadgeProps) {
+11 -11
View File
@@ -1,17 +1,17 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronRight, MoreHorizontal } from "lucide-react"
import * as React from "react" import { ComponentProps, ComponentPropsWithoutRef, ReactNode, forwardRef } from "react"
const Breadcrumb = React.forwardRef< const Breadcrumb = forwardRef<
HTMLElement, HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & { ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode separator?: ReactNode
} }
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />) >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb" Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>( const BreadcrumbList = forwardRef<HTMLOListElement, ComponentPropsWithoutRef<"ol">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<ol <ol
ref={ref} ref={ref}
@@ -25,16 +25,16 @@ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWi
) )
BreadcrumbList.displayName = "BreadcrumbList" BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>( const BreadcrumbItem = forwardRef<HTMLLIElement, ComponentPropsWithoutRef<"li">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} /> <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
), ),
) )
BreadcrumbItem.displayName = "BreadcrumbItem" BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef< const BreadcrumbLink = forwardRef<
HTMLAnchorElement, HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & { ComponentPropsWithoutRef<"a"> & {
asChild?: boolean asChild?: boolean
} }
>(({ asChild, className, ...props }, ref) => { >(({ asChild, className, ...props }, ref) => {
@@ -50,7 +50,7 @@ const BreadcrumbLink = React.forwardRef<
}) })
BreadcrumbLink.displayName = "BreadcrumbLink" BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>( const BreadcrumbPage = forwardRef<HTMLSpanElement, ComponentPropsWithoutRef<"span">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<span <span
ref={ref} ref={ref}
@@ -64,7 +64,7 @@ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWit
) )
BreadcrumbPage.displayName = "BreadcrumbPage" BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => ( const BreadcrumbSeparator = ({ children, className, ...props }: ComponentProps<"li">) => (
<li <li
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
@@ -76,7 +76,7 @@ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentP
) )
BreadcrumbSeparator.displayName = "BreadcrumbSeparator" BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => ( const BreadcrumbEllipsis = ({ className, ...props }: ComponentProps<"span">) => (
<span <span
role="presentation" role="presentation"
aria-hidden="true" aria-hidden="true"
+3 -3
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react" import { ButtonHTMLAttributes, forwardRef } from "react"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
@@ -31,12 +31,12 @@ const buttonVariants = cva(
) )
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> { VariantProps<typeof buttonVariants> {
asChild?: boolean asChild?: boolean
} }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => { ({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"
return ( return (
+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 { ComponentProps, useEffect, useRef } from "react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: ComponentProps<typeof DayPicker> & {
buttonVariant?: 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
}: ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = useRef<HTMLButtonElement>(null)
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 }
+10 -11
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as React from "react" import { HTMLAttributes, forwardRef } from "react"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
@@ -12,14 +12,14 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
) )
Card.displayName = "Card" Card.displayName = "Card"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} /> <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
), ),
) )
CardHeader.displayName = "CardHeader" CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<h3 <h3
ref={ref} ref={ref}
@@ -30,22 +30,21 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
) )
CardTitle.displayName = "CardTitle" CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef< const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} /> <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
)) ),
)
CardDescription.displayName = "CardDescription" CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
), ),
) )
CardContent.displayName = "CardContent" CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} /> <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
), ),
+4 -4
View File
@@ -1,11 +1,11 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react" import { Check } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Checkbox = React.forwardRef< const Checkbox = forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>, ComponentRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
+5 -5
View File
@@ -12,9 +12,9 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Check, ChevronDown } from "lucide-react" import { Check, ChevronDown } from "lucide-react"
import * as React from "react" import { ButtonHTMLAttributes, forwardRef, useState } from "react"
interface ComboboxProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { interface ComboboxProps extends ButtonHTMLAttributes<HTMLButtonElement> {
options: { options: {
label: string label: string
value: string value: string
@@ -26,10 +26,10 @@ interface ComboboxProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onValueChange: (value: string) => void onValueChange: (value: string) => void
} }
export const Combobox = React.forwardRef<HTMLButtonElement, ComboboxProps>( export const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(
({ options, placeholder, defaultValue, className, onValueChange, ...props }, ref) => { ({ options, placeholder, defaultValue, className, onValueChange, ...props }, ref) => {
const [open, setOpen] = React.useState(false) const [open, setOpen] = useState(false)
const [value, setValue] = React.useState(defaultValue) const [value, setValue] = useState(defaultValue)
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
+23 -23
View File
@@ -3,11 +3,11 @@ import { cn } from "@/lib/utils"
import { type DialogProps } from "@radix-ui/react-dialog" import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react" import { Search } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const Command = React.forwardRef< const Command = forwardRef<
React.ElementRef<typeof CommandPrimitive>, ComponentRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive> ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive <CommandPrimitive
ref={ref} ref={ref}
@@ -32,9 +32,9 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
) )
} }
const CommandInput = React.forwardRef< const CommandInput = forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, ComponentRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> 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="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
@@ -51,9 +51,9 @@ const CommandInput = React.forwardRef<
CommandInput.displayName = CommandPrimitive.Input.displayName CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef< const CommandList = forwardRef<
React.ElementRef<typeof CommandPrimitive.List>, ComponentRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
@@ -64,18 +64,18 @@ const CommandList = React.forwardRef<
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef< const CommandEmpty = forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>, ComponentRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> 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} />
)) ))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef< const CommandGroup = forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>, ComponentRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Group <CommandPrimitive.Group
ref={ref} ref={ref}
@@ -89,9 +89,9 @@ const CommandGroup = React.forwardRef<
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef< const CommandSeparator = forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, ComponentRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Separator <CommandPrimitive.Separator
ref={ref} ref={ref}
@@ -101,9 +101,9 @@ const CommandSeparator = React.forwardRef<
)) ))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef< const CommandItem = forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>, ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
@@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
CommandItem.displayName = CommandPrimitive.Item.displayName CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const CommandShortcut = ({ className, ...props }: HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
+17 -17
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react" import { X } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const Dialog = DialogPrimitive.Root const Dialog = DialogPrimitive.Root
@@ -11,9 +11,9 @@ const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef< const DialogOverlay = forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, ComponentRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
@@ -26,9 +26,9 @@ const DialogOverlay = React.forwardRef<
)) ))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, ComponentRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DialogPortal> <DialogPortal>
<DialogOverlay /> <DialogOverlay />
@@ -50,7 +50,7 @@ const DialogContent = React.forwardRef<
)) ))
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props} {...props}
@@ -58,7 +58,7 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
) )
DialogHeader.displayName = "DialogHeader" DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} {...props}
@@ -66,9 +66,9 @@ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
) )
DialogFooter.displayName = "DialogFooter" DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef< const DialogTitle = forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>, ComponentRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
@@ -76,11 +76,11 @@ const DialogTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
DialogTitle.displayName = DialogPrimitive.Title.displayName DialogTitle.displayName = "DialogTitle"
const DialogDescription = React.forwardRef< const DialogDescription = forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Description <DialogPrimitive.Description
ref={ref} ref={ref}
@@ -88,7 +88,7 @@ const DialogDescription = React.forwardRef<
{...props} {...props}
/> />
)) ))
DialogDescription.displayName = DialogPrimitive.Description.displayName DialogDescription.displayName = "DialogDescription"
export { export {
Dialog, Dialog,
+24 -18
View File
@@ -1,11 +1,17 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as React from "react" import {
ComponentProps,
ComponentPropsWithoutRef,
ComponentRef,
HTMLAttributes,
forwardRef,
} from "react"
import { Drawer as DrawerPrimitive } from "vaul" import { Drawer as DrawerPrimitive } from "vaul"
const Drawer = ({ const Drawer = ({
shouldScaleBackground = true, shouldScaleBackground = true,
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( }: ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} /> <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
) )
Drawer.displayName = "Drawer" Drawer.displayName = "Drawer"
@@ -16,9 +22,9 @@ const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef< const DrawerOverlay = forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>, ComponentRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
ref={ref} ref={ref}
@@ -28,9 +34,9 @@ const DrawerOverlay = React.forwardRef<
)) ))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef< const DrawerContent = forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>, ComponentRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DrawerPortal> <DrawerPortal>
<DrawerOverlay /> <DrawerOverlay />
@@ -49,19 +55,19 @@ const DrawerContent = React.forwardRef<
)) ))
DrawerContent.displayName = "DrawerContent" DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DrawerHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} /> <div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
) )
DrawerHeader.displayName = "DrawerHeader" DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DrawerFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} /> <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
) )
DrawerFooter.displayName = "DrawerFooter" DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef< const DrawerTitle = forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>, ComponentRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Title <DrawerPrimitive.Title
ref={ref} ref={ref}
@@ -69,11 +75,11 @@ const DrawerTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName DrawerTitle.displayName = "DrawerTitle"
const DrawerDescription = React.forwardRef< const DrawerDescription = forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>, ComponentRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Description <DrawerPrimitive.Description
ref={ref} ref={ref}
@@ -81,7 +87,7 @@ const DrawerDescription = React.forwardRef<
{...props} {...props}
/> />
)) ))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName DrawerDescription.displayName = "DrawerDescription"
export { export {
Drawer, Drawer,
+26 -26
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react" import { Check, ChevronRight, Circle } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, forwardRef } from "react"
const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenu = DropdownMenuPrimitive.Root
@@ -15,9 +15,9 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef< const DropdownMenuSubTrigger = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, children, ...props }, ref) => ( >(({ className, inset, children, ...props }, ref) => (
@@ -36,9 +36,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
)) ))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef< const DropdownMenuSubContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
@@ -51,9 +51,9 @@ const DropdownMenuSubContent = React.forwardRef<
)) ))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef< const DropdownMenuContent = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>, ComponentRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
@@ -69,9 +69,9 @@ const DropdownMenuContent = React.forwardRef<
)) ))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef< const DropdownMenuItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>, ComponentRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
@@ -87,9 +87,9 @@ const DropdownMenuItem = React.forwardRef<
)) ))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef< const DropdownMenuCheckboxItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => ( >(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
@@ -110,9 +110,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
)) ))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef< const DropdownMenuRadioItem = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
@@ -132,9 +132,9 @@ const DropdownMenuRadioItem = React.forwardRef<
)) ))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef< const DropdownMenuLabel = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>, ComponentRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean inset?: boolean
} }
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
@@ -146,9 +146,9 @@ const DropdownMenuLabel = React.forwardRef<
)) ))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef< const DropdownMenuSeparator = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>, ComponentRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
ref={ref} ref={ref}
@@ -158,7 +158,7 @@ const DropdownMenuSeparator = React.forwardRef<
)) ))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => { const DropdownMenuShortcut = ({ className, ...props }: HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} /> <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
) )
+30 -25
View File
@@ -2,7 +2,15 @@ import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import * as React from "react" import {
ComponentPropsWithoutRef,
ComponentRef,
HTMLAttributes,
createContext,
forwardRef,
useContext,
useId,
} from "react"
import { import {
Controller, Controller,
ControllerProps, ControllerProps,
@@ -21,7 +29,7 @@ type FormFieldContextValue<
name: TName name: TName
} }
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue) const FormFieldContext = createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@@ -37,8 +45,8 @@ const FormField = <
} }
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext) const itemContext = useContext(FormItemContext)
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState)
@@ -63,11 +71,11 @@ type FormItemContextValue = {
id: string id: string
} }
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue) const FormItemContext = createContext<FormItemContextValue>({} as FormItemContextValue)
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const FormItem = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {
const id = React.useId() const id = useId()
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
@@ -78,9 +86,9 @@ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivEl
) )
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef< const FormLabel = forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, ComponentRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField()
@@ -95,10 +103,8 @@ const FormLabel = React.forwardRef<
}) })
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef< const FormControl = forwardRef<ComponentRef<typeof Slot>, ComponentPropsWithoutRef<typeof Slot>>(
React.ElementRef<typeof Slot>, ({ ...props }, ref) => {
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return ( return (
@@ -112,13 +118,12 @@ const FormControl = React.forwardRef<
{...props} {...props}
/> />
) )
}) },
)
FormControl.displayName = "FormControl" FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef< const FormDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, ...props }, ref) => {
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField()
return ( return (
@@ -129,13 +134,12 @@ const FormDescription = React.forwardRef<
{...props} {...props}
/> />
) )
}) },
)
FormDescription.displayName = "FormDescription" FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef< const FormMessage = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
HTMLParagraphElement, ({ className, children, ...props }, ref) => {
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children const body = error ? String(error?.message) : children
@@ -153,7 +157,8 @@ const FormMessage = React.forwardRef<
{body} {body}
</p> </p>
) )
}) },
)
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage"
export { export {
+4 -6
View File
@@ -1,10 +1,9 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as React from "react" import { InputHTMLAttributes, forwardRef } from "react"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
({ className, type, ...props }, ref) => {
return ( return (
<input <input
type={type} type={type}
@@ -16,8 +15,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
{...props} {...props}
/> />
) )
}, })
)
Input.displayName = "Input" Input.displayName = "Input"
export { Input } export { Input }
+4 -4
View File
@@ -1,15 +1,15 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const labelVariants = cva( const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
) )
const Label = React.forwardRef< const Label = forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, ComponentRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants> 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} />
)) ))
+19 -19
View File
@@ -2,11 +2,11 @@ import { cn } from "@/lib/utils"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority" import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react" import { ChevronDown } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const NavigationMenu = React.forwardRef< const NavigationMenu = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>, ComponentRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root <NavigationMenuPrimitive.Root
ref={ref} ref={ref}
@@ -19,9 +19,9 @@ const NavigationMenu = React.forwardRef<
)) ))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef< const NavigationMenuList = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>, ComponentRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List <NavigationMenuPrimitive.List
ref={ref} ref={ref}
@@ -40,9 +40,9 @@ const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors hover:text-accent-foreground focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50", "group inline-flex h-10 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors hover:text-accent-foreground focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
) )
const NavigationMenuTrigger = React.forwardRef< const NavigationMenuTrigger = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>, ComponentRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger <NavigationMenuPrimitive.Trigger
ref={ref} ref={ref}
@@ -58,9 +58,9 @@ const NavigationMenuTrigger = React.forwardRef<
)) ))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef< const NavigationMenuContent = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>, ComponentRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
ref={ref} ref={ref}
@@ -75,9 +75,9 @@ NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef< const NavigationMenuViewport = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>, ComponentRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport> 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")}>
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
@@ -92,9 +92,9 @@ const NavigationMenuViewport = React.forwardRef<
)) ))
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef< const NavigationMenuIndicator = forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>, ComponentRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator> ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator <NavigationMenuPrimitive.Indicator
ref={ref} ref={ref}
+8 -11
View File
@@ -1,9 +1,9 @@
import { ButtonProps, buttonVariants } from "@/components/ui/button" import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import * as React from "react" import { ComponentProps, forwardRef } from "react"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( const Pagination = ({ className, ...props }: ComponentProps<"nav">) => (
<nav <nav
role="navigation" role="navigation"
aria-label="pagination" aria-label="pagination"
@@ -13,14 +13,14 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
) )
Pagination.displayName = "Pagination" Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>( const PaginationContent = forwardRef<HTMLUListElement, ComponentProps<"ul">>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} /> <ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
), ),
) )
PaginationContent.displayName = "PaginationContent" PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>( const PaginationItem = forwardRef<HTMLLIElement, ComponentProps<"li">>(
({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />, ({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
) )
PaginationItem.displayName = "PaginationItem" PaginationItem.displayName = "PaginationItem"
@@ -28,7 +28,7 @@ PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean
} & Pick<ButtonProps, "size"> & } & Pick<ButtonProps, "size"> &
React.ComponentProps<"a"> ComponentProps<"a">
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => ( const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a <a
@@ -45,10 +45,7 @@ const PaginationLink = ({ className, isActive, size = "icon", ...props }: Pagina
) )
PaginationLink.displayName = "PaginationLink" PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({ const PaginationPrevious = ({ className, ...props }: ComponentProps<typeof PaginationLink>) => (
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label="Go to previous page"
size="default" size="default"
@@ -61,7 +58,7 @@ const PaginationPrevious = ({
) )
PaginationPrevious.displayName = "PaginationPrevious" PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => ( const PaginationNext = ({ className, ...props }: ComponentProps<typeof PaginationLink>) => (
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label="Go to next page"
size="default" size="default"
@@ -74,7 +71,7 @@ const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof Pag
) )
PaginationNext.displayName = "PaginationNext" PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => ( const PaginationEllipsis = ({ className, ...props }: ComponentProps<"span">) => (
<span <span
aria-hidden aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)} className={cn("flex h-9 w-9 items-center justify-center", className)}
+4 -4
View File
@@ -1,14 +1,14 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from "@radix-ui/react-popover"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Popover = PopoverPrimitive.Root const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef< const PopoverContent = forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, ComponentRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content <PopoverPrimitive.Content
+7 -7
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const ScrollArea = React.forwardRef< const ScrollArea = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>, ComponentRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root <ScrollAreaPrimitive.Root
ref={ref} ref={ref}
@@ -20,9 +20,9 @@ const ScrollArea = React.forwardRef<
)) ))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef< const ScrollBar = forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => ( >(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar <ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref} ref={ref}
+22 -22
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react" import { Check, ChevronDown, ChevronUp } from "lucide-react"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Select = SelectPrimitive.Root const Select = SelectPrimitive.Root
@@ -9,9 +9,9 @@ const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef< const SelectTrigger = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, ComponentRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
@@ -29,9 +29,9 @@ const SelectTrigger = React.forwardRef<
)) ))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef< const SelectScrollUpButton = forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
ref={ref} ref={ref}
@@ -43,9 +43,9 @@ const SelectScrollUpButton = React.forwardRef<
)) ))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef< const SelectScrollDownButton = forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
ref={ref} ref={ref}
@@ -57,9 +57,9 @@ const SelectScrollDownButton = React.forwardRef<
)) ))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef< const SelectContent = forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>, ComponentRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => ( >(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
@@ -89,9 +89,9 @@ const SelectContent = React.forwardRef<
)) ))
SelectContent.displayName = SelectPrimitive.Content.displayName SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef< const SelectLabel = forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>, ComponentRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Label <SelectPrimitive.Label
ref={ref} ref={ref}
@@ -101,9 +101,9 @@ const SelectLabel = React.forwardRef<
)) ))
SelectLabel.displayName = SelectPrimitive.Label.displayName SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef< const SelectItem = forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>, ComponentRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
@@ -124,9 +124,9 @@ const SelectItem = React.forwardRef<
)) ))
SelectItem.displayName = SelectPrimitive.Item.displayName SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef< const SelectSeparator = forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>, ComponentRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SelectPrimitive.Separator <SelectPrimitive.Separator
ref={ref} ref={ref}
+4 -4
View File
@@ -1,10 +1,10 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Separator = React.forwardRef< const Separator = forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, ComponentRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} ref={ref}
+7 -8
View File
@@ -1,23 +1,22 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Switch = React.forwardRef< const Switch = forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>, ComponentRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( 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", "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 className,
)} )}
{...props} {...props}
ref={ref} ref={ref}
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( 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" "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> </SwitchPrimitives.Root>
+32 -34
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as React from "react" import { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes, forwardRef } from "react"
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>( const Table = forwardRef<HTMLTableElement, HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto"> <div className="relative w-full overflow-auto">
<table <table
@@ -14,35 +14,32 @@ const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableE
) )
Table.displayName = "Table" Table.displayName = "Table"
const TableHeader = React.forwardRef< const TableHeader = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
)) ),
)
TableHeader.displayName = "TableHeader" TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef< const TableBody = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} /> <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
)) ),
)
TableBody.displayName = "TableBody" TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef< const TableFooter = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(
HTMLTableSectionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot <tfoot
ref={ref} ref={ref}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)} className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
{...props} {...props}
/> />
)) ),
)
TableFooter.displayName = "TableFooter" TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>( const TableRow = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<tr <tr
ref={ref} ref={ref}
@@ -56,10 +53,8 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
) )
TableRow.displayName = "TableRow" TableRow.displayName = "TableRow"
const TableHead = React.forwardRef< const TableHead = forwardRef<HTMLTableCellElement, ThHTMLAttributes<HTMLTableCellElement>>(
HTMLTableCellElement, ({ className, ...props }, ref) => (
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th <th
ref={ref} ref={ref}
className={cn( className={cn(
@@ -68,27 +63,30 @@ const TableHead = React.forwardRef<
)} )}
{...props} {...props}
/> />
)) ),
)
TableHead.displayName = "TableHead" TableHead.displayName = "TableHead"
const TableCell = React.forwardRef< const TableCell = forwardRef<HTMLTableCellElement, TdHTMLAttributes<HTMLTableCellElement>>(
HTMLTableCellElement, ({ className, ...props }, ref) => (
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td <td
ref={ref} ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props} {...props}
/> />
)) ),
)
TableCell.displayName = "TableCell" TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef< const TableCaption = forwardRef<HTMLTableCaptionElement, HTMLAttributes<HTMLTableCaptionElement>>(
HTMLTableCaptionElement, ({ className, ...props }, ref) => (
React.HTMLAttributes<HTMLTableCaptionElement> <caption
>(({ className, ...props }, ref) => ( ref={ref}
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} /> className={cn("mt-4 text-sm text-muted-foreground", className)}
)) {...props}
/>
),
)
TableCaption.displayName = "TableCaption" TableCaption.displayName = "TableCaption"
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
+10 -10
View File
@@ -1,12 +1,12 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs"
import * as React from "react" import { ComponentPropsWithoutRef, ComponentRef, forwardRef } from "react"
const Tabs = TabsPrimitive.Root const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef< const TabsList = forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, ComponentRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
@@ -19,9 +19,9 @@ const TabsList = React.forwardRef<
)) ))
TabsList.displayName = TabsPrimitive.List.displayName TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef< const TabsTrigger = forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, ComponentRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
@@ -34,9 +34,9 @@ const TabsTrigger = React.forwardRef<
)) ))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef< const TabsContent = forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>, ComponentRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Content <TabsPrimitive.Content
ref={ref} ref={ref}
+2 -2
View File
@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import * as React from "react" import { ComponentProps, forwardRef } from "react"
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>( const Textarea = forwardRef<HTMLTextAreaElement, ComponentProps<"textarea">>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {
return ( return (
<textarea <textarea
+6 -6
View File
@@ -15,13 +15,13 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { formatPath } from "@/lib/utils" import { formatPath } from "@/lib/utils"
import * as React from "react" import { Dispatch, FC, Fragment, SetStateAction, useState } from "react"
const ITEMS_TO_DISPLAY = 3 const ITEMS_TO_DISPLAY = 3
interface FilepathProps { interface FilepathProps {
path: string path: string
setPath: React.Dispatch<React.SetStateAction<string>> setPath: Dispatch<SetStateAction<string>>
} }
function pathToItems(path: string) { function pathToItems(path: string) {
@@ -38,8 +38,8 @@ function pathToItems(path: string) {
return result return result
} }
export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => { export const Filepath: FC<FilepathProps> = ({ path, setPath }) => {
const [open, setOpen] = React.useState(false) const [open, setOpen] = useState(false)
const items = pathToItems(formatPath(path)) const items = pathToItems(formatPath(path))
return ( return (
@@ -87,7 +87,7 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
</> </>
) : null} ) : null}
{items.slice(-ITEMS_TO_DISPLAY).map((item, index, slicedItems) => ( {items.slice(-ITEMS_TO_DISPLAY).map((item, index, slicedItems) => (
<React.Fragment key={index}> <Fragment key={index}>
<BreadcrumbItem className="overflow-auto"> <BreadcrumbItem className="overflow-auto">
{item.href ? ( {item.href ? (
<> <>
@@ -107,7 +107,7 @@ export const Filepath: React.FC<FilepathProps> = ({ path, setPath }) => {
)} )}
</BreadcrumbItem> </BreadcrumbItem>
{index !== slicedItems.length - 1 ? <BreadcrumbSeparator /> : null} {index !== slicedItems.length - 1 ? <BreadcrumbSeparator /> : null}
</React.Fragment> </Fragment>
))} ))}
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
+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>
+6 -6
View File
@@ -38,7 +38,7 @@ import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import { CheckIcon, ChevronDown, WandSparkles, XIcon } from "lucide-react" import { CheckIcon, ChevronDown, WandSparkles, XIcon } from "lucide-react"
import * as React from "react" import { KeyboardEvent, forwardRef, useState } from "react"
/** /**
* Variants for the multi-select component to handle different styles. * Variants for the multi-select component to handle different styles.
@@ -129,7 +129,7 @@ interface MultiSelectProps
className?: string className?: string
} }
export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>( export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
( (
{ {
options, options,
@@ -146,11 +146,11 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
}, },
ref, ref,
) => { ) => {
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue) const [selectedValues, setSelectedValues] = useState<string[]>(defaultValue)
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false) const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const [isAnimating, setIsAnimating] = React.useState(false) const [isAnimating, setIsAnimating] = useState(false)
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") { if (event.key === "Enter") {
setIsPopoverOpen(true) setIsPopoverOpen(true)
} else if (event.key === "Backspace" && !event.currentTarget.value) { } else if (event.key === "Backspace" && !event.currentTarget.value) {
+24 -18
View File
@@ -2,7 +2,14 @@ import { cn } from "@/lib/utils"
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog"
import { type VariantProps, cva } from "class-variance-authority" import { type VariantProps, cva } from "class-variance-authority"
import { X } from "lucide-react" import { X } from "lucide-react"
import * as React from "react" import {
ComponentPropsWithoutRef,
ComponentRef,
Dispatch,
HTMLAttributes,
SetStateAction,
forwardRef,
} from "react"
const Sheet = SheetPrimitive.Root const Sheet = SheetPrimitive.Root
@@ -30,15 +37,13 @@ const sheetVariants = cva(
) )
interface SheetContentProps interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, extends ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { VariantProps<typeof sheetVariants> {
setOpen: React.Dispatch<React.SetStateAction<boolean>> setOpen: Dispatch<SetStateAction<boolean>>
} }
const SheetContent = React.forwardRef< const SheetContent = forwardRef<ComponentRef<typeof SheetPrimitive.Content>, SheetContentProps>(
React.ElementRef<typeof SheetPrimitive.Content>, ({ side = "right", className, children, setOpen, ...props }, ref) => (
SheetContentProps
>(({ side = "right", className, children, setOpen, ...props }, ref) => (
<SheetPortal> <SheetPortal>
<SheetPrimitive.Content <SheetPrimitive.Content
ref={ref} ref={ref}
@@ -57,15 +62,16 @@ const SheetContent = React.forwardRef<
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
)) ),
)
SheetContent.displayName = SheetPrimitive.Content.displayName SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const SheetHeader = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} /> <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
) )
SheetHeader.displayName = "SheetHeader" SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const SheetFooter = ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} {...props}
@@ -73,9 +79,9 @@ const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElemen
) )
SheetFooter.displayName = "SheetFooter" SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef< const SheetTitle = forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>, ComponentRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Title <SheetPrimitive.Title
ref={ref} ref={ref}
@@ -83,11 +89,11 @@ const SheetTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
SheetTitle.displayName = SheetPrimitive.Title.displayName SheetTitle.displayName = "SheetTitle"
const SheetDescription = React.forwardRef< const SheetDescription = forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>, ComponentRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Description <SheetPrimitive.Description
ref={ref} ref={ref}
@@ -95,7 +101,7 @@ const SheetDescription = React.forwardRef<
{...props} {...props}
/> />
)) ))
SheetDescription.displayName = SheetPrimitive.Description.displayName SheetDescription.displayName = "SheetDescription"
export { export {
Sheet, Sheet,
+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"))
}
} }
} }
+3 -3
View File
@@ -1,9 +1,9 @@
import * as React from "react" import { useEffect, useState } from "react"
export function useMediaQuery(query: string) { export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false) const [value, setValue] = useState(false)
React.useEffect(() => { useEffect(() => {
function onChange(event: MediaQueryListEvent) { function onChange(event: MediaQueryListEvent) {
setValue(event.matches) setValue(event.matches)
} }
+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": {
+182
View File
@@ -0,0 +1,182 @@
import { z } from "zod"
import i18n from "./i18n"
/**
* Zod schema for PublicNote
* Conventions:
* - All fields are strings and may be empty
* - IPv4/IPv6/autoRenewal must be "0" or "1"
* - cycle is one of Day/Week/Month/Year
* - Date fields can be empty, ISO-like, or the special value "0000-00-00T23:59:59+08:00"
*/
export const PublicNoteSchema = z.object({
billingDataMod: z
.object({
startDate: z.string().optional(),
endDate: z.string().optional(),
autoRenewal: z.string().optional(),
cycle: z.string().optional(),
amount: z.string().optional(),
})
.optional(),
planDataMod: z
.object({
bandwidth: z.string().optional(),
trafficVol: z.string().optional(),
trafficType: z.string().optional(),
IPv4: z.string().optional(),
IPv6: z.string().optional(),
networkRoute: z.string().optional(),
extra: z.string().optional(),
})
.optional(),
})
export type PublicNote = z.infer<typeof PublicNoteSchema>
export const defaultPublicNote: PublicNote = {}
export const isValidISOLike = (v: string) => {
if (!v) return true
if (v === "0000-00-00T23:59:59+08:00") return true
const d = new Date(v)
return !isNaN(d.getTime())
}
export const normalizeISO = (v?: string) => {
if (!v) return undefined
if (v === "0000-00-00T23:59:59+08:00") return v
const date = new Date(v)
return isNaN(date.getTime()) ? v : date.toISOString()
}
/**
* Parse a string into PublicNote; return the default object if not valid JSON or validation fails.
*/
export const parsePublicNote = (s?: string): PublicNote => {
if (!s) return defaultPublicNote
try {
const obj = JSON.parse(s)
const parsed = PublicNoteSchema.safeParse(obj)
if (parsed.success) {
return parsed.data
}
return defaultPublicNote
} catch {
return defaultPublicNote
}
}
export const validatePublicNote = (pn: PublicNote) => {
const errors: Partial<Record<string, string>> = {}
// Structural and enum validations
if (pn.billingDataMod?.autoRenewal && !/^(0|1)$/.test(pn.billingDataMod.autoRenewal)) {
errors["billing.autoRenewal"] = i18n.t("Validation.MustBe0Or1")
}
if (pn.billingDataMod?.cycle && !/^(Day|Week|Month|Year)$/i.test(pn.billingDataMod.cycle)) {
errors["billing.cycle"] = i18n.t("Validation.MustBeDayWeekMonthYear")
}
if (pn.planDataMod?.trafficType && !/^(1|2)$/.test(pn.planDataMod.trafficType)) {
errors["plan.trafficType"] = i18n.t("Validation.MustBe1Or2")
}
if (pn.planDataMod?.IPv4 !== undefined && !/^(0|1)$/.test(pn.planDataMod.IPv4)) {
errors["plan.IPv4"] = i18n.t("Validation.MustBe0Or1")
}
if (pn.planDataMod?.IPv6 !== undefined && !/^(0|1)$/.test(pn.planDataMod.IPv6)) {
errors["plan.IPv6"] = i18n.t("Validation.MustBe0Or1")
}
// Date validity checks
if (pn.billingDataMod?.startDate && !isValidISOLike(pn.billingDataMod.startDate)) {
errors["billing.startDate"] = i18n.t("Validation.InvalidDate")
}
if (pn.billingDataMod?.endDate && !isValidISOLike(pn.billingDataMod.endDate)) {
errors["billing.endDate"] = i18n.t("Validation.InvalidDate")
}
return { errors, valid: Object.keys(errors).length === 0 }
}
/**
* Detect default mode from string: JSON matching schema -> "structured"; otherwise "raw".
*/
export const detectPublicNoteMode = (s?: string): "structured" | "raw" => {
if (!s) return "raw"
try {
const obj = JSON.parse(s)
const parsed = PublicNoteSchema.strict().safeParse(obj)
return parsed.success ? "structured" : "raw"
} catch {
return "raw"
}
}
/**
* Immutable patch by path, for use in component wrappers around setPublicNoteObj.
* Example path: "billingDataMod.startDate"
*/
export const applyPublicNotePatch = (
obj: PublicNote,
path: string,
value: string | undefined,
): PublicNote => {
const keys = path.split(".")
const draft: any = structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj))
let cur: any = draft
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
cur[k] = { ...(cur[k] ?? {}) }
cur = cur[k]
}
cur[keys[keys.length - 1]] = value
return draft
}
/**
* Update a date field while preserving time parts: if the previous value is a valid date,
* keep hours/minutes/seconds. Path example: "billingDataMod.startDate" | "billingDataMod.endDate"
*/
export const applyPublicNoteDate = (obj: PublicNote, path: string, date: Date): PublicNote => {
const keys = path.split(".")
const draft: any = structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj))
// Read previous value to preserve time components
let curRead: any = draft
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
curRead = (curRead as any)[k]
if (!curRead) break
}
const leafKey = keys[keys.length - 1]
const prevVal: string | undefined = curRead ? curRead[leafKey] : undefined
const d = new Date(date)
if (prevVal) {
const pd = new Date(prevVal)
if (!isNaN(pd.getTime())) {
d.setHours(pd.getHours(), pd.getMinutes(), pd.getSeconds(), 0)
}
}
// Write back
let curWrite: any = draft
for (let i = 0; i < keys.length - 1; i++) {
const k = keys[i]
curWrite[k] = { ...(curWrite[k] ?? {}) }
curWrite = curWrite[k]
}
curWrite[leafKey] = d.toISOString()
return draft
}
/**
* Toggle the special "no expiry" value for endDate.
*/
export const toggleEndNoExpiry = (obj: PublicNote): PublicNote => {
const NO_EXPIRY = "0000-00-00T23:59:59+08:00"
const current = obj.billingDataMod?.endDate
const next = current === NO_EXPIRY ? "" : NO_EXPIRY
return applyPublicNotePatch(obj, "billingDataMod.endDate", next)
}
+71 -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,67 @@
"EditServerConfig": "Edit Server Config", "EditServerConfig": "Edit Server Config",
"Option": "Option", "Option": "Option",
"Value": "Value", "Value": "Value",
"Preview": "Preview" "Preview": "Preview",
"PublicNote": {
"Label": "Public Note",
"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",
"CommaSeparated": "Separate multiple items with commas",
"Has": "Has",
"None": "None",
"CustomFields": "Custom Fields",
"ClearDate": "Clear Date",
"Clear": "Clear",
"RawText": "Raw Text"
},
"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",
"InvalidJSON": "Invalid JSON"
},
"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...",
"Format": "Format",
"Formatted": "Formatted",
"Copy": "Copy",
"Paste": "Paste",
"CopiedToClipboard": "Copied to clipboard",
"ClipboardWriteFailed": "Clipboard write failed",
"PastedFromClipboard": "Pasted from clipboard",
"ClipboardReadFailed": "Clipboard read failed",
"FormatMetricUnits": "Format Metric Units"
} }
+70 -1
View File
@@ -185,5 +185,74 @@
"OverrideDDNSDomains": "Sobrescribir dominios DDNS (por configuración)", "OverrideDDNSDomains": "Sobrescribir dominios DDNS (por configuración)",
"EmptyText": "El texto está vacío", "EmptyText": "El texto está vacío",
"EmptyNote": "No tenías ninguna nota.", "EmptyNote": "No tenías ninguna nota.",
"EditServerConfig": "Editar configuración del servidor" "EditServerConfig": "Editar configuración del servidor",
"InvalidUsernameOrPassword": "Usuario o Contraseña inválidos",
"NetworkError": "Error de Red",
"Terminal": "Terminal",
"Config": "Configuración",
"Add": "Añadir",
"Delete": "Eliminar",
"AdvancedJSON": "JSON Avanzado",
"Save": "Guardar",
"PublicNote": {
"Label": "Nota pública",
"Billing": "Facturación",
"Plan": "Plan",
"StartDate": "Fecha de inicio",
"EndDate": "Fecha de finalización",
"AutoRenewal": "Renovación automática",
"Cycle": "Ciclo",
"Amount": "Cantidad",
"Bandwidth": "Ancho de banda",
"TrafficVolume": "Volumen de tráfico",
"TrafficType": "Tipo de tráfico",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "Ruta de red",
"Extra": "Extra",
"Enabled": "Activado",
"Disabled": "Desactivado",
"Inbound": "Entrante",
"Both": "Ambos",
"Day": "Dia",
"Week": "Semana",
"Month": "Mes",
"Year": "Año",
"NoExpiry": "No expira",
"SetNoExpiry": "Fijar que no expire",
"CancelNoExpiry": "Cancelar que no expire",
"Free": "Gratis",
"PayAsYouGo": "Paga sobre la marcha",
"CommaSeparated": "Separa los múltiples items con comas",
"Has": "Tiene",
"None": "Ninguno",
"CustomFields": "Campos personalizados",
"ClearDate": "Borrar fecha",
"Clear": "Borrar",
"RawText": "Texto sin formato"
},
"Validation": {
"InvalidDate": "Fecha no válida",
"MustBe0Or1": "Tiene que ser 0 o 1",
"MustBeDayWeekMonthYear": "Tiene que ser Día/Semana/Mes/Año",
"MustBe1Or2": "Tiene que ser 1 o 2",
"DigitsOnly": "Solo dígitos",
"InvalidForm": "Forma no válida",
"InvalidJSON": "JSON no válido"
},
"AlertRules": {
"CoverAllServers": "Monitorizar todos los servidores",
"IgnoreAllSelectSpecific": "Ignorar todos, seleccionar servidores específicos",
"IgnoreHint": "{{server}} ID: verdadero/falso",
"IgnoreExample": "p.e., {\"1\": verdadero, \"2\": falso}"
},
"Search": "Buscar...",
"Format": "Formato",
"Formatted": "Formateado",
"Copy": "Copiar",
"Paste": "Pegar",
"CopiedToClipboard": "Copiar al portapapeles",
"ClipboardWriteFailed": "Error al escribir en el portapapeles",
"PastedFromClipboard": "Pegado del portapapeles",
"ClipboardReadFailed": "Falló la lectura del portapapeles"
} }
+25
View File
@@ -0,0 +1,25 @@
{
"nezha": "Monitoreo Nezha",
"theme": {
"light": "Claro",
"dark": "Escuro",
"system": "Seguir o sistema"
},
"Username": "Nome de usuario",
"Password": "Contrasinal",
"InvalidUsernameOrPassword": "Nome de usuario ou contrasinal non válidos",
"NetworkError": "Error de rede",
"LoginFirst": "Por favor, inicie sesión primeiro",
"CurrentTime": "Hora actual",
"Results": {
"UsernameMin": "O nome de usuario debe ter polo menos {{number}} caracteres.",
"PasswordRequired": "O contrasinal non pode estar baleiro.",
"ErrorFetchingResource": "Erro ao obter o recurso: {{error}}",
"SelectAtLeastOneServer": "Por favor, selecciona polo menos un servidor.",
"UnExpectedError": "Erro inesperado. Consulta a consola para obter máis detalles.",
"ForceUpdate": "Actualización forzada:",
"NoRowsAreSelected": "Non hai filas seleccionadas",
"ThisOperationIsUnrecoverable": "A operación non se pode desfacer!",
"TaskTriggeredSuccessfully": "A tarefa desencadeouse correctamente"
}
}
+248 -13
View File
@@ -1,24 +1,259 @@
{ {
"nezha": "Monitor Nezha", "nezha": "Pemantauan Nezha",
"theme": { "theme": {
"light": "Terang", "light": "Terang",
"dark": "Gelap", "dark": "Gelap",
"system": "Mengikuti sistem" "system": "Ikuti Sistem"
}, },
"Username": "Nama Pengguna", "Username": "Nama Pengguna",
"Password": "Kata Sandi", "Password": "Kata Sandi",
"LoginFirst": "Mohon masuk terlebih dahulu", "LoginFirst": "Harap Masuk terlebih dahulu",
"CurrentTime": "Waktu saat ini", "CurrentTime": "Waktu Saat Ini",
"Results": { "Results": {
"UsernameMin": "Nama Pengguna setidak nya harus {{number}} karakter.", "UsernameMin": "Nama pengguna harus minimal {{number}} karakter.",
"PasswordRequired": "Kata Sandi tidak boleh kosong.", "PasswordRequired": "Kata sandi tidak boleh kosong.",
"ErrorFetchingResource": "Kesalahan mengambil sumber daya: {{error}}", "ErrorFetchingResource": "Galat Mengambil Sumber Daya : {{error}}",
"SelectAtLeastOneServer": "Silahkan pilih setidaknya satu server.", "SelectAtLeastOneServer": "Harap pilih minimal satu server.",
"UnExpectedError": "Kesalahan tidak terduga, Silahhkan lihat konsol untuk detailnya.", "UnExpectedError": "Galat tak terduga, Harap lihat konsol untuk detail.",
"ForceUpdate": "Peningkatan Paksa:", "ForceUpdate": "Peningkatan paksa:",
"NoRowsAreSelected": "Tidak ada baris yang dipilih", "NoRowsAreSelected": "Tidak ada baris yang dipilih",
"ThisOperationIsUnrecoverable": "Operasi tidak dapat dibatalkan!", "ThisOperationIsUnrecoverable": "Operasi ini tidak dapat dibatalkan!",
"TaskTriggeredSuccessfully": "Tugas berhasil dipicu", "TaskTriggeredSuccessfully": "Tugas berhasil dipicu",
"TheServerDoesNotOnline": "Server tidak ada atau masih belum terhubung" "TheServerDoesNotOnline": "Server tidak ada atau belum terhubung",
} "InstallHostRequired": "Alamat docking Agen belum diisi dalam pengaturan.",
"UnknownIdentifier": "Pengidentifikasi tidak diketahui"
},
"InvalidUsernameOrPassword": "Nama Pengguna atau Kata Sandi Tidak Valid",
"NetworkError": "Galat Jaringan",
"Login": "Masuk",
"Server": "Server",
"Service": "Layanan",
"Task": "Tugas",
"Notification": "Notifikasi",
"DDNS": "DNS Dinamis",
"NATT": "Penembusan NAT",
"Group": "Grup",
"Profile": "Profil",
"Settings": "Pengaturan sistem",
"BackToHome": "Kembali ke Beranda",
"Logout": "Keluar",
"NavigateTo": "Navigasi ke",
"SelectAPageToNavigateTo": "Pilih halaman untuk dilompati",
"Close": "Tutup",
"Error": "Galat",
"Name": "Nama",
"Version": "Versi",
"Unknown": "tidak diketahui",
"Enable": "Aktifkan",
"HideForGuest": "Tersembunyi dari Pengunjung",
"InstallCommands": "Perintah instalasi",
"Terminal": "Terminal",
"Config": "Konfigurasi",
"Note": "Catatan",
"Success": "Berhasil",
"Done": "Selesai",
"Offline": "Luring",
"Failure": "Gagal",
"Loading": "Memuat",
"NoResults": "Tidak ada hasil",
"Actions": "Tindakan",
"EditServer": "Edit Server",
"Weight": "Bobot (semakin besar angkanya, semakin tinggi ditampilkan)",
"DDNSProfiles": "ID Profil DDNS",
"SeparateWithComma": "(Pisahkan dengan koma)",
"Public": "Publik",
"Private": "Pribadi",
"Submit": "Kirim",
"Target": "Target",
"Coverage": "Cakupan",
"CoverAll": "Tutup Semua",
"IgnoreAll": "Abaikan Semua",
"OnAlert": "Server yang Terpicu Alarm",
"SpecificServers": "Server tertentu",
"Type": "Jenis",
"Interval": "Interval",
"NotifierGroupID": "ID Grup Notifikasi",
"Trigger": "Saat Dipicu",
"TasksToTriggerOnAlert": "Tugas yang dipicu saat peringatan",
"TasksToTriggerAfterRecovery": "Tugas yang dipicu setelah pemulihan",
"Add": "Tambah",
"Delete": "Hapus",
"AdvancedJSON": "JSON Lanjutan",
"Save": "Simpan",
"Confirm": "Konfirmasi",
"ConfirmDeletion": "Konfirmasi Penghapusan?",
"Services": "Layanan",
"ShowInService": "Tampilkan di Layanan",
"Coverages": {
"Excludes": "Kecualikan Server Tertentu",
"Only": "Hanya Server Tertentu",
"Alarmed": "Dijalankan di server yang memicu alarm"
},
"EnableFailureNotification": "Aktifkan Notifikasi Kegagalan",
"MaximumLatency": "Tunda Maksimum (ms)",
"MinimumLatency": "Tunda Minimum (milidetik)",
"EnableLatencyNotification": "Aktifkan Notifikasi Latensi",
"EnableTriggerTask": "Aktifkan Tugas Pemicu",
"CronExpression": "Ekspresi Cron",
"Command": "Perintah",
"NotifierGroup": "Grup Notifikasi",
"SendSuccessNotification": "Kirim Notifikasi Berhasil",
"LastExecution": "Eksekusi Terakhir",
"Result": "Hasil",
"Scheduled": "Tugas Terjadwal",
"Notifier": "Pemberi Notifikasi",
"AlertRule": "Aturan Peringatan",
"VerifyTLS": "Verifikasi TLS",
"TriggerMode": "Mode Pemicu",
"Rules": "Aturan",
"RequestMethod": "Metode Permintaan",
"RequestHeader": "Header Permintaan",
"DoNotSendTestMessage": "Jangan Kirim Pesan Uji",
"Always": "Selalu",
"Once": "Sekali",
"Provider": "Penyedia",
"Domains": "Domain",
"MaximumRetryAttempts": "Waktu maksimum untuk upaya ulang",
"Refresh": "Segarkan",
"CopyPath": "Salin jalur",
"Goto": "Pergi ke",
"UpdateProfile": "Perbarui Profil",
"NewUsername": "Nama Pengguna Baru",
"OriginalPassword": "Kata Sandi Asli",
"NewPassword": "Kata Sandi Baru",
"EditDDNS": "Edit DDNS",
"CreateDDNS": "Buat DDNS",
"Credential": "Kredensial",
"RequestType": "Jenis Permintaan",
"RequestBody": "Isi Permintaan",
"FileManager": "Manajer Berkas Pseudo",
"Downloading": "Mengunduh",
"Uploading": "Mengunggah",
"EditNAT": "Edit Konfigurasi NAT",
"CreateNAT": "Buat Konfigurasi NAT",
"LocalService": "Layanan Lokal",
"BindHostname": "Ikat Nama Domain",
"EditServerGroup": "Edit Grup Server",
"CreateServerGroup": "Buat Grup Server",
"User": "Pengguna",
"WAF": "Firewall Aplikasi Web",
"SiteName": "Nama Situs",
"DashboardOriginalHost": "Alamat koneksi Agen [nama domain/IP:port]",
"ConfigTLS": "Gunakan TLS untuk menghubungkan Agen",
"LoginFailed": "Gagal Masuk",
"BruteForceAttackingToken": "Token Serangan Brute Force",
"BruteForceAttackingAgentSecret": "Rahasia Agen Serangan Brute Force",
"Language": "Bahasa",
"CustomCodes": "Kode Kustom (Gaya dan Skrip)",
"CustomCodesDashboard": "Kode Kustom untuk Dasbor",
"CustomPublicDNSNameserversforDDNS": "DNS Publik Kustom untuk DDNS",
"WebRealIPHeader": "Header permintaan IP asli Frontend",
"AgentRealIPHeader": "Header permintaan IP asli Agen",
"UseDirectConnectingIP": "Gunakan IP koneksi langsung",
"IPChangeNotification": "Notifikasi Perubahan IP",
"FullIPNotification": "Tampilkan Alamat IP Lengkap dalam Pesan Notifikasi",
"EditService": "Edit Layanan",
"CreateService": "Buat Layanan",
"EditTask": "Edit Tugas",
"CreateTask": "Buat Tugas",
"CreateNotifier": "Buat Pemberi Notifikasi",
"EditNotifier": "Edit Pemberi Notifikasi",
"EditAlertRule": "Edit Aturan Peringatan",
"CreateAlertRule": "Buat Aturan Peringatan",
"EditNotifierGroup": "Edit Grup Pemberi Notifikasi",
"CreateNotifierGroup": "Buat Grup Pemberi Notifikasi",
"NewUser": "Pengguna Baru",
"Count": "Jumlah",
"LastBlockReason": "Alasan Blokir Terakhir",
"LastBlockTime": "Waktu larangan terakhir",
"Theme": "Tema",
"Author": "Penulis",
"Repository": "Repositori",
"Community": "Komunitas",
"Official": "Resmi",
"CommunityThemeWarning": "Anda menggunakan tema komunitas",
"CommunityThemeDescription": "Tema ini disediakan oleh komunitas, gunakan dengan risiko Anda sendiri",
"Cancel": "Batal",
"EnableDDNS": "Aktifkan DDNS",
"PushSuccessful": "Push jika Berhasil",
"GrpcAuthFailed": "Autentikasi gRPC gagal",
"APITokenInvalid": "Token API tidak valid",
"UserInvalid": "Pengguna tidak valid",
"BlockByUser": "Diblokir oleh admin",
"BlockIdentifier": "Pengidentifikasi blokir",
"UserId": "ID Pengguna",
"ConnectedAt": "Terhubung pada",
"OnlineUser": "Pengguna Daring",
"Total": "Total",
"ConfirmBlock": "Konfirmasi Blokir",
"RejectPassword": "Tolak Masuk dengan Kata Sandi",
"EmptyText": "Teks kosong",
"EmptyNote": "Anda tidak memiliki catatan.",
"OverrideDDNSDomains": "Timpa Domain DDNS (per konfigurasi)",
"EditServerConfig": "Edit Konfigurasi Server",
"Option": "Opsi",
"Value": "Nilai",
"Preview": "Pratinjau",
"PublicNote": {
"Label": "Catatan Publik",
"Billing": "Penagihan",
"Plan": "Paket",
"StartDate": "Tanggal Mulai",
"EndDate": "Tanggal Berakhir",
"AutoRenewal": "Perpanjangan Otomatis",
"Cycle": "Siklus",
"Amount": "Jumlah",
"Bandwidth": "Bandwidth",
"TrafficVolume": "Volume Lalu Lintas",
"TrafficType": "Jenis Lalu Lintas",
"IPv4": "IPv4",
"IPv6": "IPv6",
"NetworkRoute": "Rute Jaringan",
"Extra": "Ekstra",
"Enabled": "Diaktifkan",
"Disabled": "Dinonaktifkan",
"Inbound": "Masuk",
"Both": "Keduanya",
"Day": "Hari",
"Week": "Minggu",
"Month": "Bulan",
"Year": "Tahun",
"NoExpiry": "Tanpa Kedaluwarsa",
"SetNoExpiry": "Atur Tanpa Kedaluwarsa",
"CancelNoExpiry": "Batalkan Tanpa Kedaluwarsa",
"Free": "Gratis",
"PayAsYouGo": "Bayar sesuai penggunaan",
"CommaSeparated": "Pisahkan beberapa item dengan koma",
"Has": "Memiliki",
"None": "Tidak ada",
"CustomFields": "Bidang Kustom",
"ClearDate": "Hapus Tanggal",
"Clear": "Bersihkan",
"RawText": "Teks Mentah"
},
"Validation": {
"InvalidDate": "Tanggal tidak valid",
"MustBe0Or1": "Harus 0 atau 1",
"MustBeDayWeekMonthYear": "Harus Hari/Minggu/Bulan/Tahun",
"MustBe1Or2": "Harus 1 atau 2",
"DigitsOnly": "Hanya digit",
"InvalidForm": "Formulir tidak valid",
"InvalidJSON": "JSON tidak valid"
},
"AlertRules": {
"CoverAllServers": "Pantau semua server",
"IgnoreAllSelectSpecific": "Abaikan semua, pilih server tertentu",
"IgnoreHint": "{{server}} ID: true/false",
"IgnoreExample": "misalnya, {\"1\": true, \"2\": false}"
},
"Search": "Cari...",
"Format": "Format",
"Formatted": "Terformat",
"Copy": "Salin",
"Paste": "Tempel",
"CopiedToClipboard": "Disalin ke papan klip",
"ClipboardWriteFailed": "Gagal menulis ke papan klip",
"PastedFromClipboard": "Ditempel dari papan klip",
"ClipboardReadFailed": "Gagal membaca papan klip",
"FormatMetricUnits": "Format Satuan Metrik"
} }
+1
View File
@@ -0,0 +1 @@
{}
+131
View File
@@ -0,0 +1,131 @@
{
"nezha": "Nezha Моніторинг",
"theme": {
"light": "Світло",
"dark": "Темний",
"system": "Така як система"
},
"Username": "Нікнейм",
"Password": "Пароль",
"InvalidUsernameOrPassword": "Невірне Ім'я або Пароль",
"NetworkError": "Помилка мережі",
"LoginFirst": "Будь ласка, спочатку увійдіть до системи",
"CurrentTime": "Поточний час",
"Results": {
"UsernameMin": "Ім'я користувача має містити не менше {{number}} символів.",
"PasswordRequired": "Пароль не може бути порожнім.",
"ErrorFetchingResource": "Помилка при отриманні ресурсу: {{error}}",
"SelectAtLeastOneServer": "Виберіть хоча б один сервер.",
"UnExpectedError": "Непередбачена помилка. Подробиці у консолі.",
"ForceUpdate": "Примусове оновлення:",
"NoRowsAreSelected": "Не вибрані рядки",
"ThisOperationIsUnrecoverable": "Цю операцію не можна скасувати!",
"TaskTriggeredSuccessfully": "Завдання успішно запущено",
"TheServerDoesNotOnline": "Сервер не існує або ще не підключено",
"InstallHostRequired": "В установках не вказано адресу підключення агента.",
"UnknownIdentifier": "Невідомий ідентифікатор"
},
"Login": "Увійти",
"Server": "Сервер",
"Service": "Сервіс",
"Task": "Задача",
"Notification": "Повідомлення",
"DDNS": "Динамічний DNS",
"NATT": "Обхід NAT",
"Group": "Група",
"Profile": "Профіль",
"Settings": "Системні налаштування",
"BackToHome": "Повернутися на головну",
"Logout": "Вийти",
"NavigateTo": "Перейти до",
"SelectAPageToNavigateTo": "Виберіть сторінку, щоб перейти",
"Close": "Закрити",
"Error": "Помилка",
"Name": "Назва",
"Version": "Версія",
"Unknown": "невідомо",
"Enable": "Увімкнути",
"HideForGuest": "Приховано для гостей",
"InstallCommands": "Команда установки",
"Terminal": "Термінал",
"Config": "Конфігурація",
"Note": "Примітка",
"Success": "Успіх",
"Done": "Готово",
"Offline": "Оффлайн",
"Failure": "Невдача",
"Loading": "Загрузка",
"NoResults": "Немає результатів",
"Actions": "Дії",
"EditServer": "Редагувати сервер",
"Weight": "Вага (чим більше число, тим вище воно відображається)",
"DDNSProfiles": "ID профілей DDNS",
"SeparateWithComma": "(Розділіть комою)",
"Public": "Публічний",
"Private": "Приватний",
"Submit": "Відправити",
"Target": "Ціль",
"Coverage": "Покриття",
"CoverAll": "Охопити все",
"IgnoreAll": "Ігнорувати все",
"OnAlert": "Сервери з тривогами",
"SpecificServers": "Конкретний сервер",
"Type": "Тип",
"Interval": "Інтервал",
"NotifierGroupID": "ID групи повідомлень",
"Trigger": "При спрацьовуванні",
"TasksToTriggerOnAlert": "Завдання, що запускаються при тривозі",
"TasksToTriggerAfterRecovery": "Завдання після відновлення",
"Add": "Додати",
"Delete": "Видалити",
"AdvancedJSON": "Розширений JSON",
"Save": "Зберегти",
"Confirm": "Підтвердити",
"ConfirmDeletion": "Підтвердити видалення?",
"Services": "Сервіси",
"ShowInService": "Показувати у Сервісі",
"Coverages": {
"Excludes": "Виключити певні сервери",
"Only": "Тільки певні сервери",
"Alarmed": "Виконано на сервері, який спричинив тривогу"
},
"EnableFailureNotification": "Увімкнути сповіщення про помилки",
"MaximumLatency": "Максимальна затримка (мс)",
"MinimumLatency": "Мінімальна затримка (мс)",
"EnableLatencyNotification": "Увімкнути сповіщення про затримку",
"EnableTriggerTask": "Увімкнути завдання-тригер",
"CronExpression": "Cron-вираз",
"Command": "Команда",
"NotifierGroup": "Група сповіщень",
"SendSuccessNotification": "Надіслати сповіщення про успіх",
"LastExecution": "Останнє виконання",
"Result": "Результат",
"Scheduled": "Заплановані завдання",
"Notifier": "Повідомити",
"AlertRule": "Правила сповіщень",
"VerifyTLS": "Перевірка TLS",
"TriggerMode": "Режим спрацювання",
"Rules": "Правила",
"RequestMethod": "Метод запиту",
"RequestHeader": "Заголовок запиту",
"DoNotSendTestMessage": "Не надсилати тестове повідомлення",
"Always": "Зажди",
"Once": "Один раз",
"Provider": "Провайдер",
"Domains": "Домени",
"MaximumRetryAttempts": "Максимальний час для повторних спроб",
"Refresh": "Оновити",
"CopyPath": "Копіювати шлях",
"Goto": "Перейти",
"UpdateProfile": "Оновити профіль",
"NewUsername": "Нове ім'я користувача",
"OriginalPassword": "Старий пароль",
"NewPassword": "Новий пароль",
"EditDDNS": "Редагувати DDNS",
"CreateDDNS": "Створити DDNS",
"Credential": "Облікові дані",
"RequestType": "Тип запиту",
"RequestBody": "Тіло запиту",
"FileManager": "Псевдо Менеджер файлів",
"Downloading": "Завантаження"
}
+71 -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,67 @@
"OverrideDDNSDomains": "强制指定 DDNS 域名(每个配置)", "OverrideDDNSDomains": "强制指定 DDNS 域名(每个配置)",
"Value": "值", "Value": "值",
"Preview": "预览", "Preview": "预览",
"Option": "选项" "Option": "选项",
"PublicNote": {
"Label": "公开备注",
"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": "按量付费",
"CommaSeparated": "以英文逗号分隔多个",
"Has": "有",
"None": "无",
"CustomFields": "自定义字段",
"ClearDate": "清除日期",
"Clear": "清除",
"RawText": "原始文本"
},
"Validation": {
"InvalidDate": "无效的日期格式",
"MustBe0Or1": "只能为 0 或 1",
"MustBeDayWeekMonthYear": "必须为 Day/Week/Month/Year",
"MustBe1Or2": "只能为 1 或 2",
"DigitsOnly": "仅允许数字",
"InvalidForm": "表单校验失败",
"InvalidJSON": "无效的 JSON"
},
"AlertRules": {
"CoverAllServers": "监控所有服务器",
"IgnoreAllSelectSpecific": "忽略所有,选择特定服务器",
"IgnoreHint": "{{server}}ID: true/false",
"IgnoreExample": "例如:{\"1\": true, \"2\": false}"
},
"Search": "搜索...",
"Format": "格式化",
"Formatted": "已格式化",
"Copy": "复制",
"Paste": "粘贴",
"CopiedToClipboard": "已复制到剪贴板",
"ClipboardWriteFailed": "无法写入剪贴板",
"PastedFromClipboard": "已从剪贴板粘贴",
"ClipboardReadFailed": "无法读取剪贴板",
"FormatMetricUnits": "格式化数据单位"
} }
+2 -1
View File
@@ -185,5 +185,6 @@
"EditServerConfig": "編輯伺服器配置", "EditServerConfig": "編輯伺服器配置",
"Option": "選項", "Option": "選項",
"Value": "值", "Value": "值",
"Preview": "預覽" "Preview": "預覽",
"FormatMetricUnits": "格式化資料單位"
} }
+29 -21
View File
@@ -1,27 +1,31 @@
// NOTE: Do not modify the import order unless absolutely necessary.
import { createRoot } from "react-dom/client" import { createRoot } from "react-dom/client"
import { RouterProvider, createBrowserRouter } from "react-router-dom" import { RouterProvider, createBrowserRouter } from "react-router-dom"
import { TerminalPage } from "./components/terminal" import "./index.css"
import ErrorPage from "./error-page" import "./lib/i18n"
import { AuthProvider } from "./hooks/useAuth" import { AuthProvider } from "./hooks/useAuth"
import { NotificationProvider } from "./hooks/useNotfication" import { NotificationProvider } from "./hooks/useNotfication"
import { ServerProvider } from "./hooks/useServer" import { ServerProvider } from "./hooks/useServer"
import "./index.css"
import "./lib/i18n" import Root from "./routes/root"
import AlertRulePage from "./routes/alert-rule" import ErrorPage from "./error-page"
import ProtectedRoute from "./routes/protect"
import CronPage from "./routes/cron" import CronPage from "./routes/cron"
import DDNSPage from "./routes/ddns"
import LoginPage from "./routes/login" import LoginPage from "./routes/login"
import ServerPage from "./routes/server"
import ServicePage from "./routes/service"
import { TerminalPage } from "./components/terminal"
import DDNSPage from "./routes/ddns"
import NATPage from "./routes/nat" import NATPage from "./routes/nat"
import NotificationPage from "./routes/notification"
import NotificationGroupPage from "./routes/notification-group" import NotificationGroupPage from "./routes/notification-group"
import ServerGroupPage from "./routes/server-group"
import AlertRulePage from "./routes/alert-rule"
import NotificationPage from "./routes/notification"
import OnlineUserPage from "./routes/online-user" import OnlineUserPage from "./routes/online-user"
import ProfilePage from "./routes/profile" import ProfilePage from "./routes/profile"
import ProtectedRoute from "./routes/protect"
import Root from "./routes/root"
import ServerPage from "./routes/server"
import ServerGroupPage from "./routes/server-group"
import ServicePage from "./routes/service"
import SettingsPage from "./routes/settings" import SettingsPage from "./routes/settings"
import UserPage from "./routes/user" import UserPage from "./routes/user"
import WAFPage from "./routes/waf" import WAFPage from "./routes/waf"
@@ -70,14 +74,6 @@ const router = createBrowserRouter([
</ServerProvider> </ServerProvider>
), ),
}, },
{
path: "/dashboard/notification",
element: (
<NotificationProvider withNotifierGroup>
<NotificationPage />
</NotificationProvider>
),
},
{ {
path: "/dashboard/alert-rule", path: "/dashboard/alert-rule",
element: ( element: (
@@ -114,6 +110,14 @@ const router = createBrowserRouter([
path: "/dashboard/terminal/:id", path: "/dashboard/terminal/:id",
element: <TerminalPage />, element: <TerminalPage />,
}, },
{
path: "/dashboard/notification",
element: (
<NotificationProvider withNotifierGroup>
<NotificationPage />
</NotificationProvider>
),
},
{ {
path: "/dashboard/profile", path: "/dashboard/profile",
element: ( element: (
@@ -124,7 +128,11 @@ const router = createBrowserRouter([
}, },
{ {
path: "/dashboard/settings", path: "/dashboard/settings",
element: <SettingsPage />, element: (
<NotificationProvider withNotifierGroup>
<SettingsPage />
</NotificationProvider>
),
}, },
{ {
path: "/dashboard/settings/user", path: "/dashboard/settings/user",
+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).filter(Boolean) as number[], id: selectedRows.map((r) => r.original.id).filter(Boolean) as number[],
@@ -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>
) )
} }
+5 -5
View File
@@ -60,7 +60,7 @@ export default function ServicePage() {
{ {
header: "ID", header: "ID",
accessorKey: "id", accessorKey: "id",
accessorFn: (row) => row.id, accessorFn: (row) => `${row.id}(${row.display_index ?? 0})`,
}, },
{ {
header: t("Name"), header: t("Name"),
@@ -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),
+17 -8
View File
@@ -21,6 +21,8 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { Combobox } from "@/components/ui/combobox"
import { useNotification } from "@/hooks/useNotfication"
import { useAuth } from "@/hooks/useAuth" import { useAuth } from "@/hooks/useAuth"
import useSetting from "@/hooks/useSetting" import useSetting from "@/hooks/useSetting"
import { asOptionalField } from "@/lib/utils" import { asOptionalField } from "@/lib/utils"
@@ -58,14 +60,20 @@ export default function SettingsPage() {
const { profile } = useAuth() const { profile } = useAuth()
const navigate = useNavigate() const navigate = useNavigate()
const { notifierGroup } = useNotification()
const ngroupList = notifierGroup?.map((ng) => ({
value: `${ng.group.id}`,
label: ng.group.name,
})) || [{ value: "", label: "" }]
const isAdmin = profile?.role === 0 const isAdmin = profile?.role === 0
if (!isAdmin) { if (!isAdmin) {
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 +100,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()
@@ -448,12 +456,13 @@ export default function SettingsPage() {
name="ip_change_notification_group_id" name="ip_change_notification_group_id"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t("NotifierGroupID")}</FormLabel> <FormLabel>{t("NotifierGroup")}</FormLabel>
<FormControl> <FormControl>
<Input <Combobox
placeholder="0" placeholder={t("Search")}
type="number" options={ngroupList}
{...field} onValueChange={field.onChange}
defaultValue={field.value?.toString()}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
+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",
+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) => {
+91
View File
@@ -23,4 +23,95 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
}, },
}, },
build: {
cssCodeSplit: true,
sourcemap: false,
chunkSizeWarningLimit: 1500,
rollupOptions: {
output: {
manualChunks(id: string) {
if (!id.includes("node_modules")) return
// 提取顶级包名,兼容 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"
},
},
},
},
}) })