This commit is contained in:
ryan
2026-07-13 15:08:40 +08:00
parent 08e8eea932
commit 1eff7878a1
441 changed files with 26491 additions and 18405 deletions
@@ -1,92 +1,92 @@
export type PerformanceFields = {
openresty_default_server_return_status: string
openresty_worker_processes: string
openresty_worker_connections: string
openresty_worker_rlimit_nofile: string
openresty_events_use: string
openresty_events_multi_accept_enabled: boolean
openresty_keepalive_timeout: string
openresty_keepalive_requests: string
openresty_client_header_timeout: string
openresty_client_body_timeout: string
openresty_client_max_body_size: string
openresty_large_client_header_buffers: string
openresty_send_timeout: string
openresty_proxy_connect_timeout: string
openresty_proxy_send_timeout: string
openresty_proxy_read_timeout: string
openresty_websocket_enabled: boolean
openresty_http3_enabled: boolean
openresty_proxy_request_buffering_enabled: boolean
openresty_proxy_buffering_enabled: boolean
openresty_proxy_buffers: string
openresty_proxy_buffer_size: string
openresty_proxy_busy_buffers_size: string
openresty_gzip_enabled: boolean
openresty_gzip_min_length: string
openresty_gzip_comp_level: string
openresty_cache_enabled: boolean
openresty_cache_path: string
openresty_cache_levels: string
openresty_cache_inactive: string
openresty_cache_max_size: string
openresty_cache_key_template: string
openresty_cache_lock_enabled: boolean
openresty_cache_lock_timeout: string
openresty_cache_use_stale: string
openresty_resolvers: string
}
openresty_default_server_return_status: string;
openresty_worker_processes: string;
openresty_worker_connections: string;
openresty_worker_rlimit_nofile: string;
openresty_events_use: string;
openresty_events_multi_accept_enabled: boolean;
openresty_keepalive_timeout: string;
openresty_keepalive_requests: string;
openresty_client_header_timeout: string;
openresty_client_body_timeout: string;
openresty_client_max_body_size: string;
openresty_large_client_header_buffers: string;
openresty_send_timeout: string;
openresty_proxy_connect_timeout: string;
openresty_proxy_send_timeout: string;
openresty_proxy_read_timeout: string;
openresty_websocket_enabled: boolean;
openresty_http3_enabled: boolean;
openresty_proxy_request_buffering_enabled: boolean;
openresty_proxy_buffering_enabled: boolean;
openresty_proxy_buffers: string;
openresty_proxy_buffer_size: string;
openresty_proxy_busy_buffers_size: string;
openresty_gzip_enabled: boolean;
openresty_gzip_min_length: string;
openresty_gzip_comp_level: string;
openresty_cache_enabled: boolean;
openresty_cache_path: string;
openresty_cache_levels: string;
openresty_cache_inactive: string;
openresty_cache_max_size: string;
openresty_cache_key_template: string;
openresty_cache_lock_enabled: boolean;
openresty_cache_lock_timeout: string;
openresty_cache_use_stale: string;
openresty_resolvers: string;
};
export const defaultPerformanceFields: PerformanceFields = {
openresty_default_server_return_status: "421",
openresty_worker_processes: "auto",
openresty_worker_connections: "4096",
openresty_worker_rlimit_nofile: "65535",
openresty_events_use: "epoll",
openresty_default_server_return_status: '421',
openresty_worker_processes: 'auto',
openresty_worker_connections: '4096',
openresty_worker_rlimit_nofile: '65535',
openresty_events_use: 'epoll',
openresty_events_multi_accept_enabled: true,
openresty_keepalive_timeout: "20",
openresty_keepalive_requests: "1000",
openresty_client_header_timeout: "15",
openresty_client_body_timeout: "15",
openresty_client_max_body_size: "64m",
openresty_large_client_header_buffers: "4 16k",
openresty_send_timeout: "30",
openresty_proxy_connect_timeout: "3",
openresty_proxy_send_timeout: "60",
openresty_proxy_read_timeout: "60",
openresty_keepalive_timeout: '20',
openresty_keepalive_requests: '1000',
openresty_client_header_timeout: '15',
openresty_client_body_timeout: '15',
openresty_client_max_body_size: '64m',
openresty_large_client_header_buffers: '4 16k',
openresty_send_timeout: '30',
openresty_proxy_connect_timeout: '3',
openresty_proxy_send_timeout: '60',
openresty_proxy_read_timeout: '60',
openresty_websocket_enabled: true,
openresty_http3_enabled: true,
openresty_proxy_request_buffering_enabled: false,
openresty_proxy_buffering_enabled: true,
openresty_proxy_buffers: "16 16k",
openresty_proxy_buffer_size: "8k",
openresty_proxy_busy_buffers_size: "64k",
openresty_proxy_buffers: '16 16k',
openresty_proxy_buffer_size: '8k',
openresty_proxy_busy_buffers_size: '64k',
openresty_gzip_enabled: true,
openresty_gzip_min_length: "1024",
openresty_gzip_comp_level: "5",
openresty_gzip_min_length: '1024',
openresty_gzip_comp_level: '5',
openresty_cache_enabled: false,
openresty_cache_path: "",
openresty_cache_levels: "1:2",
openresty_cache_inactive: "30m",
openresty_cache_max_size: "1g",
openresty_cache_key_template: "$scheme$host$request_uri",
openresty_cache_path: '',
openresty_cache_levels: '1:2',
openresty_cache_inactive: '30m',
openresty_cache_max_size: '1g',
openresty_cache_key_template: '$scheme$host$request_uri',
openresty_cache_lock_enabled: true,
openresty_cache_lock_timeout: "5s",
openresty_cache_lock_timeout: '5s',
openresty_cache_use_stale:
"error timeout updating http_500 http_502 http_503 http_504",
openresty_resolvers: "",
}
'error timeout updating http_500 http_502 http_503 http_504',
openresty_resolvers: '',
};
export function optionsToMap(options: Array<{ key: string; value: string }>) {
return options.reduce<Record<string, string>>((acc, option) => {
acc[option.key] = option.value
return acc
}, {})
acc[option.key] = option.value;
return acc;
}, {});
}
export function toBoolean(value: string | undefined, fallback: boolean) {
if (value === undefined) return fallback
return value === "true"
if (value === undefined) return fallback;
return value === 'true';
}
export function mapOptionsToFields(
@@ -94,33 +94,43 @@ export function mapOptionsToFields(
): PerformanceFields {
return {
openresty_default_server_return_status:
optionMap.openresty_default_server_return_status ?? "421",
openresty_worker_processes: optionMap.openresty_worker_processes ?? "auto",
openresty_worker_connections: optionMap.openresty_worker_connections ?? "4096",
optionMap.openresty_default_server_return_status ?? '421',
openresty_worker_processes: optionMap.openresty_worker_processes ?? 'auto',
openresty_worker_connections:
optionMap.openresty_worker_connections ?? '4096',
openresty_worker_rlimit_nofile:
optionMap.openresty_worker_rlimit_nofile ?? "65535",
openresty_events_use: optionMap.openresty_events_use ?? "epoll",
optionMap.openresty_worker_rlimit_nofile ?? '65535',
openresty_events_use: optionMap.openresty_events_use ?? 'epoll',
openresty_events_multi_accept_enabled: toBoolean(
optionMap.openresty_events_multi_accept_enabled,
true,
),
openresty_keepalive_timeout: optionMap.openresty_keepalive_timeout ?? "20",
openresty_keepalive_requests: optionMap.openresty_keepalive_requests ?? "1000",
openresty_keepalive_timeout: optionMap.openresty_keepalive_timeout ?? '20',
openresty_keepalive_requests:
optionMap.openresty_keepalive_requests ?? '1000',
openresty_client_header_timeout:
optionMap.openresty_client_header_timeout ?? "15",
openresty_client_body_timeout: optionMap.openresty_client_body_timeout ?? "15",
openresty_client_max_body_size: optionMap.openresty_client_max_body_size ?? "64m",
optionMap.openresty_client_header_timeout ?? '15',
openresty_client_body_timeout:
optionMap.openresty_client_body_timeout ?? '15',
openresty_client_max_body_size:
optionMap.openresty_client_max_body_size ?? '64m',
openresty_large_client_header_buffers:
optionMap.openresty_large_client_header_buffers ?? "4 16k",
openresty_send_timeout: optionMap.openresty_send_timeout ?? "30",
openresty_proxy_connect_timeout: optionMap.openresty_proxy_connect_timeout ?? "3",
openresty_proxy_send_timeout: optionMap.openresty_proxy_send_timeout ?? "60",
openresty_proxy_read_timeout: optionMap.openresty_proxy_read_timeout ?? "60",
optionMap.openresty_large_client_header_buffers ?? '4 16k',
openresty_send_timeout: optionMap.openresty_send_timeout ?? '30',
openresty_proxy_connect_timeout:
optionMap.openresty_proxy_connect_timeout ?? '3',
openresty_proxy_send_timeout:
optionMap.openresty_proxy_send_timeout ?? '60',
openresty_proxy_read_timeout:
optionMap.openresty_proxy_read_timeout ?? '60',
openresty_websocket_enabled: toBoolean(
optionMap.openresty_websocket_enabled,
true,
),
openresty_http3_enabled: toBoolean(optionMap.openresty_http3_enabled, false),
openresty_http3_enabled: toBoolean(
optionMap.openresty_http3_enabled,
false,
),
openresty_proxy_request_buffering_enabled: toBoolean(
optionMap.openresty_proxy_request_buffering_enabled,
false,
@@ -129,59 +139,63 @@ export function mapOptionsToFields(
optionMap.openresty_proxy_buffering_enabled,
true,
),
openresty_proxy_buffers: optionMap.openresty_proxy_buffers ?? "16 16k",
openresty_proxy_buffer_size: optionMap.openresty_proxy_buffer_size ?? "8k",
openresty_proxy_buffers: optionMap.openresty_proxy_buffers ?? '16 16k',
openresty_proxy_buffer_size: optionMap.openresty_proxy_buffer_size ?? '8k',
openresty_proxy_busy_buffers_size:
optionMap.openresty_proxy_busy_buffers_size ?? "64k",
optionMap.openresty_proxy_busy_buffers_size ?? '64k',
openresty_gzip_enabled: toBoolean(optionMap.openresty_gzip_enabled, true),
openresty_gzip_min_length: optionMap.openresty_gzip_min_length ?? "1024",
openresty_gzip_comp_level: optionMap.openresty_gzip_comp_level ?? "5",
openresty_cache_enabled: toBoolean(optionMap.openresty_cache_enabled, false),
openresty_cache_path: optionMap.openresty_cache_path ?? "",
openresty_cache_levels: optionMap.openresty_cache_levels ?? "1:2",
openresty_cache_inactive: optionMap.openresty_cache_inactive ?? "30m",
openresty_cache_max_size: optionMap.openresty_cache_max_size ?? "1g",
openresty_gzip_min_length: optionMap.openresty_gzip_min_length ?? '1024',
openresty_gzip_comp_level: optionMap.openresty_gzip_comp_level ?? '5',
openresty_cache_enabled: toBoolean(
optionMap.openresty_cache_enabled,
false,
),
openresty_cache_path: optionMap.openresty_cache_path ?? '',
openresty_cache_levels: optionMap.openresty_cache_levels ?? '1:2',
openresty_cache_inactive: optionMap.openresty_cache_inactive ?? '30m',
openresty_cache_max_size: optionMap.openresty_cache_max_size ?? '1g',
openresty_cache_key_template:
optionMap.openresty_cache_key_template ?? "$scheme$host$request_uri",
optionMap.openresty_cache_key_template ?? '$scheme$host$request_uri',
openresty_cache_lock_enabled: toBoolean(
optionMap.openresty_cache_lock_enabled,
true,
),
openresty_cache_lock_timeout: optionMap.openresty_cache_lock_timeout ?? "5s",
openresty_cache_lock_timeout:
optionMap.openresty_cache_lock_timeout ?? '5s',
openresty_cache_use_stale:
optionMap.openresty_cache_use_stale ??
"error timeout updating http_500 http_502 http_503 http_504",
openresty_resolvers: optionMap.openresty_resolvers ?? "",
}
'error timeout updating http_500 http_502 http_503 http_504',
openresty_resolvers: optionMap.openresty_resolvers ?? '',
};
}
function isPositiveInteger(value: string) {
const parsed = Number.parseInt(value, 10)
return !Number.isNaN(parsed) && parsed > 0
const parsed = Number.parseInt(value, 10);
return !Number.isNaN(parsed) && parsed > 0;
}
function isSizeValue(value: string) {
return /^\d+[kKmMgG]?$/.test(value.trim())
return /^\d+[kKmMgG]?$/.test(value.trim());
}
function isProxyBuffersValue(value: string) {
return /^\d+\s+\d+[kKmMgG]?$/.test(value.trim())
return /^\d+\s+\d+[kKmMgG]?$/.test(value.trim());
}
function isDurationToken(value: string) {
return /^\d+[smhdwSMHDW]$/.test(value.trim())
return /^\d+[smhdwSMHDW]$/.test(value.trim());
}
function isCacheLevelsValue(value: string) {
return /^\d{1,2}(?::\d{1,2}){0,2}$/.test(value.trim())
return /^\d{1,2}(?::\d{1,2}){0,2}$/.test(value.trim());
}
export function validateRuntimeFields(fields: PerformanceFields) {
if (
fields.openresty_worker_processes !== "auto" &&
fields.openresty_worker_processes !== 'auto' &&
!isPositiveInteger(fields.openresty_worker_processes)
) {
throw new Error("worker_processes 必须为 auto 或大于 0 的整数")
throw new Error('worker_processes 必须为 auto 或大于 0 的整数');
}
const integers = [
fields.openresty_worker_connections,
@@ -191,19 +205,22 @@ export function validateRuntimeFields(fields: PerformanceFields) {
fields.openresty_client_header_timeout,
fields.openresty_client_body_timeout,
fields.openresty_send_timeout,
]
];
if (integers.some((value) => !isPositiveInteger(value))) {
throw new Error("超时与连接参数必须为大于 0 的整数")
throw new Error('超时与连接参数必须为大于 0 的整数');
}
const status = Number.parseInt(fields.openresty_default_server_return_status, 10)
const status = Number.parseInt(
fields.openresty_default_server_return_status,
10,
);
if (Number.isNaN(status) || status < 100 || status > 999) {
throw new Error("空白页面返回状态码必须在 100 到 999 之间")
throw new Error('空白页面返回状态码必须在 100 到 999 之间');
}
if (!isSizeValue(fields.openresty_client_max_body_size)) {
throw new Error("client_max_body_size 格式不合法")
throw new Error('client_max_body_size 格式不合法');
}
if (!isProxyBuffersValue(fields.openresty_large_client_header_buffers)) {
throw new Error('large_client_header_buffers 格式必须类似 "4 16k"')
throw new Error('large_client_header_buffers 格式必须类似 "4 16k"');
}
}
@@ -212,35 +229,35 @@ export function validateProxyFields(fields: PerformanceFields) {
fields.openresty_proxy_connect_timeout,
fields.openresty_proxy_send_timeout,
fields.openresty_proxy_read_timeout,
]
];
if (timeouts.some((value) => !isPositiveInteger(value))) {
throw new Error("代理超时参数必须为大于 0 的整数秒")
throw new Error('代理超时参数必须为大于 0 的整数秒');
}
if (!isProxyBuffersValue(fields.openresty_proxy_buffers)) {
throw new Error('proxy_buffers 格式必须类似 "16 16k"')
throw new Error('proxy_buffers 格式必须类似 "16 16k"');
}
if (
!isSizeValue(fields.openresty_proxy_buffer_size) ||
!isSizeValue(fields.openresty_proxy_busy_buffers_size)
) {
throw new Error("缓冲大小必须为整数或带 k/m/g 单位的值")
throw new Error('缓冲大小必须为整数或带 k/m/g 单位的值');
}
}
export function validateGzipFields(fields: PerformanceFields) {
if (!isPositiveInteger(fields.openresty_gzip_min_length)) {
throw new Error("gzip_min_length 必须为大于 0 的整数")
throw new Error('gzip_min_length 必须为大于 0 的整数');
}
const level = Number.parseInt(fields.openresty_gzip_comp_level, 10)
const level = Number.parseInt(fields.openresty_gzip_comp_level, 10);
if (Number.isNaN(level) || level < 1 || level > 9) {
throw new Error("gzip_comp_level 必须在 1 到 9 之间")
throw new Error('gzip_comp_level 必须在 1 到 9 之间');
}
}
export function validateCacheFields(fields: PerformanceFields) {
if (!fields.openresty_cache_enabled) return
if (!fields.openresty_cache_enabled) return;
if (!fields.openresty_cache_path.trim()) {
throw new Error("启用缓存时必须填写 proxy_cache_path 目录")
throw new Error('启用缓存时必须填写 proxy_cache_path 目录');
}
if (
!isCacheLevelsValue(fields.openresty_cache_levels) ||
@@ -248,10 +265,12 @@ export function validateCacheFields(fields: PerformanceFields) {
!isSizeValue(fields.openresty_cache_max_size) ||
!isDurationToken(fields.openresty_cache_lock_timeout)
) {
throw new Error("缓存 levels、inactive、max_size 或 lock_timeout 格式不合法")
throw new Error(
'缓存 levels、inactive、max_size 或 lock_timeout 格式不合法',
);
}
if (!fields.openresty_cache_key_template.trim()) {
throw new Error("启用缓存时必须填写缓存 Key 模板")
throw new Error('启用缓存时必须填写缓存 Key 模板');
}
}
@@ -262,8 +281,8 @@ export function entriesFromKeys(
return keys.map((key) => ({
key,
value:
typeof fields[key] === "boolean"
typeof fields[key] === 'boolean'
? String(fields[key])
: String(fields[key]).trim(),
}))
}
}));
}
+315 -182
View File
@@ -1,22 +1,34 @@
"use client"
'use client';
import Link from "next/link"
import {useEffect, useState} from "react"
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query"
import {ExternalLink, Gauge, Loader2, Save} from "lucide-react"
import {toast} from "sonner"
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ExternalLink, Gauge, Loader2, Save } from 'lucide-react';
import { toast } from 'sonner';
import {useAuth} from "@/components/providers/auth-provider"
import {EmptyStateWithBorder} from "@/components/layout/empty"
import {ErrorInline} from "@/components/layout/error"
import {LoadingStateWithBorder} from "@/components/layout/loading"
import {Button} from "@/components/ui/button"
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
import {Input} from "@/components/ui/input"
import {Label} from "@/components/ui/label"
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select"
import {Switch} from "@/components/ui/switch"
import {OptionService} from "@/lib/services/openflare"
import { useAuth } from '@/components/providers/auth-provider';
import { EmptyStateWithBorder } from '@/components/layout/empty';
import { ErrorInline } from '@/components/layout/error';
import { LoadingStateWithBorder } from '@/components/layout/loading';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { OptionService } from '@/lib/services/openflare';
import {
defaultPerformanceFields,
@@ -28,26 +40,28 @@ import {
validateGzipFields,
validateProxyFields,
validateRuntimeFields,
} from "./components/performance-utils"
} from './components/performance-utils';
const optionsQueryKey = ["openflare", "options"] as const
const optionsQueryKey = ['openflare', 'options'] as const;
export default function PerformancePage() {
const { user, loading: authLoading } = useAuth()
const queryClient = useQueryClient()
const [fields, setFields] = useState<PerformanceFields>(defaultPerformanceFields)
const [savingSection, setSavingSection] = useState<string | null>(null)
const { user, loading: authLoading } = useAuth();
const queryClient = useQueryClient();
const [fields, setFields] = useState<PerformanceFields>(
defaultPerformanceFields,
);
const [savingSection, setSavingSection] = useState<string | null>(null);
const optionsQuery = useQuery({
queryKey: optionsQueryKey,
queryFn: () => OptionService.list(),
enabled: !!user?.is_admin,
})
});
useEffect(() => {
if (!optionsQuery.data) return
setFields(mapOptionsToFields(optionsToMap(optionsQuery.data)))
}, [optionsQuery.data])
if (!optionsQuery.data) return;
setFields(mapOptionsToFields(optionsToMap(optionsQuery.data)));
}, [optionsQuery.data]);
const saveMutation = useMutation({
mutationFn: async ({
@@ -55,220 +69,306 @@ export default function PerformancePage() {
entries,
validator,
}: {
section: string
entries: Array<{ key: string; value: string }>
validator?: (fields: PerformanceFields) => void
section: string;
entries: Array<{ key: string; value: string }>;
validator?: (fields: PerformanceFields) => void;
}) => {
validator?.(fields)
setSavingSection(section)
await OptionService.updateBatch(entries)
validator?.(fields);
setSavingSection(section);
await OptionService.updateBatch(entries);
},
onSuccess: async () => {
toast.success("性能参数已保存")
toast.success('性能参数已保存');
await Promise.all([
queryClient.invalidateQueries({ queryKey: optionsQueryKey }),
queryClient.invalidateQueries({ queryKey: ["openflare", "config-preview"] }),
queryClient.invalidateQueries({ queryKey: ["openflare", "config-versions"] }),
])
setSavingSection(null)
queryClient.invalidateQueries({
queryKey: ['openflare', 'config-preview'],
}),
queryClient.invalidateQueries({
queryKey: ['openflare', 'config-versions'],
}),
]);
setSavingSection(null);
},
onError: (error) => {
setSavingSection(null)
toast.error(error instanceof Error ? error.message : "保存失败")
setSavingSection(null);
toast.error(error instanceof Error ? error.message : '保存失败');
},
})
});
const updateField = <K extends keyof PerformanceFields>(
key: K,
value: PerformanceFields[K],
) => {
setFields((prev) => ({ ...prev, [key]: value }))
}
setFields((prev) => ({ ...prev, [key]: value }));
};
if (authLoading) {
return (
<div className="py-6 px-1">
<LoadingStateWithBorder icon={Gauge} description="加载权限信息..." />
<div className='py-6 px-1'>
<LoadingStateWithBorder icon={Gauge} description='加载权限信息...' />
</div>
)
);
}
if (!user?.is_admin) {
return (
<div className="py-6 px-1">
<div className='py-6 px-1'>
<EmptyStateWithBorder
icon={Gauge}
title="权限不足"
description="只有管理员可以访问性能设置。"
title='权限不足'
description='只有管理员可以访问性能设置。'
/>
</div>
)
);
}
if (optionsQuery.isLoading) {
return (
<div className="py-6 px-1">
<LoadingStateWithBorder icon={Gauge} description="加载性能参数..." />
<div className='py-6 px-1'>
<LoadingStateWithBorder icon={Gauge} description='加载性能参数...' />
</div>
)
);
}
if (optionsQuery.isError) {
return (
<div className="py-6 px-1">
<div className='py-6 px-1'>
<ErrorInline
message={
optionsQuery.error instanceof Error
? optionsQuery.error.message
: "加载失败"
: '加载失败'
}
onRetry={() => void optionsQuery.refetch()}
/>
</div>
)
);
}
return (
<div className="py-6 px-1 space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="flex items-center gap-2">
<Gauge className="size-5 text-primary" />
<div className='py-6 px-1 space-y-6'>
<div className='flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
<div className='flex items-center gap-2'>
<Gauge className='size-5 text-primary' />
<div>
<h1 className="text-2xl font-semibold tracking-tight">性能</h1>
<p className="text-sm text-muted-foreground">
<h1 className='text-2xl font-semibold tracking-tight'>性能</h1>
<p className='text-sm text-muted-foreground'>
集中管理 OpenResty 全局性能参数,保存后进入统一配置发布链路。
</p>
</div>
</div>
<Button variant="outline" size="sm" asChild>
<Link href="/config-versions">
<ExternalLink className="size-3.5 mr-1" />
<Button variant='outline' size='sm' asChild>
<Link href='/config-versions'>
<ExternalLink className='size-3.5 mr-1' />
查看配置预览
</Link>
</Button>
</div>
<Card className="border-dashed shadow-none">
<CardHeader className="flex flex-row items-center justify-between">
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-center justify-between'>
<div>
<CardTitle className="text-base">连接与事件</CardTitle>
<CardDescription>worker、超时、请求体大小等运行参数</CardDescription>
<CardTitle className='text-base'>连接与事件</CardTitle>
<CardDescription>
worker、超时、请求体大小等运行参数
</CardDescription>
</div>
<Button
size="sm"
disabled={savingSection === "runtime"}
size='sm'
disabled={savingSection === 'runtime'}
onClick={() =>
saveMutation.mutate({
section: "runtime",
section: 'runtime',
validator: validateRuntimeFields,
entries: entriesFromKeys(fields, [
"openresty_default_server_return_status",
"openresty_worker_processes",
"openresty_resolvers",
"openresty_worker_connections",
"openresty_worker_rlimit_nofile",
"openresty_events_use",
"openresty_events_multi_accept_enabled",
"openresty_keepalive_timeout",
"openresty_keepalive_requests",
"openresty_client_header_timeout",
"openresty_client_body_timeout",
"openresty_client_max_body_size",
"openresty_large_client_header_buffers",
"openresty_send_timeout",
'openresty_default_server_return_status',
'openresty_worker_processes',
'openresty_resolvers',
'openresty_worker_connections',
'openresty_worker_rlimit_nofile',
'openresty_events_use',
'openresty_events_multi_accept_enabled',
'openresty_keepalive_timeout',
'openresty_keepalive_requests',
'openresty_client_header_timeout',
'openresty_client_body_timeout',
'openresty_client_max_body_size',
'openresty_large_client_header_buffers',
'openresty_send_timeout',
]),
})
}
>
{savingSection === "runtime" ? (
<Loader2 className="size-4 animate-spin mr-1" />
{savingSection === 'runtime' ? (
<Loader2 className='size-4 animate-spin mr-1' />
) : (
<Save className="size-3.5 mr-1" />
<Save className='size-3.5 mr-1' />
)}
保存
</Button>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<FieldInput label="worker_processes" value={fields.openresty_worker_processes} onChange={(v) => updateField("openresty_worker_processes", v)} />
<FieldInput label="worker_connections" value={fields.openresty_worker_connections} onChange={(v) => updateField("openresty_worker_connections", v)} type="number" />
<FieldInput label="worker_rlimit_nofile" value={fields.openresty_worker_rlimit_nofile} onChange={(v) => updateField("openresty_worker_rlimit_nofile", v)} type="number" />
<div className="space-y-1.5">
<CardContent className='grid gap-4 md:grid-cols-2 xl:grid-cols-3'>
<FieldInput
label='worker_processes'
value={fields.openresty_worker_processes}
onChange={(v) => updateField('openresty_worker_processes', v)}
/>
<FieldInput
label='worker_connections'
value={fields.openresty_worker_connections}
onChange={(v) => updateField('openresty_worker_connections', v)}
type='number'
/>
<FieldInput
label='worker_rlimit_nofile'
value={fields.openresty_worker_rlimit_nofile}
onChange={(v) => updateField('openresty_worker_rlimit_nofile', v)}
type='number'
/>
<div className='space-y-1.5'>
<Label>events use</Label>
<Select value={fields.openresty_events_use} onValueChange={(v) => updateField("openresty_events_use", v)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<Select
value={fields.openresty_events_use}
onValueChange={(v) => updateField('openresty_events_use', v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="epoll">epoll</SelectItem>
<SelectItem value="kqueue">kqueue</SelectItem>
<SelectItem value="poll">poll</SelectItem>
<SelectItem value="select">select</SelectItem>
<SelectItem value='epoll'>epoll</SelectItem>
<SelectItem value='kqueue'>kqueue</SelectItem>
<SelectItem value='poll'>poll</SelectItem>
<SelectItem value='select'>select</SelectItem>
</SelectContent>
</Select>
</div>
<ToggleRow label="multi_accept" checked={fields.openresty_events_multi_accept_enabled} onChange={(v) => updateField("openresty_events_multi_accept_enabled", v)} />
<FieldInput label="keepalive_timeout" value={fields.openresty_keepalive_timeout} onChange={(v) => updateField("openresty_keepalive_timeout", v)} type="number" />
<FieldInput label="client_max_body_size" value={fields.openresty_client_max_body_size} onChange={(v) => updateField("openresty_client_max_body_size", v)} />
<FieldInput label="resolvers" value={fields.openresty_resolvers} onChange={(v) => updateField("openresty_resolvers", v)} placeholder="1.1.1.1 8.8.8.8" />
<FieldInput label="default_server_return_status" value={fields.openresty_default_server_return_status} onChange={(v) => updateField("openresty_default_server_return_status", v)} type="number" />
<ToggleRow
label='multi_accept'
checked={fields.openresty_events_multi_accept_enabled}
onChange={(v) =>
updateField('openresty_events_multi_accept_enabled', v)
}
/>
<FieldInput
label='keepalive_timeout'
value={fields.openresty_keepalive_timeout}
onChange={(v) => updateField('openresty_keepalive_timeout', v)}
type='number'
/>
<FieldInput
label='client_max_body_size'
value={fields.openresty_client_max_body_size}
onChange={(v) => updateField('openresty_client_max_body_size', v)}
/>
<FieldInput
label='resolvers'
value={fields.openresty_resolvers}
onChange={(v) => updateField('openresty_resolvers', v)}
placeholder='1.1.1.1 8.8.8.8'
/>
<FieldInput
label='default_server_return_status'
value={fields.openresty_default_server_return_status}
onChange={(v) =>
updateField('openresty_default_server_return_status', v)
}
type='number'
/>
</CardContent>
</Card>
<Card className="border-dashed shadow-none">
<CardHeader className="flex flex-row items-center justify-between">
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-center justify-between'>
<div>
<CardTitle className="text-base">反代缓冲与超时</CardTitle>
<CardDescription>upstream 连接、缓冲与 WebSocket/HTTP3 开关</CardDescription>
<CardTitle className='text-base'>反代缓冲与超时</CardTitle>
<CardDescription>
upstream 连接、缓冲与 WebSocket/HTTP3 开关
</CardDescription>
</div>
<Button
size="sm"
disabled={savingSection === "proxy"}
size='sm'
disabled={savingSection === 'proxy'}
onClick={() =>
saveMutation.mutate({
section: "proxy",
section: 'proxy',
validator: validateProxyFields,
entries: entriesFromKeys(fields, [
"openresty_proxy_connect_timeout",
"openresty_proxy_send_timeout",
"openresty_proxy_read_timeout",
"openresty_websocket_enabled",
"openresty_http3_enabled",
"openresty_proxy_request_buffering_enabled",
"openresty_proxy_buffering_enabled",
"openresty_proxy_buffers",
"openresty_proxy_buffer_size",
"openresty_proxy_busy_buffers_size",
'openresty_proxy_connect_timeout',
'openresty_proxy_send_timeout',
'openresty_proxy_read_timeout',
'openresty_websocket_enabled',
'openresty_http3_enabled',
'openresty_proxy_request_buffering_enabled',
'openresty_proxy_buffering_enabled',
'openresty_proxy_buffers',
'openresty_proxy_buffer_size',
'openresty_proxy_busy_buffers_size',
]),
})
}
>
{savingSection === "proxy" ? <Loader2 className="size-4 animate-spin mr-1" /> : <Save className="size-3.5 mr-1" />}
{savingSection === 'proxy' ? (
<Loader2 className='size-4 animate-spin mr-1' />
) : (
<Save className='size-3.5 mr-1' />
)}
保存
</Button>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<FieldInput label="proxy_connect_timeout" value={fields.openresty_proxy_connect_timeout} onChange={(v) => updateField("openresty_proxy_connect_timeout", v)} type="number" />
<FieldInput label="proxy_read_timeout" value={fields.openresty_proxy_read_timeout} onChange={(v) => updateField("openresty_proxy_read_timeout", v)} type="number" />
<ToggleRow label="websocket" checked={fields.openresty_websocket_enabled} onChange={(v) => updateField("openresty_websocket_enabled", v)} />
<ToggleRow label="http3" checked={fields.openresty_http3_enabled} onChange={(v) => updateField("openresty_http3_enabled", v)} />
<ToggleRow label="proxy_buffering" checked={fields.openresty_proxy_buffering_enabled} onChange={(v) => updateField("openresty_proxy_buffering_enabled", v)} />
<FieldInput label="proxy_buffers" value={fields.openresty_proxy_buffers} onChange={(v) => updateField("openresty_proxy_buffers", v)} />
<CardContent className='grid gap-4 md:grid-cols-2'>
<FieldInput
label='proxy_connect_timeout'
value={fields.openresty_proxy_connect_timeout}
onChange={(v) => updateField('openresty_proxy_connect_timeout', v)}
type='number'
/>
<FieldInput
label='proxy_read_timeout'
value={fields.openresty_proxy_read_timeout}
onChange={(v) => updateField('openresty_proxy_read_timeout', v)}
type='number'
/>
<ToggleRow
label='websocket'
checked={fields.openresty_websocket_enabled}
onChange={(v) => updateField('openresty_websocket_enabled', v)}
/>
<ToggleRow
label='http3'
checked={fields.openresty_http3_enabled}
onChange={(v) => updateField('openresty_http3_enabled', v)}
/>
<ToggleRow
label='proxy_buffering'
checked={fields.openresty_proxy_buffering_enabled}
onChange={(v) =>
updateField('openresty_proxy_buffering_enabled', v)
}
/>
<FieldInput
label='proxy_buffers'
value={fields.openresty_proxy_buffers}
onChange={(v) => updateField('openresty_proxy_buffers', v)}
/>
</CardContent>
</Card>
<div className="grid gap-6 xl:grid-cols-2">
<Card className="border-dashed shadow-none">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">压缩</CardTitle>
<div className='grid gap-6 xl:grid-cols-2'>
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle className='text-base'>压缩</CardTitle>
<Button
size="sm"
disabled={savingSection === "gzip"}
size='sm'
disabled={savingSection === 'gzip'}
onClick={() =>
saveMutation.mutate({
section: "gzip",
section: 'gzip',
validator: validateGzipFields,
entries: entriesFromKeys(fields, [
"openresty_gzip_enabled",
"openresty_gzip_min_length",
"openresty_gzip_comp_level",
'openresty_gzip_enabled',
'openresty_gzip_min_length',
'openresty_gzip_comp_level',
]),
})
}
@@ -276,36 +376,50 @@ export default function PerformancePage() {
保存
</Button>
</CardHeader>
<CardContent className="space-y-4">
<ToggleRow label="gzip" checked={fields.openresty_gzip_enabled} onChange={(v) => updateField("openresty_gzip_enabled", v)} />
<FieldInput label="gzip_min_length" value={fields.openresty_gzip_min_length} onChange={(v) => updateField("openresty_gzip_min_length", v)} type="number" />
<FieldInput label="gzip_comp_level" value={fields.openresty_gzip_comp_level} onChange={(v) => updateField("openresty_gzip_comp_level", v)} type="number" />
<CardContent className='space-y-4'>
<ToggleRow
label='gzip'
checked={fields.openresty_gzip_enabled}
onChange={(v) => updateField('openresty_gzip_enabled', v)}
/>
<FieldInput
label='gzip_min_length'
value={fields.openresty_gzip_min_length}
onChange={(v) => updateField('openresty_gzip_min_length', v)}
type='number'
/>
<FieldInput
label='gzip_comp_level'
value={fields.openresty_gzip_comp_level}
onChange={(v) => updateField('openresty_gzip_comp_level', v)}
type='number'
/>
</CardContent>
</Card>
<Card className="border-dashed shadow-none">
<CardHeader className="flex flex-row items-center justify-between">
<Card className='border-dashed shadow-none'>
<CardHeader className='flex flex-row items-center justify-between'>
<div>
<CardTitle className="text-base">缓存</CardTitle>
<CardTitle className='text-base'>缓存</CardTitle>
<CardDescription>单节点反代缓存优化场景</CardDescription>
</div>
<Button
size="sm"
disabled={savingSection === "cache"}
size='sm'
disabled={savingSection === 'cache'}
onClick={() =>
saveMutation.mutate({
section: "cache",
section: 'cache',
validator: validateCacheFields,
entries: entriesFromKeys(fields, [
"openresty_cache_enabled",
"openresty_cache_path",
"openresty_cache_levels",
"openresty_cache_inactive",
"openresty_cache_max_size",
"openresty_cache_key_template",
"openresty_cache_lock_enabled",
"openresty_cache_lock_timeout",
"openresty_cache_use_stale",
'openresty_cache_enabled',
'openresty_cache_path',
'openresty_cache_levels',
'openresty_cache_inactive',
'openresty_cache_max_size',
'openresty_cache_key_template',
'openresty_cache_lock_enabled',
'openresty_cache_lock_timeout',
'openresty_cache_use_stale',
]),
})
}
@@ -313,46 +427,65 @@ export default function PerformancePage() {
保存
</Button>
</CardHeader>
<CardContent className="space-y-4">
<ToggleRow label="cache_enabled" checked={fields.openresty_cache_enabled} onChange={(v) => updateField("openresty_cache_enabled", v)} />
<FieldInput label="proxy_cache_path" value={fields.openresty_cache_path} onChange={(v) => updateField("openresty_cache_path", v)} disabled={!fields.openresty_cache_enabled} />
<FieldInput label="levels" value={fields.openresty_cache_levels} onChange={(v) => updateField("openresty_cache_levels", v)} disabled={!fields.openresty_cache_enabled} />
<FieldInput label="max_size" value={fields.openresty_cache_max_size} onChange={(v) => updateField("openresty_cache_max_size", v)} disabled={!fields.openresty_cache_enabled} />
<CardContent className='space-y-4'>
<ToggleRow
label='cache_enabled'
checked={fields.openresty_cache_enabled}
onChange={(v) => updateField('openresty_cache_enabled', v)}
/>
<FieldInput
label='proxy_cache_path'
value={fields.openresty_cache_path}
onChange={(v) => updateField('openresty_cache_path', v)}
disabled={!fields.openresty_cache_enabled}
/>
<FieldInput
label='levels'
value={fields.openresty_cache_levels}
onChange={(v) => updateField('openresty_cache_levels', v)}
disabled={!fields.openresty_cache_enabled}
/>
<FieldInput
label='max_size'
value={fields.openresty_cache_max_size}
onChange={(v) => updateField('openresty_cache_max_size', v)}
disabled={!fields.openresty_cache_enabled}
/>
</CardContent>
</Card>
</div>
</div>
)
);
}
function FieldInput({
label,
value,
onChange,
type = "text",
type = 'text',
placeholder,
disabled,
}: {
label: string
value: string
onChange: (value: string) => void
type?: string
placeholder?: string
disabled?: boolean
label: string;
value: string;
onChange: (value: string) => void;
type?: string;
placeholder?: string;
disabled?: boolean;
}) {
return (
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">{label}</Label>
<div className='space-y-1.5'>
<Label className='text-xs text-muted-foreground'>{label}</Label>
<Input
type={type}
value={value}
placeholder={placeholder}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
className="h-9 text-xs"
className='h-9 text-xs'
/>
</div>
)
);
}
function ToggleRow({
@@ -360,14 +493,14 @@ function ToggleRow({
checked,
onChange,
}: {
label: string
checked: boolean
onChange: (value: boolean) => void
label: string;
checked: boolean;
onChange: (value: boolean) => void;
}) {
return (
<div className="flex items-center justify-between rounded-lg border border-dashed px-3 py-2">
<Label className="text-xs">{label}</Label>
<div className='flex items-center justify-between rounded-lg border border-dashed px-3 py-2'>
<Label className='text-xs'>{label}</Label>
<Switch checked={checked} onCheckedChange={onChange} />
</div>
)
);
}