Files
LokiStack/frontend/src/router/routes.tsx
T
opsx 6d6e17745c fix(logs): 恢复 POST /logs/histogram(直方图 404)+ 系统设置改为侧栏五分页
=== 1. 修复 /analysis 与 /logs 直方图 404(我上一轮 patch 的误伤)===
现象:分析页直接报 "API endpoint not found";日志查询页直方图不显示。
根因:37e2f0f 里一次模糊匹配的 patch 把
    logs.POST("/histogram", logsCtrl.Histogram)
  改成了
    logs.GET("/histogram", logsCtrl.Histogram)
前端 logsApi.histogram 一直用 POST(body 带 sourceId/query/start/end),
于是 POST /logs/histogram 全部 404。访问日志实证:
    POST /api/v1/logs/histogram → 404   ×7
这类"方法写错"编译期查不出来,go build / tsc 都是绿的。

修法:同时注册 POST(前端用)与 GET(curl 调试用)。

新增 backend/routers/router_test.go 守门:
  TestCriticalRoutes       断言 19 条关键路由的 method+path 组合都存在
  TestHistogramAcceptsPost 单独断言 histogram 必须同时有 POST 与 GET
以后再把 POST 写成 GET,测试会直接红。

=== 2. 系统设置放回侧栏,改成 5 个顶层 tab ===
按使用反馈调整:
  系统设置(侧栏一个入口)
    ├ 通用配置      /settings            ← tab 起点
    ├ 用户管理      /users
    ├ 审计日志      /audit
    ├ LDAP 接入     /settings/ldap       ← 新拆分
    └ 默认镜像源    /settings/registry   ← 新拆分

为什么拆:原来 /settings 页面内部已有 4 个 tab(通用/告警通知/LDAP/镜像源),
如果侧栏再加一层 tab 就是"两层 tab",点两下才知道自己在哪。现在扁平化成单层。

配套改动:
  - 新增 pages/settings/ldap.tsx、registry.tsx(各自只渲染一块内容)
  - settings 主页去掉内层 Tabs,只留通用配置(外观/语言/改密码/关于)
  - 上一版把设置放到了右上角齿轮,现在移除齿轮入口
  - 菜单文案:settings 页标题 系统设置 → 通用配置;新增 LDAP 接入 / 默认镜像源
  - 删除设置页副标题「通用配置、告警通知、LDAP 接入与默认镜像源」(用户要求去掉)
  - 删除 settings 里已不存在的 tabs.notify 文案
    (注意 settings.notify.* 这些 key 仍被告警页的 NotifySection 用着,保留)

=== 3. 告警通知已不在系统设置里 ===
它在上一轮就搬到了「日志告警 → 通知通道」tab,
本次顺带把系统设置里遗留的 tab 文案一并清掉,避免出现"看似有两处配置入口"。

=== 实测 ===
  POST /api/v1/logs/histogram → 200(修复前 404)
  go test ./... 全绿(含新增的 routers 包)
  tsc -b --force 通过
2026-10-28 23:12:42 +00:00

166 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 路由配置 — 全站唯一路由声明处。
// 页面组件一律 React.lazy 懒加载;菜单(menuItems)与页面标题(findRouteMeta)
// 均由本文件派生,新增页面只需在这里加一条配置。
import { lazy } from 'react';
import {
LayoutDashboard,
Server,
Monitor,
HardDrive,
Rocket,
RadioTower,
Settings,
UsersRound,
Wand2,
ScrollText,
BellRing,
BarChart3,
History,
Boxes,
ShieldCheck,
Container,
} from 'lucide-react';
import type { AppRoute, MenuItem, RouteMeta } from '@/types/router';
const Login = lazy(() => import('@/pages/login'));
const Dashboard = lazy(() => import('@/pages/dashboard'));
const Clusters = lazy(() => import('@/pages/clusters'));
const Hosts = lazy(() => import('@/pages/hosts'));
const Storage = lazy(() => import('@/pages/storage'));
const Deployments = lazy(() => import('@/pages/deployments'));
const Collectors = lazy(() => import('@/pages/collectors'));
const SettingsPage = lazy(() => import('@/pages/settings'));
const LdapPage = lazy(() => import('@/pages/settings/ldap'));
const RegistryPage = lazy(() => import('@/pages/settings/registry'));
const NotFound = lazy(() => import('@/pages/not-found'));
const WizardPage = lazy(() => import('@/pages/wizard'));
const LogsPage = lazy(() => import('@/pages/logs'));
const AlertsPage = lazy(() => import('@/pages/alerts'));
const UsersPage = lazy(() => import('@/pages/users'));
const AnalysisPage = lazy(() => import('@/pages/analysis'));
const AuditPage = lazy(() => import('@/pages/audit'));
export const routes: AppRoute[] = [
{ path: '/login', component: Login, auth: false, meta: { title: 'menu.login', hideInMenu: true } },
{ path: '/', component: Dashboard, auth: true, meta: { title: 'menu.dashboard', icon: LayoutDashboard, order: 1, adminOnly: true } },
{ path: '/wizard', component: WizardPage, auth: true, meta: { title: 'menu.wizard', icon: Wand2, order: 2, adminOnly: true } },
// ── 资源接入(集群 / 主机 / 存储):装完基本不动的配置,合并为一个入口 ──
{
path: '/clusters', component: Clusters, auth: true,
meta: {
title: 'menu.clusters', icon: Server, order: 3, adminOnly: true,
group: { key: 'resources', label: 'menu.groups.resources', icon: Boxes, order: 3 },
},
},
{ path: '/hosts', component: Hosts, auth: true, meta: { title: 'menu.hosts', icon: Monitor, order: 4, adminOnly: true, groupKey: 'resources' } },
{ path: '/storage', component: Storage, auth: true, meta: { title: 'menu.storage', icon: HardDrive, order: 5, adminOnly: true, groupKey: 'resources' } },
// ── 日志服务(Loki 实例 / 采集器):部署与采集是同一件事的两半 ──
{
path: '/deployments', component: Deployments, auth: true,
meta: {
// 组内 tab 名用「组件部署」:部署对象是基础设施组件(Loki 实例),
// 与业务侧大量 cex-* 业务服务明确区分,避免误以为这里是发布业务服务。
title: 'menu.deployments', icon: Rocket, order: 4, adminOnly: true,
group: { key: 'services', label: 'menu.groups.services', icon: Rocket, order: 4 },
},
},
{ path: '/collectors', component: Collectors, auth: true, meta: { title: 'menu.collectors', icon: RadioTower, order: 7, adminOnly: true, groupKey: 'services' } },
// ── 日志查询 / 日志分析:高频入口,保持独立菜单项(不折叠)──
{ path: '/logs', component: LogsPage, auth: true, meta: { title: 'menu.logs', icon: ScrollText, order: 5 } },
{ path: '/analysis', component: AnalysisPage, auth: true, meta: { title: 'menu.analysis', icon: BarChart3, order: 6, adminOnly: true } },
// ── 告警:独立入口(要盯的,不能被折叠) ──
// 告警仅管理员:普通用户按「查询授权」只看指定的日志(菜单只剩日志查询)。
// 告警规则涉及阈值/通知通道等平台级配置,不属于日志查询者的职责。
{ path: '/alerts', component: AlertsPage, auth: true, meta: { title: 'menu.alerts', icon: BellRing, order: 7, adminOnly: true } },
// ── 系统管理(用户 / 审计 / 设置):低频管理动作 ──
{
path: '/users', component: UsersPage, auth: true,
meta: {
// 系统设置(tab 起点)/ 用户管理 / 审计日志 同属「系统设置」分组,
// 侧栏只占一个入口,页面内用 tab 切换。
title: 'menu.users', icon: UsersRound, order: 9, adminOnly: true,
groupKey: 'system',
},
},
{ path: '/audit', component: AuditPage, auth: true, meta: { title: 'menu.audit', icon: History, order: 10, adminOnly: true, groupKey: 'system' } },
{ path: '/settings/ldap', component: LdapPage, auth: true, meta: { title: 'menu.ldap', icon: ShieldCheck, order: 11, adminOnly: true, groupKey: 'system' } },
{ path: '/settings/registry', component: RegistryPage, auth: true, meta: { title: 'menu.registry', icon: Container, order: 12, adminOnly: true, groupKey: 'system' } },
// 系统设置放回侧栏,并作为「系统设置」分组的 tab 起点(order 最小 = 排在第一个)
{
path: '/settings', component: SettingsPage, auth: true,
meta: {
title: 'menu.settings', icon: Settings, order: 8, adminOnly: true,
group: { key: 'system', label: 'menu.groups.system', icon: Settings, order: 8 },
},
},
{ path: '*', component: NotFound, auth: false, meta: { title: 'menu.notFound', hideInMenu: true } },
];
/**
* 侧栏菜单:登录后可见的路由,按 order 排序。
*
* 分组规则(解决"菜单太多"的观感问题):
* - 带 meta.group 的路由是一个分组的**代表入口**,菜单显示分组的 label/icon
* - 带 meta.groupKey 的路由说明它属于某个已有分组 → 不进菜单,只在页内 tab 出现
* 这样 13 个路由收敛成 7 个菜单项,且 URL 全部保持不变(书签不会失效)。
*/
export const menuItems: MenuItem[] = (() => {
const grouped = new Set<string>();
const items: MenuItem[] = [];
const sorted = routes
.filter((route) => route.auth && !route.meta.hideInMenu)
.sort((a, b) => {
const orderA = a.meta.group?.order ?? a.meta.order ?? 0;
const orderB = b.meta.group?.order ?? b.meta.order ?? 0;
return orderA - orderB;
});
for (const route of sorted) {
const { group, groupKey } = route.meta;
// 属于某个分组的非代表路由:不占菜单位
if (groupKey && !group) continue;
if (group) {
if (grouped.has(group.key)) continue;
grouped.add(group.key);
items.push({
path: route.path,
icon: group.icon ?? route.meta.icon,
label: group.label,
adminOnly: route.meta.adminOnly,
groupKey: group.key,
});
continue;
}
items.push({
path: route.path,
icon: route.meta.icon,
label: route.meta.title,
adminOnly: route.meta.adminOnly,
});
}
return items;
})();
/** 取某个分组下的所有页面(用于页内 tab 切换),按 order 排序 */
export function findGroupRoutes(groupKey: string): AppRoute[] {
return routes
.filter((route) => route.meta.groupKey === groupKey || route.meta.group?.key === groupKey)
.sort((a, b) => (a.meta.order ?? 0) - (b.meta.order ?? 0));
}
/** 当前路径所属的分组标识(不在分组内返回 undefined) */
export function findGroupKey(pathname: string): string | undefined {
const meta = findRouteMeta(pathname);
return meta?.group?.key ?? meta?.groupKey;
}
/** 按路径查 meta(通配/未匹配返回 undefined,调用方自行兜底) */
export function findRouteMeta(pathname: string): RouteMeta | undefined {
return routes.find((route) => route.path === pathname)?.meta;
}