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,13 +1,20 @@
|
||||
import { memo } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Rocket } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { EmptyState } from '@/components/EmptyState'
|
||||
import { StatusIcon } from '@/components/StatusBadge'
|
||||
import { getStatusColorClass } from '@/components/status-config'
|
||||
import { statusLabel } from '@/lib/utils'
|
||||
import type { LokiDeployment } from '@/types'
|
||||
|
||||
// 状态标签走 common.status.*;未收录的状态原样展示(与 StatusBadge 口径一致)
|
||||
function statusLabelOf(t: (key: string, opts?: { ns?: string }) => string, status: string): string {
|
||||
const key = `status.${status}`
|
||||
const label = t(key, { ns: 'common' })
|
||||
return label === key ? status : label
|
||||
}
|
||||
|
||||
/** 最近部署卡片 — 展示最近 5 条部署,点击跳转部署页 */
|
||||
export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
|
||||
deployments,
|
||||
@@ -16,18 +23,19 @@ export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
|
||||
deployments: LokiDeployment[]
|
||||
onViewAll: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('dashboard')
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<Card className="border-border">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">最近部署</CardTitle>
|
||||
<CardTitle className="text-base">{t('recent.title')}</CardTitle>
|
||||
<button
|
||||
onClick={onViewAll}
|
||||
className="text-xs text-primary hover:text-primary/80 font-medium"
|
||||
>
|
||||
查看全部
|
||||
{t('recent.viewAll')}
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -36,8 +44,8 @@ export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
|
||||
{deployments.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Rocket}
|
||||
title="暂无部署"
|
||||
description="创建你的第一个 Loki 部署"
|
||||
title={t('recent.emptyTitle')}
|
||||
description={t('recent.emptyDesc')}
|
||||
/>
|
||||
) : (
|
||||
deployments.map((d) => (
|
||||
@@ -62,7 +70,7 @@ export const RecentDeploymentsCard = memo(function RecentDeploymentsCard({
|
||||
d.status
|
||||
)}`}
|
||||
>
|
||||
{statusLabel(d.status)}
|
||||
{statusLabelOf(t, d.status)}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { getStatusColorClass } from '@/components/status-config'
|
||||
import { statusLabel } from '@/lib/utils'
|
||||
|
||||
// 用具体结构类型而非泛型:memo 包裹泛型组件会导致 TS 无法从 props 推断 T
|
||||
interface ResourceItem {
|
||||
@@ -22,6 +22,13 @@ interface ResourceListCardProps {
|
||||
getItemExtra?: (item: ResourceItem) => string | undefined
|
||||
}
|
||||
|
||||
// 状态标签走 common.status.*;未收录的状态原样展示(与 StatusBadge 口径一致)
|
||||
function statusLabelOf(t: (key: string, opts?: { ns?: string }) => string, status: string): string {
|
||||
const key = `status.${status}`
|
||||
const label = t(key, { ns: 'common' })
|
||||
return label === key ? status : label
|
||||
}
|
||||
|
||||
/** 资源列表卡片 — 集群/主机快捷列表(仪表盘) */
|
||||
export const ResourceListCard = memo(function ResourceListCard({
|
||||
title,
|
||||
@@ -33,6 +40,7 @@ export const ResourceListCard = memo(function ResourceListCard({
|
||||
getItemStatus,
|
||||
getItemExtra,
|
||||
}: ResourceListCardProps) {
|
||||
const { t } = useTranslation('dashboard')
|
||||
return (
|
||||
<Card className="border-border">
|
||||
<CardHeader className="pb-3">
|
||||
@@ -45,14 +53,16 @@ export const ResourceListCard = memo(function ResourceListCard({
|
||||
onClick={onManage}
|
||||
className="text-xs text-primary hover:text-primary/80 font-medium"
|
||||
>
|
||||
管理
|
||||
{t('list.manage')}
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{items.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">暂无{title}</p>
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
{t('list.empty', { title })}
|
||||
</p>
|
||||
) : (
|
||||
items.map((item) => (
|
||||
<div
|
||||
@@ -73,7 +83,7 @@ export const ResourceListCard = memo(function ResourceListCard({
|
||||
getItemStatus(item)
|
||||
)}`}
|
||||
>
|
||||
{statusLabel(getItemStatus(item))}
|
||||
{statusLabelOf(t, getItemStatus(item))}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import i18n from '@/i18n'
|
||||
import { clusterApi, deploymentApi, hostApi } from '@/api'
|
||||
import type { Cluster, DashboardStats, Host, LokiDeployment } from '@/types'
|
||||
|
||||
@@ -47,7 +48,7 @@ export function useDashboardStats(intervalMs: number = REFRESH_INTERVAL) {
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
if (version !== versionRef.current) return
|
||||
setError(err instanceof Error ? err.message : '加载仪表盘数据失败')
|
||||
setError(err instanceof Error ? err.message : i18n.t('dashboard:loadFailed'))
|
||||
} finally {
|
||||
if (version === versionRef.current) {
|
||||
setLoading(false)
|
||||
|
||||
@@ -2,6 +2,8 @@ import { memo, useCallback, useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Server, Monitor, Activity, TrendingUp, CheckCircle, Rocket, Wand2 } from 'lucide-react'
|
||||
import { Link } from 'react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import '@/i18n/domains'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { PageContainer } from '@/components/PageContainer'
|
||||
@@ -13,10 +15,11 @@ import { RecentDeploymentsCard } from './components/RecentDeploymentsCard'
|
||||
import { ResourceListCard } from './components/ResourceListCard'
|
||||
import type { DashboardStats } from '@/types'
|
||||
|
||||
// 状态概览三色指标;标签走 common.status.*(渲染处翻译)
|
||||
const STATUS_ITEMS = [
|
||||
{ key: 'running', label: '运行中', colorClass: 'text-primary' },
|
||||
{ key: 'failed', label: '失败', colorClass: 'text-destructive' },
|
||||
{ key: 'pending', label: '等待中', colorClass: 'text-warning' },
|
||||
{ key: 'running', colorClass: 'text-primary' },
|
||||
{ key: 'failed', colorClass: 'text-destructive' },
|
||||
{ key: 'pending', colorClass: 'text-warning' },
|
||||
] as const
|
||||
|
||||
/**
|
||||
@@ -24,6 +27,7 @@ const STATUS_ITEMS = [
|
||||
* 数据由 useDashboardStats 轮询(页面隐藏暂停);首次失败渲染错误态而非空数据。
|
||||
*/
|
||||
const DashboardPage = memo(function DashboardPage() {
|
||||
const { t } = useTranslation('dashboard')
|
||||
const { data, loading, error, refresh } = useDashboardStats()
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -39,7 +43,7 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
const cards: Array<React.ReactNode> = [
|
||||
<StatCard
|
||||
key="deployments"
|
||||
label="部署总数"
|
||||
label={t('stat.totalDeployments')}
|
||||
value={stats.totalDeployments}
|
||||
icon={Rocket}
|
||||
iconColor="text-primary"
|
||||
@@ -48,12 +52,12 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
icon: TrendingUp,
|
||||
color: 'text-primary',
|
||||
textColor: 'text-primary',
|
||||
text: `${stats.running} 个运行中`,
|
||||
text: t('stat.runningCount', { count: stats.running }),
|
||||
}}
|
||||
/>,
|
||||
<StatCard
|
||||
key="clusters"
|
||||
label="集群"
|
||||
label={t('stat.clusters')}
|
||||
value={stats.clusterCount}
|
||||
icon={Server}
|
||||
iconColor="text-info"
|
||||
@@ -62,12 +66,12 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
icon: CheckCircle,
|
||||
color: 'text-primary',
|
||||
textColor: 'text-muted-foreground',
|
||||
text: '已接入',
|
||||
text: t('stat.connected'),
|
||||
}}
|
||||
/>,
|
||||
<StatCard
|
||||
key="hosts"
|
||||
label="主机"
|
||||
label={t('stat.hosts')}
|
||||
value={stats.hostCount}
|
||||
icon={Monitor}
|
||||
iconColor="text-purple-600"
|
||||
@@ -76,12 +80,12 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
icon: CheckCircle,
|
||||
color: 'text-primary',
|
||||
textColor: 'text-muted-foreground',
|
||||
text: 'SSH 就绪',
|
||||
text: t('stat.sshReady'),
|
||||
}}
|
||||
/>,
|
||||
]
|
||||
return cards
|
||||
}, [stats])
|
||||
}, [stats, t])
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner className="h-64" />
|
||||
@@ -93,7 +97,7 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{/* 快速开始引导:尚无任何部署时置顶(状态推导引导,opsx-sec 范式) */}
|
||||
{/* 快速开始引导:尚无任何部署时置顶(状态推导式引导) */}
|
||||
{(data?.stats.totalDeployments ?? 0) === 0 && (
|
||||
<Card className="border-primary/30 bg-primary/5">
|
||||
<CardContent className="p-5 flex items-center justify-between flex-wrap gap-3">
|
||||
@@ -102,15 +106,15 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
<Wand2 className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">三步开始收集日志</p>
|
||||
<p className="font-medium">{t('quickStart.title')}</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
环境与目标 → 存储 → 部署 Loki 与 Grafana,最后添加采集策略
|
||||
{t('quickStart.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild className="bg-primary hover:bg-primary/90">
|
||||
<Link to="/wizard">
|
||||
进入部署向导
|
||||
{t('quickStart.cta')}
|
||||
<Wand2 className="w-4 h-4 ml-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -127,14 +131,16 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">状态</p>
|
||||
<p className="text-sm text-muted-foreground">{t('stat.status')}</p>
|
||||
<div className="flex gap-3 mt-1">
|
||||
{STATUS_ITEMS.map((item) => (
|
||||
<div key={item.key} className="text-center">
|
||||
<p className={`text-lg font-bold tabular-nums ${item.colorClass}`}>
|
||||
{(stats?.[item.key as keyof DashboardStats] as number) || 0}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{item.label}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(`status.${item.key}`, { ns: 'common' })}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -156,7 +162,7 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
|
||||
<div className="space-y-4">
|
||||
<ResourceListCard
|
||||
title="集群"
|
||||
title={t('stat.clusters')}
|
||||
icon={<Server className="w-4 h-4 text-info" />}
|
||||
items={(data?.clusters ?? []).slice(0, 3)}
|
||||
onManage={navigateTo('/clusters')}
|
||||
@@ -165,11 +171,11 @@ const DashboardPage = memo(function DashboardPage() {
|
||||
getItemStatus={(c) => c.status}
|
||||
/>
|
||||
<ResourceListCard
|
||||
title="主机"
|
||||
icon={<Monitor className="w-4 h-4 text-purple-500" />}
|
||||
title={t('stat.hosts')}
|
||||
icon={<Monitor className="w-4 h-4 text-info" />}
|
||||
items={(data?.hosts ?? []).slice(0, 3)}
|
||||
onManage={navigateTo('/hosts')}
|
||||
getItemIcon={() => <Monitor className="w-4 h-4 text-purple-500" />}
|
||||
getItemIcon={() => <Monitor className="w-4 h-4 text-info" />}
|
||||
getItemLabel={(h) => h.name}
|
||||
getItemStatus={(h) => h.status}
|
||||
getItemExtra={(h) => h.hostIP}
|
||||
|
||||
Reference in new issue
Block a user