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:
1 parent
02d1bd87a7
commit
f3733f0bfc
201 files changed
+5438
-2388
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { logsApi } from '@/api/logs'
|
||||
import { SearchableValueSelect } from './SearchableValueSelect'
|
||||
@@ -21,6 +22,7 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
onQueryChange: (query: string) => void
|
||||
compact?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation('logs')
|
||||
const [namespace, setNamespace] = useState('')
|
||||
const [app, setApp] = useState('')
|
||||
const [pod, setPod] = useState('')
|
||||
@@ -30,7 +32,7 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
/**
|
||||
* 实际用来标识「应用」的标签名。
|
||||
*
|
||||
* K8s 惯例是 app 标签,但很多集群(如本环境)Deployment 模板没打 app,
|
||||
* Kubernetes 惯例是 app 标签,但很多集群(如本环境)Deployment 模板没打 app,
|
||||
* 只有 Kuboard 注入的 k8s.kuboard.cn/name,此时 Loki 里 app 只剩零星几个值。
|
||||
* Promtail 会从容器名生成 service_name / container,覆盖度更高,
|
||||
* 因此按 app → service_name → container 逐级回退。
|
||||
@@ -153,47 +155,47 @@ export function QuickQueryTemplates({ sourceId, query, onQueryChange, compact }:
|
||||
const hasSelector = extractSelector(query).replace(/[{}\s]/g, '').length > 0
|
||||
const hasScope = Boolean(app.trim() || namespace.trim() || pod.trim() || hasSelector)
|
||||
|
||||
return <section className="space-y-2" aria-label="快捷查询模板">
|
||||
{!compact && <h2 className="text-sm font-medium">快捷生成 LogQL</h2>}
|
||||
{!compact && <p className="text-xs text-muted-foreground">先选 namespace,再选 app,最后选副本 pod;app 候选只来自所选命名空间。</p>}
|
||||
return <section className="space-y-2" aria-label={t('templates.aria')}>
|
||||
{!compact && <h2 className="text-sm font-medium">{t('templates.title')}</h2>}
|
||||
{!compact && <p className="text-xs text-muted-foreground">{t('templates.hint')}</p>}
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
{/* 用 SearchableValueSelect(点击即展开列表 + 可搜索),
|
||||
原生 datalist 只在输入时才提示、样式弱,容易让人以为「下拉是空的」 */}
|
||||
<SearchableValueSelect
|
||||
label="命名空间"
|
||||
label={t('templates.namespace')}
|
||||
values={namespaceOptions}
|
||||
value={namespace}
|
||||
onChange={chooseNamespace}
|
||||
placeholder={`请选择(共 ${namespaceOptions.length} 个)`}
|
||||
placeholder={t('templates.selectCount', { count: namespaceOptions.length })}
|
||||
allowEmpty
|
||||
loading={!namespaceOptions.length}
|
||||
/>
|
||||
<SearchableValueSelect
|
||||
label={appLabel === 'app' ? 'App' : `App(${appLabel})`}
|
||||
label={appLabel === 'app' ? t('templates.app') : t('templates.appWithKey', { key: appLabel })}
|
||||
values={apps}
|
||||
value={app}
|
||||
onChange={chooseApp}
|
||||
placeholder={apps.length ? `请选择(共 ${apps.length} 个)` : namespace ? '该范围内无 app' : '先选命名空间可缩小范围'}
|
||||
placeholder={apps.length ? t('templates.selectCount', { count: apps.length }) : namespace ? t('templates.noApp') : t('templates.pickNsFirst')}
|
||||
allowEmpty
|
||||
disabled={!sourceId}
|
||||
/>
|
||||
<SearchableValueSelect
|
||||
label="副本 / Pod"
|
||||
label={t('templates.pod')}
|
||||
values={pods}
|
||||
value={pod}
|
||||
onChange={setPod}
|
||||
placeholder={pods.length ? `请选择(共 ${pods.length} 个)` : app ? '该 app 下无副本' : '可选'}
|
||||
placeholder={pods.length ? t('templates.selectCount', { count: pods.length }) : app ? t('templates.noPod') : t('templates.optional')}
|
||||
allowEmpty
|
||||
disabled={!sourceId}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" disabled={!hasScope} onClick={() => generate('app')}>查询该范围日志</Button>
|
||||
<Button size="sm" variant="outline" disabled={!hasScope} onClick={() => generate('error')}>错误 error</Button>
|
||||
<Button size="sm" variant="outline" disabled={!hasScope} onClick={() => generate('warn')}>告警 warn</Button>
|
||||
<Button size="sm" variant="outline" disabled={!pod.trim()} onClick={() => generate('pod')}>指定副本日志</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => generate('ingress')}>Ingress 访问日志</Button>
|
||||
{!hasScope && <span className="self-center text-xs text-muted-foreground">先选 namespace,再选 app 或 pod,也可在 Label 浏览器选值</span>}
|
||||
<Button size="sm" variant="outline" disabled={!hasScope} onClick={() => generate('app')}>{t('templates.scopeLogs')}</Button>
|
||||
<Button size="sm" variant="outline" disabled={!hasScope} onClick={() => generate('error')}>{t('templates.errorLogs')}</Button>
|
||||
<Button size="sm" variant="outline" disabled={!hasScope} onClick={() => generate('warn')}>{t('templates.warnLogs')}</Button>
|
||||
<Button size="sm" variant="outline" disabled={!pod.trim()} onClick={() => generate('pod')}>{t('templates.podLogs')}</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => generate('ingress')}>{t('templates.ingressLogs')}</Button>
|
||||
{!hasScope && <span className="self-center text-xs text-muted-foreground">{t('templates.scopeHint')}</span>}
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
Reference in new issue
Block a user