mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-11-04 00:40:21 +00:00
chore: merge upstream/main into admin-frontend-domain preserving custom domain and branding features
This commit is contained in:
commit
68f3e02e26
114 files changed
+8616
-5337
No files matched your search
@@ -36,7 +36,7 @@ import { ModelAlertRule } from "@/types"
|
||||
import { triggerModes } from "@/types"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import { KeyedMutator } from "swr"
|
||||
@@ -50,13 +50,15 @@ interface AlertRuleCardProps {
|
||||
mutate: KeyedMutator<ModelAlertRule[]>
|
||||
}
|
||||
|
||||
const cycleUnitSchema = z.enum(["hour", "day", "week", "month", "year"])
|
||||
|
||||
const ruleSchema = z.object({
|
||||
type: z.string(),
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
cycle_start: z.string().optional(),
|
||||
cycle_interval: z.number().optional(),
|
||||
cycle_unit: z.enum(["hour", "day", "week", "month", "year"]).optional(),
|
||||
cycle_unit: cycleUnitSchema.optional(),
|
||||
duration: z.number().optional(),
|
||||
cover: z.number().int().min(0),
|
||||
ignore: z.record(z.string(), z.boolean()).optional(),
|
||||
@@ -71,7 +73,7 @@ const alertRuleFormSchema = z.object({
|
||||
try {
|
||||
JSON.parse(val)
|
||||
return true
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
@@ -92,10 +94,12 @@ const alertRuleFormSchema = z.object({
|
||||
export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
type AlertRuleFormData = z.infer<typeof alertRuleFormSchema>
|
||||
type AlertRuleEntry = z.output<typeof ruleSchema>
|
||||
type AlertRuleFormInput = z.input<typeof alertRuleFormSchema>
|
||||
type AlertRuleFormData = z.output<typeof alertRuleFormSchema>
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(alertRuleFormSchema) as any,
|
||||
const form = useForm<AlertRuleFormInput, unknown, AlertRuleFormData>({
|
||||
resolver: zodResolver(alertRuleFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
...data,
|
||||
@@ -124,14 +128,16 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
// 结构化规则编辑状态:从已有数据或 rules_raw 初始化
|
||||
const initialRules = (() => {
|
||||
try {
|
||||
if (data?.rules) return data.rules as any[]
|
||||
if (data?.rules) return data.rules
|
||||
const raw = form.getValues("rules_raw")
|
||||
return raw ? JSON.parse(raw) : []
|
||||
if (!raw) return []
|
||||
const parsed: unknown = JSON.parse(raw)
|
||||
return z.array(ruleSchema).parse(parsed)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})()
|
||||
const [rulesUI, setRulesUI] = useState<any[]>(initialRules)
|
||||
const [rulesUI, setRulesUI] = useState<AlertRuleEntry[]>(initialRules)
|
||||
|
||||
// 同步到 rules_raw(提交仍走 JSON 字符串)
|
||||
useEffect(() => {
|
||||
@@ -140,17 +146,22 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [rulesUI])
|
||||
}, [form, rulesUI])
|
||||
|
||||
const rulesRaw = useWatch({ control: form.control, name: "rules_raw" })
|
||||
|
||||
const onSubmit = async (values: AlertRuleFormData) => {
|
||||
values.rules = JSON.parse(values.rules_raw)
|
||||
values.rules = z.array(ruleSchema).parse(JSON.parse(values.rules_raw))
|
||||
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)
|
||||
const { rules_raw, ...requiredFields } = values
|
||||
const requiredFields = { ...values }
|
||||
delete (requiredFields as Record<string, unknown>).rules_raw
|
||||
try {
|
||||
data?.id
|
||||
? await updateAlertRule(data.id, requiredFields)
|
||||
: await createAlertRule(requiredFields)
|
||||
if (data?.id) {
|
||||
await updateAlertRule(data.id, requiredFields)
|
||||
} else {
|
||||
await createAlertRule(requiredFields)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
@@ -184,10 +195,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
<DialogDescription />
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit as any)}
|
||||
className="space-y-2 my-2"
|
||||
>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -531,7 +539,10 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
const next = [...rulesUI]
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
cycle_unit: val,
|
||||
cycle_unit:
|
||||
cycleUnitSchema.parse(
|
||||
val,
|
||||
),
|
||||
}
|
||||
setRulesUI(next)
|
||||
}}
|
||||
@@ -603,7 +614,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="resize-y"
|
||||
value={form.watch("rules_raw")}
|
||||
value={rulesRaw}
|
||||
onChange={(e) => {
|
||||
// 同步到结构化编辑器
|
||||
form.setValue("rules_raw", e.target.value, {
|
||||
@@ -630,7 +641,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
placeholder={t("Search")}
|
||||
options={ngroupList}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value.toString()}
|
||||
defaultValue={String(field.value ?? "")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -33,8 +33,9 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({
|
||||
const [toUserId, setToUserId] = useState<number | undefined>(undefined)
|
||||
|
||||
const onSubmit = async () => {
|
||||
let results
|
||||
try {
|
||||
await batchMoveServer({
|
||||
results = await batchMoveServer({
|
||||
ids: serverIds,
|
||||
to_user: toUserId!,
|
||||
})
|
||||
@@ -45,7 +46,30 @@ export const BatchMoveServerIcon: React.FC<BatchMoveServerIconProps> = ({
|
||||
})
|
||||
return
|
||||
}
|
||||
toast(t("Done"))
|
||||
|
||||
// The backend now responds per-server; render a structured summary
|
||||
// instead of "Done" so the operator can see which ids hit a snag
|
||||
// (DisableCommandExecute / already_transferring / etc). Pending ids
|
||||
// remain async — watching /ws/transfer or the Transfers page will
|
||||
// surface their terminal state.
|
||||
const pending = results.filter((r) => r.status === "pending").length
|
||||
const denied = results.filter((r) => r.status === "permission_denied").length
|
||||
const dup = results.filter((r) => r.status === "already_transferring").length
|
||||
const missing = results.filter((r) => r.status === "server_not_found").length
|
||||
const same = results.filter((r) => r.status === "same_owner").length
|
||||
const tooOld = results.filter((r) => r.status === "agent_too_old").length
|
||||
|
||||
const parts: string[] = []
|
||||
if (pending) parts.push(t("Transfer.PendingCount", { count: pending }))
|
||||
if (denied) parts.push(t("Transfer.PermissionDeniedCount", { count: denied }))
|
||||
if (dup) parts.push(t("Transfer.AlreadyTransferringCount", { count: dup }))
|
||||
if (missing) parts.push(t("Transfer.ServerNotFoundCount", { count: missing }))
|
||||
if (same) parts.push(t("Transfer.SameOwnerCount", { count: same }))
|
||||
if (tooOld) parts.push(t("Transfer.AgentTooOldCount", { count: tooOld }))
|
||||
|
||||
toast(t("Transfer.BatchSubmitted"), {
|
||||
description: parts.join(" · ") || t("Done"),
|
||||
})
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -63,21 +63,22 @@ const cronFormSchema = z.object({
|
||||
notification_group_id: z.coerce.number().int(),
|
||||
})
|
||||
|
||||
type CronFormData = z.infer<typeof cronFormSchema>
|
||||
type CronFormInput = z.input<typeof cronFormSchema>
|
||||
type CronFormData = z.output<typeof cronFormSchema>
|
||||
|
||||
export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = useForm<CronFormData>({
|
||||
resolver: zodResolver(cronFormSchema as any),
|
||||
const form = useForm<CronFormInput, unknown, CronFormData>({
|
||||
resolver: zodResolver(cronFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
task_type: data.task_type ?? 0,
|
||||
name: data.name ?? "",
|
||||
scheduler: data.scheduler ?? "",
|
||||
command: (data as any).command ?? "",
|
||||
command: data.command ?? "",
|
||||
servers: data.servers ?? [],
|
||||
cover: data.cover ?? 0,
|
||||
push_successful: (data as any).push_successful ?? false,
|
||||
push_successful: data.push_successful ?? false,
|
||||
notification_group_id: data.notification_group_id ?? 0,
|
||||
}
|
||||
: {
|
||||
@@ -99,7 +100,11 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
||||
|
||||
const onSubmit = async (values: CronFormData) => {
|
||||
try {
|
||||
data?.id ? await updateCron(data.id, values) : await createCron(values)
|
||||
if (data?.id) {
|
||||
await updateCron(data.id, values)
|
||||
} else {
|
||||
await createCron(values)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
@@ -267,7 +272,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
||||
placeholder="Search..."
|
||||
options={ngroupList}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value.toString()}
|
||||
defaultValue={String(field.value ?? "")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -282,7 +287,7 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
checked={field.value === true}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">
|
||||
|
||||
+37
-24
@@ -67,28 +67,29 @@ const ddnsFormSchema = z.object({
|
||||
webhook_headers: asOptionalField(z.string()),
|
||||
})
|
||||
|
||||
type DDNSFormData = z.infer<typeof ddnsFormSchema>
|
||||
type DDNSFormInput = z.input<typeof ddnsFormSchema>
|
||||
type DDNSFormData = z.output<typeof ddnsFormSchema>
|
||||
|
||||
export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = useForm<DDNSFormData>({
|
||||
resolver: zodResolver(ddnsFormSchema as any),
|
||||
const form = useForm<DDNSFormInput, unknown, DDNSFormData>({
|
||||
resolver: zodResolver(ddnsFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
max_retries: data.max_retries ?? 3,
|
||||
enable_ipv4: (data as any).enable_ipv4 ?? false,
|
||||
enable_ipv6: (data as any).enable_ipv6 ?? false,
|
||||
enable_ipv4: data.enable_ipv4 ?? false,
|
||||
enable_ipv6: data.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 ?? "",
|
||||
access_id: data.access_id ?? "",
|
||||
access_secret: data.access_secret ?? "",
|
||||
webhook_url: data.webhook_url ?? "",
|
||||
webhook_method: data.webhook_method,
|
||||
webhook_request_type: data.webhook_request_type,
|
||||
webhook_request_body: data.webhook_request_body ?? "",
|
||||
webhook_headers: data.webhook_headers ?? "",
|
||||
}
|
||||
: {
|
||||
max_retries: 3,
|
||||
@@ -116,7 +117,11 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
|
||||
const onSubmit = async (values: DDNSFormData) => {
|
||||
try {
|
||||
values.domains = conv.strToArr(values.domains_raw)
|
||||
data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values)
|
||||
if (data?.id) {
|
||||
await updateDDNSProfile(data.id, values)
|
||||
} else {
|
||||
await createDDNSProfile(values)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
@@ -227,15 +232,23 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="max_retries"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("MaximumRetryAttempts")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="3" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const { value, ...fieldProps } = field
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>{t("MaximumRetryAttempts")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="3"
|
||||
value={String(value ?? "")}
|
||||
{...fieldProps}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -351,7 +364,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
checked={field.value === true}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">
|
||||
@@ -371,7 +384,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
checked={field.value === true}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">
|
||||
|
||||
+107
-89
@@ -25,6 +25,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery"
|
||||
import { virtualizedTableFeatures } from "@/lib/table"
|
||||
import { copyToClipboard, fm, formatPath, fmWorker as worker } from "@/lib/utils"
|
||||
import {
|
||||
FMEntry,
|
||||
@@ -37,7 +38,7 @@ import {
|
||||
import { ColumnDef } from "@tanstack/react-table"
|
||||
import { Row, flexRender } from "@tanstack/react-table"
|
||||
import { File, Folder } from "lucide-react"
|
||||
import { HTMLAttributes, JSX, useEffect, useRef, useState } from "react"
|
||||
import { HTMLAttributes, JSX, useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
@@ -59,6 +60,10 @@ interface FMProps {
|
||||
wsUrl: string
|
||||
}
|
||||
|
||||
type VirtualizedTableRowProps = HTMLAttributes<HTMLTableRowElement> & {
|
||||
"data-index"?: number | string
|
||||
}
|
||||
|
||||
const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
@@ -67,21 +72,20 @@ const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl, ...props }) => {
|
||||
export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({
|
||||
wsUrl,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const fmRef = useRef<HTMLDivElement>(null)
|
||||
const wsRef = useRef<WebSocket | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
wsRef.current?.close()
|
||||
}
|
||||
}, [])
|
||||
const tRef = useRef(t)
|
||||
tRef.current = t
|
||||
|
||||
const [dOpen, setdOpen] = useState(false)
|
||||
const [uOpen, setuOpen] = useState(false)
|
||||
|
||||
const columns: ColumnDef<FMEntry>[] = [
|
||||
const columns: ColumnDef<typeof virtualizedTableFeatures, FMEntry>[] = [
|
||||
{
|
||||
id: "type",
|
||||
header: () => <span>{t("Type")}</span>,
|
||||
@@ -119,10 +123,9 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
|
||||
},
|
||||
]
|
||||
|
||||
const tableRowComponent = (rows: Row<FMEntry>[]) =>
|
||||
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
|
||||
// @ts-expect-error data-index is a valid attribute
|
||||
const index = props["data-index"]
|
||||
const tableRowComponent = (rows: Row<typeof virtualizedTableFeatures, FMEntry>[]) =>
|
||||
function getTableRow(props: VirtualizedTableRowProps) {
|
||||
const index = Number(props["data-index"])
|
||||
const row = rows[index]
|
||||
|
||||
if (!row) return null
|
||||
@@ -160,71 +163,6 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(wsUrl, window.location.origin)
|
||||
url.protocol = url.protocol.replace("http", "ws")
|
||||
const ws = new WebSocket(url)
|
||||
wsRef.current = ws
|
||||
ws.binaryType = "arraybuffer"
|
||||
ws.onopen = () => {
|
||||
listFile()
|
||||
}
|
||||
ws.onclose = (e) => {
|
||||
console.log("WebSocket connection closed:", e)
|
||||
}
|
||||
ws.onerror = (e) => {
|
||||
console.error(e)
|
||||
toast("Websocket" + " " + t("Error"), {
|
||||
description: t("Results.UnExpectedError"),
|
||||
})
|
||||
}
|
||||
ws.onmessage = async (e: MessageEvent<ArrayBufferLike>) => {
|
||||
try {
|
||||
const identifier = new Uint8Array(e.data, 0, 4)
|
||||
if (arraysEqual(identifier, FMIdentifier.error)) {
|
||||
const errBytes = e.data.slice(4)
|
||||
const errMsg = new TextDecoder("utf-8").decode(errBytes)
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
|
||||
if (firstChunk.current) {
|
||||
if (arraysEqual(identifier, FMIdentifier.file)) {
|
||||
worker.postMessage({
|
||||
operation: 1,
|
||||
arrayBuffer: e.data,
|
||||
fileName: currentBasename.current,
|
||||
})
|
||||
firstChunk.current = false
|
||||
} else if (arraysEqual(identifier, FMIdentifier.fileName)) {
|
||||
const { path, fmList } = await fm.parseFMList(e.data)
|
||||
setPath(path)
|
||||
setFMEntries(fmList)
|
||||
} else if (arraysEqual(identifier, FMIdentifier.complete)) {
|
||||
// Upload completed
|
||||
setuOpen(false)
|
||||
listFile()
|
||||
} else {
|
||||
throw new Error(t("Results.UnknownIdentifier"))
|
||||
}
|
||||
} else {
|
||||
await waitForHandleReady()
|
||||
worker.postMessage({
|
||||
operation: 2,
|
||||
arrayBuffer: e.data,
|
||||
fileName: currentBasename.current,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing received data:", error)
|
||||
toast("FM" + " " + t("Error"), {
|
||||
description: t("Results.UnExpectedError"),
|
||||
})
|
||||
setdOpen(false)
|
||||
setuOpen(false)
|
||||
}
|
||||
}
|
||||
}, [wsUrl])
|
||||
|
||||
useEffect(() => {
|
||||
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
|
||||
switch (event.data.type) {
|
||||
@@ -265,25 +203,101 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload)
|
||||
}
|
||||
}, [worker, dOpen])
|
||||
}, [dOpen])
|
||||
|
||||
const [currentPath, setPath] = useState("")
|
||||
useEffect(() => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||
listFile()
|
||||
}
|
||||
}, [wsRef.current, currentPath])
|
||||
const currentPathRef = useRef(currentPath)
|
||||
currentPathRef.current = currentPath
|
||||
|
||||
const listFile = () => {
|
||||
const listFile = useCallback(() => {
|
||||
const prefix = new Int8Array([FMOpcode.List])
|
||||
const pathMsg = new TextEncoder().encode(currentPath)
|
||||
const pathMsg = new TextEncoder().encode(currentPathRef.current)
|
||||
|
||||
const msg = new Int8Array(prefix.length + pathMsg.length)
|
||||
msg.set(prefix)
|
||||
msg.set(pathMsg, prefix.length)
|
||||
|
||||
wsRef.current?.send(msg)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// The WebSocket initialization must not depend on listFile or currentPath,
|
||||
// otherwise navigating directories triggers a disconnect and reconnect.
|
||||
useEffect(() => {
|
||||
const url = new URL(wsUrl, window.location.origin)
|
||||
url.protocol = url.protocol.replace("http", "ws")
|
||||
const ws = new WebSocket(url)
|
||||
wsRef.current = ws
|
||||
ws.binaryType = "arraybuffer"
|
||||
ws.onopen = () => {
|
||||
listFile()
|
||||
}
|
||||
ws.onclose = (e) => {
|
||||
console.log("WebSocket connection closed:", e)
|
||||
}
|
||||
ws.onerror = (e) => {
|
||||
console.error(e)
|
||||
toast("Websocket" + " " + tRef.current("Error"), {
|
||||
description: tRef.current("Results.UnExpectedError"),
|
||||
})
|
||||
}
|
||||
ws.onmessage = async (e: MessageEvent<ArrayBufferLike>) => {
|
||||
try {
|
||||
const identifier = new Uint8Array(e.data, 0, 4)
|
||||
if (arraysEqual(identifier, FMIdentifier.error)) {
|
||||
const errBytes = e.data.slice(4)
|
||||
const errMsg = new TextDecoder("utf-8").decode(errBytes)
|
||||
throw new Error(errMsg)
|
||||
}
|
||||
if (firstChunk.current) {
|
||||
if (arraysEqual(identifier, FMIdentifier.file)) {
|
||||
worker.postMessage({
|
||||
operation: 1,
|
||||
arrayBuffer: e.data,
|
||||
fileName: currentBasename.current,
|
||||
})
|
||||
firstChunk.current = false
|
||||
} else if (arraysEqual(identifier, FMIdentifier.fileName)) {
|
||||
const { path, fmList } = await fm.parseFMList(e.data)
|
||||
setPath(path)
|
||||
setFMEntries(fmList)
|
||||
} else if (arraysEqual(identifier, FMIdentifier.complete)) {
|
||||
// Upload completed
|
||||
setuOpen(false)
|
||||
listFile()
|
||||
} else {
|
||||
throw new Error(tRef.current("Results.UnknownIdentifier"))
|
||||
}
|
||||
} else {
|
||||
await waitForHandleReady()
|
||||
worker.postMessage({
|
||||
operation: 2,
|
||||
arrayBuffer: e.data,
|
||||
fileName: currentBasename.current,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing received data:", error)
|
||||
toast("FM" + " " + tRef.current("Error"), {
|
||||
description: tRef.current("Results.UnExpectedError"),
|
||||
})
|
||||
setdOpen(false)
|
||||
setuOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
ws.close()
|
||||
if (wsRef.current === ws) {
|
||||
wsRef.current = null
|
||||
}
|
||||
}
|
||||
}, [listFile, wsUrl])
|
||||
|
||||
useEffect(() => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
||||
listFile()
|
||||
}
|
||||
}, [currentPath, listFile])
|
||||
|
||||
const downloadFile = (basename: string) => {
|
||||
currentBasename.current = basename
|
||||
@@ -331,9 +345,13 @@ const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({ wsUrl,
|
||||
onClick={async () => {
|
||||
try {
|
||||
await copyToClipboard(formatPath(currentPath))
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
const description =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("Results.UnExpectedError")
|
||||
toast("FM" + " " + t("Error"), {
|
||||
description: error.message,
|
||||
description,
|
||||
})
|
||||
console.error("copy error: ", error)
|
||||
}
|
||||
|
||||
+40
-26
@@ -51,6 +51,7 @@ const pages = [
|
||||
{ href: "/dashboard/nat", label: i18next.t("NATT") },
|
||||
{ href: "/dashboard/domain", label: i18next.t("Domain") }, // <-- 新增的域名监控链接
|
||||
{ href: "/dashboard/server-group", label: i18next.t("Group") },
|
||||
{ href: "/dashboard/transfer", label: i18next.t("Transfer.Title") },
|
||||
]
|
||||
// ^^^^^^^^^^^ 1. 在这里为移动端菜单添加新页面 ^^^^^^^^^^^
|
||||
// =======================================================
|
||||
@@ -59,6 +60,7 @@ export default function Header() {
|
||||
const { t } = useTranslation()
|
||||
const { logout } = useAuth()
|
||||
const profile = useMainStore((store) => store.profile)
|
||||
const isAdmin = profile?.role === 0
|
||||
|
||||
const location = useLocation()
|
||||
const isDesktop = useMediaQuery("(min-width: 890px)")
|
||||
@@ -146,18 +148,20 @@ export default function Header() {
|
||||
{t("Profile")}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false)
|
||||
navigate("/dashboard/settings")
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Settings />
|
||||
{t("Settings")}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false)
|
||||
navigate("/dashboard/settings")
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Settings />
|
||||
{t("Settings")}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
@@ -264,6 +268,15 @@ export default function Header() {
|
||||
<Link to="/dashboard/server-group">{t("Group")}</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NzNavigationMenuLink
|
||||
asChild
|
||||
active={location.pathname === "/dashboard/transfer"}
|
||||
className={navigationMenuTriggerStyle()}
|
||||
>
|
||||
<Link to="/dashboard/transfer">{t("Transfer.Title")}</Link>
|
||||
</NzNavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -353,18 +366,20 @@ export default function Header() {
|
||||
{t("Profile")}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false)
|
||||
navigate("/dashboard/settings")
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Settings />
|
||||
{t("Settings")}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDropdownOpen(false)
|
||||
navigate("/dashboard/settings")
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Settings />
|
||||
{t("Settings")}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={logout} className="cursor-pointer">
|
||||
@@ -399,8 +414,7 @@ const useInterval = (callback: () => void, delay?: number | null) => {
|
||||
function Overview() {
|
||||
const { t } = useTranslation()
|
||||
const profile = useMainStore((store) => store.profile)
|
||||
const timeOption = DateTime.TIME_SIMPLE
|
||||
timeOption.hour12 = true
|
||||
const timeOption = { ...DateTime.TIME_SIMPLE, hour12: true }
|
||||
const [timeString, setTimeString] = useState(
|
||||
DateTime.now().setLocale("en-US").toLocaleString(timeOption),
|
||||
)
|
||||
|
||||
@@ -147,11 +147,11 @@ const generateCommand = (
|
||||
const env = envParts.join(" ")
|
||||
|
||||
const envWinParts = [
|
||||
`$env:NZ_SERVER=\"${install_host}\";`,
|
||||
`$env:NZ_TLS=\"${tls || false}\";`,
|
||||
`$env:NZ_CLIENT_SECRET=\"${agent_secret}\";`,
|
||||
`$env:NZ_SERVER="${install_host}";`,
|
||||
`$env:NZ_TLS="${tls || false}";`,
|
||||
`$env:NZ_CLIENT_SECRET="${agent_secret}";`,
|
||||
]
|
||||
if (uuid) envWinParts.push(`$env:NZ_UUID=\"${uuid}\";`)
|
||||
if (uuid) envWinParts.push(`$env:NZ_UUID="${uuid}";`)
|
||||
const env_win = envWinParts.join("")
|
||||
|
||||
switch (type) {
|
||||
@@ -161,6 +161,7 @@ const generateCommand = (
|
||||
}
|
||||
case OSTypes.Windows: {
|
||||
return `${env_win} [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Ssl3 -bor [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12;set-ExecutionPolicy RemoteSigned;Invoke-WebRequest ${window.location.origin}/script/agent.ps1 -OutFile C:\\install.ps1;powershell.exe C:\\install.ps1`
|
||||
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown OS: ${type}`)
|
||||
|
||||
+31
-17
@@ -46,16 +46,17 @@ const natFormSchema = z.object({
|
||||
domain: z.string(),
|
||||
})
|
||||
|
||||
type NatFormData = z.infer<typeof natFormSchema>
|
||||
type NatFormInput = z.input<typeof natFormSchema>
|
||||
type NatFormData = z.output<typeof natFormSchema>
|
||||
|
||||
export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = useForm<NatFormData>({
|
||||
resolver: zodResolver(natFormSchema as any),
|
||||
const form = useForm<NatFormInput, unknown, NatFormData>({
|
||||
resolver: zodResolver(natFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
name: data.name ?? "",
|
||||
enabled: (data as any).enabled ?? false,
|
||||
enabled: data.enabled ?? false,
|
||||
server_id: data.server_id ?? 0,
|
||||
host: data.host ?? "",
|
||||
domain: data.domain ?? "",
|
||||
@@ -76,11 +77,16 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
|
||||
|
||||
const onSubmit = async (values: NatFormData) => {
|
||||
try {
|
||||
data?.id ? await updateNAT(data.id, values) : await createNAT(values)
|
||||
} catch (e) {
|
||||
if (data?.id) {
|
||||
await updateNAT(data.id, values)
|
||||
} else {
|
||||
await createNAT(values)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
description: t("Results.UnExpectedError"),
|
||||
description:
|
||||
e instanceof Error && e.message ? e.message : t("Results.UnExpectedError"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -119,15 +125,23 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="server_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Server")} ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="1" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
const { value, ...fieldProps } = field
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Server")} ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={String(value ?? "")}
|
||||
{...fieldProps}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -169,7 +183,7 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
checked={field.value === true}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">{t("Enable")}</Label>
|
||||
|
||||
@@ -64,9 +64,11 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof notificationGroupFormSchema>) => {
|
||||
try {
|
||||
data?.group.id
|
||||
? await updateNotificationGroup(data.group.id, values)
|
||||
: await createNotificationGroup(values)
|
||||
if (data?.group.id) {
|
||||
await updateNotificationGroup(data.group.id, values)
|
||||
} else {
|
||||
await createNotificationGroup(values)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
|
||||
+19
-11
@@ -62,10 +62,14 @@ const notificationFormSchema = z.object({
|
||||
|
||||
export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
||||
const { t } = useTranslation()
|
||||
type notificationFormData = z.infer<typeof notificationFormSchema>
|
||||
type NotificationFormInput = z.input<typeof notificationFormSchema>
|
||||
type NotificationFormData = z.output<typeof notificationFormSchema>
|
||||
type NotificationDefaults = ModelNotification &
|
||||
Partial<Pick<NotificationFormData, "skip_check">>
|
||||
const notificationDefaults: NotificationDefaults | undefined = data
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(notificationFormSchema) as any,
|
||||
const form = useForm<NotificationFormInput, unknown, NotificationFormData>({
|
||||
resolver: zodResolver(notificationFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
name: data.name ?? "",
|
||||
@@ -75,10 +79,12 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
||||
request_header: data.request_header ?? "",
|
||||
request_body: data.request_body ?? "",
|
||||
verify_tls: data.verify_tls ?? false,
|
||||
skip_check: data.skip_check ?? false,
|
||||
skip_check: notificationDefaults?.skip_check ?? false,
|
||||
format_metric_units: data.format_metric_units ?? false,
|
||||
type: data.type ?? 1,
|
||||
type: (data as any)?.type ?? 1,
|
||||
}
|
||||
|
||||
|
||||
: {
|
||||
name: "",
|
||||
url: "",
|
||||
@@ -98,9 +104,13 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const onSubmit = async (values: notificationFormData) => {
|
||||
const onSubmit = async (values: NotificationFormData) => {
|
||||
try {
|
||||
data?.id ? await updateNotification(data.id, values) : await createNotification(values)
|
||||
if (data?.id) {
|
||||
await updateNotification(data.id, values)
|
||||
} else {
|
||||
await createNotification(values)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
@@ -128,10 +138,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
||||
<DialogDescription />
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit as any)}
|
||||
className="space-y-2 my-2"
|
||||
>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -343,6 +350,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="justify-end">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" className="my-2" variant="secondary">
|
||||
|
||||
@@ -49,7 +49,7 @@ const agentConfigSchema = z.object({
|
||||
try {
|
||||
JSON.parse(val)
|
||||
return true
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
@@ -66,7 +66,7 @@ const agentConfigSchema = z.object({
|
||||
try {
|
||||
JSON.parse(val)
|
||||
return true
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
@@ -129,7 +129,7 @@ export const ServerConfigCard = ({ sid, menuItem = false, ...props }: ServerConf
|
||||
}
|
||||
}
|
||||
if (open) fetchData()
|
||||
}, [open])
|
||||
}, [open, sid, t])
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(agentConfigSchema) as any,
|
||||
|
||||
@@ -64,9 +64,11 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof serverGroupFormSchema>) => {
|
||||
try {
|
||||
data?.group.id
|
||||
? await updateServerGroup(data.group.id, values)
|
||||
: await createServerGroup(values)
|
||||
if (data?.group.id) {
|
||||
await updateServerGroup(data.group.id, values)
|
||||
} else {
|
||||
await createServerGroup(values)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
|
||||
@@ -28,7 +28,8 @@ import { asOptionalField } from "@/lib/utils"
|
||||
import { ModelServer } from "@/types"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useForm, useWatch } from "react-hook-form"
|
||||
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
import { KeyedMutator } from "swr"
|
||||
@@ -55,7 +56,7 @@ const serverFormSchema = z.object({
|
||||
try {
|
||||
JSON.parse(val)
|
||||
return true
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
@@ -88,6 +89,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
})
|
||||
const enableDDNS = useWatch({ control: form.control, name: "enable_ddns" })
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@@ -189,7 +191,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{form.watch("enable_ddns") ? (
|
||||
{enableDDNS ? (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control as any}
|
||||
@@ -331,6 +333,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter className="justify-end">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" className="my-2" variant="secondary">
|
||||
|
||||
@@ -56,8 +56,8 @@ const serviceFormSchema = z.object({
|
||||
cover: z.coerce.number().int().min(0),
|
||||
display_index: z.coerce.number().int(),
|
||||
duration: z.coerce.number().int().min(30),
|
||||
enable_show_in_service: asOptionalField(z.boolean()),
|
||||
enable_trigger_task: asOptionalField(z.boolean()),
|
||||
hide_for_guest: asOptionalField(z.boolean()),
|
||||
fail_trigger_tasks: z.array(z.number()),
|
||||
fail_trigger_tasks_raw: z.string(),
|
||||
latency_notify: asOptionalField(z.boolean()),
|
||||
@@ -113,11 +113,14 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
values.skip_servers = conv.arrToRecord(values.skip_servers_raw)
|
||||
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)
|
||||
const { skip_servers_raw, ...requiredFields } = values
|
||||
const requiredFields = { ...values }
|
||||
delete (requiredFields as Record<string, unknown>).skip_servers_raw
|
||||
try {
|
||||
data?.id
|
||||
? await updateService(data.id, requiredFields)
|
||||
: await createService(requiredFields)
|
||||
if (data?.id) {
|
||||
await updateService(data.id, requiredFields)
|
||||
} else {
|
||||
await createService(requiredFields)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast(t("Error"), {
|
||||
@@ -232,7 +235,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enable_show_in_service"
|
||||
name="hide_for_guest"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
@@ -242,7 +245,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">
|
||||
{t("ShowInService")}
|
||||
{t("HideForGuest")}
|
||||
</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
@@ -8,10 +8,11 @@ export const SettingsTab = ({ className }: { className?: string }) => {
|
||||
const { profile } = useAuth()
|
||||
|
||||
const isAdmin = profile?.role === 0
|
||||
const colsClass = isAdmin ? "grid-cols-5" : "grid-cols-1"
|
||||
|
||||
return (
|
||||
<Tabs defaultValue={window.location.pathname} className={className}>
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsList className={`grid w-full ${colsClass}`}>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<TabsTrigger value="/dashboard/settings" asChild>
|
||||
@@ -20,13 +21,16 @@ export const SettingsTab = ({ className }: { className?: string }) => {
|
||||
<TabsTrigger value="/dashboard/settings/user" asChild>
|
||||
<Link to="/dashboard/settings/user">{t("User")}</Link>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="/dashboard/settings/online-user" asChild>
|
||||
<Link to="/dashboard/settings/online-user">{t("OnlineUser")}</Link>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="/dashboard/settings/waf" asChild>
|
||||
<Link to="/dashboard/settings/waf">{t("WAF")}</Link>
|
||||
</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
<TabsTrigger value="/dashboard/settings/online-user" asChild>
|
||||
<Link to="/dashboard/settings/online-user">{t("OnlineUser")}</Link>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="/dashboard/settings/waf" asChild>
|
||||
<Link to="/dashboard/settings/waf">{t("WAF")}</Link>
|
||||
<TabsTrigger value="/dashboard/settings/api-tokens" asChild>
|
||||
<Link to="/dashboard/settings/api-tokens">{t("ApiTokens")}</Link>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -70,7 +70,7 @@ function Calendar({
|
||||
: "[&>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",
|
||||
month_grid: "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",
|
||||
|
||||
+113
-3
@@ -1,7 +1,117 @@
|
||||
export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
type SVGProps = React.ComponentPropsWithoutRef<"svg">
|
||||
type IconMarkupState = { status: "ready"; icon: ParsedIconMarkup } | { status: "missing" }
|
||||
|
||||
type ParsedIconMarkup = {
|
||||
title?: string
|
||||
viewBox: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const simpleIconModules = import.meta.glob("/node_modules/simple-icons/icons/*.svg", {
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
})
|
||||
|
||||
const iconLoadersBySlug = Object.fromEntries(
|
||||
Object.entries(simpleIconModules).map(([path, loader]) => [
|
||||
path.slice(path.lastIndexOf("/") + 1, -".svg".length),
|
||||
loader as () => Promise<string>,
|
||||
]),
|
||||
)
|
||||
|
||||
const iconMarkupCache = new Map<string, IconMarkupState>()
|
||||
|
||||
function toProviderSlug(provider: string) {
|
||||
return provider
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/gi, "")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function loadProviderIconMarkup(provider: string) {
|
||||
const loader = iconLoadersBySlug[toProviderSlug(provider)]
|
||||
return loader ? loader() : null
|
||||
}
|
||||
|
||||
function parseIconMarkup(markup: string): ParsedIconMarkup | null {
|
||||
const viewBox = markup.match(/viewBox="([^"]+)"/i)?.[1] ?? "0 0 24 24"
|
||||
const title = markup.match(/<title>([^<]*)<\/title>/i)?.[1]
|
||||
const path = markup.match(/<path d="([^"]+)"/i)?.[1]
|
||||
|
||||
if (!path) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { title, viewBox, path }
|
||||
}
|
||||
|
||||
async function loadProviderIcon(provider: string): Promise<IconMarkupState> {
|
||||
const markup = await loadProviderIconMarkup(provider)
|
||||
if (!markup) {
|
||||
return { status: "missing" }
|
||||
}
|
||||
|
||||
const parsed = parseIconMarkup(markup)
|
||||
if (!parsed) {
|
||||
return { status: "missing" }
|
||||
}
|
||||
|
||||
return { status: "ready", icon: parsed }
|
||||
}
|
||||
|
||||
export function OAuthProviderIcon({
|
||||
provider,
|
||||
title,
|
||||
...props
|
||||
}: SVGProps & { provider: string; title?: string }) {
|
||||
const providerSlug = toProviderSlug(provider)
|
||||
const [iconState, setIconState] = useState<IconMarkupState | null>(() => {
|
||||
return iconMarkupCache.get(providerSlug) ?? null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (iconMarkupCache.has(providerSlug)) {
|
||||
setIconState(iconMarkupCache.get(providerSlug) ?? null)
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
loadProviderIcon(provider)
|
||||
.then((result) => {
|
||||
if (cancelled) return
|
||||
iconMarkupCache.set(providerSlug, result)
|
||||
setIconState(result)
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
const missingState = { status: "missing" } as const
|
||||
iconMarkupCache.set(providerSlug, missingState)
|
||||
setIconState(missingState)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [provider, providerSlug])
|
||||
|
||||
if (!iconState || iconState.status !== "ready") {
|
||||
return null
|
||||
}
|
||||
|
||||
const iconTitle = title ?? iconState.icon.title
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 496 512" fill="currentColor" {...props}>
|
||||
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
|
||||
<svg
|
||||
viewBox={iconState.icon.viewBox}
|
||||
fill="currentColor"
|
||||
aria-hidden={iconTitle ? undefined : "true"}
|
||||
role={iconTitle ? "img" : undefined}
|
||||
{...props}
|
||||
>
|
||||
{iconTitle ? <title>{iconTitle}</title> : null}
|
||||
<path d={iconState.icon.path} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { InputHTMLAttributes, forwardRef } from "react"
|
||||
|
||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
|
||||
export type InputProps = InputHTMLAttributes<HTMLInputElement>
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
|
||||
@@ -141,7 +141,6 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
animation = 0,
|
||||
maxCount = 3,
|
||||
modalPopover = false,
|
||||
asChild = false,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { TableCell, TableHead, TableRow } from "@/components/ui/table"
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery"
|
||||
import { virtualizedTableFeatures } from "@/lib/table"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
ColumnDef,
|
||||
Row,
|
||||
RowData,
|
||||
SortDirection,
|
||||
SortingState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
useTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { HTMLAttributes, JSX, forwardRef, useEffect, useRef, useState } from "react"
|
||||
import { TableVirtuoso } from "react-virtuoso"
|
||||
@@ -26,7 +26,9 @@ const TableComponent = forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTab
|
||||
)
|
||||
TableComponent.displayName = "TableComponent"
|
||||
|
||||
const TableRowComponent = <TData,>(rows: Row<TData>[]) =>
|
||||
const TableRowComponent = <TData extends RowData>(
|
||||
rows: Row<typeof virtualizedTableFeatures, TData>[],
|
||||
) =>
|
||||
function getTableRow(props: HTMLAttributes<HTMLTableRowElement>) {
|
||||
// @ts-expect-error data-index is a valid attribute
|
||||
const index = props["data-index"]
|
||||
@@ -64,34 +66,33 @@ function SortingIndicator({ isSorted }: { isSorted: SortDirection | false }) {
|
||||
)
|
||||
}
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
interface DataTableProps<TData extends RowData> {
|
||||
columns: ColumnDef<typeof virtualizedTableFeatures, TData>[]
|
||||
data: TData[]
|
||||
rowComponent?: (
|
||||
rows: Row<TData>[],
|
||||
rows: Row<typeof virtualizedTableFeatures, TData>[],
|
||||
) => (props: HTMLAttributes<HTMLTableRowElement>) => JSX.Element | null
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
export function DataTable<TData extends RowData>({
|
||||
columns,
|
||||
data,
|
||||
rowComponent,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
}: DataTableProps<TData>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{
|
||||
id: "type",
|
||||
desc: true,
|
||||
},
|
||||
])
|
||||
const table = useReactTable({
|
||||
const table = useTable({
|
||||
features: virtualizedTableFeatures,
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
})
|
||||
|
||||
const { rows } = table.getRowModel()
|
||||
|
||||
Reference in new issue
Block a user