mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-09-19 09:40:13 +00:00
Stabilize CI runtime for updated deps and unblock Dependabot auto-format flow (#185)
* chore: apply ci and dependabot remediation changes * chore: auto-fix linting and formatting issues --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
+18
-18
@@ -1,21 +1,21 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
npm-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
npm-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22.19.0
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
src/main.tsx
|
||||
.github/workflows/*
|
||||
|
||||
Generated
+3
-1
@@ -3460,7 +3460,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
|
||||
@@ -24,9 +24,9 @@ export default defineConfig({
|
||||
webServer: process.env.E2E_SKIP_WEBSERVER
|
||||
? undefined
|
||||
: {
|
||||
command: "npm run dev -- --host 127.0.0.1",
|
||||
url: baseURL + "/dashboard/login",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
command: "npm run dev -- --host 127.0.0.1",
|
||||
url: baseURL + "/dashboard/login",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120_000,
|
||||
},
|
||||
})
|
||||
|
||||
+94
-23
@@ -47,20 +47,56 @@ export const deleteApiToken = async (id: number): Promise<void> => {
|
||||
}
|
||||
|
||||
export const SCOPE_OPTIONS = [
|
||||
{ value: "nezha:inventory:read", label: "Inventory: read", desc: "List servers & groups (server.list)" },
|
||||
{ value: "nezha:inventory:delete", label: "Inventory: delete", desc: "Delete servers & server groups" },
|
||||
{
|
||||
value: "nezha:inventory:read",
|
||||
label: "Inventory: read",
|
||||
desc: "List servers & groups (server.list)",
|
||||
},
|
||||
{
|
||||
value: "nezha:inventory:delete",
|
||||
label: "Inventory: delete",
|
||||
desc: "Delete servers & server groups",
|
||||
},
|
||||
{ value: "nezha:inventory:*", label: "Inventory: all", desc: "List + delete servers & groups" },
|
||||
{ value: "nezha:server:read", label: "Server: read", desc: "Inspect a server, read files & metrics (needs server id)" },
|
||||
{
|
||||
value: "nezha:server:read",
|
||||
label: "Server: read",
|
||||
desc: "Inspect a server, read files & metrics (needs server id)",
|
||||
},
|
||||
{ value: "nezha:server:write", label: "Server: write", desc: "Edit a server, push files" },
|
||||
{ value: "nezha:server:delete", label: "Server: delete", desc: "Delete files on a server" },
|
||||
{ value: "nezha:server:exec", label: "Server: exec", desc: "Run shell commands on servers" },
|
||||
{ value: "nezha:server:*", label: "Server: all", desc: "Every server operation (read+write+delete+exec)" },
|
||||
{ value: "nezha:service:read", label: "Service monitor: read", desc: "List service monitors & history" },
|
||||
{ value: "nezha:service:write", label: "Service monitor: write", desc: "Create / edit service monitors" },
|
||||
{ value: "nezha:service:delete", label: "Service monitor: delete", desc: "Delete service monitors" },
|
||||
{ value: "nezha:service:*", label: "Service monitor: all", desc: "Every service monitor permission" },
|
||||
{
|
||||
value: "nezha:server:*",
|
||||
label: "Server: all",
|
||||
desc: "Every server operation (read+write+delete+exec)",
|
||||
},
|
||||
{
|
||||
value: "nezha:service:read",
|
||||
label: "Service monitor: read",
|
||||
desc: "List service monitors & history",
|
||||
},
|
||||
{
|
||||
value: "nezha:service:write",
|
||||
label: "Service monitor: write",
|
||||
desc: "Create / edit service monitors",
|
||||
},
|
||||
{
|
||||
value: "nezha:service:delete",
|
||||
label: "Service monitor: delete",
|
||||
desc: "Delete service monitors",
|
||||
},
|
||||
{
|
||||
value: "nezha:service:*",
|
||||
label: "Service monitor: all",
|
||||
desc: "Every service monitor permission",
|
||||
},
|
||||
{ value: "nezha:alertrule:read", label: "Alert rule: read", desc: "List alert rules" },
|
||||
{ value: "nezha:alertrule:write", label: "Alert rule: write", desc: "Create / edit alert rules" },
|
||||
{
|
||||
value: "nezha:alertrule:write",
|
||||
label: "Alert rule: write",
|
||||
desc: "Create / edit alert rules",
|
||||
},
|
||||
{ value: "nezha:alertrule:delete", label: "Alert rule: delete", desc: "Delete alert rules" },
|
||||
{ value: "nezha:alertrule:*", label: "Alert rule: all", desc: "Every alert-rule permission" },
|
||||
{ value: "nezha:cron:read", label: "Cron: read", desc: "List scheduled tasks" },
|
||||
@@ -69,13 +105,41 @@ export const SCOPE_OPTIONS = [
|
||||
{ value: "nezha:cron:exec", label: "Cron: trigger", desc: "Manually trigger scheduled tasks" },
|
||||
{ value: "nezha:cron:*", label: "Cron: all", desc: "Every cron permission" },
|
||||
{ value: "nezha:notification:read", label: "Notification: read", desc: "List notifications" },
|
||||
{ value: "nezha:notification:write", label: "Notification: write", desc: "Create / edit notifications" },
|
||||
{ value: "nezha:notification:delete", label: "Notification: delete", desc: "Delete notifications" },
|
||||
{ value: "nezha:notification:*", label: "Notification: all", desc: "Every notification permission" },
|
||||
{ value: "nezha:notification-group:read", label: "Notification group: read", desc: "List notification groups" },
|
||||
{ value: "nezha:notification-group:write", label: "Notification group: write", desc: "Create / edit groups" },
|
||||
{ value: "nezha:notification-group:delete", label: "Notification group: delete", desc: "Delete groups" },
|
||||
{ value: "nezha:notification-group:*", label: "Notification group: all", desc: "Every notification-group permission" },
|
||||
{
|
||||
value: "nezha:notification:write",
|
||||
label: "Notification: write",
|
||||
desc: "Create / edit notifications",
|
||||
},
|
||||
{
|
||||
value: "nezha:notification:delete",
|
||||
label: "Notification: delete",
|
||||
desc: "Delete notifications",
|
||||
},
|
||||
{
|
||||
value: "nezha:notification:*",
|
||||
label: "Notification: all",
|
||||
desc: "Every notification permission",
|
||||
},
|
||||
{
|
||||
value: "nezha:notification-group:read",
|
||||
label: "Notification group: read",
|
||||
desc: "List notification groups",
|
||||
},
|
||||
{
|
||||
value: "nezha:notification-group:write",
|
||||
label: "Notification group: write",
|
||||
desc: "Create / edit groups",
|
||||
},
|
||||
{
|
||||
value: "nezha:notification-group:delete",
|
||||
label: "Notification group: delete",
|
||||
desc: "Delete groups",
|
||||
},
|
||||
{
|
||||
value: "nezha:notification-group:*",
|
||||
label: "Notification group: all",
|
||||
desc: "Every notification-group permission",
|
||||
},
|
||||
{ value: "nezha:ddns:read", label: "DDNS: read", desc: "List DDNS profiles" },
|
||||
{ value: "nezha:ddns:write", label: "DDNS: write", desc: "Create / edit DDNS profiles" },
|
||||
{ value: "nezha:ddns:delete", label: "DDNS: delete", desc: "Delete DDNS profiles" },
|
||||
@@ -86,21 +150,27 @@ export const SCOPE_OPTIONS = [
|
||||
{ value: "nezha:nat:*", label: "NAT: all", desc: "Every NAT permission" },
|
||||
{ value: "nezha:transfer:read", label: "Transfer: read", desc: "Read server transfer state" },
|
||||
{ value: "nezha:transfer:write", label: "Transfer: write", desc: "Cancel / retry transfers" },
|
||||
{ value: "nezha:transfer:delete", label: "Transfer: delete", desc: "Delete server transfer records" },
|
||||
{
|
||||
value: "nezha:transfer:delete",
|
||||
label: "Transfer: delete",
|
||||
desc: "Delete server transfer records",
|
||||
},
|
||||
{ value: "nezha:transfer:*", label: "Transfer: all", desc: "Every transfer permission" },
|
||||
{ value: "nezha:admin:*", label: "Admin: all (admin only)", desc: "User / WAF / Setting / Online-user management" },
|
||||
{
|
||||
value: "nezha:admin:*",
|
||||
label: "Admin: all (admin only)",
|
||||
desc: "User / WAF / Setting / Online-user management",
|
||||
},
|
||||
{ value: "nezha:*", label: "Everything (admin only)", desc: "Full access to all resources" },
|
||||
] as const
|
||||
|
||||
export type Scope = (typeof SCOPE_OPTIONS)[number]["value"]
|
||||
|
||||
export type ParseServerIDsResult =
|
||||
| { ok: true; value: number[] | undefined }
|
||||
| { ok: false; error: string }
|
||||
{ ok: true; value: number[] | undefined } | { ok: false; error: string }
|
||||
|
||||
export type ParseExpiresInDaysResult =
|
||||
| { ok: true; value: number | undefined }
|
||||
| { ok: false; error: string }
|
||||
{ ok: true; value: number | undefined } | { ok: false; error: string }
|
||||
|
||||
// Validates the "expires in days" field before it is sent to the backend.
|
||||
// The backend model field is `ExpiresInDays int`, so a fractional value would
|
||||
@@ -130,7 +200,8 @@ export function parseServerIDsInput(raw: string): ParseServerIDsResult {
|
||||
// Server IDs are uint64 on the backend; reject values that lose
|
||||
// precision as a JS number, otherwise the PAT could bind to a
|
||||
// different server than the operator typed.
|
||||
if (!Number.isSafeInteger(n) || n <= 0) return { ok: false, error: `invalid server id: ${p}` }
|
||||
if (!Number.isSafeInteger(n) || n <= 0)
|
||||
return { ok: false, error: `invalid server id: ${p}` }
|
||||
if (seen.has(n)) continue
|
||||
seen.add(n)
|
||||
out.push(n)
|
||||
|
||||
+5
-1
@@ -26,7 +26,11 @@ export const deleteServer = async (id: number[]): Promise<void> => {
|
||||
export const batchMoveServer = async (
|
||||
data: ModelBatchMoveServerForm,
|
||||
): Promise<ModelBatchMoveServerResult[]> => {
|
||||
return fetcher<ModelBatchMoveServerResult[]>(FetcherMethod.POST, "/api/v1/batch-move/server", data)
|
||||
return fetcher<ModelBatchMoveServerResult[]>(
|
||||
FetcherMethod.POST,
|
||||
"/api/v1/batch-move/server",
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
export const forceUpdateServer = async (id: number[]): Promise<ModelServerTaskResponse> => {
|
||||
|
||||
@@ -102,22 +102,22 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
resolver: zodResolver(alertRuleFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
...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),
|
||||
}
|
||||
...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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
@@ -195,10 +195,7 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
<DialogDescription />
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-2 my-2"
|
||||
>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -369,9 +366,9 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
...next[idx],
|
||||
min: e.target.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: undefined,
|
||||
}
|
||||
setRulesUI(next)
|
||||
@@ -392,9 +389,9 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
...next[idx],
|
||||
max: e.target.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: undefined,
|
||||
}
|
||||
setRulesUI(next)
|
||||
@@ -456,8 +453,8 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
try {
|
||||
return r.ignore
|
||||
? JSON.stringify(
|
||||
r.ignore,
|
||||
)
|
||||
r.ignore,
|
||||
)
|
||||
: ""
|
||||
} catch {
|
||||
return ""
|
||||
@@ -468,8 +465,8 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
try {
|
||||
const obj = e.target.value
|
||||
? JSON.parse(
|
||||
e.target.value,
|
||||
)
|
||||
e.target.value,
|
||||
)
|
||||
: undefined
|
||||
next[idx] = {
|
||||
...next[idx],
|
||||
@@ -523,9 +520,9 @@ export const AlertRuleCard: React.FC<AlertRuleCardProps> = ({ data, mutate }) =>
|
||||
cycle_interval: e.target
|
||||
.value
|
||||
? Number(
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
e.target
|
||||
.value,
|
||||
)
|
||||
: undefined,
|
||||
}
|
||||
setRulesUI(next)
|
||||
|
||||
+18
-18
@@ -72,25 +72,25 @@ export const CronCard: React.FC<CronCardProps> = ({ data, mutate }) => {
|
||||
resolver: zodResolver(cronFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
task_type: data.task_type ?? 0,
|
||||
name: data.name ?? "",
|
||||
scheduler: data.scheduler ?? "",
|
||||
command: data.command ?? "",
|
||||
servers: data.servers ?? [],
|
||||
cover: data.cover ?? 0,
|
||||
push_successful: data.push_successful ?? false,
|
||||
notification_group_id: data.notification_group_id ?? 0,
|
||||
}
|
||||
task_type: data.task_type ?? 0,
|
||||
name: data.name ?? "",
|
||||
scheduler: data.scheduler ?? "",
|
||||
command: data.command ?? "",
|
||||
servers: data.servers ?? [],
|
||||
cover: data.cover ?? 0,
|
||||
push_successful: data.push_successful ?? false,
|
||||
notification_group_id: data.notification_group_id ?? 0,
|
||||
}
|
||||
: {
|
||||
task_type: 0,
|
||||
name: "",
|
||||
scheduler: "",
|
||||
command: "",
|
||||
servers: [],
|
||||
cover: 0,
|
||||
push_successful: false,
|
||||
notification_group_id: 0,
|
||||
},
|
||||
task_type: 0,
|
||||
name: "",
|
||||
scheduler: "",
|
||||
command: "",
|
||||
servers: [],
|
||||
cover: 0,
|
||||
push_successful: false,
|
||||
notification_group_id: 0,
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
|
||||
+30
-30
@@ -76,37 +76,37 @@ export const DDNSCard: React.FC<DDNSCardProps> = ({ data, providers, mutate }) =
|
||||
resolver: zodResolver(ddnsFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
max_retries: data.max_retries ?? 3,
|
||||
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.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: data.max_retries ?? 3,
|
||||
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.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,
|
||||
enable_ipv4: false,
|
||||
enable_ipv6: false,
|
||||
name: "",
|
||||
provider: "dummy",
|
||||
domains: [],
|
||||
domains_raw: "",
|
||||
access_id: "",
|
||||
access_secret: "",
|
||||
webhook_url: "",
|
||||
webhook_method: undefined,
|
||||
webhook_request_type: undefined,
|
||||
webhook_request_body: "",
|
||||
webhook_headers: "",
|
||||
},
|
||||
max_retries: 3,
|
||||
enable_ipv4: false,
|
||||
enable_ipv6: false,
|
||||
name: "",
|
||||
provider: "dummy",
|
||||
domains: [],
|
||||
domains_raw: "",
|
||||
access_id: "",
|
||||
access_secret: "",
|
||||
webhook_url: "",
|
||||
webhook_method: undefined,
|
||||
webhook_request_type: undefined,
|
||||
webhook_request_body: "",
|
||||
webhook_headers: "",
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
|
||||
+30
-24
@@ -71,7 +71,10 @@ const arraysEqual = (a: Uint8Array, b: Uint8Array) => {
|
||||
return true
|
||||
}
|
||||
|
||||
export 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)
|
||||
@@ -162,30 +165,30 @@ export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({
|
||||
useEffect(() => {
|
||||
worker.onmessage = async (event: MessageEvent<FMWorkerData>) => {
|
||||
switch (event.data.type) {
|
||||
case FMWorkerOpcode.Error: {
|
||||
console.error("Error from worker", event.data.error)
|
||||
break
|
||||
}
|
||||
case FMWorkerOpcode.Progress: {
|
||||
handleReady.current = true
|
||||
break
|
||||
}
|
||||
case FMWorkerOpcode.Result: {
|
||||
handleReady.current = false
|
||||
|
||||
if (event.data.blob && event.data.fileName) {
|
||||
const url = URL.createObjectURL(event.data.blob)
|
||||
const anchor = document.createElement("a")
|
||||
anchor.href = url
|
||||
anchor.download = event.data.fileName
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
case FMWorkerOpcode.Error: {
|
||||
console.error("Error from worker", event.data.error)
|
||||
break
|
||||
}
|
||||
case FMWorkerOpcode.Progress: {
|
||||
handleReady.current = true
|
||||
break
|
||||
}
|
||||
case FMWorkerOpcode.Result: {
|
||||
handleReady.current = false
|
||||
|
||||
firstChunk.current = true
|
||||
if (dOpen) setdOpen(false)
|
||||
break
|
||||
}
|
||||
if (event.data.blob && event.data.fileName) {
|
||||
const url = URL.createObjectURL(event.data.blob)
|
||||
const anchor = document.createElement("a")
|
||||
anchor.href = url
|
||||
anchor.download = event.data.fileName
|
||||
anchor.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
firstChunk.current = true
|
||||
if (dOpen) setdOpen(false)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +345,10 @@ export const FMComponent: React.FC<FMProps & JSX.IntrinsicElements["div"]> = ({
|
||||
try {
|
||||
await copyToClipboard(formatPath(currentPath))
|
||||
} catch (error) {
|
||||
const description = error instanceof Error ? error.message : t("Results.UnExpectedError")
|
||||
const description =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("Results.UnExpectedError")
|
||||
toast("FM" + " " + t("Error"), {
|
||||
description,
|
||||
})
|
||||
|
||||
@@ -154,15 +154,15 @@ const generateCommand = (
|
||||
const env_win = envWinParts.join("")
|
||||
|
||||
switch (type) {
|
||||
case OSTypes.Linux:
|
||||
case OSTypes.macOS: {
|
||||
return `curl -L https://raw.githubusercontent.com/nezhahq/scripts/main/agent/install.sh -o agent.sh && chmod +x agent.sh && env ${env} ./agent.sh`
|
||||
}
|
||||
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 https://raw.githubusercontent.com/nezhahq/scripts/main/agent/install.ps1 -OutFile C:\\install.ps1;powershell.exe C:\\install.ps1`
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown OS: ${type}`)
|
||||
}
|
||||
case OSTypes.Linux:
|
||||
case OSTypes.macOS: {
|
||||
return `curl -L https://raw.githubusercontent.com/nezhahq/scripts/main/agent/install.sh -o agent.sh && chmod +x agent.sh && env ${env} ./agent.sh`
|
||||
}
|
||||
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 https://raw.githubusercontent.com/nezhahq/scripts/main/agent/install.ps1 -OutFile C:\\install.ps1;powershell.exe C:\\install.ps1`
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown OS: ${type}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -55,19 +55,19 @@ export const NATCard: React.FC<NATCardProps> = ({ data, mutate }) => {
|
||||
resolver: zodResolver(natFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
name: data.name ?? "",
|
||||
enabled: data.enabled ?? false,
|
||||
server_id: data.server_id ?? 0,
|
||||
host: data.host ?? "",
|
||||
domain: data.domain ?? "",
|
||||
}
|
||||
name: data.name ?? "",
|
||||
enabled: data.enabled ?? false,
|
||||
server_id: data.server_id ?? 0,
|
||||
host: data.host ?? "",
|
||||
domain: data.domain ?? "",
|
||||
}
|
||||
: {
|
||||
name: "",
|
||||
enabled: false,
|
||||
server_id: 0,
|
||||
host: "",
|
||||
domain: "",
|
||||
},
|
||||
name: "",
|
||||
enabled: false,
|
||||
server_id: 0,
|
||||
host: "",
|
||||
domain: "",
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
|
||||
@@ -48,13 +48,13 @@ export const NotificationGroupCard: React.FC<NotificationGroupCardProps> = ({ da
|
||||
resolver: zodResolver(notificationGroupFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
name: data.group.name,
|
||||
notifications: data.notifications,
|
||||
}
|
||||
name: data.group.name,
|
||||
notifications: data.notifications,
|
||||
}
|
||||
: {
|
||||
name: "",
|
||||
notifications: [],
|
||||
},
|
||||
name: "",
|
||||
notifications: [],
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
|
||||
+23
-25
@@ -64,34 +64,35 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
||||
const { t } = useTranslation()
|
||||
type NotificationFormInput = z.input<typeof notificationFormSchema>
|
||||
type NotificationFormData = z.output<typeof notificationFormSchema>
|
||||
type NotificationDefaults = ModelNotification & Partial<Pick<NotificationFormData, "skip_check">>
|
||||
type NotificationDefaults = ModelNotification &
|
||||
Partial<Pick<NotificationFormData, "skip_check">>
|
||||
const notificationDefaults: NotificationDefaults | undefined = data
|
||||
|
||||
const form = useForm<NotificationFormInput, unknown, NotificationFormData>({
|
||||
resolver: zodResolver(notificationFormSchema),
|
||||
defaultValues: 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.verify_tls ?? false,
|
||||
skip_check: notificationDefaults?.skip_check ?? false,
|
||||
format_metric_units: data.format_metric_units ?? false,
|
||||
}
|
||||
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.verify_tls ?? false,
|
||||
skip_check: notificationDefaults?.skip_check ?? false,
|
||||
format_metric_units: data.format_metric_units ?? false,
|
||||
}
|
||||
: {
|
||||
name: "",
|
||||
url: "",
|
||||
request_method: 1,
|
||||
request_type: 1,
|
||||
request_header: "",
|
||||
request_body: "",
|
||||
verify_tls: false,
|
||||
skip_check: false,
|
||||
format_metric_units: false,
|
||||
},
|
||||
name: "",
|
||||
url: "",
|
||||
request_method: 1,
|
||||
request_type: 1,
|
||||
request_header: "",
|
||||
request_body: "",
|
||||
verify_tls: false,
|
||||
skip_check: false,
|
||||
format_metric_units: false,
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
@@ -133,10 +134,7 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
||||
<DialogDescription />
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-2 my-2"
|
||||
>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2 my-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
|
||||
@@ -48,13 +48,13 @@ export const ServerGroupCard: React.FC<ServerGroupCardProps> = ({ data, mutate }
|
||||
resolver: zodResolver(serverGroupFormSchema),
|
||||
defaultValues: data
|
||||
? {
|
||||
name: data.group.name,
|
||||
servers: data.servers,
|
||||
}
|
||||
name: data.group.name,
|
||||
servers: data.servers,
|
||||
}
|
||||
: {
|
||||
name: "",
|
||||
servers: [],
|
||||
},
|
||||
name: "",
|
||||
servers: [],
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
|
||||
+14
-10
@@ -467,8 +467,10 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
{publicNoteObj.billingDataMod
|
||||
?.startDate
|
||||
? new Date(
|
||||
publicNoteObj.billingDataMod!.startDate!,
|
||||
).toLocaleDateString()
|
||||
publicNoteObj
|
||||
.billingDataMod!
|
||||
.startDate!,
|
||||
).toLocaleDateString()
|
||||
: "YYYY-MM-DD"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -492,8 +494,10 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
.billingDataMod
|
||||
?.startDate
|
||||
? new Date(
|
||||
publicNoteObj.billingDataMod!.startDate!,
|
||||
)
|
||||
publicNoteObj
|
||||
.billingDataMod!
|
||||
.startDate!,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onSelect={(d) => {
|
||||
@@ -560,17 +564,17 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
{publicNoteObj.billingDataMod
|
||||
?.endDate
|
||||
? publicNoteObj
|
||||
.billingDataMod
|
||||
?.endDate ===
|
||||
.billingDataMod
|
||||
?.endDate ===
|
||||
"0000-00-00T23:59:59+08:00"
|
||||
? t(
|
||||
"PublicNote.NoExpiry",
|
||||
)
|
||||
"PublicNote.NoExpiry",
|
||||
)
|
||||
: new Date(
|
||||
publicNoteObj
|
||||
.billingDataMod
|
||||
?.endDate as string,
|
||||
).toLocaleDateString()
|
||||
).toLocaleDateString()
|
||||
: "YYYY-MM-DD"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -601,7 +605,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
publicNoteObj
|
||||
.billingDataMod
|
||||
?.endDate as string,
|
||||
)
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onSelect={(d) => {
|
||||
|
||||
+21
-21
@@ -80,28 +80,28 @@ export const ServiceCard: React.FC<ServiceCardProps> = ({ data, mutate }) => {
|
||||
resolver: zodResolver(serviceFormSchema) as any,
|
||||
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 : {}),
|
||||
}
|
||||
...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 : {}),
|
||||
}
|
||||
: {
|
||||
type: 1,
|
||||
cover: 0,
|
||||
display_index: 0,
|
||||
name: "",
|
||||
target: "",
|
||||
max_latency: 0.0,
|
||||
min_latency: 0.0,
|
||||
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: [],
|
||||
},
|
||||
type: 1,
|
||||
cover: 0,
|
||||
display_index: 0,
|
||||
name: "",
|
||||
target: "",
|
||||
max_latency: 0.0,
|
||||
min_latency: 0.0,
|
||||
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: [],
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
|
||||
@@ -23,8 +23,7 @@ const badgeVariants = cva(
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
|
||||
@@ -31,8 +31,7 @@ const buttonVariants = cva(
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
type SVGProps = React.ComponentPropsWithoutRef<"svg">
|
||||
type IconMarkupState =
|
||||
| { status: "ready"; icon: ParsedIconMarkup }
|
||||
| { status: "missing" }
|
||||
type IconMarkupState = { status: "ready"; icon: ParsedIconMarkup } | { status: "missing" }
|
||||
|
||||
type ParsedIconMarkup = {
|
||||
title?: string
|
||||
@@ -26,7 +24,10 @@ const iconLoadersBySlug = Object.fromEntries(
|
||||
const iconMarkupCache = new Map<string, IconMarkupState>()
|
||||
|
||||
function toProviderSlug(provider: string) {
|
||||
return provider.trim().replace(/[^a-z0-9]+/gi, "").toLowerCase()
|
||||
return provider
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/gi, "")
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function loadProviderIconMarkup(provider: string) {
|
||||
|
||||
@@ -53,60 +53,60 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>((props,
|
||||
>
|
||||
{(() => {
|
||||
switch (props.icon) {
|
||||
case "clipboard": {
|
||||
return <Clipboard />
|
||||
}
|
||||
case "check": {
|
||||
return <Check />
|
||||
}
|
||||
case "edit": {
|
||||
return <Edit2 />
|
||||
}
|
||||
case "trash": {
|
||||
return <Trash2 />
|
||||
}
|
||||
case "plus": {
|
||||
return <Plus />
|
||||
}
|
||||
case "terminal": {
|
||||
return <Terminal />
|
||||
}
|
||||
case "update": {
|
||||
return <CircleArrowUp />
|
||||
}
|
||||
case "folder-closed": {
|
||||
return <FolderClosed />
|
||||
}
|
||||
case "play": {
|
||||
return <Play />
|
||||
}
|
||||
case "download": {
|
||||
return <Download />
|
||||
}
|
||||
case "upload": {
|
||||
return <Upload />
|
||||
}
|
||||
case "menu": {
|
||||
return <Menu />
|
||||
}
|
||||
case "ban": {
|
||||
return <BanIcon />
|
||||
}
|
||||
case "expand": {
|
||||
return <Expand />
|
||||
}
|
||||
case "cog": {
|
||||
return <CogIcon />
|
||||
}
|
||||
case "minus": {
|
||||
return <Minus />
|
||||
}
|
||||
case "user-pen": {
|
||||
return <UserPen />
|
||||
}
|
||||
case "more": {
|
||||
return <MoreHorizontal />
|
||||
}
|
||||
case "clipboard": {
|
||||
return <Clipboard />
|
||||
}
|
||||
case "check": {
|
||||
return <Check />
|
||||
}
|
||||
case "edit": {
|
||||
return <Edit2 />
|
||||
}
|
||||
case "trash": {
|
||||
return <Trash2 />
|
||||
}
|
||||
case "plus": {
|
||||
return <Plus />
|
||||
}
|
||||
case "terminal": {
|
||||
return <Terminal />
|
||||
}
|
||||
case "update": {
|
||||
return <CircleArrowUp />
|
||||
}
|
||||
case "folder-closed": {
|
||||
return <FolderClosed />
|
||||
}
|
||||
case "play": {
|
||||
return <Play />
|
||||
}
|
||||
case "download": {
|
||||
return <Download />
|
||||
}
|
||||
case "upload": {
|
||||
return <Upload />
|
||||
}
|
||||
case "menu": {
|
||||
return <Menu />
|
||||
}
|
||||
case "ban": {
|
||||
return <BanIcon />
|
||||
}
|
||||
case "expand": {
|
||||
return <Expand />
|
||||
}
|
||||
case "cog": {
|
||||
return <CogIcon />
|
||||
}
|
||||
case "minus": {
|
||||
return <Minus />
|
||||
}
|
||||
case "user-pen": {
|
||||
return <UserPen />
|
||||
}
|
||||
case "more": {
|
||||
return <MoreHorizontal />
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</Button>
|
||||
|
||||
@@ -67,7 +67,8 @@ const multiSelectVariants = cva(
|
||||
* Props for MultiSelect component
|
||||
*/
|
||||
interface MultiSelectProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
extends
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof multiSelectVariants> {
|
||||
/**
|
||||
* An array of option objects to be displayed in the multi-select component.
|
||||
|
||||
@@ -37,7 +37,8 @@ const sheetVariants = cva(
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
extends
|
||||
ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {
|
||||
setOpen: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
@@ -158,9 +158,9 @@ export function DataTable<TData, TValue>({
|
||||
{...{
|
||||
style: header.column.getCanSort()
|
||||
? {
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
}
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
}
|
||||
: {},
|
||||
onClick:
|
||||
header.column.getToggleSortingHandler(),
|
||||
|
||||
+23
-20
@@ -53,27 +53,30 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const login = useCallback(async (username: string, password: string) => {
|
||||
try {
|
||||
await loginRequest(username, password)
|
||||
const user = await getProfile()
|
||||
authEpoch.current++
|
||||
user.role = normalizeRole(user.role)
|
||||
setProfile(user)
|
||||
navigate("/dashboard")
|
||||
} catch (error: any) {
|
||||
const msg = error?.message
|
||||
if (msg === "ApiErrorUnauthorized" || msg === "Unauthorized") {
|
||||
toast(t("InvalidUsernameOrPassword"))
|
||||
} else {
|
||||
toast(msg || t("NetworkError"))
|
||||
const login = useCallback(
|
||||
async (username: string, password: string) => {
|
||||
try {
|
||||
await loginRequest(username, password)
|
||||
const user = await getProfile()
|
||||
authEpoch.current++
|
||||
user.role = normalizeRole(user.role)
|
||||
setProfile(user)
|
||||
navigate("/dashboard")
|
||||
} catch (error: any) {
|
||||
const msg = error?.message
|
||||
if (msg === "ApiErrorUnauthorized" || msg === "Unauthorized") {
|
||||
toast(t("InvalidUsernameOrPassword"))
|
||||
} else {
|
||||
toast(msg || t("NetworkError"))
|
||||
}
|
||||
} finally {
|
||||
// An explicit login resolves auth regardless of the still-pending
|
||||
// mount probe; clear loading so ProtectedRoute stops blanking.
|
||||
setLoading(false)
|
||||
}
|
||||
} finally {
|
||||
// An explicit login resolves auth regardless of the still-pending
|
||||
// mount probe; clear loading so ProtectedRoute stops blanking.
|
||||
setLoading(false)
|
||||
}
|
||||
}, [navigate, setProfile, t])
|
||||
},
|
||||
[navigate, setProfile, t],
|
||||
)
|
||||
|
||||
const loginOauth2 = useCallback(async () => {
|
||||
try {
|
||||
|
||||
+1
-1
@@ -117,5 +117,5 @@ body,
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply rounded-full border-[1px] border-solid border-transparent bg-border bg-clip-padding;
|
||||
@apply bg-border rounded-full border-[1px] border-solid border-transparent bg-clip-padding;
|
||||
}
|
||||
|
||||
+42
-42
@@ -15,53 +15,53 @@ onmessage = async function (event) {
|
||||
const { operation, arrayBuffer, fileName } = event.data
|
||||
|
||||
switch (operation) {
|
||||
case Operation.WriteHeader: {
|
||||
const dataView = new DataView(arrayBuffer)
|
||||
expectedLength = Number(dataView.getBigUint64(4, false))
|
||||
receivedLength = 0
|
||||
case Operation.WriteHeader: {
|
||||
const dataView = new DataView(arrayBuffer)
|
||||
expectedLength = Number(dataView.getBigUint64(4, false))
|
||||
receivedLength = 0
|
||||
|
||||
// Create a new temporary file
|
||||
root = await navigator.storage.getDirectory()
|
||||
draftHandle = await root.getFileHandle(fileName, { create: true })
|
||||
accessHandle = await draftHandle.createSyncAccessHandle()
|
||||
// Create a new temporary file
|
||||
root = await navigator.storage.getDirectory()
|
||||
draftHandle = await root.getFileHandle(fileName, { create: true })
|
||||
accessHandle = await draftHandle.createSyncAccessHandle()
|
||||
|
||||
// Inform that file handle is created
|
||||
const dataChunk = arrayBuffer.slice(12)
|
||||
receivedLength += dataChunk.byteLength
|
||||
accessHandle.write(dataChunk, { at: 0 })
|
||||
const progress = "got handle"
|
||||
postMessage({ type: 1, progress: progress })
|
||||
break
|
||||
}
|
||||
case Operation.WriteChunks: {
|
||||
if (!accessHandle) {
|
||||
throw new Error("accessHandle is undefined")
|
||||
// Inform that file handle is created
|
||||
const dataChunk = arrayBuffer.slice(12)
|
||||
receivedLength += dataChunk.byteLength
|
||||
accessHandle.write(dataChunk, { at: 0 })
|
||||
const progress = "got handle"
|
||||
postMessage({ type: 1, progress: progress })
|
||||
break
|
||||
}
|
||||
|
||||
accessHandle.write(arrayBuffer, { at: receivedLength })
|
||||
receivedLength += arrayBuffer.byteLength
|
||||
|
||||
if (receivedLength === expectedLength) {
|
||||
accessHandle.flush()
|
||||
accessHandle.close()
|
||||
|
||||
const fileBlob = await draftHandle.getFile()
|
||||
const blob = new Blob([fileBlob], { type: "application/octet-stream" })
|
||||
|
||||
postMessage({ type: 2, blob: blob, fileName: fileName })
|
||||
}
|
||||
break
|
||||
}
|
||||
case Operation.DeleteFiles: {
|
||||
for await (const [name, handle] of root.entries()) {
|
||||
if (handle.kind === "file") {
|
||||
await root.removeEntry(name)
|
||||
} else if (handle.kind === "directory") {
|
||||
await root.removeEntry(name, { recursive: true })
|
||||
case Operation.WriteChunks: {
|
||||
if (!accessHandle) {
|
||||
throw new Error("accessHandle is undefined")
|
||||
}
|
||||
|
||||
accessHandle.write(arrayBuffer, { at: receivedLength })
|
||||
receivedLength += arrayBuffer.byteLength
|
||||
|
||||
if (receivedLength === expectedLength) {
|
||||
accessHandle.flush()
|
||||
accessHandle.close()
|
||||
|
||||
const fileBlob = await draftHandle.getFile()
|
||||
const blob = new Blob([fileBlob], { type: "application/octet-stream" })
|
||||
|
||||
postMessage({ type: 2, blob: blob, fileName: fileName })
|
||||
}
|
||||
break
|
||||
}
|
||||
case Operation.DeleteFiles: {
|
||||
for await (const [name, handle] of root.entries()) {
|
||||
if (handle.kind === "file") {
|
||||
await root.removeEntry(name)
|
||||
} else if (handle.kind === "directory") {
|
||||
await root.removeEntry(name, { recursive: true })
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) postMessage({ type: 0, error: error.message })
|
||||
|
||||
@@ -165,9 +165,9 @@ export default function AlertRulePage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -98,7 +98,10 @@ export default function ApiTokensPage() {
|
||||
</TableRow>
|
||||
) : !data || data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-24 text-center text-muted-foreground">
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
className="h-24 text-center text-muted-foreground"
|
||||
>
|
||||
{t("NoResults")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -119,10 +122,14 @@ export default function ApiTokensPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{tok.server_ids?.length ? tok.server_ids.join(", ") : t("ApiTokenAllServers")}
|
||||
{tok.server_ids?.length
|
||||
? tok.server_ids.join(", ")
|
||||
: t("ApiTokenAllServers")}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{tok.expires_at ? new Date(tok.expires_at).toLocaleString() : t("ApiTokenNever")}
|
||||
{tok.expires_at
|
||||
? new Date(tok.expires_at).toLocaleString()
|
||||
: t("ApiTokenNever")}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{tok.last_used_at
|
||||
@@ -239,7 +246,8 @@ function CreateApiTokenDialog({
|
||||
<Label>{t("Scopes")}</Label>
|
||||
<div className="max-h-72 space-y-2 overflow-y-auto pr-2">
|
||||
{SCOPE_OPTIONS.map((s) => {
|
||||
const adminOnly = s.value === "nezha:*" || s.value === "nezha:admin:*"
|
||||
const adminOnly =
|
||||
s.value === "nezha:*" || s.value === "nezha:admin:*"
|
||||
const disabled = adminOnly && !isAdmin
|
||||
return (
|
||||
<label
|
||||
@@ -248,12 +256,16 @@ function CreateApiTokenDialog({
|
||||
>
|
||||
<Checkbox
|
||||
checked={scopes.includes(s.value)}
|
||||
onCheckedChange={() => !disabled && toggleScope(s.value)}
|
||||
onCheckedChange={() =>
|
||||
!disabled && toggleScope(s.value)
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-mono text-xs">{s.value}</span>
|
||||
<span className="text-xs text-muted-foreground">{s.desc}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{s.desc}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
|
||||
+12
-12
@@ -110,15 +110,15 @@ export default function CronPage() {
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{(() => {
|
||||
switch (s.cover) {
|
||||
case 0: {
|
||||
return <span>{t("IgnoreAll")}</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>{t("CoverAll")}</span>
|
||||
}
|
||||
case 2: {
|
||||
return <span>{t("OnAlert")}</span>
|
||||
}
|
||||
case 0: {
|
||||
return <span>{t("IgnoreAll")}</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>{t("CoverAll")}</span>
|
||||
}
|
||||
case 2: {
|
||||
return <span>{t("OnAlert")}</span>
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
@@ -233,9 +233,9 @@ export default function CronPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
+3
-3
@@ -170,9 +170,9 @@ export default function DDNSPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -108,14 +108,14 @@ function Login() {
|
||||
</form>
|
||||
{settingData?.config?.oauth2_providers &&
|
||||
settingData?.config?.oauth2_providers.length > 0 && (
|
||||
<section className="flex items-center my-3 w-full">
|
||||
<Separator className="flex-1" />
|
||||
<div className="flex justify-center text-xs text-muted-foreground w-full max-w-[100px]">
|
||||
<section className="flex items-center my-3 w-full">
|
||||
<Separator className="flex-1" />
|
||||
<div className="flex justify-center text-xs text-muted-foreground w-full max-w-[100px]">
|
||||
OAuth2
|
||||
</div>
|
||||
<Separator className="flex-1" />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
<Separator className="flex-1" />
|
||||
</section>
|
||||
)}
|
||||
</Form>
|
||||
<div className="mt-3 flex flex-col gap-3">
|
||||
{settingData?.config?.oauth2_providers?.map((p: string) => (
|
||||
|
||||
+3
-3
@@ -153,9 +153,9 @@ export default function NATPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -146,9 +146,9 @@ export default function NotificationGroupPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -161,9 +161,9 @@ export default function NotificationPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -264,9 +264,9 @@ export default function OnlineUserPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -145,9 +145,9 @@ export default function ServerGroupPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -292,9 +292,9 @@ export default function ServerPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -90,12 +90,12 @@ export default function ServicePage() {
|
||||
<div className="max-w-48 whitespace-normal break-words">
|
||||
{(() => {
|
||||
switch (s.cover) {
|
||||
case 0: {
|
||||
return <span>{t("CoverAll")}</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>{t("IgnoreAll")}</span>
|
||||
}
|
||||
case 0: {
|
||||
return <span>{t("CoverAll")}</span>
|
||||
}
|
||||
case 1: {
|
||||
return <span>{t("IgnoreAll")}</span>
|
||||
}
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
@@ -199,9 +199,9 @@ export default function ServicePage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
+16
-14
@@ -76,19 +76,19 @@ export default function SettingsPage() {
|
||||
resolver: zodResolver(settingFormSchema) as any,
|
||||
defaultValues: config
|
||||
? {
|
||||
...config.config,
|
||||
user_template:
|
||||
...config.config,
|
||||
user_template:
|
||||
config.config?.user_template ||
|
||||
Object.keys(config.frontend_templates?.filter((t) => !t.is_admin) || {})[0] ||
|
||||
"user-dist",
|
||||
}
|
||||
}
|
||||
: {
|
||||
ip_change_notification_group_id: 0,
|
||||
cover: 1,
|
||||
site_name: "",
|
||||
language: "",
|
||||
user_template: "user-dist",
|
||||
},
|
||||
ip_change_notification_group_id: 0,
|
||||
cover: 1,
|
||||
site_name: "",
|
||||
language: "",
|
||||
user_template: "user-dist",
|
||||
},
|
||||
resetOptions: {
|
||||
keepDefaultValues: false,
|
||||
},
|
||||
@@ -224,9 +224,13 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</SelectItem>
|
||||
<div className="px-8 py-1">
|
||||
{safeExternalHref(template.repository) ? (
|
||||
{safeExternalHref(
|
||||
template.repository,
|
||||
) ? (
|
||||
<a
|
||||
href={safeExternalHref(template.repository)}
|
||||
href={safeExternalHref(
|
||||
template.repository,
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
|
||||
@@ -552,9 +556,7 @@ export default function SettingsPage() {
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<Label className="text-sm">
|
||||
{t("EnableMCP")}
|
||||
</Label>
|
||||
<Label className="text-sm">{t("EnableMCP")}</Label>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { swrFetcher } from "@/api/api"
|
||||
import { cancelServerTransfer, retryServerTransfer } from "@/api/transfer"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -45,10 +45,7 @@ export default function TransferPage() {
|
||||
const { profile } = useAuth()
|
||||
const isAdmin = profile?.role === 0
|
||||
const callerID = profile?.id
|
||||
const { data, mutate, error } = useSWR<ModelServerTransfer[]>(
|
||||
"/api/v1/transfer",
|
||||
swrFetcher,
|
||||
)
|
||||
const { data, mutate, error } = useSWR<ModelServerTransfer[]>("/api/v1/transfer", swrFetcher)
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
@@ -182,9 +179,7 @@ export default function TransferPage() {
|
||||
return (
|
||||
<div className="px-3">
|
||||
<h1 className="mt-6 text-2xl font-semibold">{t("Transfer.Title")}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1 max-w-2xl">
|
||||
{t("Transfer.PageHint")}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1 max-w-2xl">{t("Transfer.PageHint")}</p>
|
||||
<Table className="mt-6">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
+3
-3
@@ -139,9 +139,9 @@ export default function UserPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
+3
-3
@@ -283,9 +283,9 @@ export default function WAFPage() {
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -6,10 +6,13 @@ const realFetch = globalThis.fetch
|
||||
|
||||
function mockFetch(payload: unknown, ok = true, success = true) {
|
||||
globalThis.fetch = vi.fn(async () => {
|
||||
return new Response(JSON.stringify({ success, error: success ? "" : "boom", data: payload }), {
|
||||
status: ok ? 200 : 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
return new Response(
|
||||
JSON.stringify({ success, error: success ? "" : "boom", data: payload }),
|
||||
{
|
||||
status: ok ? 200 : 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
)
|
||||
}) as unknown as typeof fetch
|
||||
}
|
||||
|
||||
@@ -94,9 +97,9 @@ test("createApiToken POSTs /api/v1/api-tokens and serializes scopes / server_ids
|
||||
|
||||
test("createApiToken surfaces server error via thrown Error", async () => {
|
||||
mockFetch(null, true, false)
|
||||
await expect(
|
||||
createApiToken({ name: "x", scopes: ["nezha:server:read"] }),
|
||||
).rejects.toThrow("boom")
|
||||
await expect(createApiToken({ name: "x", scopes: ["nezha:server:read"] })).rejects.toThrow(
|
||||
"boom",
|
||||
)
|
||||
})
|
||||
|
||||
test("listApiTokens normalizes null scopes to an empty array so the table cannot crash", async () => {
|
||||
@@ -107,9 +110,7 @@ test("listApiTokens normalizes null scopes to an empty array so the table cannot
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
data: [
|
||||
{ id: 1, name: "legacy", scopes: null, created_at: "2025-01-01T00:00:00Z" },
|
||||
],
|
||||
data: [{ id: 1, name: "legacy", scopes: null, created_at: "2025-01-01T00:00:00Z" }],
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
)
|
||||
|
||||
@@ -9,8 +9,7 @@ const setProfileSpy = vi.fn((p: any) => {
|
||||
|
||||
vi.mock("./useMainStore", () => ({}))
|
||||
vi.mock("@/hooks/useMainStore", () => ({
|
||||
useMainStore: (selector: any) =>
|
||||
selector({ profile: profileStore, setProfile: setProfileSpy }),
|
||||
useMainStore: (selector: any) => selector({ profile: profileStore, setProfile: setProfileSpy }),
|
||||
}))
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
|
||||
@@ -9,8 +9,7 @@ const setProfileSpy = vi.fn((p: any) => {
|
||||
|
||||
vi.mock("./useMainStore", () => ({}))
|
||||
vi.mock("@/hooks/useMainStore", () => ({
|
||||
useMainStore: (selector: any) =>
|
||||
selector({ profile: profileStore, setProfile: setProfileSpy }),
|
||||
useMainStore: (selector: any) => selector({ profile: profileStore, setProfile: setProfileSpy }),
|
||||
}))
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BatchMoveServerIcon } from "@/components/batch-move-server-icon"
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
||||
|
||||
@@ -26,8 +27,6 @@ vi.mock("@/api/server", () => ({
|
||||
batchMoveServer: (...args: unknown[]) => batchMoveServer(...args),
|
||||
}))
|
||||
|
||||
import { BatchMoveServerIcon } from "@/components/batch-move-server-icon"
|
||||
|
||||
beforeEach(() => {
|
||||
toastCalls.length = 0
|
||||
batchMoveServer.mockReset()
|
||||
|
||||
@@ -83,7 +83,9 @@ test("auto refresh-token uses POST (backend route is POST)", async () => {
|
||||
// Revoke (DELETE) commonly returns 204 / empty body. The fetcher must not
|
||||
// blow up on response.json() of an empty body and must resolve successfully.
|
||||
test("DELETE tolerates an empty 204 response body", async () => {
|
||||
globalThis.fetch = vi.fn(async () => new Response(null, { status: 204 })) as unknown as typeof fetch
|
||||
globalThis.fetch = vi.fn(
|
||||
async () => new Response(null, { status: 204 }),
|
||||
) as unknown as typeof fetch
|
||||
|
||||
await expect(fetcher(FetcherMethod.DELETE, "/api/v1/api-tokens/9")).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { act, render, waitFor } from "@testing-library/react"
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
||||
|
||||
import { FMComponent } from "../components/fm"
|
||||
|
||||
type DivProps = React.ComponentPropsWithoutRef<"div"> & { asChild?: boolean }
|
||||
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
|
||||
asChild?: boolean
|
||||
@@ -106,8 +108,6 @@ vi.mock("@/lib/utils", () => ({
|
||||
formatPath: (path: string) => path,
|
||||
}))
|
||||
|
||||
import { FMComponent } from "../components/fm"
|
||||
|
||||
const webSockets: MockWebSocket[] = []
|
||||
|
||||
class MockWebSocket extends EventTarget implements WebSocket {
|
||||
@@ -235,7 +235,10 @@ test("FM websocket lifecycle reuses the socket on path changes", async () => {
|
||||
|
||||
socket.sent.length = 0
|
||||
await act(async () => {
|
||||
await socket.onmessage?.call(socket, new MessageEvent("message", { data: encodeCompleteMessage() }))
|
||||
await socket.onmessage?.call(
|
||||
socket,
|
||||
new MessageEvent("message", { data: encodeCompleteMessage() }),
|
||||
)
|
||||
})
|
||||
|
||||
expect(webSockets).toHaveLength(1)
|
||||
|
||||
@@ -32,15 +32,11 @@ function renderAtPath(path: string, child: React.ReactNode) {
|
||||
<Routes>
|
||||
<Route
|
||||
path="/dashboard/login"
|
||||
element={
|
||||
<ProtectedRoute>{child}</ProtectedRoute>
|
||||
}
|
||||
element={<ProtectedRoute>{child}</ProtectedRoute>}
|
||||
/>
|
||||
<Route
|
||||
path="/dashboard/*"
|
||||
element={
|
||||
<ProtectedRoute>{child}</ProtectedRoute>
|
||||
}
|
||||
element={<ProtectedRoute>{child}</ProtectedRoute>}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
@@ -50,18 +46,12 @@ function renderAtPath(path: string, child: React.ReactNode) {
|
||||
}
|
||||
|
||||
test("ProtectedRoute does not mount children for protected paths while auth is loading", async () => {
|
||||
await renderAtPath(
|
||||
"/dashboard",
|
||||
<div data-testid="protected-child">protected</div>,
|
||||
)
|
||||
await renderAtPath("/dashboard", <div data-testid="protected-child">protected</div>)
|
||||
expect(document.querySelector("[data-testid='protected-child']")).toBeNull()
|
||||
})
|
||||
|
||||
test("ProtectedRoute renders children on the login page even while auth is loading", async () => {
|
||||
await renderAtPath(
|
||||
"/dashboard/login",
|
||||
<div data-testid="login-child">login</div>,
|
||||
)
|
||||
await renderAtPath("/dashboard/login", <div data-testid="login-child">login</div>)
|
||||
expect(document.querySelector("[data-testid='login-child']")).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -100,9 +90,6 @@ test("ProtectedRoute redirects unauthenticated users without mounting protected
|
||||
test("ProtectedRoute renders children once an authenticated profile resolves", async () => {
|
||||
mockLoading = false
|
||||
mockProfile = { id: 1, role: 0 }
|
||||
await renderAtPath(
|
||||
"/dashboard",
|
||||
<div data-testid="protected-child">protected</div>,
|
||||
)
|
||||
await renderAtPath("/dashboard", <div data-testid="protected-child">protected</div>)
|
||||
expect(document.querySelector("[data-testid='protected-child']")).not.toBeNull()
|
||||
})
|
||||
|
||||
@@ -47,7 +47,9 @@ vi.mock("@/hooks/useNotfication", () => ({
|
||||
}))
|
||||
|
||||
const updateSettings = vi.fn()
|
||||
vi.mock("@/api/settings", () => ({ updateSettings: (...args: unknown[]) => updateSettings(...args) }))
|
||||
vi.mock("@/api/settings", () => ({
|
||||
updateSettings: (...args: unknown[]) => updateSettings(...args),
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
toastCalls.length = 0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ModelServerTransfer } from "@/types"
|
||||
import { act, render, screen, waitFor } from "@testing-library/react"
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest"
|
||||
|
||||
@@ -47,8 +48,6 @@ vi.mock("@/hooks/useMainStore", () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
import type { ModelServerTransfer } from "@/types"
|
||||
|
||||
let mockedRows: ModelServerTransfer[] = []
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -127,10 +126,7 @@ test("non-admin member who is only the ToUserID or InitiatorID sees neither Canc
|
||||
|
||||
test("admin sees both Cancel for pending and Retry for terminal", async () => {
|
||||
mockProfile = { id: 1, role: 0 }
|
||||
mockedRows = [
|
||||
makeRow({ id: 1, status: 0 }),
|
||||
makeRow({ id: 2, status: 2 }),
|
||||
]
|
||||
mockedRows = [makeRow({ id: 1, status: 0 }), makeRow({ id: 2, status: 2 })]
|
||||
|
||||
await renderPage()
|
||||
|
||||
|
||||
@@ -85,7 +85,10 @@ test("an API token can authenticate /mcp", async ({ page }) => {
|
||||
headers: await csrfHeaders(page),
|
||||
data: { ...baseSettings, enable_mcp: true },
|
||||
})
|
||||
expect(enableResp.ok(), "PATCH /api/v1/setting must succeed to enable MCP for the test").toBeTruthy()
|
||||
expect(
|
||||
enableResp.ok(),
|
||||
"PATCH /api/v1/setting must succeed to enable MCP for the test",
|
||||
).toBeTruthy()
|
||||
|
||||
try {
|
||||
const apiResp = await page.request.post("/api/v1/api-tokens", {
|
||||
|
||||
+12
-2
@@ -1,6 +1,13 @@
|
||||
import { expect } from "@playwright/test"
|
||||
|
||||
import { csrfRequest, defaultAdmin, expectAuthenticated, expectUnauthenticated, loginAs, test } from "./fixtures"
|
||||
import {
|
||||
csrfRequest,
|
||||
defaultAdmin,
|
||||
expectAuthenticated,
|
||||
expectUnauthenticated,
|
||||
loginAs,
|
||||
test,
|
||||
} from "./fixtures"
|
||||
|
||||
test("login persists session via cookie and getProfile succeeds", async ({ page }) => {
|
||||
await loginAs(page, defaultAdmin)
|
||||
@@ -51,7 +58,10 @@ test("password change rotates TokenVersion and revokes existing session", async
|
||||
reject_password: false,
|
||||
},
|
||||
})
|
||||
expect(restoreResp.ok(), "password restore must succeed so other suites can still log in").toBeTruthy()
|
||||
expect(
|
||||
restoreResp.ok(),
|
||||
"password restore must succeed so other suites can still log in",
|
||||
).toBeTruthy()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Page, Request, expect, test as base } from "@playwright/test"
|
||||
import { Page, Request, test as base, expect } from "@playwright/test"
|
||||
|
||||
export type LoginContext = {
|
||||
username: string
|
||||
@@ -39,7 +39,8 @@ export async function csrfHeaders(page: Page): Promise<Record<string, string>> {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
value = (await page.context().cookies()).find((c) => c.name === "nz-csrf")?.value ?? ""
|
||||
value =
|
||||
(await page.context().cookies()).find((c) => c.name === "nz-csrf")?.value ?? ""
|
||||
return value
|
||||
},
|
||||
{ timeout: 10_000 },
|
||||
|
||||
@@ -17,6 +17,9 @@ test("file manager creation only accepts POST", async ({ adminPage: page }) => {
|
||||
})
|
||||
expect(postResp.status()).toBe(200)
|
||||
const body = await postResp.json()
|
||||
expect(body.success, "without a connected agent server the POST surfaces a Service error, but the route is reachable").not.toBe(true)
|
||||
expect(
|
||||
body.success,
|
||||
"without a connected agent server the POST surfaces a Service error, but the route is reachable",
|
||||
).not.toBe(true)
|
||||
expect(body.error).toBeTruthy()
|
||||
})
|
||||
|
||||
@@ -2,7 +2,10 @@ import { expect } from "@playwright/test"
|
||||
|
||||
import { csrfHeaders, test } from "./fixtures"
|
||||
|
||||
test("server-group hides guest-empty groups from anonymous callers", async ({ adminPage: page, browser }) => {
|
||||
test("server-group hides guest-empty groups from anonymous callers", async ({
|
||||
adminPage: page,
|
||||
browser,
|
||||
}) => {
|
||||
const tag = Date.now().toString(36)
|
||||
const visibleName = `e2e-visible-${tag}`
|
||||
const hiddenName = `e2e-hidden-${tag}`
|
||||
@@ -12,7 +15,9 @@ test("server-group hides guest-empty groups from anonymous callers", async ({ ad
|
||||
// scenario boils down to "no group is guest-visible", which the assertion below still covers.
|
||||
const serversResp = await page.request.get("/api/v1/server")
|
||||
expect(serversResp.ok()).toBeTruthy()
|
||||
const serversBody = (await serversResp.json()) as { data: Array<{ id: number; hide_for_guest?: boolean }> }
|
||||
const serversBody = (await serversResp.json()) as {
|
||||
data: Array<{ id: number; hide_for_guest?: boolean }>
|
||||
}
|
||||
const publicServer = serversBody.data?.find((s) => !s.hide_for_guest)
|
||||
|
||||
const createdGroupIDs: number[] = []
|
||||
@@ -40,9 +45,14 @@ test("server-group hides guest-empty groups from anonymous callers", async ({ ad
|
||||
data: Array<{ group: { name: string }; servers: number[] }>
|
||||
}
|
||||
const names = (guestBody.data || []).map((it) => it.group.name)
|
||||
expect(names, "guest must NOT see groups with zero visible servers").not.toContain(hiddenName)
|
||||
expect(names, "guest must NOT see groups with zero visible servers").not.toContain(
|
||||
hiddenName,
|
||||
)
|
||||
if (publicServer) {
|
||||
expect(names, "guest still sees groups that contain a guest-visible server").toContain(visibleName)
|
||||
expect(
|
||||
names,
|
||||
"guest still sees groups that contain a guest-visible server",
|
||||
).toContain(visibleName)
|
||||
}
|
||||
} finally {
|
||||
await guestCtx.close()
|
||||
|
||||
Reference in New Issue
Block a user