mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
[优化] 增加 OpenResty 配置选项和性能页面工具提示
This commit is contained in:
@@ -65,10 +65,13 @@ var OpenRestyKeepaliveTimeout = 65
|
||||
var OpenRestyKeepaliveRequests = 1000
|
||||
var OpenRestyClientHeaderTimeout = 15
|
||||
var OpenRestyClientBodyTimeout = 15
|
||||
var OpenRestyClientMaxBodySize = "64m"
|
||||
var OpenRestyLargeClientHeaderBuffers = "4 16k"
|
||||
var OpenRestySendTimeout = 30
|
||||
var OpenRestyProxyConnectTimeout = 5
|
||||
var OpenRestyProxySendTimeout = 60
|
||||
var OpenRestyProxyReadTimeout = 60
|
||||
var OpenRestyProxyRequestBufferingEnabled = true
|
||||
var OpenRestyProxyBufferingEnabled = true
|
||||
var OpenRestyProxyBuffers = "16 16k"
|
||||
var OpenRestyProxyBufferSize = "8k"
|
||||
@@ -104,10 +107,13 @@ http {
|
||||
keepalive_requests {{OpenRestyKeepaliveRequests}};
|
||||
client_header_timeout {{OpenRestyClientHeaderTimeout}};
|
||||
client_body_timeout {{OpenRestyClientBodyTimeout}};
|
||||
client_max_body_size {{OpenRestyClientMaxBodySize}};
|
||||
large_client_header_buffers {{OpenRestyLargeClientHeaderBuffers}};
|
||||
send_timeout {{OpenRestySendTimeout}};
|
||||
proxy_connect_timeout {{OpenRestyProxyConnectTimeout}};
|
||||
proxy_send_timeout {{OpenRestyProxySendTimeout}};
|
||||
proxy_read_timeout {{OpenRestyProxyReadTimeout}};
|
||||
proxy_request_buffering {{OpenRestyProxyRequestBuffering}};
|
||||
proxy_buffering {{OpenRestyProxyBuffering}};
|
||||
proxy_buffers {{OpenRestyProxyBuffers}};
|
||||
proxy_buffer_size {{OpenRestyProxyBufferSize}};
|
||||
|
||||
@@ -64,10 +64,13 @@ func InitOptionMap() {
|
||||
common.OptionMap["OpenRestyKeepaliveRequests"] = strconv.Itoa(common.OpenRestyKeepaliveRequests)
|
||||
common.OptionMap["OpenRestyClientHeaderTimeout"] = strconv.Itoa(common.OpenRestyClientHeaderTimeout)
|
||||
common.OptionMap["OpenRestyClientBodyTimeout"] = strconv.Itoa(common.OpenRestyClientBodyTimeout)
|
||||
common.OptionMap["OpenRestyClientMaxBodySize"] = common.OpenRestyClientMaxBodySize
|
||||
common.OptionMap["OpenRestyLargeClientHeaderBuffers"] = common.OpenRestyLargeClientHeaderBuffers
|
||||
common.OptionMap["OpenRestySendTimeout"] = strconv.Itoa(common.OpenRestySendTimeout)
|
||||
common.OptionMap["OpenRestyProxyConnectTimeout"] = strconv.Itoa(common.OpenRestyProxyConnectTimeout)
|
||||
common.OptionMap["OpenRestyProxySendTimeout"] = strconv.Itoa(common.OpenRestyProxySendTimeout)
|
||||
common.OptionMap["OpenRestyProxyReadTimeout"] = strconv.Itoa(common.OpenRestyProxyReadTimeout)
|
||||
common.OptionMap["OpenRestyProxyRequestBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyRequestBufferingEnabled)
|
||||
common.OptionMap["OpenRestyProxyBufferingEnabled"] = strconv.FormatBool(common.OpenRestyProxyBufferingEnabled)
|
||||
common.OptionMap["OpenRestyProxyBuffers"] = common.OpenRestyProxyBuffers
|
||||
common.OptionMap["OpenRestyProxyBufferSize"] = common.OpenRestyProxyBufferSize
|
||||
@@ -240,6 +243,14 @@ func updateOptionMap(key string, value string) {
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
common.OpenRestyClientBodyTimeout = v
|
||||
}
|
||||
case "OpenRestyClientMaxBodySize":
|
||||
if strings.TrimSpace(value) != "" {
|
||||
common.OpenRestyClientMaxBodySize = value
|
||||
}
|
||||
case "OpenRestyLargeClientHeaderBuffers":
|
||||
if strings.TrimSpace(value) != "" {
|
||||
common.OpenRestyLargeClientHeaderBuffers = value
|
||||
}
|
||||
case "OpenRestySendTimeout":
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
common.OpenRestySendTimeout = v
|
||||
@@ -256,6 +267,8 @@ func updateOptionMap(key string, value string) {
|
||||
if v, err := strconv.Atoi(value); err == nil && v > 0 {
|
||||
common.OpenRestyProxyReadTimeout = v
|
||||
}
|
||||
case "OpenRestyProxyRequestBufferingEnabled":
|
||||
common.OpenRestyProxyRequestBufferingEnabled = value == "true"
|
||||
case "OpenRestyProxyBufferingEnabled":
|
||||
common.OpenRestyProxyBufferingEnabled = value == "true"
|
||||
case "OpenRestyProxyBuffers":
|
||||
|
||||
@@ -61,6 +61,47 @@ const defaultPerformanceFields = {
|
||||
'error timeout updating http_500 http_502 http_503 http_504',
|
||||
};
|
||||
|
||||
const performanceFieldTooltips: Record<string, string> = {
|
||||
worker_processes:
|
||||
'Nginx worker 进程数。通常建议保持 auto,让 OpenResty 按 CPU 核数自动分配。',
|
||||
worker_connections:
|
||||
'每个 worker 可同时处理的最大连接数。值越大,可承载的并发连接越多。',
|
||||
worker_rlimit_nofile:
|
||||
'提升 worker 可打开的文件描述符上限,避免高并发下连接或文件句柄不足。',
|
||||
events_use:
|
||||
'指定事件驱动模型。Linux 常见是 epoll,留空时由 OpenResty 自动选择。',
|
||||
multi_accept:
|
||||
'开启后,worker 会尽可能一次接受多个新连接,适合高吞吐接入场景。',
|
||||
keepalive_timeout: '客户端 Keep-Alive 空闲保持时间,单位秒。',
|
||||
keepalive_requests: '单个长连接允许复用的最大请求数。',
|
||||
client_header_timeout: '读取客户端请求头的超时时间,单位秒。',
|
||||
client_body_timeout: '读取客户端请求体的超时时间,单位秒。',
|
||||
send_timeout: '向客户端发送响应时的超时时间,单位秒。',
|
||||
proxy_connect_timeout: '连接上游源站的超时时间,单位秒。',
|
||||
proxy_send_timeout: '向上游发送请求的超时时间,单位秒。',
|
||||
proxy_read_timeout: '等待上游返回响应的超时时间,单位秒。',
|
||||
proxy_buffering:
|
||||
'控制是否启用代理响应缓冲。开启后通常有更平滑的吞吐,但会增加内存占用。',
|
||||
proxy_buffers: '设置代理响应缓冲区的数量和大小,例如 16 16k。',
|
||||
proxy_buffer_size: '保存响应头等小块数据的基础缓冲区大小。',
|
||||
proxy_busy_buffers_size: '限制 busy 状态下可同时占用的缓冲区总大小。',
|
||||
gzip: '控制是否启用 gzip 压缩响应。',
|
||||
gzip_min_length:
|
||||
'只有响应体超过该字节数时才会启用 gzip,避免对极小响应做无意义压缩。',
|
||||
gzip_comp_level: 'gzip 压缩等级,1 更省 CPU,9 压缩更高但更耗 CPU。',
|
||||
proxy_cache_path: '缓存目录路径,对应 proxy_cache_path 指令中的磁盘位置。',
|
||||
levels: '缓存目录层级,例如 1:2,可控制缓存文件的目录分布。',
|
||||
inactive: '缓存对象在未命中访问时的失活时间,例如 30m。',
|
||||
max_size:
|
||||
'缓存目录允许占用的最大磁盘空间,会渲染到 proxy_cache_path 的 max_size。',
|
||||
cache_key_template: '生成缓存 Key 的模板,决定不同请求如何命中同一缓存对象。',
|
||||
proxy_cache_lock:
|
||||
'启用后,同一缓存 Key 未命中时只允许一个请求回源,减少击穿。',
|
||||
proxy_cache_lock_timeout: '等待缓存锁的最长时间,例如 5s。',
|
||||
proxy_cache_use_stale:
|
||||
'上游异常时允许返回旧缓存的条件列表,例如 error、timeout、http_500。',
|
||||
};
|
||||
|
||||
type PerformanceTab = 'settings' | 'editor';
|
||||
|
||||
type FeedbackState = {
|
||||
@@ -609,7 +650,10 @@ export function PerformancePage() {
|
||||
}
|
||||
>
|
||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||
<ResourceField label="worker_processes">
|
||||
<ResourceField
|
||||
label="worker_processes"
|
||||
tooltip={performanceFieldTooltips.worker_processes}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyWorkerProcesses}
|
||||
onChange={(event) =>
|
||||
@@ -621,7 +665,10 @@ export function PerformancePage() {
|
||||
placeholder="auto"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="worker_connections">
|
||||
<ResourceField
|
||||
label="worker_connections"
|
||||
tooltip={performanceFieldTooltips.worker_connections}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyWorkerConnections}
|
||||
@@ -633,7 +680,10 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="worker_rlimit_nofile">
|
||||
<ResourceField
|
||||
label="worker_rlimit_nofile"
|
||||
tooltip={performanceFieldTooltips.worker_rlimit_nofile}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyWorkerRlimitNofile}
|
||||
@@ -645,7 +695,11 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="events use" hint="留空表示不显式渲染。">
|
||||
<ResourceField
|
||||
label="events use"
|
||||
hint="留空表示不显式渲染。"
|
||||
tooltip={performanceFieldTooltips.events_use}
|
||||
>
|
||||
<ResourceSelect
|
||||
value={performanceFields.OpenRestyEventsUse}
|
||||
onChange={(event) =>
|
||||
@@ -665,6 +719,7 @@ export function PerformancePage() {
|
||||
<ToggleField
|
||||
label="multi_accept"
|
||||
description=""
|
||||
tooltip={performanceFieldTooltips.multi_accept}
|
||||
checked={performanceFields.OpenRestyEventsMultiAcceptEnabled}
|
||||
onChange={(checked) =>
|
||||
setPerformanceFields((previous) => ({
|
||||
@@ -673,7 +728,10 @@ export function PerformancePage() {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ResourceField label="keepalive_timeout (秒)">
|
||||
<ResourceField
|
||||
label="keepalive_timeout (秒)"
|
||||
tooltip={performanceFieldTooltips.keepalive_timeout}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyKeepaliveTimeout}
|
||||
@@ -685,7 +743,10 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="keepalive_requests">
|
||||
<ResourceField
|
||||
label="keepalive_requests"
|
||||
tooltip={performanceFieldTooltips.keepalive_requests}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyKeepaliveRequests}
|
||||
@@ -697,7 +758,10 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="client_header_timeout (秒)">
|
||||
<ResourceField
|
||||
label="client_header_timeout (秒)"
|
||||
tooltip={performanceFieldTooltips.client_header_timeout}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyClientHeaderTimeout}
|
||||
@@ -709,7 +773,10 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="client_body_timeout (秒)">
|
||||
<ResourceField
|
||||
label="client_body_timeout (秒)"
|
||||
tooltip={performanceFieldTooltips.client_body_timeout}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyClientBodyTimeout}
|
||||
@@ -721,7 +788,10 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="send_timeout (秒)">
|
||||
<ResourceField
|
||||
label="send_timeout (秒)"
|
||||
tooltip={performanceFieldTooltips.send_timeout}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestySendTimeout}
|
||||
@@ -753,7 +823,10 @@ export function PerformancePage() {
|
||||
}
|
||||
>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<ResourceField label="proxy_connect_timeout (秒)">
|
||||
<ResourceField
|
||||
label="proxy_connect_timeout (秒)"
|
||||
tooltip={performanceFieldTooltips.proxy_connect_timeout}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyProxyConnectTimeout}
|
||||
@@ -765,7 +838,10 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_send_timeout (秒)">
|
||||
<ResourceField
|
||||
label="proxy_send_timeout (秒)"
|
||||
tooltip={performanceFieldTooltips.proxy_send_timeout}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyProxySendTimeout}
|
||||
@@ -777,7 +853,10 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_read_timeout (秒)">
|
||||
<ResourceField
|
||||
label="proxy_read_timeout (秒)"
|
||||
tooltip={performanceFieldTooltips.proxy_read_timeout}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyProxyReadTimeout}
|
||||
@@ -792,6 +871,7 @@ export function PerformancePage() {
|
||||
<ToggleField
|
||||
label="proxy_buffering"
|
||||
description="是否启用 proxy_buffering。"
|
||||
tooltip={performanceFieldTooltips.proxy_buffering}
|
||||
checked={performanceFields.OpenRestyProxyBufferingEnabled}
|
||||
onChange={(checked) =>
|
||||
setPerformanceFields((previous) => ({
|
||||
@@ -800,7 +880,10 @@ export function PerformancePage() {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ResourceField label="proxy_buffers">
|
||||
<ResourceField
|
||||
label="proxy_buffers"
|
||||
tooltip={performanceFieldTooltips.proxy_buffers}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyProxyBuffers}
|
||||
onChange={(event) =>
|
||||
@@ -812,7 +895,10 @@ export function PerformancePage() {
|
||||
placeholder="16 16k"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_buffer_size">
|
||||
<ResourceField
|
||||
label="proxy_buffer_size"
|
||||
tooltip={performanceFieldTooltips.proxy_buffer_size}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyProxyBufferSize}
|
||||
onChange={(event) =>
|
||||
@@ -824,7 +910,10 @@ export function PerformancePage() {
|
||||
placeholder="8k"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_busy_buffers_size">
|
||||
<ResourceField
|
||||
label="proxy_busy_buffers_size"
|
||||
tooltip={performanceFieldTooltips.proxy_busy_buffers_size}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyProxyBusyBuffersSize}
|
||||
onChange={(event) =>
|
||||
@@ -860,6 +949,7 @@ export function PerformancePage() {
|
||||
<ToggleField
|
||||
label="gzip"
|
||||
description="是否启用 gzip on。"
|
||||
tooltip={performanceFieldTooltips.gzip}
|
||||
checked={performanceFields.OpenRestyGzipEnabled}
|
||||
onChange={(checked) =>
|
||||
setPerformanceFields((previous) => ({
|
||||
@@ -882,7 +972,10 @@ export function PerformancePage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ResourceField label="gzip_min_length">
|
||||
<ResourceField
|
||||
label="gzip_min_length"
|
||||
tooltip={performanceFieldTooltips.gzip_min_length}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
value={performanceFields.OpenRestyGzipMinLength}
|
||||
@@ -894,7 +987,10 @@ export function PerformancePage() {
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="gzip_comp_level">
|
||||
<ResourceField
|
||||
label="gzip_comp_level"
|
||||
tooltip={performanceFieldTooltips.gzip_comp_level}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
min="1"
|
||||
@@ -1018,6 +1114,7 @@ export function PerformancePage() {
|
||||
>
|
||||
<ResourceField
|
||||
label="proxy_cache_path"
|
||||
tooltip={performanceFieldTooltips.proxy_cache_path}
|
||||
hint={
|
||||
performanceFields.OpenRestyCacheEnabled
|
||||
? '缓存目录,启用缓存时必填。'
|
||||
@@ -1036,7 +1133,10 @@ export function PerformancePage() {
|
||||
placeholder="/var/cache/openresty/atsflare"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="levels">
|
||||
<ResourceField
|
||||
label="levels"
|
||||
tooltip={performanceFieldTooltips.levels}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyCacheLevels}
|
||||
disabled={!performanceFields.OpenRestyCacheEnabled}
|
||||
@@ -1049,7 +1149,10 @@ export function PerformancePage() {
|
||||
placeholder="1:2"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="inactive">
|
||||
<ResourceField
|
||||
label="inactive"
|
||||
tooltip={performanceFieldTooltips.inactive}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyCacheInactive}
|
||||
disabled={!performanceFields.OpenRestyCacheEnabled}
|
||||
@@ -1062,7 +1165,10 @@ export function PerformancePage() {
|
||||
placeholder="30m"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="max_size">
|
||||
<ResourceField
|
||||
label="max_size"
|
||||
tooltip={performanceFieldTooltips.max_size}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyCacheMaxSize}
|
||||
disabled={!performanceFields.OpenRestyCacheEnabled}
|
||||
@@ -1075,7 +1181,10 @@ export function PerformancePage() {
|
||||
placeholder="1g"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="cache key template">
|
||||
<ResourceField
|
||||
label="cache key template"
|
||||
tooltip={performanceFieldTooltips.cache_key_template}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyCacheKeyTemplate}
|
||||
disabled={!performanceFields.OpenRestyCacheEnabled}
|
||||
@@ -1090,6 +1199,7 @@ export function PerformancePage() {
|
||||
<ToggleField
|
||||
label="proxy_cache_lock"
|
||||
description="是否启用 proxy_cache_lock。"
|
||||
tooltip={performanceFieldTooltips.proxy_cache_lock}
|
||||
checked={performanceFields.OpenRestyCacheLockEnabled}
|
||||
disabled={!performanceFields.OpenRestyCacheEnabled}
|
||||
onChange={(checked) =>
|
||||
@@ -1099,7 +1209,12 @@ export function PerformancePage() {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ResourceField label="proxy_cache_lock_timeout">
|
||||
<ResourceField
|
||||
label="proxy_cache_lock_timeout"
|
||||
tooltip={
|
||||
performanceFieldTooltips.proxy_cache_lock_timeout
|
||||
}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyCacheLockTimeout}
|
||||
disabled={!performanceFields.OpenRestyCacheEnabled}
|
||||
@@ -1112,7 +1227,10 @@ export function PerformancePage() {
|
||||
placeholder="5s"
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="proxy_cache_use_stale">
|
||||
<ResourceField
|
||||
label="proxy_cache_use_stale"
|
||||
tooltip={performanceFieldTooltips.proxy_cache_use_stale}
|
||||
>
|
||||
<ResourceInput
|
||||
value={performanceFields.OpenRestyCacheUseStale}
|
||||
disabled={!performanceFields.OpenRestyCacheEnabled}
|
||||
|
||||
@@ -1,179 +1,213 @@
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
InputHTMLAttributes,
|
||||
ReactNode,
|
||||
SelectHTMLAttributes,
|
||||
TextareaHTMLAttributes,
|
||||
} from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
interface ResourceFieldProps {
|
||||
label: string;
|
||||
hint?: string;
|
||||
error?: string;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ResourceField({
|
||||
label,
|
||||
hint,
|
||||
error,
|
||||
className,
|
||||
children,
|
||||
}: ResourceFieldProps) {
|
||||
return (
|
||||
<label className={cn('block space-y-2', className)}>
|
||||
<span className='text-sm font-medium text-[var(--foreground-primary)]'>{label}</span>
|
||||
{children}
|
||||
{error ? (
|
||||
<span className='block text-xs text-[var(--status-danger-foreground)]'>{error}</span>
|
||||
) : hint ? (
|
||||
<span className='block text-xs text-[var(--foreground-secondary)]'>{hint}</span>
|
||||
) : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceInput(props: InputHTMLAttributes<HTMLInputElement>) {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
className={cn(
|
||||
'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] outline-none transition placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceTextarea(props: TextareaHTMLAttributes<HTMLTextAreaElement>) {
|
||||
return (
|
||||
<textarea
|
||||
{...props}
|
||||
className={cn(
|
||||
'min-h-28 w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] outline-none transition placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceSelect(props: SelectHTMLAttributes<HTMLSelectElement>) {
|
||||
return (
|
||||
<select
|
||||
{...props}
|
||||
className={cn(
|
||||
'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] outline-none transition focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function baseButtonClassName(className?: string) {
|
||||
return cn(
|
||||
'inline-flex items-center justify-center rounded-2xl px-4 py-3 text-sm font-medium transition disabled:cursor-not-allowed disabled:opacity-60',
|
||||
className,
|
||||
);
|
||||
}
|
||||
|
||||
export function PrimaryButton({
|
||||
className,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={baseButtonClassName(
|
||||
cn('bg-[var(--brand-primary)] text-[var(--foreground-inverse)] hover:opacity-90', className),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SecondaryButton({
|
||||
className,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={baseButtonClassName(
|
||||
cn(
|
||||
'border border-[var(--border-default)] bg-[var(--control-background)] text-[var(--foreground-primary)] hover:bg-[var(--control-background-hover)]',
|
||||
className,
|
||||
),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DangerButton({
|
||||
className,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={baseButtonClassName(
|
||||
cn(
|
||||
'border border-[var(--status-danger-border)] bg-[var(--status-danger-soft)] text-[var(--status-danger-foreground)] hover:opacity-90',
|
||||
className,
|
||||
),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToggleFieldProps {
|
||||
label: string;
|
||||
description: string;
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export function ToggleField({
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
disabled,
|
||||
onChange,
|
||||
}: ToggleFieldProps) {
|
||||
return (
|
||||
<label className='flex cursor-pointer items-start gap-3 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3'>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.checked)}
|
||||
className='mt-1 h-4 w-4 rounded border-[var(--border-default)] accent-[var(--brand-primary)]'
|
||||
/>
|
||||
<span className='space-y-1'>
|
||||
<span className='block text-sm font-medium text-[var(--foreground-primary)]'>{label}</span>
|
||||
<span className='block text-xs leading-5 text-[var(--foreground-secondary)]'>{description}</span>
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeBlock({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<pre
|
||||
className={cn(
|
||||
'overflow-x-auto rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4 text-xs leading-6 text-[var(--foreground-primary)]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
InputHTMLAttributes,
|
||||
ReactNode,
|
||||
SelectHTMLAttributes,
|
||||
TextareaHTMLAttributes,
|
||||
} from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
|
||||
interface ResourceFieldProps {
|
||||
label: string;
|
||||
hint?: string;
|
||||
error?: string;
|
||||
className?: string;
|
||||
tooltip?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function FieldTooltip({ content }: { content: string }) {
|
||||
return (
|
||||
<span className="group/tooltip relative inline-flex">
|
||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full border border-[var(--border-default)] bg-[var(--surface-elevated)] text-[11px] font-semibold text-[var(--foreground-secondary)] transition group-hover/tooltip:border-[var(--border-strong)] group-hover/tooltip:text-[var(--foreground-primary)]">
|
||||
?
|
||||
</span>
|
||||
<span className="pointer-events-none absolute top-full left-1/2 z-20 mt-2 w-64 -translate-x-1/2 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-panel)] px-3 py-2 text-xs leading-5 text-[var(--foreground-secondary)] opacity-0 shadow-[var(--shadow-soft)] transition-opacity delay-700 duration-200 group-hover/tooltip:opacity-100">
|
||||
{content}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceField({
|
||||
label,
|
||||
hint,
|
||||
error,
|
||||
className,
|
||||
tooltip,
|
||||
children,
|
||||
}: ResourceFieldProps) {
|
||||
return (
|
||||
<label className={cn('block space-y-2', className)}>
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-[var(--foreground-primary)]">
|
||||
<span>{label}</span>
|
||||
{tooltip ? <FieldTooltip content={tooltip} /> : null}
|
||||
</span>
|
||||
{children}
|
||||
{error ? (
|
||||
<span className="block text-xs text-[var(--status-danger-foreground)]">
|
||||
{error}
|
||||
</span>
|
||||
) : hint ? (
|
||||
<span className="block text-xs text-[var(--foreground-secondary)]">
|
||||
{hint}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceInput(props: InputHTMLAttributes<HTMLInputElement>) {
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
className={cn(
|
||||
'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] transition outline-none placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceTextarea(
|
||||
props: TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
) {
|
||||
return (
|
||||
<textarea
|
||||
{...props}
|
||||
className={cn(
|
||||
'min-h-28 w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] transition outline-none placeholder:text-[var(--foreground-muted)] focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResourceSelect(props: SelectHTMLAttributes<HTMLSelectElement>) {
|
||||
return (
|
||||
<select
|
||||
{...props}
|
||||
className={cn(
|
||||
'w-full rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3 text-sm text-[var(--foreground-primary)] transition outline-none focus:border-[var(--border-strong)] focus:ring-2 focus:ring-[var(--accent-soft)] disabled:cursor-not-allowed disabled:opacity-60',
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function baseButtonClassName(className?: string) {
|
||||
return cn(
|
||||
'inline-flex items-center justify-center rounded-2xl px-4 py-3 text-sm font-medium transition disabled:cursor-not-allowed disabled:opacity-60',
|
||||
className,
|
||||
);
|
||||
}
|
||||
|
||||
export function PrimaryButton({
|
||||
className,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={baseButtonClassName(
|
||||
cn(
|
||||
'bg-[var(--brand-primary)] text-[var(--foreground-inverse)] hover:opacity-90',
|
||||
className,
|
||||
),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SecondaryButton({
|
||||
className,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={baseButtonClassName(
|
||||
cn(
|
||||
'border border-[var(--border-default)] bg-[var(--control-background)] text-[var(--foreground-primary)] hover:bg-[var(--control-background-hover)]',
|
||||
className,
|
||||
),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DangerButton({
|
||||
className,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={baseButtonClassName(
|
||||
cn(
|
||||
'border border-[var(--status-danger-border)] bg-[var(--status-danger-soft)] text-[var(--status-danger-foreground)] hover:opacity-90',
|
||||
className,
|
||||
),
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToggleFieldProps {
|
||||
label: string;
|
||||
description: string;
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export function ToggleField({
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
disabled,
|
||||
tooltip,
|
||||
onChange,
|
||||
}: ToggleFieldProps) {
|
||||
return (
|
||||
<label className="flex cursor-pointer items-start gap-3 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.checked)}
|
||||
className="mt-1 h-4 w-4 rounded border-[var(--border-default)] accent-[var(--brand-primary)]"
|
||||
/>
|
||||
<span className="space-y-1">
|
||||
<span className="flex items-center gap-2 text-sm font-medium text-[var(--foreground-primary)]">
|
||||
<span>{label}</span>
|
||||
{tooltip ? <FieldTooltip content={tooltip} /> : null}
|
||||
</span>
|
||||
<span className="block text-xs leading-5 text-[var(--foreground-secondary)]">
|
||||
{description}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeBlock({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<pre
|
||||
className={cn(
|
||||
'overflow-x-auto rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4 text-xs leading-6 text-[var(--foreground-primary)]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user