feat(web): Update config page
This commit is contained in:
parent
82ff997f2e
commit
686f5d449e
9 changed files with 379 additions and 19 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -44,6 +44,7 @@ tasks/
|
|||
|
||||
# Added by goreleaser init:
|
||||
dist/
|
||||
*.vite/
|
||||
|
||||
# Windows Application Icon/Resource
|
||||
*.syso
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@tabler/icons-react": "^3.38.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@tanstack/react-router": "^1.163.3",
|
||||
"@tanstack/react-router-devtools": "^1.163.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
"react-textarea-autosize": "^8.5.9",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shadcn": "^3.8.5",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
|
|
|
|||
53
web/frontend/pnpm-lock.yaml
generated
53
web/frontend/pnpm-lock.yaml
generated
|
|
@ -17,6 +17,9 @@ importers:
|
|||
'@tailwindcss/vite':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.90.21
|
||||
version: 5.90.21(react@19.2.4)
|
||||
'@tanstack/react-router':
|
||||
specifier: ^1.163.3
|
||||
version: 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -65,6 +68,9 @@ importers:
|
|||
shadcn:
|
||||
specifier: ^3.8.5
|
||||
version: 3.8.5(@types/node@24.11.0)(typescript@5.9.3)
|
||||
sonner:
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
tailwind-merge:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0
|
||||
|
|
@ -1353,66 +1359,79 @@ packages:
|
|||
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
|
||||
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.59.0':
|
||||
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
|
||||
|
|
@ -1497,24 +1516,28 @@ packages:
|
|||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
|
||||
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
|
||||
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
|
||||
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
|
||||
|
|
@ -1558,6 +1581,14 @@ packages:
|
|||
resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
'@tanstack/query-core@5.90.20':
|
||||
resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==}
|
||||
|
||||
'@tanstack/react-query@5.90.21':
|
||||
resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
'@tanstack/react-router-devtools@1.163.3':
|
||||
resolution: {integrity: sha512-42VMkV/2Z8ro7xzblPBRNZIEmCNXMzm2jD68G52p2qhjXm38wGpg46qneAESN9FtTQeVWk5aSXs47/jt7lkzmw==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
|
@ -2754,24 +2785,28 @@ packages:
|
|||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
||||
|
|
@ -3504,6 +3539,12 @@ packages:
|
|||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
sonner@2.0.7:
|
||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -5323,6 +5364,13 @@ snapshots:
|
|||
|
||||
'@tanstack/history@1.161.4': {}
|
||||
|
||||
'@tanstack/query-core@5.90.20': {}
|
||||
|
||||
'@tanstack/react-query@5.90.21(react@19.2.4)':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.90.20
|
||||
react: 19.2.4
|
||||
|
||||
'@tanstack/react-router-devtools@1.163.3(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.163.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@tanstack/react-router': 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -7570,6 +7618,11 @@ snapshots:
|
|||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||
dependencies:
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ReactNode } from "react"
|
||||
import { Toaster } from "sonner"
|
||||
|
||||
import { AppHeader } from "@/components/app-header"
|
||||
import { AppSidebar } from "@/components/app-sidebar"
|
||||
|
|
@ -19,6 +20,7 @@ export function AppLayout({ children }: { children: ReactNode }) {
|
|||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-center" />
|
||||
</SidebarProvider>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
|
|
|||
103
web/frontend/src/components/ui/card.tsx
Normal file
103
web/frontend/src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-6 overflow-hidden rounded-xl bg-card py-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"text-base leading-normal font-medium group-data-[size=sm]/card:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
|
|
@ -71,7 +71,9 @@
|
|||
},
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"reset": "Reset"
|
||||
},
|
||||
"models": {
|
||||
"description": "Configure API keys for each AI provider. Only configured models are available for chat.",
|
||||
|
|
@ -155,7 +157,17 @@
|
|||
"description": "Securely manage your API keys and credentials."
|
||||
},
|
||||
"config": {
|
||||
"description": "System configuration and preferences."
|
||||
"description": "System configuration and preferences.",
|
||||
"raw_json_title": "Raw JSON Configuration",
|
||||
"raw_json_desc": "Advanced users can directly edit the raw JSON configuration below.",
|
||||
"json_placeholder": "Enter valid JSON configuration...",
|
||||
"save_success": "Configuration saved successfully.",
|
||||
"save_error": "Failed to save configuration.",
|
||||
"reset_success": "Configuration has been reset.",
|
||||
"invalid_json": "Invalid JSON format.",
|
||||
"format_success": "JSON formatted successfully.",
|
||||
"format_error": "Invalid JSON format.",
|
||||
"format": "Format"
|
||||
},
|
||||
"logs": {
|
||||
"description": "System logs and monitoring."
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@
|
|||
},
|
||||
"common": {
|
||||
"cancel": "取消",
|
||||
"save": "保存"
|
||||
"save": "保存",
|
||||
"saving": "保存中...",
|
||||
"reset": "重置"
|
||||
},
|
||||
"models": {
|
||||
"description": "为每个 AI 服务商配置 API Key。只有已配置的模型可用于对话。",
|
||||
|
|
@ -155,7 +157,17 @@
|
|||
"description": "安全管理您的 API 密钥与访问凭据。"
|
||||
},
|
||||
"config": {
|
||||
"description": "系统配置和偏好设置。"
|
||||
"description": "系统配置和偏好设置。",
|
||||
"raw_json_title": "原始 JSON 配置",
|
||||
"raw_json_desc": "高级用户可以直接编辑下方的原始 JSON 配置。",
|
||||
"json_placeholder": "请输入有效的 JSON 配置...",
|
||||
"save_success": "配置保存成功。",
|
||||
"save_error": "配置保存失败。",
|
||||
"reset_success": "配置已重置。",
|
||||
"invalid_json": "JSON 格式无效。",
|
||||
"format_success": "JSON 格式化成功。",
|
||||
"format_error": "JSON 格式无效。",
|
||||
"format": "格式化"
|
||||
},
|
||||
"logs": {
|
||||
"description": "系统日志和监控。"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from "@tanstack/react-query"
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router"
|
||||
import { StrictMode } from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
|
|
@ -6,7 +10,14 @@ import "./i18n"
|
|||
import "./index.css"
|
||||
import { routeTree } from "./routeTree.gen"
|
||||
|
||||
const router = createRouter({ routeTree })
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
queryClient,
|
||||
},
|
||||
})
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
|
|
@ -19,7 +30,9 @@ if (!rootElement.innerHTML) {
|
|||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,31 @@
|
|||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export const Route = createFileRoute("/config")({
|
||||
component: ConfigPage,
|
||||
|
|
@ -12,19 +36,157 @@ function ConfigPage() {
|
|||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<PageHeader title={t("navigation.config", "Config")} />
|
||||
<div className="flex flex-1 items-center justify-center p-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
{t("navigation.config", "Config")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{t(
|
||||
"pages.config.description",
|
||||
"System configuration and preferences.",
|
||||
)}
|
||||
</p>
|
||||
<div className="flex-1 overflow-auto p-4 lg:p-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<RawJsonPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RawJsonPanel() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: config, isLoading } = useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/config")
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to fetch config")
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
})
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (newConfig: string) => {
|
||||
const res = await fetch("/api/config", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: newConfig,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to save config")
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(t("pages.config.save_success", "Configuration saved successfully."))
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] })
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("pages.config.save_error", "Failed to save configuration."))
|
||||
},
|
||||
})
|
||||
|
||||
const [editorValue, setEditorValue] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setEditorValue(JSON.stringify(config, null, 2))
|
||||
}
|
||||
}, [config])
|
||||
|
||||
const handleSave = () => {
|
||||
try {
|
||||
// Validate JSON before saving
|
||||
JSON.parse(editorValue)
|
||||
mutation.mutate(editorValue)
|
||||
} catch (e) {
|
||||
toast.error(t("pages.config.invalid_json", "Invalid JSON format."))
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormat = () => {
|
||||
try {
|
||||
const formatted = JSON.stringify(JSON.parse(editorValue), null, 2)
|
||||
setEditorValue(formatted)
|
||||
toast.success(t("pages.config.format_success", "JSON formatted successfully."))
|
||||
} catch (error) {
|
||||
toast.error(t("pages.config.format_error", "Invalid JSON format."))
|
||||
}
|
||||
}
|
||||
|
||||
const [showResetDialog, setShowResetDialog] = useState(false)
|
||||
|
||||
const confirmReset = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] })
|
||||
toast.info(t("pages.config.reset_success", "Configuration has been reset."))
|
||||
setShowResetDialog(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{t("pages.config.raw_json_title", "Raw JSON Configuration")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t(
|
||||
"pages.config.raw_json_desc",
|
||||
"Advanced users can directly edit the raw JSON configuration below.",
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<p>{t("labels.loading", "Loading...")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-muted/30 relative rounded-lg border">
|
||||
<ScrollArea className="h-[calc(100vh-20rem)] min-h-[200px]">
|
||||
<Textarea
|
||||
value={editorValue}
|
||||
onChange={(e) => setEditorValue(e.target.value)}
|
||||
className="font-mono text-sm min-h-[200px] resize-none border-0 bg-transparent px-4 py-3 shadow-none focus-visible:ring-0"
|
||||
placeholder={t(
|
||||
"pages.config.json_placeholder",
|
||||
"Enter valid JSON configuration...",
|
||||
)}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" onClick={handleFormat} disabled={mutation.isPending}>
|
||||
{t("pages.config.format", "Format")}
|
||||
</Button>
|
||||
<AlertDialog open={showResetDialog} onOpenChange={setShowResetDialog}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => setShowResetDialog(true)}
|
||||
>
|
||||
{t("common.reset", "Reset")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("pages.config.reset_confirm_title", "Reset Configuration")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("pages.config.reset_confirm_desc", "Are you sure you want to reset the configuration? This action cannot be undone.")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("common.cancel", "Cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmReset}>
|
||||
{t("common.confirm", "Confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Button onClick={handleSave} disabled={mutation.isPending}>
|
||||
{mutation.isPending
|
||||
? t("common.saving", "Saving...")
|
||||
: t("common.save", "Save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue