refactor: move ops triage cards to ops page
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { type AnchorHTMLAttributes, type KeyboardEvent as ReactKeyboardEvent, type ReactNode, Suspense, lazy, useEffect, useState, useCallback } from "react";
|
import { type AnchorHTMLAttributes, type KeyboardEvent as ReactKeyboardEvent, type ReactNode, Suspense, lazy, useEffect, useState, useCallback } from "react";
|
||||||
import { Link, useInRouterContext } from "react-router-dom";
|
import { Link, useInRouterContext } from "react-router-dom";
|
||||||
import { api } from "../api.js";
|
import { api } from "../api.js";
|
||||||
import DiagnosticActionGroup from "../components/DiagnosticActionGroup.js";
|
|
||||||
import { useToast } from "../components/Toast.js";
|
import { useToast } from "../components/Toast.js";
|
||||||
import { useIsMobile } from "../components/useIsMobile.js";
|
import { useIsMobile } from "../components/useIsMobile.js";
|
||||||
import { useOptionalI18n } from "../i18n.js";
|
import { useOptionalI18n } from "../i18n.js";
|
||||||
@@ -657,16 +656,6 @@ export default function Dashboard({
|
|||||||
: overallHealthTone === "warning"
|
: overallHealthTone === "warning"
|
||||||
? t("需关注")
|
? t("需关注")
|
||||||
: t("异常");
|
: t("异常");
|
||||||
const spendTone =
|
|
||||||
todaySpend <= todayReward
|
|
||||||
? "success"
|
|
||||||
: todaySpend <= Math.max(1, todayReward * 2)
|
|
||||||
? "warning"
|
|
||||||
: "danger";
|
|
||||||
const throughputLabel =
|
|
||||||
requestsPerMinute > 0
|
|
||||||
? `${Math.round(requestsPerMinute).toLocaleString()} RPM`
|
|
||||||
: t("暂无流量");
|
|
||||||
const avgActiveSiteLatency = activeSites.length > 0
|
const avgActiveSiteLatency = activeSites.length > 0
|
||||||
? Math.round(
|
? Math.round(
|
||||||
activeSites.reduce(
|
activeSites.reduce(
|
||||||
@@ -703,69 +692,6 @@ export default function Dashboard({
|
|||||||
tone: requestsPerMinute > 0 ? "primary" : "muted",
|
tone: requestsPerMinute > 0 ? "primary" : "muted",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const operationsPulse = [
|
|
||||||
{
|
|
||||||
label: t("整体状态"),
|
|
||||||
value: overallHealthLabel,
|
|
||||||
note: `${proxySuccessRate.toFixed(1)}% ${t("成功率")}`,
|
|
||||||
tone: overallHealthTone,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("站点风险"),
|
|
||||||
value: `${siteRiskCount}`,
|
|
||||||
note: `${activeSites.length}/${rawSiteAvailability.length} ${t("活跃站点")}`,
|
|
||||||
tone: siteRiskCount > 0 ? "warning" : "success",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("今日成本"),
|
|
||||||
value: `$${todaySpend.toFixed(2)}`,
|
|
||||||
note: `${t("奖励")} $${todayReward.toFixed(2)}`,
|
|
||||||
tone: spendTone,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("当前吞吐"),
|
|
||||||
value: throughputLabel,
|
|
||||||
note: `${formatCompactTokenMetric(tokensPerMinute)} TPM`,
|
|
||||||
tone: requestsPerMinute > 0 ? "primary" : "muted",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const dashboardCommandLinks = [
|
|
||||||
{
|
|
||||||
to: "/ops",
|
|
||||||
label: t("运维监控"),
|
|
||||||
value: overallHealthLabel,
|
|
||||||
note: t("值班入口"),
|
|
||||||
tone: overallHealthTone,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: "/routes",
|
|
||||||
label: t("路由健康"),
|
|
||||||
value: routeHealthItems.length > 0 ? String(routeRiskCount) : t("暂无"),
|
|
||||||
note: routeHealthItems.length > 0 ? (routeRiskCount > 0 ? t("需处理路由") : t("配置正常")) : t("等待路由"),
|
|
||||||
tone: routeHealthItems.length > 0 ? (routeHealthCounts.danger > 0 ? "danger" : routeRiskCount > 0 ? "warning" : "success") : "muted",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: "/accounts",
|
|
||||||
label: t("连接管理"),
|
|
||||||
value: `${Math.round(activeAccounts)}/${Math.round(totalAccounts)}`,
|
|
||||||
note: t("账号健康"),
|
|
||||||
tone: activeAccountRate >= 80 ? "success" : "warning",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: "/sites",
|
|
||||||
label: t("站点管理"),
|
|
||||||
value: String(siteRiskCount),
|
|
||||||
note: siteRiskCount > 0 ? t("需关注站点") : t("风险站点"),
|
|
||||||
tone: siteRiskCount > 0 ? "warning" : "success",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: "/ip-management",
|
|
||||||
label: t("IP 管理"),
|
|
||||||
value: siteRiskCount > 0 ? t("检查") : t("正常"),
|
|
||||||
note: t("代理与出口"),
|
|
||||||
tone: siteRiskCount > 0 ? "warning" : "success",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const getLatencyColor = (ms: number) =>
|
const getLatencyColor = (ms: number) =>
|
||||||
ms <= 500
|
ms <= 500
|
||||||
? "var(--color-success)"
|
? "var(--color-success)"
|
||||||
@@ -887,78 +813,6 @@ export default function Dashboard({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="dashboard-war-room" aria-label={t("运营战情室")}>
|
|
||||||
<div className="dashboard-pulse-grid">
|
|
||||||
{operationsPulse.map((item) => (
|
|
||||||
<div className="dashboard-pulse-card" data-tone={item.tone} key={item.label}>
|
|
||||||
<span>{item.label}</span>
|
|
||||||
<strong>{item.value}</strong>
|
|
||||||
<em>{item.note}</em>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="dashboard-risk-panel">
|
|
||||||
<div className="dashboard-risk-head">
|
|
||||||
<div>
|
|
||||||
<span>{t("风险雷达")}</span>
|
|
||||||
<strong>{riskySites.length > 0 ? t("优先检查这些站点") : t("暂无明显站点风险")}</strong>
|
|
||||||
</div>
|
|
||||||
<DashboardLink to="/sites" className="btn btn-ghost">{t("站点管理")}</DashboardLink>
|
|
||||||
</div>
|
|
||||||
{riskySites.length > 0 ? (
|
|
||||||
<div className="dashboard-risk-list">
|
|
||||||
{riskySites.map((site) => (
|
|
||||||
<article
|
|
||||||
key={site.siteId}
|
|
||||||
className="dashboard-risk-row"
|
|
||||||
>
|
|
||||||
<div className="dashboard-risk-row-main">
|
|
||||||
<strong>{site.siteName}</strong>
|
|
||||||
<span>{formatAvailabilityPercent(site.availabilityPercent)}</span>
|
|
||||||
<em>{t(formatDashboardLatency(site.averageLatencyMs))}</em>
|
|
||||||
</div>
|
|
||||||
<DiagnosticActionGroup
|
|
||||||
className="dashboard-risk-actions"
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
label: t("运维监控"),
|
|
||||||
href: buildSiteLast24hOpsRoute(site.siteId),
|
|
||||||
hint: t("查看该站点 24h 请求"),
|
|
||||||
tone: (site.availabilityPercent ?? 100) < 95 ? "danger" : "primary",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("站点管理"),
|
|
||||||
href: "/sites",
|
|
||||||
hint: t("检查代理和模型配置"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="dashboard-risk-empty">
|
|
||||||
{t("站点可用性、平均延迟和失败数都处于可接受范围。")}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<nav className="dashboard-command-strip" aria-label={t("运营快捷入口")}>
|
|
||||||
{dashboardCommandLinks.map((item) => (
|
|
||||||
<DashboardLink
|
|
||||||
key={item.to}
|
|
||||||
to={item.to}
|
|
||||||
className="dashboard-command-item"
|
|
||||||
data-tone={item.tone}
|
|
||||||
>
|
|
||||||
<span>{item.label}</span>
|
|
||||||
<strong>{item.value}</strong>
|
|
||||||
<em>{item.note}</em>
|
|
||||||
</DashboardLink>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="dashboard-stat-grid">
|
<div className="dashboard-stat-grid">
|
||||||
<div className="stat-card animate-slide-up stagger-1">
|
<div className="stat-card animate-slide-up stagger-1">
|
||||||
<div className="stat-card-header">
|
<div className="stat-card-header">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
|
import DiagnosticActionGroup from '../components/DiagnosticActionGroup.js';
|
||||||
import { useOptionalI18n } from '../i18n.js';
|
import { useOptionalI18n } from '../i18n.js';
|
||||||
import { formatCompactTokenMetric } from '../numberFormat.js';
|
import { formatCompactTokenMetric } from '../numberFormat.js';
|
||||||
import { formatDateTimeMinuteLocal } from './helpers/checkinLogTime.js';
|
import { formatDateTimeMinuteLocal } from './helpers/checkinLogTime.js';
|
||||||
@@ -187,6 +188,8 @@ export default function OpsMonitor() {
|
|||||||
const successRate = proxy24hTotal > 0 ? (proxy24hSuccess / proxy24hTotal) * 100 : 100;
|
const successRate = proxy24hTotal > 0 ? (proxy24hSuccess / proxy24hTotal) * 100 : 100;
|
||||||
const requestsPerMinute = safeNumber(dashboard?.performance?.requestsPerMinute);
|
const requestsPerMinute = safeNumber(dashboard?.performance?.requestsPerMinute);
|
||||||
const tokensPerMinute = safeNumber(dashboard?.performance?.tokensPerMinute);
|
const tokensPerMinute = safeNumber(dashboard?.performance?.tokensPerMinute);
|
||||||
|
const todaySpend = safeNumber(dashboard?.todaySpend);
|
||||||
|
const todayReward = safeNumber(dashboard?.todayReward);
|
||||||
const activeAccounts = safeNumber(dashboard?.activeAccounts);
|
const activeAccounts = safeNumber(dashboard?.activeAccounts);
|
||||||
const totalAccounts = safeNumber(dashboard?.totalAccounts);
|
const totalAccounts = safeNumber(dashboard?.totalAccounts);
|
||||||
const activeAccountRate = totalAccounts > 0 ? (activeAccounts / totalAccounts) * 100 : 0;
|
const activeAccountRate = totalAccounts > 0 ? (activeAccounts / totalAccounts) * 100 : 0;
|
||||||
@@ -235,6 +238,8 @@ export default function OpsMonitor() {
|
|||||||
successRate,
|
successRate,
|
||||||
requestsPerMinute,
|
requestsPerMinute,
|
||||||
tokensPerMinute,
|
tokensPerMinute,
|
||||||
|
todaySpend,
|
||||||
|
todayReward,
|
||||||
activeAccounts,
|
activeAccounts,
|
||||||
totalAccounts,
|
totalAccounts,
|
||||||
activeAccountRate,
|
activeAccountRate,
|
||||||
@@ -252,6 +257,84 @@ export default function OpsMonitor() {
|
|||||||
};
|
};
|
||||||
}, [state, t]);
|
}, [state, t]);
|
||||||
|
|
||||||
|
const operationsPulse = useMemo(() => {
|
||||||
|
const spendTone: Tone = metrics.todaySpend <= metrics.todayReward
|
||||||
|
? 'success'
|
||||||
|
: metrics.todaySpend <= Math.max(1, metrics.todayReward * 2)
|
||||||
|
? 'warning'
|
||||||
|
: 'danger';
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: t('整体状态'),
|
||||||
|
value: metrics.overallLabel,
|
||||||
|
note: `${formatPercent(metrics.successRate)} ${t('成功率')}`,
|
||||||
|
tone: metrics.overallTone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('站点风险'),
|
||||||
|
value: `${metrics.riskySites.length}`,
|
||||||
|
note: `${metrics.activeSites.length}/${Array.isArray(state.insights?.siteAvailability) ? state.insights.siteAvailability.length : 0} ${t('活跃站点')}`,
|
||||||
|
tone: metrics.riskySites.length > 0 ? 'warning' : 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('今日成本'),
|
||||||
|
value: `$${metrics.todaySpend.toFixed(2)}`,
|
||||||
|
note: `${t('奖励')} $${metrics.todayReward.toFixed(2)}`,
|
||||||
|
tone: spendTone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('当前吞吐'),
|
||||||
|
value: metrics.requestsPerMinute > 0 ? `${Math.round(metrics.requestsPerMinute).toLocaleString()} RPM` : t('暂无流量'),
|
||||||
|
note: `${formatCompactTokenMetric(metrics.tokensPerMinute)} TPM`,
|
||||||
|
tone: metrics.requestsPerMinute > 0 ? 'primary' : 'muted',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [metrics, state.insights, t]);
|
||||||
|
|
||||||
|
const commandLinks = useMemo(() => {
|
||||||
|
const routeRiskCount = metrics.riskyRoutes.length;
|
||||||
|
const hasDangerRoute = metrics.riskyRoutes.some((item) => item.health.tone === 'danger');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
to: '/ops',
|
||||||
|
label: t('运维监控'),
|
||||||
|
value: metrics.overallLabel,
|
||||||
|
note: t('值班入口'),
|
||||||
|
tone: metrics.overallTone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/routes',
|
||||||
|
label: t('路由健康'),
|
||||||
|
value: metrics.routeItems.length > 0 ? String(routeRiskCount) : t('暂无'),
|
||||||
|
note: metrics.routeItems.length > 0 ? (routeRiskCount > 0 ? t('需处理路由') : t('配置正常')) : t('等待路由'),
|
||||||
|
tone: metrics.routeItems.length > 0 ? (hasDangerRoute ? 'danger' : routeRiskCount > 0 ? 'warning' : 'success') : 'muted',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/accounts',
|
||||||
|
label: t('连接管理'),
|
||||||
|
value: `${Math.round(metrics.activeAccounts)}/${Math.round(metrics.totalAccounts)}`,
|
||||||
|
note: t('账号健康'),
|
||||||
|
tone: metrics.activeAccountRate >= 80 ? 'success' : 'warning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/sites',
|
||||||
|
label: t('站点管理'),
|
||||||
|
value: String(metrics.riskySites.length),
|
||||||
|
note: metrics.riskySites.length > 0 ? t('需关注站点') : t('风险站点'),
|
||||||
|
tone: metrics.riskySites.length > 0 ? 'warning' : 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/ip-management',
|
||||||
|
label: t('IP 管理'),
|
||||||
|
value: metrics.riskySites.length > 0 ? t('检查') : t('正常'),
|
||||||
|
note: t('代理与出口'),
|
||||||
|
tone: metrics.riskySites.length > 0 ? 'warning' : 'success',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [metrics, t]);
|
||||||
|
|
||||||
const queue = useMemo(() => {
|
const queue = useMemo(() => {
|
||||||
const items: Array<{
|
const items: Array<{
|
||||||
key: string;
|
key: string;
|
||||||
@@ -412,6 +495,70 @@ export default function OpsMonitor() {
|
|||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="dashboard-war-room ops-monitor-war-room" aria-label={t('运营战情室')}>
|
||||||
|
<div className="dashboard-pulse-grid">
|
||||||
|
{operationsPulse.map((item) => (
|
||||||
|
<div className="dashboard-pulse-card" data-tone={item.tone} key={item.label}>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<strong>{item.value}</strong>
|
||||||
|
<em>{item.note}</em>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="dashboard-risk-panel">
|
||||||
|
<div className="dashboard-risk-head">
|
||||||
|
<div>
|
||||||
|
<span>{t('风险雷达')}</span>
|
||||||
|
<strong>{metrics.riskySites.length > 0 ? t('优先检查这些站点') : t('暂无明显站点风险')}</strong>
|
||||||
|
</div>
|
||||||
|
<Link to="/sites" className="btn btn-ghost">{t('站点管理')}</Link>
|
||||||
|
</div>
|
||||||
|
{metrics.riskySites.length > 0 ? (
|
||||||
|
<div className="dashboard-risk-list">
|
||||||
|
{metrics.riskySites.map((site: any) => (
|
||||||
|
<article key={site.siteId} className="dashboard-risk-row">
|
||||||
|
<div className="dashboard-risk-row-main">
|
||||||
|
<strong>{site.siteName || `${t('站点')} ${site.siteId}`}</strong>
|
||||||
|
<span>{typeof site.availabilityPercent === 'number' ? formatPercent(site.availabilityPercent) : t('暂无可用率')}</span>
|
||||||
|
<em>{formatLatency(site.averageLatencyMs, t)}</em>
|
||||||
|
</div>
|
||||||
|
<DiagnosticActionGroup
|
||||||
|
className="dashboard-risk-actions"
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: t('运维监控'),
|
||||||
|
href: buildSiteLogsHref(site.siteId, undefined, logContextSearch),
|
||||||
|
hint: t('查看该站点 24h 请求'),
|
||||||
|
tone: (site.availabilityPercent ?? 100) < 95 ? 'danger' : 'primary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('站点管理'),
|
||||||
|
href: '/sites',
|
||||||
|
hint: t('检查代理和模型配置'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="dashboard-risk-empty">
|
||||||
|
{t('站点可用性、平均延迟和失败数都处于可接受范围。')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav className="dashboard-command-strip ops-monitor-command-strip" aria-label={t('运营快捷入口')}>
|
||||||
|
{commandLinks.map((item) => (
|
||||||
|
<Link key={item.to} to={item.to} className="dashboard-command-item" data-tone={item.tone}>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<strong>{item.value}</strong>
|
||||||
|
<em>{item.note}</em>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
<section className="ops-monitor-layout">
|
<section className="ops-monitor-layout">
|
||||||
<div className="ops-monitor-main-stack">
|
<div className="ops-monitor-main-stack">
|
||||||
<section className="card ops-monitor-panel ops-monitor-log-center" aria-label={t('日志中心')}>
|
<section className="card ops-monitor-panel ops-monitor-log-center" aria-label={t('日志中心')}>
|
||||||
|
|||||||
@@ -146,25 +146,11 @@ describe('Dashboard site observability panel', () => {
|
|||||||
expect(trendPanel.props.role).toBe('tabpanel');
|
expect(trendPanel.props.role).toBe('tabpanel');
|
||||||
expect(trendPanel.props['aria-labelledby']).toBe('dashboard-range-tab-90');
|
expect(trendPanel.props['aria-labelledby']).toBe('dashboard-range-tab-90');
|
||||||
|
|
||||||
const riskPanel = root!.root.find((node) => (
|
expect(root!.root.findAll((node) => (
|
||||||
typeof node.props.className === 'string'
|
typeof node.props.className === 'string'
|
||||||
&& node.props.className.includes('dashboard-risk-panel')
|
&& node.props.className.includes('dashboard-risk-panel')
|
||||||
));
|
))).toHaveLength(0);
|
||||||
const riskText = collectText(riskPanel);
|
expect(collectText(root!.root)).not.toContain('风险雷达');
|
||||||
expect(riskText).toContain('风险雷达');
|
|
||||||
expect(riskText).toContain('Demo Site');
|
|
||||||
expect(riskText).toContain('75%');
|
|
||||||
expect(riskText).toContain('320ms');
|
|
||||||
expect(riskText).toContain('运维监控');
|
|
||||||
expect(riskText).toContain('查看该站点 24h 请求');
|
|
||||||
expect(riskText).toContain('站点管理');
|
|
||||||
expect(riskText).toContain('检查代理和模型配置');
|
|
||||||
|
|
||||||
const riskLogLink = riskPanel.find((node) => (
|
|
||||||
node.type === 'a'
|
|
||||||
&& collectText(node).trim() === '运维监控'
|
|
||||||
));
|
|
||||||
expect(String(riskLogLink.props.href || riskLogLink.props.to || '')).toContain('/ops?siteId=1');
|
|
||||||
|
|
||||||
const panel = root!.root.find((node) => (
|
const panel = root!.root.find((node) => (
|
||||||
typeof node.props.className === 'string'
|
typeof node.props.className === 'string'
|
||||||
|
|||||||
@@ -152,17 +152,21 @@ describe('Dashboard and ops log placement', () => {
|
|||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
const dashboardText = collectText(root.root);
|
const dashboardText = collectText(root.root);
|
||||||
expect(dashboardText).toContain('运维监控');
|
expect(dashboardText).not.toContain('值班入口');
|
||||||
expect(dashboardText).toContain('值班入口');
|
expect(dashboardText).not.toContain('风险雷达');
|
||||||
expect(dashboardText).not.toContain('排障队列');
|
expect(dashboardText).not.toContain('排障队列');
|
||||||
expect(dashboardText).not.toContain('运行健康处理队列');
|
expect(dashboardText).not.toContain('运行健康处理队列');
|
||||||
expect(dashboardText).not.toContain('日志中心');
|
expect(dashboardText).not.toContain('日志中心');
|
||||||
expect(dashboardText).not.toContain('程序日志');
|
expect(dashboardText).not.toContain('程序日志');
|
||||||
|
|
||||||
const dashboardLinks = root.root.findAll((node) => node.type === 'a');
|
expect(root.root.findAll((node) => (
|
||||||
expect(dashboardLinks.some((link) => link.props.href === '/ops' || link.props.to === '/ops')).toBe(true);
|
typeof node.props.className === 'string'
|
||||||
expect(dashboardLinks.some((link) => link.props.href === '/logs?status=failed' || link.props.to === '/logs?status=failed')).toBe(false);
|
&& node.props.className.includes('dashboard-war-room')
|
||||||
expect(dashboardLinks.some((link) => link.props.href === '/events' || link.props.to === '/events')).toBe(false);
|
))).toHaveLength(0);
|
||||||
|
expect(root.root.findAll((node) => (
|
||||||
|
typeof node.props.className === 'string'
|
||||||
|
&& node.props.className.includes('dashboard-command-strip')
|
||||||
|
))).toHaveLength(0);
|
||||||
} finally {
|
} finally {
|
||||||
root?.unmount();
|
root?.unmount();
|
||||||
}
|
}
|
||||||
@@ -179,6 +183,31 @@ describe('Dashboard and ops log placement', () => {
|
|||||||
});
|
});
|
||||||
await flushMicrotasks();
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const warRoom = root.root.find((node) => (
|
||||||
|
node.type === 'section'
|
||||||
|
&& node.props['aria-label'] === '运营战情室'
|
||||||
|
));
|
||||||
|
const warRoomText = collectText(warRoom);
|
||||||
|
expect(warRoomText).toContain('整体状态');
|
||||||
|
expect(warRoomText).toContain('异常');
|
||||||
|
expect(warRoomText).toContain('风险雷达');
|
||||||
|
expect(warRoomText).toContain('优先检查这些站点');
|
||||||
|
expect(warRoomText).toContain('Slow Site');
|
||||||
|
expect(warRoomText).toContain('运维监控');
|
||||||
|
expect(warRoomText).toContain('查看该站点 24h 请求');
|
||||||
|
expect(warRoomText).toContain('站点管理');
|
||||||
|
expect(warRoomText).toContain('检查代理和模型配置');
|
||||||
|
|
||||||
|
const commandStrip = root.root.find((node) => (
|
||||||
|
node.type === 'nav'
|
||||||
|
&& node.props['aria-label'] === '运营快捷入口'
|
||||||
|
));
|
||||||
|
const commandStripText = collectText(commandStrip);
|
||||||
|
expect(commandStripText).toContain('值班入口');
|
||||||
|
expect(commandStripText).toContain('路由健康');
|
||||||
|
expect(commandStripText).toContain('连接管理');
|
||||||
|
expect(commandStripText).toContain('IP 管理');
|
||||||
|
|
||||||
const logCenter = root.root.find((node) => (
|
const logCenter = root.root.find((node) => (
|
||||||
node.type === 'section'
|
node.type === 'section'
|
||||||
&& node.props['aria-label'] === '日志中心'
|
&& node.props['aria-label'] === '日志中心'
|
||||||
@@ -241,7 +270,6 @@ describe('Dashboard and ops log placement', () => {
|
|||||||
|
|
||||||
const dashboardText = collectText(root.root);
|
const dashboardText = collectText(root.root);
|
||||||
expect(dashboardText).toContain('健康');
|
expect(dashboardText).toContain('健康');
|
||||||
expect(dashboardText).toContain('100.0% 成功率');
|
|
||||||
expect(dashboardText).toContain('平台运行平稳,可以继续观察吞吐和成本变化。');
|
expect(dashboardText).toContain('平台运行平稳,可以继续观察吞吐和成本变化。');
|
||||||
expect(dashboardText).not.toContain('平台存在明显异常');
|
expect(dashboardText).not.toContain('平台存在明显异常');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user