mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-18 02:30:51 +08:00
feat(status-bar): add gradient colors and tooltip with mobile support
This commit is contained in:
@@ -1,36 +1,143 @@
|
||||
import { calculateStatusBarData } from '@/utils/usage';
|
||||
import styles from '@/pages/AiProvidersPage.module.scss';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { StatusBarData, StatusBlockDetail } from '@/utils/usage';
|
||||
import defaultStyles from '@/pages/AiProvidersPage.module.scss';
|
||||
|
||||
interface ProviderStatusBarProps {
|
||||
statusData: ReturnType<typeof calculateStatusBarData>;
|
||||
/**
|
||||
* 根据成功率 (0–1) 在三个色标之间做 RGB 线性插值
|
||||
* 0 → 红 (#ef4444) → 0.5 → 金黄 (#facc15) → 1 → 绿 (#22c55e)
|
||||
*/
|
||||
const COLOR_STOPS = [
|
||||
{ r: 239, g: 68, b: 68 }, // #ef4444
|
||||
{ r: 250, g: 204, b: 21 }, // #facc15
|
||||
{ r: 34, g: 197, b: 94 }, // #22c55e
|
||||
] as const;
|
||||
|
||||
function rateToColor(rate: number): string {
|
||||
const t = Math.max(0, Math.min(1, rate));
|
||||
const segment = t < 0.5 ? 0 : 1;
|
||||
const localT = segment === 0 ? t * 2 : (t - 0.5) * 2;
|
||||
const from = COLOR_STOPS[segment];
|
||||
const to = COLOR_STOPS[segment + 1];
|
||||
const r = Math.round(from.r + (to.r - from.r) * localT);
|
||||
const g = Math.round(from.g + (to.g - from.g) * localT);
|
||||
const b = Math.round(from.b + (to.b - from.b) * localT);
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
export function ProviderStatusBar({ statusData }: ProviderStatusBarProps) {
|
||||
function formatTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const h = date.getHours().toString().padStart(2, '0');
|
||||
const m = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${h}:${m}`;
|
||||
}
|
||||
|
||||
type StylesModule = Record<string, string>;
|
||||
|
||||
interface ProviderStatusBarProps {
|
||||
statusData: StatusBarData;
|
||||
styles?: StylesModule;
|
||||
}
|
||||
|
||||
export function ProviderStatusBar({ statusData, styles: stylesProp }: ProviderStatusBarProps) {
|
||||
const { t } = useTranslation();
|
||||
const s = (stylesProp || defaultStyles) as StylesModule;
|
||||
const [activeTooltip, setActiveTooltip] = useState<number | null>(null);
|
||||
const blocksRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
|
||||
const rateClass = !hasData
|
||||
? ''
|
||||
: statusData.successRate >= 90
|
||||
? styles.statusRateHigh
|
||||
? s.statusRateHigh
|
||||
: statusData.successRate >= 50
|
||||
? styles.statusRateMedium
|
||||
: styles.statusRateLow;
|
||||
? s.statusRateMedium
|
||||
: s.statusRateLow;
|
||||
|
||||
// 点击外部关闭 tooltip(移动端)
|
||||
useEffect(() => {
|
||||
if (activeTooltip === null) return;
|
||||
const handler = (e: PointerEvent) => {
|
||||
if (blocksRef.current && !blocksRef.current.contains(e.target as Node)) {
|
||||
setActiveTooltip(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('pointerdown', handler);
|
||||
return () => document.removeEventListener('pointerdown', handler);
|
||||
}, [activeTooltip]);
|
||||
|
||||
const handlePointerEnter = useCallback((e: React.PointerEvent, idx: number) => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
setActiveTooltip(idx);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePointerLeave = useCallback((e: React.PointerEvent) => {
|
||||
if (e.pointerType === 'mouse') {
|
||||
setActiveTooltip(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePointerDown = useCallback((e: React.PointerEvent, idx: number) => {
|
||||
if (e.pointerType === 'touch') {
|
||||
e.preventDefault();
|
||||
setActiveTooltip((prev) => (prev === idx ? null : idx));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTooltipPositionClass = (idx: number, total: number): string => {
|
||||
if (idx <= 2) return s.statusTooltipLeft;
|
||||
if (idx >= total - 3) return s.statusTooltipRight;
|
||||
return '';
|
||||
};
|
||||
|
||||
const renderTooltip = (detail: StatusBlockDetail, idx: number) => {
|
||||
const total = detail.success + detail.failure;
|
||||
const posClass = getTooltipPositionClass(idx, statusData.blockDetails.length);
|
||||
const timeRange = `${formatTime(detail.startTime)} – ${formatTime(detail.endTime)}`;
|
||||
|
||||
return (
|
||||
<div className={`${s.statusTooltip} ${posClass}`}>
|
||||
<span className={s.tooltipTime}>{timeRange}</span>
|
||||
{total > 0 ? (
|
||||
<span className={s.tooltipStats}>
|
||||
<span className={s.tooltipSuccess}>{t('status_bar.success_short')} {detail.success}</span>
|
||||
<span className={s.tooltipFailure}>{t('status_bar.failure_short')} {detail.failure}</span>
|
||||
<span className={s.tooltipRate}>({(detail.rate * 100).toFixed(1)}%)</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className={s.tooltipStats}>{t('status_bar.no_requests')}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.statusBar}>
|
||||
<div className={styles.statusBlocks}>
|
||||
{statusData.blocks.map((state, idx) => {
|
||||
const blockClass =
|
||||
state === 'success'
|
||||
? styles.statusBlockSuccess
|
||||
: state === 'failure'
|
||||
? styles.statusBlockFailure
|
||||
: state === 'mixed'
|
||||
? styles.statusBlockMixed
|
||||
: styles.statusBlockIdle;
|
||||
return <div key={idx} className={`${styles.statusBlock} ${blockClass}`} />;
|
||||
<div className={s.statusBar}>
|
||||
<div className={s.statusBlocks} ref={blocksRef}>
|
||||
{statusData.blockDetails.map((detail, idx) => {
|
||||
const isIdle = detail.rate === -1;
|
||||
const blockStyle = isIdle ? undefined : { backgroundColor: rateToColor(detail.rate) };
|
||||
const isActive = activeTooltip === idx;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`${s.statusBlockWrapper} ${isActive ? s.statusBlockActive : ''}`}
|
||||
onPointerEnter={(e) => handlePointerEnter(e, idx)}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
onPointerDown={(e) => handlePointerDown(e, idx)}
|
||||
>
|
||||
<div
|
||||
className={`${s.statusBlock} ${isIdle ? s.statusBlockIdle : ''}`}
|
||||
style={blockStyle}
|
||||
/>
|
||||
{isActive && renderTooltip(detail, idx)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span className={`${styles.statusRate} ${rateClass}`}>
|
||||
<span className={`${s.statusRate} ${rateClass}`}>
|
||||
{hasData ? `${statusData.successRate.toFixed(1)}%` : '--'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/Button';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
||||
import { IconBot, IconCode, IconDownload, IconInfo, IconTrash2 } from '@/components/ui/icons';
|
||||
import { ProviderStatusBar } from '@/components/providers/ProviderStatusBar';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { resolveAuthProvider } from '@/utils/quota';
|
||||
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
||||
@@ -88,14 +89,6 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
const authIndexKey = normalizeAuthIndexValue(rawAuthIndex);
|
||||
const statusData =
|
||||
(authIndexKey && statusBarCache.get(authIndexKey)) || calculateStatusBarData([]);
|
||||
const hasData = statusData.totalSuccess + statusData.totalFailure > 0;
|
||||
const rateClass = !hasData
|
||||
? ''
|
||||
: statusData.successRate >= 90
|
||||
? styles.statusRateHigh
|
||||
: statusData.successRate >= 50
|
||||
? styles.statusRateMedium
|
||||
: styles.statusRateLow;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -135,24 +128,7 @@ export function AuthFileCard(props: AuthFileCardProps) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statusBar}>
|
||||
<div className={styles.statusBlocks}>
|
||||
{statusData.blocks.map((state, idx) => {
|
||||
const blockClass =
|
||||
state === 'success'
|
||||
? styles.statusBlockSuccess
|
||||
: state === 'failure'
|
||||
? styles.statusBlockFailure
|
||||
: state === 'mixed'
|
||||
? styles.statusBlockMixed
|
||||
: styles.statusBlockIdle;
|
||||
return <div key={idx} className={`${styles.statusBlock} ${blockClass}`} />;
|
||||
})}
|
||||
</div>
|
||||
<span className={`${styles.statusRate} ${rateClass}`}>
|
||||
{hasData ? `${statusData.successRate.toFixed(1)}%` : '--'}
|
||||
</span>
|
||||
</div>
|
||||
<ProviderStatusBar statusData={statusData} styles={styles} />
|
||||
|
||||
{showQuotaLayout && quotaType && (
|
||||
<AuthFileQuotaSection file={file} quotaType={quotaType} disableControls={disableControls} />
|
||||
|
||||
@@ -832,6 +832,11 @@
|
||||
"success": "Success",
|
||||
"failure": "Failure"
|
||||
},
|
||||
"status_bar": {
|
||||
"success_short": "✓",
|
||||
"failure_short": "✗",
|
||||
"no_requests": "No requests"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs Viewer",
|
||||
"refresh_button": "Refresh Logs",
|
||||
|
||||
@@ -835,6 +835,11 @@
|
||||
"success": "Успех",
|
||||
"failure": "Сбой"
|
||||
},
|
||||
"status_bar": {
|
||||
"success_short": "✓",
|
||||
"failure_short": "✗",
|
||||
"no_requests": "Нет запросов"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Просмотр журналов",
|
||||
"refresh_button": "Обновить журналы",
|
||||
|
||||
@@ -832,6 +832,11 @@
|
||||
"success": "成功",
|
||||
"failure": "失败"
|
||||
},
|
||||
"status_bar": {
|
||||
"success_short": "✓",
|
||||
"failure_short": "✗",
|
||||
"no_requests": "无请求"
|
||||
},
|
||||
"logs": {
|
||||
"title": "日志查看",
|
||||
"refresh_button": "刷新日志",
|
||||
|
||||
@@ -402,37 +402,123 @@
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.statusBlockWrapper {
|
||||
flex: 1;
|
||||
min-width: 6px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.statusBlock {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
min-width: 6px;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scaleY(1.5);
|
||||
opacity: 0.85;
|
||||
.statusBlockWrapper:hover &,
|
||||
.statusBlockWrapper.statusBlockActive & {
|
||||
transform: scaleY(1.8);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.statusBlockSuccess {
|
||||
background-color: var(--success-color, #22c55e);
|
||||
}
|
||||
|
||||
.statusBlockFailure {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.statusBlockMixed {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.statusBlockIdle {
|
||||
background-color: var(--border-secondary, #e5e7eb);
|
||||
}
|
||||
|
||||
.statusTooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--bg-primary, #fff);
|
||||
border: 1px solid var(--border-secondary, #e5e7eb);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
z-index: $z-dropdown;
|
||||
pointer-events: none;
|
||||
color: var(--text-primary);
|
||||
|
||||
// 小箭头
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--bg-primary, #fff);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: var(--border-secondary, #e5e7eb);
|
||||
}
|
||||
}
|
||||
|
||||
// 防止左右溢出
|
||||
.statusTooltipLeft {
|
||||
left: 0;
|
||||
transform: translateX(0);
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
left: 8px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.statusTooltipRight {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(0);
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
left: auto;
|
||||
right: 8px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltipTime {
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tooltipStats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tooltipSuccess {
|
||||
color: var(--success-color, #22c55e);
|
||||
}
|
||||
|
||||
.tooltipFailure {
|
||||
color: var(--danger-color, #ef4444);
|
||||
}
|
||||
|
||||
.tooltipRate {
|
||||
color: var(--text-secondary);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.statusRate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -460,6 +546,17 @@
|
||||
background: var(--failure-badge-bg);
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.statusTooltip {
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.statusBlocks {
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Model Config Section - Unified Layout
|
||||
// ============================================
|
||||
@@ -816,6 +913,20 @@
|
||||
background-color: var(--border-primary, #374151);
|
||||
}
|
||||
|
||||
.statusTooltip {
|
||||
background: var(--bg-secondary, #1f2937);
|
||||
border-color: var(--border-primary, #374151);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
|
||||
&::after {
|
||||
border-top-color: var(--bg-secondary, #1f2937);
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-top-color: var(--border-primary, #374151);
|
||||
}
|
||||
}
|
||||
|
||||
.statusRateHigh {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #86efac;
|
||||
|
||||
@@ -605,39 +605,121 @@
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.statusBlockWrapper {
|
||||
flex: 1;
|
||||
min-width: 6px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.statusBlock {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
min-width: 6px;
|
||||
transition:
|
||||
transform 0.15s ease,
|
||||
opacity 0.15s ease;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scaleY(1.5);
|
||||
opacity: 0.85;
|
||||
.statusBlockWrapper:hover &,
|
||||
.statusBlockWrapper.statusBlockActive & {
|
||||
transform: scaleY(1.8);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.statusBlockSuccess {
|
||||
background-color: var(--success-color, #22c55e);
|
||||
}
|
||||
|
||||
.statusBlockFailure {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.statusBlockMixed {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.statusBlockIdle {
|
||||
background-color: var(--border-secondary, #e5e7eb);
|
||||
}
|
||||
|
||||
.statusTooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--bg-primary, #fff);
|
||||
border: 1px solid var(--border-secondary, #e5e7eb);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
z-index: $z-dropdown;
|
||||
pointer-events: none;
|
||||
color: var(--text-primary);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--bg-primary, #fff);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: var(--border-secondary, #e5e7eb);
|
||||
}
|
||||
}
|
||||
|
||||
.statusTooltipLeft {
|
||||
left: 0;
|
||||
transform: translateX(0);
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
left: 8px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.statusTooltipRight {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(0);
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
left: auto;
|
||||
right: 8px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltipTime {
|
||||
color: var(--text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.tooltipStats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tooltipSuccess {
|
||||
color: var(--success-color, #22c55e);
|
||||
}
|
||||
|
||||
.tooltipFailure {
|
||||
color: var(--danger-color, #ef4444);
|
||||
}
|
||||
|
||||
.tooltipRate {
|
||||
color: var(--text-secondary);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.statusRate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -665,6 +747,17 @@
|
||||
background: var(--failure-badge-bg);
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
.statusTooltip {
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.statusBlocks {
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
.prefixProxyEditor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1117,11 +1117,26 @@ export function buildChartData(
|
||||
*/
|
||||
export type StatusBlockState = 'success' | 'failure' | 'mixed' | 'idle';
|
||||
|
||||
/**
|
||||
* 状态栏单个格子的详细信息
|
||||
*/
|
||||
export interface StatusBlockDetail {
|
||||
success: number;
|
||||
failure: number;
|
||||
/** 该格子的成功率 (0–1),无请求时为 -1 */
|
||||
rate: number;
|
||||
/** 格子起始时间戳 (ms) */
|
||||
startTime: number;
|
||||
/** 格子结束时间戳 (ms) */
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态栏数据
|
||||
*/
|
||||
export interface StatusBarData {
|
||||
blocks: StatusBlockState[];
|
||||
blockDetails: StatusBlockDetail[];
|
||||
successRate: number;
|
||||
totalSuccess: number;
|
||||
totalFailure: number;
|
||||
@@ -1138,7 +1153,7 @@ export function calculateStatusBarData(
|
||||
): StatusBarData {
|
||||
const BLOCK_COUNT = 20;
|
||||
const BLOCK_DURATION_MS = 10 * 60 * 1000; // 10 minutes
|
||||
const WINDOW_MS = 200 * 60 * 1000; // 200 minutes
|
||||
const WINDOW_MS = BLOCK_COUNT * BLOCK_DURATION_MS; // 200 minutes
|
||||
|
||||
const now = Date.now();
|
||||
const windowStart = now - WINDOW_MS;
|
||||
@@ -1182,18 +1197,30 @@ export function calculateStatusBarData(
|
||||
}
|
||||
});
|
||||
|
||||
// Convert stats to block states
|
||||
const blocks: StatusBlockState[] = blockStats.map((stat) => {
|
||||
if (stat.success === 0 && stat.failure === 0) {
|
||||
return 'idle';
|
||||
// Convert stats to block states and build details
|
||||
const blocks: StatusBlockState[] = [];
|
||||
const blockDetails: StatusBlockDetail[] = [];
|
||||
|
||||
blockStats.forEach((stat, idx) => {
|
||||
const total = stat.success + stat.failure;
|
||||
if (total === 0) {
|
||||
blocks.push('idle');
|
||||
} else if (stat.failure === 0) {
|
||||
blocks.push('success');
|
||||
} else if (stat.success === 0) {
|
||||
blocks.push('failure');
|
||||
} else {
|
||||
blocks.push('mixed');
|
||||
}
|
||||
if (stat.failure === 0) {
|
||||
return 'success';
|
||||
}
|
||||
if (stat.success === 0) {
|
||||
return 'failure';
|
||||
}
|
||||
return 'mixed';
|
||||
|
||||
const blockStartTime = windowStart + idx * BLOCK_DURATION_MS;
|
||||
blockDetails.push({
|
||||
success: stat.success,
|
||||
failure: stat.failure,
|
||||
rate: total > 0 ? stat.success / total : -1,
|
||||
startTime: blockStartTime,
|
||||
endTime: blockStartTime + BLOCK_DURATION_MS,
|
||||
});
|
||||
});
|
||||
|
||||
// Calculate success rate
|
||||
@@ -1202,6 +1229,7 @@ export function calculateStatusBarData(
|
||||
|
||||
return {
|
||||
blocks,
|
||||
blockDetails,
|
||||
successRate,
|
||||
totalSuccess,
|
||||
totalFailure
|
||||
|
||||
Reference in New Issue
Block a user