- 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 滚动与长命令换行,修复内容溢出
165 lines
6.5 KiB
TypeScript
165 lines
6.5 KiB
TypeScript
import { memo, useCallback, useEffect, useState } from 'react'
|
||
import { Globe, Save } from 'lucide-react'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Input } from '@/components/ui/input'
|
||
import { Label } from '@/components/ui/label'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import { cn } from '@/lib/utils'
|
||
import { settingsApi, type ImageRegistryConfig } from '@/api/settings'
|
||
import { toast } from 'sonner'
|
||
|
||
const MODES = [
|
||
{ value: 'overseas', label: '海外官方', desc: 'docker.io,海外网络环境' },
|
||
{ value: 'cn', label: '国内加速', desc: 'docker.m.daocloud.io 镜像,国内环境防被墙' },
|
||
{ value: 'custom', label: '私有仓库', desc: '自有 registry 前缀(如华为云 SWR)' },
|
||
] as const
|
||
|
||
const COMPONENTS = [
|
||
{ key: 'loki', label: 'Loki', repo: 'grafana/loki', tagHint: '跟随部署版本' },
|
||
{ key: 'grafana', label: 'Grafana', repo: 'grafana/grafana', tagHint: '跟随部署版本' },
|
||
{ key: 'promtail', label: 'Promtail', repo: 'grafana/promtail', tagHint: '3.4.0' },
|
||
{ key: 'fluentbit', label: 'Fluent Bit', repo: 'fluent/fluent-bit', tagHint: '5.1.5' },
|
||
] as const
|
||
|
||
/**
|
||
* 镜像源设置 — 影响平台拉取的全部镜像(Loki/Grafana/Promtail/Fluent Bit),
|
||
* 参照 kuboard 的国内/海外选择;实时预览四类镜像的生效地址。
|
||
*/
|
||
export const ImageRegistrySection = memo(function ImageRegistrySection() {
|
||
const [cfg, setCfg] = useState<ImageRegistryConfig | null>(null)
|
||
const [customHost, setCustomHost] = useState('')
|
||
const [saving, setSaving] = useState(false)
|
||
|
||
useEffect(() => {
|
||
settingsApi
|
||
.getImageRegistry()
|
||
.then((c) => {
|
||
setCfg(c)
|
||
setCustomHost(c.customHost || '')
|
||
})
|
||
.catch(() => toast.error('加载镜像源配置失败'))
|
||
}, [])
|
||
|
||
const handleSave = useCallback(async () => {
|
||
if (!cfg) return
|
||
setSaving(true)
|
||
try {
|
||
const saved = await settingsApi.saveImageRegistry({
|
||
mode: cfg.mode,
|
||
customHost: cfg.mode === 'custom' ? customHost : undefined,
|
||
images: cfg.images,
|
||
})
|
||
setCfg(saved)
|
||
toast.success('镜像源已保存,后续部署/采集将使用新镜像地址')
|
||
} catch (err) {
|
||
toast.error(err instanceof Error ? err.message : '保存失败')
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}, [cfg, customHost])
|
||
|
||
if (!cfg) return null
|
||
|
||
return (
|
||
<Card className="border-border">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-base flex items-center gap-2">
|
||
<Globe className="w-4 h-4" />
|
||
镜像仓库源
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
{MODES.map((m) => (
|
||
<button
|
||
key={m.value}
|
||
type="button"
|
||
aria-pressed={cfg.mode === m.value}
|
||
onClick={() => setCfg({ ...cfg, mode: m.value })}
|
||
className={cn(
|
||
'text-left rounded-xl border-2 p-3 transition-all',
|
||
cfg.mode === m.value ? 'border-primary bg-primary/5' : 'border-border hover:border-input'
|
||
)}
|
||
>
|
||
<p className={cn('text-sm font-medium', cfg.mode === m.value && 'text-primary')}>{m.label}</p>
|
||
<p className="text-xs text-muted-foreground mt-1">{m.desc}</p>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{cfg.mode === 'custom' && (
|
||
<div className="space-y-2">
|
||
<Label>私有仓库前缀 *</Label>
|
||
<Input
|
||
placeholder="swr.cn-east-2.myhuaweicloud.com/your-namespace"
|
||
value={customHost}
|
||
onChange={(e) => setCustomHost(e.target.value)}
|
||
/>
|
||
<p className="text-xs text-muted-foreground">平台会在镜像名前拼接此前缀,如 {customHost || 'your-registry'}/grafana/loki:3.4.0</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 各服务镜像自定义 */}
|
||
<div className="space-y-2">
|
||
<Label>各服务镜像(可单独覆盖仓库 / Tag)</Label>
|
||
<div className="space-y-2">
|
||
{COMPONENTS.map(({ key, label, repo, tagHint }) => {
|
||
const img = cfg.images?.[key] || { repository: '', tag: '' }
|
||
const update = (patch: Partial<{ repository: string; tag: string }>) =>
|
||
setCfg({
|
||
...cfg,
|
||
images: { ...(cfg.images || {}), [key]: { ...img, ...patch } },
|
||
})
|
||
return (
|
||
<div key={key} className="grid items-center gap-2 sm:grid-cols-[110px_1fr_150px]">
|
||
<span className="text-sm font-medium">{label}</span>
|
||
<Input
|
||
aria-label={`${label} 镜像仓库`}
|
||
value={img.repository}
|
||
placeholder={repo}
|
||
onChange={(e) => update({ repository: e.target.value })}
|
||
className="font-mono text-xs"
|
||
/>
|
||
<Input
|
||
aria-label={`${label} 镜像 Tag`}
|
||
value={img.tag}
|
||
placeholder={tagHint}
|
||
onChange={(e) => update({ tag: e.target.value })}
|
||
className="font-mono text-xs"
|
||
/>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
仓库填镜像路径(如 <code>grafana/loki</code>);Tag 留空时 Loki / Grafana 跟随部署版本,Promtail / Fluent Bit 用内置默认。
|
||
</p>
|
||
</div>
|
||
|
||
{/* 生效镜像预览 */}
|
||
<div className="rounded-lg bg-muted/50 p-3 space-y-1.5">
|
||
<p className="text-xs font-medium text-muted-foreground">生效镜像预览</p>
|
||
{([
|
||
['Loki', cfg.sampleLoki],
|
||
['Grafana', cfg.sampleGrafana],
|
||
['Promtail', cfg.samplePromtail],
|
||
['Fluent Bit', cfg.sampleFluentBit],
|
||
] as const).map(([name, img]) => (
|
||
<p key={name} className="text-xs font-mono flex gap-2">
|
||
<span className="text-muted-foreground w-20 shrink-0">{name}</span>
|
||
<span className="truncate" title={img}>{img}</span>
|
||
</p>
|
||
))}
|
||
</div>
|
||
|
||
<div className="flex justify-end">
|
||
<Button onClick={handleSave} disabled={saving}>
|
||
<Save className="w-4 h-4 mr-1" />
|
||
{saving ? '保存中...' : '保存镜像源'}
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
})
|