- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂 - i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏 - 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条 - 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器 - UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正 - 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简 - 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
import { memo, useEffect, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { z } from 'zod'
|
|
import { toast } from 'sonner'
|
|
import { deploymentApi } from '@/api'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import i18n from '@/i18n'
|
|
import type { LokiDeployment } from '@/types'
|
|
|
|
const schema = z.object({ yaml: z.string().trim().min(1, i18n.t('deployments:retention.yamlRequired')) })
|
|
type Values = z.infer<typeof schema>
|
|
|
|
interface Props {
|
|
deployment: LokiDeployment | null
|
|
onClose: () => void
|
|
onSaved: () => void
|
|
}
|
|
|
|
function configLocation(deployment: LokiDeployment, t: (k: string, o?: Record<string, unknown>) => string) {
|
|
if (deployment.mode === 'host-binary') return t('retention.locationHostBinary')
|
|
if (deployment.mode === 'host-docker') return t('retention.locationHostDocker', { id: deployment.id })
|
|
return t('retention.locationK8s', { namespace: deployment.namespace || 'log' })
|
|
}
|
|
|
|
export const RetentionDialog = memo(function RetentionDialog({ deployment, onClose, onSaved }: Props) {
|
|
const { t } = useTranslation('deployments')
|
|
const [saving, setSaving] = useState(false)
|
|
const [liveConfig, setLiveConfig] = useState<{ location: string; yaml: string } | null>(null)
|
|
const [configLoading, setConfigLoading] = useState(false)
|
|
const [configError, setConfigError] = useState('')
|
|
const form = useForm<Values>({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: { yaml: 'limits_config:\n retention_period: 30d\n' },
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (deployment) {
|
|
form.reset({ yaml: `limits_config:\n retention_period: ${deployment.retentionDays || 30}d\n` })
|
|
}
|
|
setLiveConfig(null)
|
|
setConfigError('')
|
|
}, [deployment, form])
|
|
|
|
const loadConfig = async () => {
|
|
if (!deployment) return
|
|
setConfigLoading(true)
|
|
setConfigError('')
|
|
try {
|
|
setLiveConfig(await deploymentApi.liveConfig(deployment.id))
|
|
} catch (err) {
|
|
setConfigError(err instanceof Error ? err.message : t('retention.errLoad'))
|
|
} finally {
|
|
setConfigLoading(false)
|
|
}
|
|
}
|
|
|
|
const save = (apply: boolean) => form.handleSubmit(async ({ yaml }) => {
|
|
if (!deployment) return
|
|
setSaving(true)
|
|
try {
|
|
const result = await deploymentApi.updateRetention(deployment.id, yaml, apply)
|
|
onSaved()
|
|
onClose()
|
|
if (apply) toast.info(t('retention.savedApplied', { days: result.retentionDays }))
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : t('retention.errSave'))
|
|
onSaved()
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
})
|
|
|
|
return (
|
|
<Dialog open={deployment !== null} onOpenChange={(open) => { if (!open && !saving) onClose() }}>
|
|
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>{deployment ? t('retention.title', { name: deployment.name }) : ''}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-1 text-sm text-muted-foreground">
|
|
<p>{t('retention.locationLabel')}{deployment ? configLocation(deployment, t) : ''}</p>
|
|
<p>{t('retention.compactorNote')}</p>
|
|
</div>
|
|
<div className="space-y-2 rounded-md border border-border p-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-medium">{t('retention.liveTitle')}</p>
|
|
<p className="text-xs text-muted-foreground">{t('retention.liveHint')}</p>
|
|
</div>
|
|
<Button type="button" variant="outline" size="sm" onClick={loadConfig} disabled={configLoading}>
|
|
{configLoading ? t('retention.loading') : liveConfig ? t('retention.refresh') : t('retention.view')}
|
|
</Button>
|
|
</div>
|
|
{configError && <p className="text-sm text-destructive">{configError}</p>}
|
|
{liveConfig && (
|
|
<div>
|
|
<p className="mb-2 text-xs text-muted-foreground">{liveConfig.location}</p>
|
|
<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>
|
|
<div className="rounded-md border border-border bg-muted/40 p-3 text-xs">
|
|
<p className="mb-1 font-medium text-foreground">{t('retention.templateNote')}</p>
|
|
<pre className="text-muted-foreground">{'compactor:\n retention_enabled: true\n retention_delete_delay: 2h'}</pre>
|
|
</div>
|
|
<Form {...form}>
|
|
<form onSubmit={save(false)} className="space-y-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="yaml"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('retention.yamlLabel')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={4} spellCheck={false} className="font-mono text-sm" {...field} />
|
|
</FormControl>
|
|
<p className="text-xs text-muted-foreground">{t('retention.yamlHint')}</p>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
{deployment?.status === 'running' ? t('retention.applyHintRunning') : t('retention.applyHintIdle')}
|
|
</p>
|
|
<div className="flex justify-end gap-2">
|
|
<Button type="button" variant="outline" onClick={onClose} disabled={saving}>{t('retention.cancel')}</Button>
|
|
<Button type="submit" variant="outline" disabled={saving}>{t('retention.saveOnly')}</Button>
|
|
{deployment?.status === 'running' && (
|
|
<Button type="button" onClick={save(true)} disabled={saving}>
|
|
{saving ? t('retention.processing') : t('retention.saveApply')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
})
|