chore: update translations (#101)

* chore: update translations

* fix: form values type conversion

* feat: terminal fullscreen mode

* fix
This commit is contained in:
UUBulb
2025-01-19 21:09:52 +08:00
committed by GitHub
parent 53a937af4b
commit ff11783945
16 changed files with 188 additions and 168 deletions

View File

@@ -82,7 +82,9 @@ const alertRuleFormSchema = z.object({
),
rules: z.array(ruleSchema),
fail_trigger_tasks: z.array(z.number()),
fail_trigger_tasks_raw: z.string(),
recover_trigger_tasks: z.array(z.number()),
recover_trigger_tasks_raw: z.string(),
notification_group_id: z.coerce.number().int(),
trigger_mode: z.coerce.number().int().min(0),
enable: asOptionalField(z.boolean()),
@@ -96,13 +98,17 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
? {
...data,
rules_raw: JSON.stringify(data.rules),
fail_trigger_tasks_raw: conv.arrToStr(data.fail_trigger_tasks),
recover_trigger_tasks_raw: conv.arrToStr(data.recover_trigger_tasks),
}
: {
name: "",
rules_raw: "",
rules: [],
fail_trigger_tasks: [],
fail_trigger_tasks_raw: "",
recover_trigger_tasks: [],
recover_trigger_tasks_raw: "",
notification_group_id: 0,
trigger_mode: 0,
},
@@ -115,6 +121,8 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
const onSubmit = async (values: z.infer<typeof alertRuleFormSchema>) => {
values.rules = 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
try {
data?.id
@@ -227,7 +235,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
/>
<FormField
control={form.control}
name="fail_trigger_tasks"
name="fail_trigger_tasks_raw"
render={({ field }) => (
<FormItem>
<FormLabel>
@@ -235,17 +243,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
{...field}
value={conv.arrToStr(field.value ?? [])}
onChange={(e) => {
const arr = conv
.strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}}
/>
<Input placeholder="1,2,3" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -253,7 +251,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
/>
<FormField
control={form.control}
name="recover_trigger_tasks"
name="recover_trigger_tasks_raw"
render={({ field }) => (
<FormItem>
<FormLabel>
@@ -261,17 +259,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
{...field}
value={conv.arrToStr(field.value ?? [])}
onChange={(e) => {
const arr = conv
.strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}}
/>
<Input placeholder="1,2,3" {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -57,6 +57,7 @@ const ddnsFormSchema = z.object({
name: z.string().min(1),
provider: z.string(),
domains: z.array(z.string()),
domains_raw: z.string(),
access_id: asOptionalField(z.string()),
access_secret: asOptionalField(z.string()),
webhook_url: asOptionalField(z.string().url()),
@@ -71,12 +72,16 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
const form = useForm<z.infer<typeof ddnsFormSchema>>({
resolver: zodResolver(ddnsFormSchema),
defaultValues: data
? data
? {
...data,
domains_raw: conv.arrToStr(data.domains),
}
: {
max_retries: 3,
name: "",
provider: "dummy",
domains: [],
domains_raw: "",
},
resetOptions: {
keepDefaultValues: false,
@@ -87,6 +92,7 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
const onSubmit = async (values: z.infer<typeof ddnsFormSchema>) => {
try {
values.domains = conv.strToArr(values.domains_raw)
data?.id ? await updateDDNSProfile(data.id, values) : await createDDNSProfile(values)
} catch (e) {
console.error(e)
@@ -156,22 +162,14 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
/>
<FormField
control={form.control}
name="domains"
name="domains_raw"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("Domains") + t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input
placeholder="www.example.com"
{...field}
value={conv.arrToStr(field.value ?? [])}
onChange={(e) => {
const arr = conv.strToArr(e.target.value)
field.onChange(arr)
}}
/>
<Input placeholder="www.example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -48,13 +48,17 @@ const serverFormSchema = z.object({
hide_for_guest: asOptionalField(z.boolean()),
enable_ddns: asOptionalField(z.boolean()),
ddns_profiles: asOptionalField(z.array(z.number())),
ddns_profiles_raw: asOptionalField(z.string()),
})
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const { t } = useTranslation()
const form = useForm<z.infer<typeof serverFormSchema>>({
resolver: zodResolver(serverFormSchema),
defaultValues: data,
defaultValues: {
...data,
ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined,
},
resetOptions: {
keepDefaultValues: false,
},
@@ -64,6 +68,9 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
const onSubmit = async (values: z.infer<typeof serverFormSchema>) => {
try {
values.ddns_profiles = values.ddns_profiles_raw
? conv.strToArr(values.ddns_profiles_raw).map(Number)
: undefined
await updateServer(data!.id!, values)
} catch (e) {
console.error(e)
@@ -119,24 +126,14 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
/>
<FormField
control={form.control}
name="ddns_profiles"
name="ddns_profiles_raw"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("DDNSProfiles") + t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
{...field}
value={conv.arrToStr(field.value || [])}
onChange={(e) => {
const arr = conv
.strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}}
/>
<Input placeholder="1,2,3" {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -58,6 +58,7 @@ const serviceFormSchema = z.object({
enable_show_in_service: asOptionalField(z.boolean()),
enable_trigger_task: asOptionalField(z.boolean()),
fail_trigger_tasks: z.array(z.number()),
fail_trigger_tasks_raw: z.string(),
latency_notify: asOptionalField(z.boolean()),
max_latency: z.coerce.number().int().min(0),
min_latency: z.coerce.number().int().min(0),
@@ -65,6 +66,7 @@ const serviceFormSchema = z.object({
notification_group_id: z.coerce.number().int(),
notify: asOptionalField(z.boolean()),
recover_trigger_tasks: z.array(z.number()),
recover_trigger_tasks_raw: z.string(),
skip_servers: z.record(z.boolean()),
skip_servers_raw: z.array(z.string()),
target: z.string(),
@@ -78,6 +80,8 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
defaultValues: data
? {
...data,
fail_trigger_tasks_raw: conv.arrToStr(data.fail_trigger_tasks),
recover_trigger_tasks_raw: conv.arrToStr(data.recover_trigger_tasks),
skip_servers_raw: conv.recordToStrArr(data.skip_servers ? data.skip_servers : {}),
}
: {
@@ -90,7 +94,9 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
duration: 30,
notification_group_id: 0,
fail_trigger_tasks: [],
fail_trigger_tasks_raw: "",
recover_trigger_tasks: [],
recover_trigger_tasks_raw: "",
skip_servers: {},
skip_servers_raw: [],
},
@@ -103,6 +109,8 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
const onSubmit = async (values: z.infer<typeof serviceFormSchema>) => {
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
try {
data?.id
@@ -400,7 +408,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
/>
<FormField
control={form.control}
name="fail_trigger_tasks"
name="fail_trigger_tasks_raw"
render={({ field }) => (
<FormItem>
<FormLabel>
@@ -408,17 +416,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
{...field}
value={conv.arrToStr(field.value ?? [])}
onChange={(e) => {
const arr = conv
.strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}}
/>
<Input placeholder="1,2,3" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -426,7 +424,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
/>
<FormField
control={form.control}
name="recover_trigger_tasks"
name="recover_trigger_tasks_raw"
render={({ field }) => (
<FormItem>
<FormLabel>
@@ -434,17 +432,7 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input
placeholder="1,2,3"
{...field}
value={conv.arrToStr(field.value ?? [])}
onChange={(e) => {
const arr = conv
.strToArr(e.target.value)
.map(Number)
field.onChange(arr)
}}
/>
<Input placeholder="1,2,3" {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -13,7 +13,7 @@ import { AttachAddon } from "@xterm/addon-attach"
import { FitAddon } from "@xterm/addon-fit"
import { Terminal } from "@xterm/xterm"
import "@xterm/xterm/css/xterm.css"
import { useEffect, useMemo, useRef, useState } from "react"
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"
import { useParams } from "react-router-dom"
import { toast } from "sonner"
@@ -26,119 +26,134 @@ interface XtermProps {
setClose: React.Dispatch<React.SetStateAction<boolean>>
}
const XtermComponent: React.FC<XtermProps & JSX.IntrinsicElements["div"]> = ({
wsUrl,
setClose,
...props
}) => {
const terminalIdRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<Terminal | null>(null)
const wsRef = useRef<WebSocket | null>(null)
const XtermComponent = forwardRef<HTMLDivElement, XtermProps & JSX.IntrinsicElements["div"]>(
({ wsUrl, setClose, ...props }, ref) => {
const terminalIdRef = useRef<HTMLDivElement>(null)
const terminalRef = useRef<Terminal | null>(null)
const wsRef = useRef<WebSocket | null>(null)
useEffect(() => {
return () => {
wsRef.current?.close()
terminalRef.current?.dispose()
}
}, [])
useImperativeHandle(ref, () => {
return {
...terminalIdRef.current!,
async requestFullscreen() {
await terminalIdRef.current?.requestFullscreen()
},
}
}, [])
useEffect(() => {
terminalRef.current = new Terminal({
cursorBlink: true,
fontSize: 16,
})
const ws = new WebSocket(wsUrl)
wsRef.current = ws
ws.binaryType = "arraybuffer"
ws.onopen = () => {
onResize()
}
ws.onclose = () => {
terminalRef.current?.dispose()
setClose(true)
}
ws.onerror = (e) => {
console.error(e)
toast("Websocket error", {
description: "View console for details.",
useEffect(() => {
return () => {
wsRef.current?.close()
terminalRef.current?.dispose()
}
}, [])
useEffect(() => {
terminalRef.current = new Terminal({
cursorBlink: true,
fontSize: 16,
})
}
}, [wsUrl])
const ws = new WebSocket(wsUrl)
wsRef.current = ws
ws.binaryType = "arraybuffer"
ws.onopen = () => {
onResize()
}
ws.onclose = () => {
terminalRef.current?.dispose()
setClose(true)
}
ws.onerror = (e) => {
console.error(e)
toast("Websocket error", {
description: "View console for details.",
})
}
}, [wsUrl])
const fitAddon = useRef(new FitAddon()).current
const sendResize = useRef(false)
const fitAddon = useRef(new FitAddon()).current
const sendResize = useRef(false)
const doResize = () => {
if (!terminalIdRef.current) return
const doResize = () => {
if (!terminalIdRef.current) return
fitAddon.fit()
fitAddon.fit()
const dimensions = fitAddon.proposeDimensions()
const dimensions = fitAddon.proposeDimensions()
if (dimensions) {
const prefix = new Int8Array([1])
const resizeMessage = new TextEncoder().encode(
JSON.stringify({
Rows: dimensions.rows,
Cols: dimensions.cols,
}),
)
if (dimensions) {
const prefix = new Int8Array([1])
const resizeMessage = new TextEncoder().encode(
JSON.stringify({
Rows: dimensions.rows,
Cols: dimensions.cols,
}),
)
const msg = new Int8Array(prefix.length + resizeMessage.length)
msg.set(prefix)
msg.set(resizeMessage, prefix.length)
const msg = new Int8Array(prefix.length + resizeMessage.length)
msg.set(prefix)
msg.set(resizeMessage, prefix.length)
wsRef.current?.send(msg)
}
}
const onResize = async () => {
if (sendResize.current) return
sendResize.current = true
try {
await sleep(1500)
doResize()
} catch (error) {
console.error("resize error", error)
} finally {
sendResize.current = false
}
}
useEffect(() => {
if (!wsRef.current || !terminalIdRef.current || !terminalRef.current) return
const attachAddon = new AttachAddon(wsRef.current)
terminalRef.current.loadAddon(attachAddon)
terminalRef.current.loadAddon(fitAddon)
terminalRef.current.open(terminalIdRef.current)
window.addEventListener("resize", onResize)
return () => {
window.removeEventListener("resize", onResize)
if (wsRef.current) {
wsRef.current.close()
wsRef.current?.send(msg)
}
}
}, [wsRef.current, terminalRef.current, terminalIdRef.current])
return <div ref={terminalIdRef} {...props} />
}
const onResize = async () => {
if (sendResize.current) return
sendResize.current = true
try {
await sleep(1500)
doResize()
} catch (error) {
console.error("resize error", error)
} finally {
sendResize.current = false
}
}
useEffect(() => {
if (!wsRef.current || !terminalIdRef.current || !terminalRef.current) return
const attachAddon = new AttachAddon(wsRef.current)
terminalRef.current.loadAddon(attachAddon)
terminalRef.current.loadAddon(fitAddon)
terminalRef.current.open(terminalIdRef.current)
window.addEventListener("resize", onResize)
return () => {
window.removeEventListener("resize", onResize)
if (wsRef.current) {
wsRef.current.close()
}
}
}, [wsRef.current, terminalRef.current, terminalIdRef.current])
return <div ref={terminalIdRef} {...props} />
},
)
export const TerminalPage = () => {
const { id } = useParams<{ id: string }>()
const [open, setOpen] = useState(false)
const terminal = useTerminal(id ? parseInt(id) : undefined)
const terminalIdRef = useRef<HTMLDivElement>(null)
return (
<div className="px-8">
<div className="flex mt-6 mb-4">
<h1 className="flex-1 text-3xl font-bold tracking-tight">{`Terminal (${id})`}</h1>
<div className="flex-2 flex ml-auto gap-2">
<IconButton
icon="expand"
onClick={async () => {
await terminalIdRef.current?.requestFullscreen()
}}
/>
<FMCard id={id} />
</div>
</div>
{terminal?.session_id ? (
<XtermComponent
className="max-h-[60%] mb-5"
ref={terminalIdRef}
className="max-h-[60%] mb-5 overflow-auto"
wsUrl={`/api/v1/ws/terminal/${terminal?.session_id}`}
setClose={setOpen}
/>

View File

@@ -6,6 +6,7 @@ import {
Clipboard,
Download,
Edit2,
Expand,
FolderClosed,
Menu,
Play,
@@ -31,6 +32,7 @@ export interface IconButtonProps extends ButtonProps {
| "upload"
| "menu"
| "ban"
| "expand"
}
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
@@ -82,6 +84,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
case "ban": {
return <BanIcon />
}
case "expand": {
return <Expand />
}
}
})()}
</Button>

View File

@@ -138,7 +138,7 @@
"InstallCommands": "Installationsbefehl",
"Actions": "Aktionen",
"Weight": "Gewicht (je größer die Zahl, desto höher wird es angezeigt)",
"TasksToTriggerOnAlert": "Die Aufgabe, die den Alarm ausgelöst hat",
"TasksToTriggerOnAlert": "Aufgaben, die bei Alarm ausgelöst werden sollen",
"NotifierGroup": "Benachrichtigungsgruppe",
"EditNAT": "NAT-Konfiguration bearbeiten",
"BindHostname": "Bind Domain Name",

View File

@@ -64,12 +64,13 @@
"Coverage": "Coverage",
"CoverAll": "Cover All",
"IgnoreAll": "Ignore All",
"OnAlert": "Alarmed Servers",
"SpecificServers": "Specific server",
"Type": "Type",
"Interval": "Interval",
"NotifierGroupID": "Notifier Group ID",
"Trigger": "On Trigger",
"TasksToTriggerOnAlert": "The task that triggered the alert",
"TasksToTriggerOnAlert": "Tasks to be triggered on alert",
"TasksToTriggerAfterRecovery": "Tasks to be triggered after recovery",
"Confirm": "Confirm",
"ConfirmDeletion": "Confirm Deletion?",

View File

@@ -31,7 +31,7 @@
"Weight": "Peso (cuanto mayor sea el número, más alto se mostrará)",
"DDNSProfiles": "IDs de perfil de DDNS",
"Target": "Objetivo",
"TasksToTriggerOnAlert": "La tarea que activó la alerta",
"TasksToTriggerOnAlert": "Tareas que se activarán en caso de alerta",
"Services": "Servicios",
"MaximumLatency": "Retraso máximo (ms)",
"Server": "Servidor",

View File

@@ -68,7 +68,7 @@
"Interval": "Intervallo",
"NotifierGroupID": "ID del gruppo di notifiche",
"Trigger": "Grilletto",
"TasksToTriggerOnAlert": "L'attività che ha attivato l'avviso",
"TasksToTriggerOnAlert": "Attività da attivare in caso di allarme",
"TasksToTriggerAfterRecovery": "Attività da attivare dopo il ripristino",
"Confirm": "Confermo",
"ConfirmDeletion": "Confermi l'eliminazione?",

View File

@@ -64,12 +64,13 @@
"Coverage": "覆盖范围",
"CoverAll": "覆盖全部",
"IgnoreAll": "忽略全部",
"OnAlert": "告警服务器",
"SpecificServers": "特定服务器",
"Type": "类型",
"Interval": "间隔",
"NotifierGroupID": "通知组ID",
"Trigger": "触发",
"TasksToTriggerOnAlert": "触发警报的任务",
"TasksToTriggerOnAlert": "告警时要触发的任务",
"TasksToTriggerAfterRecovery": "恢复后要触发的任务",
"Confirm": "确认",
"ConfirmDeletion": "确认删除?",

View File

@@ -64,12 +64,13 @@
"Coverage": "覆蓋範圍",
"CoverAll": "覆蓋全部",
"IgnoreAll": "忽略全部",
"OnAlert": "告警伺服器",
"SpecificServers": "特定伺服器",
"Type": "類型",
"Interval": "間隔",
"NotifierGroupID": "通知群組ID",
"Trigger": "觸發",
"TasksToTriggerOnAlert": "觸發警報的任務",
"TasksToTriggerOnAlert": "告警時要觸發的任務",
"TasksToTriggerAfterRecovery": "恢復後要觸發的任務",
"Confirm": "確認",
"ConfirmDeletion": "確認刪除?",

View File

@@ -111,13 +111,13 @@ export default function CronPage() {
{(() => {
switch (s.cover) {
case 0: {
return <span>Ignore All</span>
return <span>{t("IgnoreAll")}</span>
}
case 1: {
return <span>Cover All</span>
return <span>{t("CoverAll")}</span>
}
case 2: {
return <span>On alert</span>
return <span>{t("OnAlert")}</span>
}
}
})()}
@@ -129,6 +129,14 @@ export default function CronPage() {
header: t("SpecificServers"),
accessorKey: "servers",
accessorFn: (row) => row.servers,
cell: ({ row }) => {
const s = row.original
return (
<div className="max-w-16 whitespace-normal break-words">
<span>{s.servers.join(",")}</span>
</div>
)
},
},
{
header: t("LastExecution"),

View File

@@ -78,6 +78,14 @@ export default function NotificationGroupPage() {
header: t("Notifier") + "(ID)",
accessorKey: "notifications",
accessorFn: (row) => row.notifications,
cell: ({ row }) => {
const s = row.original
return (
<div className="max-w-48 whitespace-normal break-words">
<span>{s.notifications.join(",")}</span>
</div>
)
},
},
{
id: "actions",

View File

@@ -78,6 +78,14 @@ export default function ServerGroupPage() {
header: t("Server") + "(ID)",
accessorKey: "servers",
accessorFn: (row) => row.servers,
cell: ({ row }) => {
const s = row.original
return (
<div className="max-w-48 whitespace-normal break-words">
<span>{s.servers.join(",")}</span>
</div>
)
},
},
{
id: "actions",

View File

@@ -1,3 +1,5 @@
import animatePlugin from "tailwindcss-animate"
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
@@ -56,5 +58,5 @@ export default {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [animatePlugin],
}