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

@@ -6,36 +6,69 @@ import { applyLabelFilter, extractSelector } from '../query-utils'
type Template = 'app' | 'error' | 'warn' | 'pod' | 'ingress'
function selectorOf(labels: Record<string, string>) {
const entries = Object.entries(labels).filter(([, value]) => value.trim())
if (entries.length === 0) return ''
return `{${entries.map(([key, value]) => `${key}=${JSON.stringify(value.trim())}`).join(',')}}`
}
export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }: {
sourceId: number
query: string
onQueryChange: (query: string) => void
compact?: boolean
}) {
const [app, setApp] = useState('')
const [namespace, setNamespace] = useState('')
const [app, setApp] = useState('')
const [pod, setPod] = useState('')
const [apps, setApps] = useState<string[]>([])
const [namespaces, setNamespaces] = useState<string[]>([])
const [apps, setApps] = useState<string[]>([])
const [pods, setPods] = useState<string[]>([])
// namespace 列表只加载一次
useEffect(() => {
if (!sourceId) return
let active = true
Promise.allSettled([
logsApi.labelValues(sourceId, 'app'),
logsApi.labelValues(sourceId, 'namespace'),
logsApi.labelValues(sourceId, 'pod'),
]).then(([nextApps, nextNamespaces, nextPods]) => {
if (active) {
setApps(nextApps.status === 'fulfilled' ? nextApps.value : [])
setNamespaces(nextNamespaces.status === 'fulfilled' ? nextNamespaces.value : [])
setPods(nextPods.status === 'fulfilled' ? nextPods.value : [])
}
})
logsApi.labelValues(sourceId, 'namespace')
.then(next => { if (active) setNamespaces(next) })
.catch(() => { if (active) setNamespaces([]) })
return () => { active = false }
}, [sourceId])
// app 候选:选了 namespace 就只在该命名空间内搜索
useEffect(() => {
if (!sourceId) return
let active = true
const selector = selectorOf({ namespace })
logsApi.labelValues(sourceId, 'app', selector)
.then(next => { if (active) setApps(next) })
.catch(() => { if (active) setApps([]) })
return () => { active = false }
}, [sourceId, namespace])
// pod 候选:按 namespace + app 收窄
useEffect(() => {
if (!sourceId) return
let active = true
const selector = selectorOf({ namespace, app })
logsApi.labelValues(sourceId, 'pod', selector)
.then(next => { if (active) setPods(next) })
.catch(() => { if (active) setPods([]) })
return () => { active = false }
}, [sourceId, namespace, app])
function chooseNamespace(value: string) {
setNamespace(value)
// 换命名空间后旧的 app / pod 条件失效
setApp('')
setPod('')
}
function chooseApp(value: string) {
setApp(value)
setPod('')
}
function generate(template: Template) {
let next = extractSelector(query) || '{}'
if (template === 'ingress') next = applyLabelFilter(next, 'namespace', namespace.trim() || 'ingress-nginx')
@@ -54,18 +87,18 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
return <section className="space-y-2" aria-label="快捷查询模板">
{!compact && <h2 className="text-sm font-medium">快捷生成 LogQL</h2>}
{!compact && <p className="text-xs text-muted-foreground">选 app、namespace 或副本,再生成可编辑的 LogQL。</p>}
{!compact && <p className="text-xs text-muted-foreground">先选 namespace,再选 app,最后选副本 pod;app 候选只来自所选命名空间。</p>}
<div className="flex flex-wrap items-end gap-2">
<label className="min-w-36 flex-1 text-xs text-muted-foreground">app
<Input className="mt-1 h-8" list="quick-log-apps" value={app} onChange={event => setApp(event.target.value)} placeholder="搜索或输入 app" />
</label>
<datalist id="quick-log-apps">{apps.map(value => <option value={value} key={value} />)}</datalist>
<label className="min-w-36 flex-1 text-xs text-muted-foreground">namespace
<Input className="mt-1 h-8" list="quick-log-namespaces" value={namespace} onChange={event => setNamespace(event.target.value)} placeholder="可选" />
<Input className="mt-1 h-8" list="quick-log-namespaces" value={namespace} onChange={event => chooseNamespace(event.target.value)} placeholder="先选命名空间" />
</label>
<datalist id="quick-log-namespaces">{namespaces.map(value => <option value={value} key={value} />)}</datalist>
<label className="min-w-36 flex-1 text-xs text-muted-foreground">app
<Input className="mt-1 h-8" list="quick-log-apps" value={app} onChange={event => chooseApp(event.target.value)} placeholder={namespace ? '搜索该命名空间下的 app' : '先选 namespace 可缩小范围'} />
</label>
<datalist id="quick-log-apps">{apps.map(value => <option value={value} key={value} />)}</datalist>
<label className="min-w-36 flex-1 text-xs text-muted-foreground">副本 pod
<Input className="mt-1 h-8" list="quick-log-pods" value={pod} onChange={event => setPod(event.target.value)} placeholder="可选" />
<Input className="mt-1 h-8" list="quick-log-pods" value={pod} onChange={event => setPod(event.target.value)} placeholder={app ? '搜索该 app 的副本' : '可选'} />
</label>
<datalist id="quick-log-pods">{pods.map(value => <option value={value} key={value} />)}</datalist>
</div>
@@ -75,7 +108,7 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
<Button size="sm" variant="outline" disabled={!hasScope} onClick={() => generate('warn')}>告警 warn</Button>
<Button size="sm" variant="outline" disabled={!pod.trim()} onClick={() => generate('pod')}>指定副本日志</Button>
<Button size="sm" variant="outline" onClick={() => generate('ingress')}>Ingress 访问日志</Button>
{!hasScope && <span className="self-center text-xs text-muted-foreground">先填写 app、namespace 或 pod,也可在 Label 浏览器选值</span>}
{!hasScope && <span className="self-center text-xs text-muted-foreground">先选 namespace,再选 app 或 pod,也可在 Label 浏览器选值</span>}
</div>
</section>
}