Files
LokiStack/frontend/src/pages/settings/components/ImageRegistrySection.tsx
T
logan 9f7c522157 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 滚动与长命令换行,修复内容溢出
2026-10-25 23:44:07 +08:00

165 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
})