feat: K8s 部署清单、Token 接入加固与日志/设置页优化

- kubernetes/: 30Gi PVC、单副本 Recreate、Secret/Service/NodePort/Ingress 清单与部署文档
- 数据安全: kubeconfig/token 统一加密存储,启动迁移历史明文,DB 0600
- Token 接入: 命名空间级最小权限提示、长期 Token、自签证书 TLS 兼容
- 部署: 命名空间级 Token 下跳过 Namespace patch/枚举,节点信息容错
- 日志页: namespace → app → pod 选择顺序与作用域收窄
- 设置页: 拆分为通用/告警/LDAP/镜像源四个 tab
- 弹框: 统一 max-h 滚动与长命令换行,修复内容溢出
This commit is contained in:
logan committed 2026-10-25 23:44:07 +08:00
1 parent 2d73661dd1
commit 9f7c522157
50 files changed
+1346 -149

No files matched your search

@@ -1,4 +1,5 @@
import { memo, useEffect, useState } from 'react'
import { Link } from 'react-router'
import { Play, Square, RotateCw, Trash2, Terminal, ExternalLink, Pencil, Rocket } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { StatusBadge } from '@/components/StatusBadge'
@@ -91,7 +92,12 @@ export const DeploymentCard = memo(function DeploymentCard({
</a>
) }] : []),
]}
message={d.statusMessage || undefined}
message={
d.statusMessage ||
(d.status === 'running'
? '提示:到「日志查询 → 数据源」关联 Loki 后即可查询;跨集群/公网访问需先为 Loki 配置 Ingress 域名。'
: undefined)
}
actions={
<>
<Button variant="outline" size="sm" className="text-xs" onClick={() => onEditRetention(d)} disabled={isDeploying || isBusy}>
@@ -115,6 +121,16 @@ export const DeploymentCard = memo(function DeploymentCard({
</Button>
</>
)}
{d.status === 'running' && (
<>
<Button variant="outline" size="sm" className="text-xs" asChild>
<Link to="/logs">日志查询</Link>
</Button>
<Button variant="outline" size="sm" className="text-xs" asChild>
<Link to="/collectors">采集器</Link>
</Button>
</>
)}
<Button variant="outline" size="sm" className="text-xs" onClick={() => onViewLogs(d)}>
<Terminal className="size-3 mr-1" />日志
</Button>
@@ -347,7 +347,7 @@ export const DeploymentFormDialog = memo(function DeploymentFormDialog({
<FormItem>
<FormLabel>存储容量</FormLabel>
<FormControl>
<Input placeholder="10Gi" {...field} />
<Input placeholder="20Gi" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -363,7 +363,7 @@ export const DeploymentFormDialog = memo(function DeploymentFormDialog({
<FormItem>
<FormLabel>CPU</FormLabel>
<FormControl>
<Input placeholder="500m" {...field} />
<Input placeholder="1" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -376,7 +376,7 @@ export const DeploymentFormDialog = memo(function DeploymentFormDialog({
<FormItem>
<FormLabel>内存</FormLabel>
<FormControl>
<Input placeholder="512Mi" {...field} />
<Input placeholder="2Gi" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -98,7 +98,7 @@ export const RetentionDialog = memo(function RetentionDialog({ deployment, onClo
{liveConfig && (
<div>
<p className="mb-2 text-xs text-muted-foreground">{liveConfig.location}</p>
<pre className="max-h-[45vh] overflow-auto rounded-md bg-muted/50 p-3 font-mono text-xs whitespace-pre">{liveConfig.yaml}</pre>
<pre className="max-h-[45vh] max-w-full overflow-auto rounded-md bg-muted/50 p-3 font-mono text-xs whitespace-pre">{liveConfig.yaml}</pre>
</div>
)}
</div>
+21 -4
View File
@@ -15,6 +15,17 @@ const isPositiveInt = (v: string, min: number, max?: number) => {
return Number.isInteger(n) && n >= min && (max === undefined || n <= max)
}
// 解析存储容量为 GiB(支持 Gi/Mi/Ti),无法解析返回 null
const parseGiB = (v: string): number | null => {
const m = /^(\d+(?:\.\d+)?)\s*(Gi|Mi|Ti)$/i.exec(v.trim())
if (!m) return null
const n = Number(m[1])
const unit = m[2].toLowerCase()
if (unit === 'ti') return n * 1024
if (unit === 'mi') return n / 1024
return n
}
export const deploymentFormSchema = z
.object({
name: z.string().min(1, '请输入部署名称'),
@@ -36,7 +47,7 @@ export const deploymentFormSchema = z
.refine((v) => isPositiveInt(v, 1, 10), '副本数需为 1-10 之间的整数'),
resourcesCPU: z.string().min(1, '请填写 CPU 配额(如 500m)'),
resourcesMem: z.string().min(1, '请填写内存配额(如 512Mi)'),
storageSize: z.string().min(1, '请填写存储容量(如 10Gi)'),
storageSize: z.string().min(1, '请填写存储容量(如 20Gi)'),
})
.superRefine((data, ctx) => {
if (isK8sMode(data.mode) && !data.clusterID) {
@@ -51,6 +62,12 @@ export const deploymentFormSchema = z
if (isK8sMode(data.mode) && !data.namespace.trim()) {
ctx.addIssue({ code: 'custom', path: ['namespace'], message: '请填写命名空间' })
}
if (isK8sMode(data.mode)) {
const gi = parseGiB(data.storageSize)
if (gi === null || gi < 20) {
ctx.addIssue({ code: 'custom', path: ['storageSize'], message: '阿里云 ESSD 最小 20Gi,请填 20Gi 或以上' })
}
}
})
export type DeploymentFormValues = z.infer<typeof deploymentFormSchema>
@@ -69,7 +86,7 @@ export const DEFAULT_VALUES: DeploymentFormValues = {
authEnabled: false,
retentionDays: '30',
replicas: '1',
resourcesCPU: '500m',
resourcesMem: '512Mi',
storageSize: '10Gi',
resourcesCPU: '1',
resourcesMem: '2Gi',
storageSize: '20Gi',
}