import { memo, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' 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' /** * 镜像源设置 — 影响平台拉取的全部镜像(Loki/Grafana/Promtail/Fluent Bit), * 参照 kuboard 的国内/海外选择;实时预览四类镜像的生效地址。 */ export const ImageRegistrySection = memo(function ImageRegistrySection() { const { t } = useTranslation('settings') const [cfg, setCfg] = useState(null) const [customHost, setCustomHost] = useState('') const [saving, setSaving] = useState(false) // 源选项(文案走词典;value 保持字面量类型以匹配 cfg.mode) const MODES = [ { value: 'overseas', label: t('registry.modeOverseasLabel'), desc: t('registry.modeOverseasDesc') }, { value: 'cn', label: t('registry.modeCnLabel'), desc: t('registry.modeCnDesc') }, { value: 'custom', label: t('registry.modeCustomLabel'), desc: t('registry.modeCustomDesc') }, ] as const // 组件镜像行(repo 为 placeholder 示例;tagHint 区分「跟随部署版本」与内置版本号) const COMPONENTS = [ { key: 'loki', label: 'Loki', repo: 'grafana/loki', tagHint: t('registry.tagFollowVersion') }, { key: 'grafana', label: 'Grafana', repo: 'grafana/grafana', tagHint: t('registry.tagFollowVersion') }, { 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 useEffect(() => { settingsApi .getImageRegistry() .then((c) => { setCfg(c) setCustomHost(c.customHost || '') }) .catch(() => toast.error(t('registry.errLoad'))) // 仅首载拉取配置;t 只影响失败 toast,不随语言切换重跑(避免覆盖未保存输入) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) 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(t('registry.savedToast')) } catch (err) { toast.error(err instanceof Error ? err.message : t('notify.errSave')) } finally { setSaving(false) } }, [cfg, customHost, t]) if (!cfg) return null return ( {t('registry.cardTitle')}
{MODES.map((m) => ( ))}
{cfg.mode === 'custom' && (
setCustomHost(e.target.value)} />

{t('registry.customPrefixHint', { example: customHost || 'your-registry' })}

)} {/* 各服务镜像自定义 */}
{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 (
{label} update({ repository: e.target.value })} className="font-mono text-xs" /> update({ tag: e.target.value })} className="font-mono text-xs" />
) })}

{t('registry.imagesHintBefore')} grafana/loki {t('registry.imagesHintAfter')}

{/* 生效镜像预览 */}

{t('registry.previewTitle')}

{([ ['Loki', cfg.sampleLoki], ['Grafana', cfg.sampleGrafana], ['Promtail', cfg.samplePromtail], ['Fluent Bit', cfg.sampleFluentBit], ] as const).map(([name, img]) => (

{name} {img}

))}
) })