feat: 品牌统一 LokiStack + 全站中英 i18n + 用户批量删除 + 主题定时切换与浅色侧栏

- 品牌: 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 未翻译、词典双逗号等
This commit is contained in:
opsx committed 2026-10-27 17:11:38 +08:00
1 parent 02d1bd87a7
commit f3733f0bfc
201 files changed
+5438 -2388

No files matched your search

@@ -1,4 +1,5 @@
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'
@@ -8,9 +9,10 @@ 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, '请输入 YAML 配置') })
const schema = z.object({ yaml: z.string().trim().min(1, i18n.t('deployments:retention.yamlRequired')) })
type Values = z.infer<typeof schema>
interface Props {
@@ -19,14 +21,14 @@ interface Props {
onSaved: () => void
}
function configLocation(deployment: LokiDeployment) {
if (deployment.mode === 'k8s-helm') return 'Helm values.yaml → loki.limits_config.retention_period'
if (deployment.mode === 'host-binary') return '/etc/loki/loki.yaml → limits_config.retention_period'
if (deployment.mode === 'host-docker') return `主机 /etc/lokideploy/loki-${deployment.id}/loki.yaml → limits_config.retention_period`
return `ConfigMap ${deployment.namespace || 'log'}/loki-config → loki.yaml`
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)
@@ -51,7 +53,7 @@ export const RetentionDialog = memo(function RetentionDialog({ deployment, onClo
try {
setLiveConfig(await deploymentApi.liveConfig(deployment.id))
} catch (err) {
setConfigError(err instanceof Error ? err.message : '读取运行配置失败')
setConfigError(err instanceof Error ? err.message : t('retention.errLoad'))
} finally {
setConfigLoading(false)
}
@@ -64,9 +66,9 @@ export const RetentionDialog = memo(function RetentionDialog({ deployment, onClo
const result = await deploymentApi.updateRetention(deployment.id, yaml, apply)
onSaved()
onClose()
if (apply) toast.info(`已保存 ${result.retentionDays} 天,Loki 正在重新部署`)
if (apply) toast.info(t('retention.savedApplied', { days: result.retentionDays }))
} catch (err) {
toast.error(err instanceof Error ? err.message : '保存 Loki 配置失败')
toast.error(err instanceof Error ? err.message : t('retention.errSave'))
onSaved()
} finally {
setSaving(false)
@@ -77,23 +79,22 @@ export const RetentionDialog = memo(function RetentionDialog({ deployment, onClo
<Dialog open={deployment !== null} onOpenChange={(open) => { if (!open && !saving) onClose() }}>
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>编辑 Loki 配置 · {deployment?.name}</DialogTitle>
<DialogTitle>{deployment ? t('retention.title', { name: deployment.name }) : ''}</DialogTitle>
</DialogHeader>
<div className="space-y-1 text-sm text-muted-foreground">
<p>配置位置:{deployment ? configLocation(deployment) : ''}</p>
<p>保留策略由 Loki compactor 执行;MinIO 只负责存储日志对象。</p>
<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">运行中的完整 loki.yaml</p>
<p className="text-xs text-muted-foreground">从部署目标读取,存储凭证已遮盖;下方保留天数仍单独编辑。</p>
<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 || deployment?.mode === 'k8s-helm'}>
{configLoading ? '读取中…' : liveConfig ? '刷新配置' : '查看完整配置'}
<Button type="button" variant="outline" size="sm" onClick={loadConfig} disabled={configLoading}>
{configLoading ? t('retention.loading') : liveConfig ? t('retention.refresh') : t('retention.view')}
</Button>
</div>
{deployment?.mode === 'k8s-helm' && <p className="text-xs text-muted-foreground">Helm 模式暂不支持读取运行配置。</p>}
{configError && <p className="text-sm text-destructive">{configError}</p>}
{liveConfig && (
<div>
@@ -103,7 +104,7 @@ export const RetentionDialog = memo(function RetentionDialog({ deployment, onClo
)}
</div>
<div className="rounded-md border border-border bg-muted/40 p-3 text-xs">
<p className="mb-1 font-medium text-foreground">由部署模板生成</p>
<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}>
@@ -113,28 +114,24 @@ export const RetentionDialog = memo(function RetentionDialog({ deployment, onClo
name="yaml"
render={({ field }) => (
<FormItem>
<FormLabel>保留配置 YAML</FormLabel>
<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">
编辑 limits_config.retention_period,例如 90d 或 2160h。此处只接受这一项配置。
</p>
<p className="text-xs text-muted-foreground">{t('retention.yamlHint')}</p>
<FormMessage />
</FormItem>
)}
/>
<p className="text-sm text-muted-foreground">
{deployment?.status === 'running'
? '仅保存会更新部署记录;保存并应用会重新部署 Loki,查询可能短暂中断。'
: '保存后在下次部署时生效。'}
{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}>取消</Button>
<Button type="submit" variant="outline" disabled={saving}>仅保存</Button>
<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 ? '处理中…' : '保存并应用'}
{saving ? t('retention.processing') : t('retention.saveApply')}
</Button>
)}
</div>