feat(web): move runtime status to dashboard, clean up plugin debug popover

Add SystemStatusCards component to the monitoring dashboard showing
Plugin Runtime and Box Runtime connection status with details (backend,
profile, sandbox count). Remove all Box/session status from the plugin
page debug popover — it now only shows debug URL and key.

Includes i18n for all 8 supported languages.
This commit is contained in:
Junyan Qin
2026-04-19 13:48:26 +08:00
committed by WangCham
parent 29eadcb5ab
commit b71f690886
11 changed files with 243 additions and 142 deletions

View File

@@ -0,0 +1,157 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Plug, Box, CircleCheck, CircleX, Loader2 } from 'lucide-react';
import {
ApiRespPluginSystemStatus,
ApiRespBoxStatus,
} from '@/app/infra/entities/api';
import { httpClient } from '@/app/infra/http/HttpClient';
interface StatusCardProps {
icon: React.ReactNode;
title: string;
connected: boolean | null;
connectedLabel: string;
disconnectedLabel: string;
details?: { label: string; value: string }[];
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() {
const { t } = useTranslation();
const [pluginStatus, setPluginStatus] =
useState<ApiRespPluginSystemStatus | null>(null);
const [boxStatus, setBoxStatus] = useState<ApiRespBoxStatus | null>(null);
const [loading, setLoading] = useState(true);
const fetchStatus = useCallback(async () => {
setLoading(true);
try {
const [plugin, box] = await Promise.all([
httpClient.getPluginSystemStatus().catch(() => null),
httpClient.getBoxStatus().catch(() => null),
]);
setPluginStatus(plugin);
setBoxStatus(box);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchStatus();
}, [fetchStatus]);
const pluginConnected = pluginStatus
? pluginStatus.is_enable && pluginStatus.is_connected
: null;
const pluginDetails: { label: string; value: string }[] = [];
if (pluginStatus && !pluginStatus.is_enable) {
pluginDetails.push({
label: t('monitoring.statusDetail'),
value: t('monitoring.pluginDisabled'),
});
}
const boxConnected = boxStatus ? boxStatus.available : null;
const boxDetails: { label: string; value: string }[] = [];
if (boxStatus) {
if (boxStatus.backend) {
boxDetails.push({
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 (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<StatusCard
icon={<Plug className="w-4 h-4" />}
title={t('monitoring.pluginRuntime')}
connected={pluginConnected}
connectedLabel={t('monitoring.connected')}
disconnectedLabel={
pluginStatus && !pluginStatus.is_enable
? t('monitoring.disabled')
: t('monitoring.disconnected')
}
details={pluginDetails}
loading={loading}
/>
<StatusCard
icon={<Box className="w-4 h-4" />}
title={t('monitoring.boxRuntime')}
connected={boxConnected}
connectedLabel={t('monitoring.connected')}
disconnectedLabel={t('monitoring.disconnected')}
details={boxDetails}
loading={loading}
/>
</div>
);
}

View File

@@ -12,6 +12,7 @@ import {
CheckCircle2,
} from 'lucide-react';
import OverviewCards from './components/overview-cards/OverviewCards';
import SystemStatusCards from './components/overview-cards/SystemStatusCards';
import MonitoringFilters from './components/filters/MonitoringFilters';
import { ExportDropdown } from './components/ExportDropdown';
import { useMonitoringFilters } from './hooks/useMonitoringFilters';
@@ -296,6 +297,9 @@ function MonitoringPageContent() {
{/* Content Area */}
<div className="flex flex-col gap-6 px-[0.8rem] pb-4">
{/* System Status */}
<SystemStatusCards />
{/* Overview Section */}
<OverviewCards
metrics={data?.overview || null}

View File

@@ -17,11 +17,6 @@ import {
Check,
Bug,
Unlink,
Box,
CircleCheck,
CircleX,
Container,
Clock,
} from 'lucide-react';
import {
DropdownMenu,
@@ -48,11 +43,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { systemInfo } from '@/app/infra/http/HttpClient';
import {
ApiRespPluginSystemStatus,
ApiRespBoxStatus,
BoxSessionInfo,
} from '@/app/infra/entities/api';
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import {
PluginInstallTaskQueue,
@@ -142,8 +133,6 @@ function PluginListView() {
const [debugPopoverOpen, setDebugPopoverOpen] = useState(false);
const [copiedDebugUrl, setCopiedDebugUrl] = useState(false);
const [copiedDebugKey, setCopiedDebugKey] = useState(false);
const [boxStatus, setBoxStatus] = useState<ApiRespBoxStatus | null>(null);
const [boxSessions, setBoxSessions] = useState<BoxSessionInfo[]>([]);
useEffect(() => {
const fetchPluginSystemStatus = async () => {
@@ -465,14 +454,8 @@ function PluginListView() {
const handleShowDebugInfo = async () => {
try {
const [info, box, sessions] = await Promise.all([
httpClient.getPluginDebugInfo(),
httpClient.getBoxStatus().catch(() => null),
httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]),
]);
const info = await httpClient.getPluginDebugInfo();
setDebugInfo(info);
setBoxStatus(box);
setBoxSessions(sessions);
setDebugPopoverOpen(true);
} catch (error) {
console.error('Failed to fetch debug info:', error);
@@ -662,129 +645,6 @@ function PluginListView() {
</p>
)}
</div>
{/* Box Runtime Status */}
<div className="pt-2 border-t">
<div className="flex items-center gap-2 pb-2">
<Box className="w-4 h-4" />
<h4 className="font-semibold text-sm">
{t('plugins.boxStatusTitle')}
</h4>
</div>
{boxStatus ? (
<div className="space-y-1.5 text-xs">
<div className="flex items-center gap-2">
<span className="text-muted-foreground min-w-[80px]">
{t('plugins.boxStatus')}:
</span>
{boxStatus.available ? (
<span className="flex items-center gap-1 text-green-600">
<CircleCheck className="w-3.5 h-3.5" />
{t('plugins.boxConnected')}
</span>
) : (
<span className="flex items-center gap-1 text-red-500">
<CircleX className="w-3.5 h-3.5" />
{t('plugins.boxUnavailable')}
</span>
)}
</div>
{boxStatus.backend && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground min-w-[80px]">
{t('plugins.boxBackend')}:
</span>
<span className="font-mono">
{boxStatus.backend.name}
{boxStatus.backend.available
? ''
: ` (${t('plugins.boxUnavailable')})`}
</span>
</div>
)}
<div className="flex items-center gap-2">
<span className="text-muted-foreground min-w-[80px]">
{t('plugins.boxProfile')}:
</span>
<span className="font-mono">{boxStatus.profile}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground min-w-[80px]">
{t('plugins.boxSandboxes')}:
</span>
<span className="font-mono">
{boxStatus.active_sessions ?? 0}
</span>
</div>
{(boxStatus.recent_error_count ?? 0) > 0 && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground min-w-[80px]">
{t('plugins.boxErrors')}:
</span>
<span className="text-amber-600 font-mono">
{boxStatus.recent_error_count}
</span>
</div>
)}
{boxSessions.length > 0 && (
<div className="pt-1.5 mt-1.5 border-t border-dashed space-y-2">
{boxSessions.map((session) => (
<div
key={session.session_id}
className="rounded border p-2 space-y-1"
>
<div className="flex items-center gap-1.5">
<Container className="w-3 h-3 text-muted-foreground" />
<span className="font-mono font-medium truncate">
{session.session_id}
</span>
</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-muted-foreground">
<span>
{t('plugins.boxSessionImage')}:{' '}
<span className="text-foreground font-mono">
{session.image.split(':').pop() ||
session.image}
</span>
</span>
<span>
{t('plugins.boxSessionBackend')}:{' '}
<span className="text-foreground font-mono">
{session.backend_name}
</span>
</span>
<span>
{t('plugins.boxSessionResources')}:{' '}
<span className="text-foreground font-mono">
{session.cpus} CPU / {session.memory_mb}MB
</span>
</span>
<span>
{t('plugins.boxSessionNetwork')}:{' '}
<span className="text-foreground font-mono">
{session.network}
</span>
</span>
</div>
<div className="flex items-center gap-1 text-muted-foreground">
<Clock className="w-3 h-3" />
<span>
{new Date(
session.last_used_at,
).toLocaleString()}
</span>
</div>
</div>
))}
</div>
)}
</div>
) : (
<p className="text-xs text-muted-foreground">
{t('plugins.boxStatusLoadFailed')}
</p>
)}
</div>
</div>
</PopoverContent>
</Popover>