refactor(web): compact system status into a single card alongside metrics

Replace the separate two-card row with a single compact 'System Status'
card placed as the 5th column in the metrics grid. Shows green/red dots
for Plugin Runtime and Box Runtime. Click to expand a popover with
connection details (backend, profile, sandbox count).
This commit is contained in:
Junyan Qin
2026-04-19 13:54:23 +08:00
committed by WangCham
parent b71f690886
commit eaffde0f89
11 changed files with 161 additions and 120 deletions
@@ -2,6 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MessageSquare, Sparkles, Check, Users } from 'lucide-react'; import { MessageSquare, Sparkles, Check, Users } from 'lucide-react';
import MetricCard from './MetricCard'; import MetricCard from './MetricCard';
import SystemStatusCard from './SystemStatusCards';
import TrafficChart from './TrafficChart'; import TrafficChart from './TrafficChart';
import { import {
OverviewMetrics, OverviewMetrics,
@@ -81,8 +82,8 @@ export default function OverviewCards({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Metric Cards */} {/* Metric Cards + System Status */}
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-6">
{cards.map((card, index) => ( {cards.map((card, index) => (
<MetricCard <MetricCard
key={index} key={index}
@@ -93,6 +94,7 @@ export default function OverviewCards({
loading={loading} loading={loading}
/> />
))} ))}
<SystemStatusCard />
</div> </div>
{/* Traffic Chart */} {/* Traffic Chart */}
@@ -1,75 +1,36 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Plug, Box, CircleCheck, CircleX, Loader2 } from 'lucide-react'; import {
Plug,
Box,
CircleCheck,
CircleX,
Loader2,
ChevronDown,
} from 'lucide-react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { import {
ApiRespPluginSystemStatus, ApiRespPluginSystemStatus,
ApiRespBoxStatus, ApiRespBoxStatus,
} from '@/app/infra/entities/api'; } from '@/app/infra/entities/api';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
interface StatusCardProps { function StatusDot({ ok }: { ok: boolean | null }) {
icon: React.ReactNode; if (ok === null)
title: string; return <span className="w-2 h-2 rounded-full bg-muted-foreground/40" />;
connected: boolean | null; return ok ? (
connectedLabel: string; <span className="w-2 h-2 rounded-full bg-green-500" />
disconnectedLabel: string; ) : (
details?: { label: string; value: string }[]; <span className="w-2 h-2 rounded-full bg-red-500" />
loading: boolean;
}
function StatusCard({
icon,
title,
connected,
connectedLabel,
disconnectedLabel,
details,
loading,
}: StatusCardProps) {
return (
<div className="bg-card rounded-xl border p-4 flex items-start gap-3">
<div className="rounded-lg bg-muted p-2 text-muted-foreground">
{icon}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
{loading ? (
<div className="flex items-center gap-1.5 mt-1">
<Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground" />
</div>
) : connected === null ? (
<span className="text-sm text-muted-foreground"></span>
) : connected ? (
<div className="flex items-center gap-1.5 mt-0.5">
<CircleCheck className="w-4 h-4 text-green-600" />
<span className="text-sm font-semibold text-green-600">
{connectedLabel}
</span>
</div>
) : (
<div className="flex items-center gap-1.5 mt-0.5">
<CircleX className="w-4 h-4 text-red-500" />
<span className="text-sm font-semibold text-red-500">
{disconnectedLabel}
</span>
</div>
)}
{!loading && details && details.length > 0 && (
<div className="flex flex-wrap gap-x-4 gap-y-0.5 mt-1 text-xs text-muted-foreground">
{details.map((d) => (
<span key={d.label}>
{d.label}:{' '}
<span className="text-foreground font-mono">{d.value}</span>
</span>
))}
</div>
)}
</div>
</div>
); );
} }
export default function SystemStatusCards() { export default function SystemStatusCard() {
const { t } = useTranslation(); const { t } = useTranslation();
const [pluginStatus, setPluginStatus] = const [pluginStatus, setPluginStatus] =
useState<ApiRespPluginSystemStatus | null>(null); useState<ApiRespPluginSystemStatus | null>(null);
@@ -94,64 +55,138 @@ export default function SystemStatusCards() {
fetchStatus(); fetchStatus();
}, [fetchStatus]); }, [fetchStatus]);
const pluginConnected = pluginStatus const pluginOk = pluginStatus
? pluginStatus.is_enable && pluginStatus.is_connected ? pluginStatus.is_enable && pluginStatus.is_connected
: null; : null;
const boxOk = boxStatus ? boxStatus.available : null;
const pluginDetails: { label: string; value: string }[] = []; if (loading) {
if (pluginStatus && !pluginStatus.is_enable) { return (
pluginDetails.push({ <Card className="transition-all duration-300">
label: t('monitoring.statusDetail'), <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
value: t('monitoring.pluginDisabled'), <CardTitle className="text-sm font-medium text-muted-foreground">
}); {t('monitoring.systemStatus')}
} </CardTitle>
</CardHeader>
const boxConnected = boxStatus ? boxStatus.available : null; <CardContent>
<div className="flex items-center gap-2">
const boxDetails: { label: string; value: string }[] = []; <Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
if (boxStatus) { </div>
if (boxStatus.backend) { </CardContent>
boxDetails.push({ </Card>
label: t('monitoring.boxBackend'), );
value: `${boxStatus.backend.name}`,
});
}
boxDetails.push({
label: t('monitoring.boxProfile'),
value: boxStatus.profile,
});
if (boxStatus.available && boxStatus.active_sessions !== undefined) {
boxDetails.push({
label: t('monitoring.boxSandboxes'),
value: String(boxStatus.active_sessions),
});
}
} }
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <Popover>
<StatusCard <PopoverTrigger asChild>
icon={<Plug className="w-4 h-4" />} <Card className="transition-all duration-300 group cursor-pointer hover:border-primary/30">
title={t('monitoring.pluginRuntime')} <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
connected={pluginConnected} <CardTitle className="text-sm font-medium text-muted-foreground">
connectedLabel={t('monitoring.connected')} {t('monitoring.systemStatus')}
disconnectedLabel={ </CardTitle>
pluginStatus && !pluginStatus.is_enable <ChevronDown className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
? t('monitoring.disabled') </CardHeader>
: t('monitoring.disconnected') <CardContent className="space-y-2">
} <div className="flex items-center gap-2">
details={pluginDetails} <StatusDot ok={pluginOk} />
loading={loading} <Plug className="w-3.5 h-3.5 text-muted-foreground" />
/> <span className="text-sm">{t('monitoring.pluginRuntime')}</span>
<StatusCard </div>
icon={<Box className="w-4 h-4" />} <div className="flex items-center gap-2">
title={t('monitoring.boxRuntime')} <StatusDot ok={boxOk} />
connected={boxConnected} <Box className="w-3.5 h-3.5 text-muted-foreground" />
connectedLabel={t('monitoring.connected')} <span className="text-sm">{t('monitoring.boxRuntime')}</span>
disconnectedLabel={t('monitoring.disconnected')} </div>
details={boxDetails} </CardContent>
loading={loading} </Card>
/> </PopoverTrigger>
</div> <PopoverContent className="w-72" align="start">
<div className="space-y-3">
{/* Plugin Runtime */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<Plug className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">
{t('monitoring.pluginRuntime')}
</span>
</div>
<div className="ml-6 text-xs space-y-0.5">
<div className="flex items-center gap-1.5">
{pluginOk ? (
<CircleCheck className="w-3.5 h-3.5 text-green-600" />
) : (
<CircleX className="w-3.5 h-3.5 text-red-500" />
)}
<span className={pluginOk ? 'text-green-600' : 'text-red-500'}>
{pluginOk
? t('monitoring.connected')
: pluginStatus && !pluginStatus.is_enable
? t('monitoring.disabled')
: t('monitoring.disconnected')}
</span>
</div>
{pluginStatus && !pluginStatus.is_enable && (
<p className="text-muted-foreground">
{t('monitoring.pluginDisabled')}
</p>
)}
</div>
</div>
<div className="border-t" />
{/* Box Runtime */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<Box className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium">
{t('monitoring.boxRuntime')}
</span>
</div>
<div className="ml-6 text-xs space-y-0.5">
<div className="flex items-center gap-1.5">
{boxOk ? (
<CircleCheck className="w-3.5 h-3.5 text-green-600" />
) : (
<CircleX className="w-3.5 h-3.5 text-red-500" />
)}
<span className={boxOk ? 'text-green-600' : 'text-red-500'}>
{boxOk
? t('monitoring.connected')
: t('monitoring.disconnected')}
</span>
</div>
{boxStatus && (
<div className="text-muted-foreground space-y-0.5">
{boxStatus.backend && (
<p>
{t('monitoring.boxBackend')}:{' '}
<span className="text-foreground font-mono">
{boxStatus.backend.name}
</span>
</p>
)}
<p>
{t('monitoring.boxProfile')}:{' '}
<span className="text-foreground font-mono">
{boxStatus.profile}
</span>
</p>
{boxOk && boxStatus.active_sessions !== undefined && (
<p>
{t('monitoring.boxSandboxes')}:{' '}
<span className="text-foreground font-mono">
{boxStatus.active_sessions}
</span>
</p>
)}
</div>
)}
</div>
</div>
</div>
</PopoverContent>
</Popover>
); );
} }
-4
View File
@@ -12,7 +12,6 @@ import {
CheckCircle2, CheckCircle2,
} from 'lucide-react'; } from 'lucide-react';
import OverviewCards from './components/overview-cards/OverviewCards'; import OverviewCards from './components/overview-cards/OverviewCards';
import SystemStatusCards from './components/overview-cards/SystemStatusCards';
import MonitoringFilters from './components/filters/MonitoringFilters'; import MonitoringFilters from './components/filters/MonitoringFilters';
import { ExportDropdown } from './components/ExportDropdown'; import { ExportDropdown } from './components/ExportDropdown';
import { useMonitoringFilters } from './hooks/useMonitoringFilters'; import { useMonitoringFilters } from './hooks/useMonitoringFilters';
@@ -297,9 +296,6 @@ function MonitoringPageContent() {
{/* Content Area */} {/* Content Area */}
<div className="flex flex-col gap-6 px-[0.8rem] pb-4"> <div className="flex flex-col gap-6 px-[0.8rem] pb-4">
{/* System Status */}
<SystemStatusCards />
{/* Overview Section */} {/* Overview Section */}
<OverviewCards <OverviewCards
metrics={data?.overview || null} metrics={data?.overview || null}
+1
View File
@@ -1253,6 +1253,7 @@ const enUS = {
sessions: 'Sessions', sessions: 'Sessions',
feedback: 'User Feedback', feedback: 'User Feedback',
}, },
systemStatus: 'System Status',
pluginRuntime: 'Plugin Runtime', pluginRuntime: 'Plugin Runtime',
boxRuntime: 'Box Runtime', boxRuntime: 'Box Runtime',
connected: 'Connected', connected: 'Connected',
+1
View File
@@ -1279,6 +1279,7 @@ const esES = {
sessions: 'Sesiones', sessions: 'Sesiones',
feedback: 'Comentarios de usuarios', feedback: 'Comentarios de usuarios',
}, },
systemStatus: 'Estado del sistema',
pluginRuntime: 'Plugin Runtime', pluginRuntime: 'Plugin Runtime',
boxRuntime: 'Box Runtime', boxRuntime: 'Box Runtime',
connected: 'Conectado', connected: 'Conectado',
+1
View File
@@ -1251,6 +1251,7 @@ const jaJP = {
sessions: 'セッション', sessions: 'セッション',
feedback: 'ユーザーフィードバック', feedback: 'ユーザーフィードバック',
}, },
systemStatus: 'システム状態',
pluginRuntime: 'プラグインランタイム', pluginRuntime: 'プラグインランタイム',
boxRuntime: 'Box ランタイム', boxRuntime: 'Box ランタイム',
connected: '接続済み', connected: '接続済み',
+1
View File
@@ -1253,6 +1253,7 @@ const ruRU = {
sessions: 'Сессии', sessions: 'Сессии',
feedback: 'Отзывы пользователей', feedback: 'Отзывы пользователей',
}, },
systemStatus: 'Состояние системы',
pluginRuntime: 'Среда плагинов', pluginRuntime: 'Среда плагинов',
boxRuntime: 'Среда Box', boxRuntime: 'Среда Box',
connected: 'Подключено', connected: 'Подключено',
+1
View File
@@ -1226,6 +1226,7 @@ const thTH = {
sessions: 'เซสชัน', sessions: 'เซสชัน',
feedback: 'ความคิดเห็นผู้ใช้', feedback: 'ความคิดเห็นผู้ใช้',
}, },
systemStatus: 'สถานะระบบ',
pluginRuntime: 'Plugin Runtime', pluginRuntime: 'Plugin Runtime',
boxRuntime: 'Box Runtime', boxRuntime: 'Box Runtime',
connected: 'เชื่อมต่อแล้ว', connected: 'เชื่อมต่อแล้ว',
+1
View File
@@ -1247,6 +1247,7 @@ const viVN = {
sessions: 'Phiên', sessions: 'Phiên',
feedback: 'Phản hồi người dùng', feedback: 'Phản hồi người dùng',
}, },
systemStatus: 'Trạng thái hệ thống',
pluginRuntime: 'Plugin Runtime', pluginRuntime: 'Plugin Runtime',
boxRuntime: 'Box Runtime', boxRuntime: 'Box Runtime',
connected: 'Đã kết nối', connected: 'Đã kết nối',
+1
View File
@@ -1199,6 +1199,7 @@ const zhHans = {
sessions: '会话记录', sessions: '会话记录',
feedback: '用户反馈', feedback: '用户反馈',
}, },
systemStatus: '系统状态',
pluginRuntime: '插件运行时', pluginRuntime: '插件运行时',
boxRuntime: 'Box 运行时', boxRuntime: 'Box 运行时',
connected: '已连接', connected: '已连接',
+1
View File
@@ -1192,6 +1192,7 @@ const zhHant = {
sessions: '會話記錄', sessions: '會話記錄',
feedback: '使用者回饋', feedback: '使用者回饋',
}, },
systemStatus: '系統狀態',
pluginRuntime: '外掛執行時', pluginRuntime: '外掛執行時',
boxRuntime: 'Box 執行時', boxRuntime: 'Box 執行時',
connected: '已連線', connected: '已連線',