refactor(web): replace popover with dialog for system status details

Replace the dropdown popover with a proper Dialog for runtime status
details. Add a small info button on the System Status card that opens
the dialog. Session details now show in a spacious 2-column grid layout
with full image name, backend, CPU/memory, network, mount path, and
created/last-used timestamps.
This commit is contained in:
Junyan Qin
2026-04-19 15:23:38 +08:00
committed by WangCham
parent e955b3d6e8
commit f19cd4032d
9 changed files with 254 additions and 156 deletions

View File

@@ -6,16 +6,23 @@ import {
CircleCheck, CircleCheck,
CircleX, CircleX,
Loader2, Loader2,
ChevronDown, Info,
Container, Container,
Clock, Clock,
Cpu,
HardDrive,
Network,
Image,
FolderOpen,
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { import {
Popover, Dialog,
PopoverContent, DialogContent,
PopoverTrigger, DialogHeader,
} from '@/components/ui/popover'; DialogTitle,
} from '@/components/ui/dialog';
import { import {
ApiRespPluginSystemStatus, ApiRespPluginSystemStatus,
ApiRespBoxStatus, ApiRespBoxStatus,
@@ -46,6 +53,7 @@ export default function SystemStatusCard({
const [boxStatus, setBoxStatus] = useState<ApiRespBoxStatus | null>(null); const [boxStatus, setBoxStatus] = useState<ApiRespBoxStatus | null>(null);
const [boxSessions, setBoxSessions] = useState<BoxSessionInfo[]>([]); const [boxSessions, setBoxSessions] = useState<BoxSessionInfo[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const fetchStatus = useCallback(async () => { const fetchStatus = useCallback(async () => {
try { try {
@@ -74,6 +82,12 @@ export default function SystemStatusCard({
: null; : null;
const boxOk = boxStatus ? boxStatus.available : null; const boxOk = boxStatus ? boxStatus.available : null;
const handleOpenDialog = (e: React.MouseEvent) => {
e.stopPropagation();
fetchStatus();
setDialogOpen(true);
};
if (loading) { if (loading) {
return ( return (
<Card className="transition-all duration-300"> <Card className="transition-all duration-300">
@@ -92,164 +106,232 @@ export default function SystemStatusCard({
} }
return ( return (
<Popover <>
onOpenChange={(open) => { <Card className="transition-all duration-300 group">
if (open) fetchStatus(); <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
}} <CardTitle className="text-sm font-medium text-muted-foreground">
> {t('monitoring.systemStatus')}
<PopoverTrigger asChild> </CardTitle>
<Card className="transition-all duration-300 group cursor-pointer hover:border-primary/30"> <Button
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3"> variant="ghost"
<CardTitle className="text-sm font-medium text-muted-foreground"> size="icon"
{t('monitoring.systemStatus')} className="h-7 w-7 text-muted-foreground hover:text-foreground"
</CardTitle> onClick={handleOpenDialog}
<ChevronDown className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" /> >
</CardHeader> <Info className="w-4 h-4" />
<CardContent className="space-y-2"> </Button>
<div className="flex items-center gap-2"> </CardHeader>
<StatusDot ok={pluginOk} /> <CardContent className="space-y-2">
<Plug className="w-3.5 h-3.5 text-muted-foreground" /> <div className="flex items-center gap-2">
<span className="text-sm">{t('monitoring.pluginRuntime')}</span> <StatusDot ok={pluginOk} />
</div> <Plug className="w-3.5 h-3.5 text-muted-foreground" />
<div className="flex items-center gap-2"> <span className="text-sm">{t('monitoring.pluginRuntime')}</span>
<StatusDot ok={boxOk} /> </div>
<Box className="w-3.5 h-3.5 text-muted-foreground" /> <div className="flex items-center gap-2">
<span className="text-sm">{t('monitoring.boxRuntime')}</span> <StatusDot ok={boxOk} />
</div> <Box className="w-3.5 h-3.5 text-muted-foreground" />
</CardContent> <span className="text-sm">{t('monitoring.boxRuntime')}</span>
</Card> </div>
</PopoverTrigger> </CardContent>
<PopoverContent className="w-72" align="start"> </Card>
<div className="space-y-3">
{/* Plugin Runtime */} <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<div className="space-y-1"> <DialogContent className="max-w-lg max-h-[80vh] overflow-y-auto">
<div className="flex items-center gap-2"> <DialogHeader>
<Plug className="w-4 h-4 text-muted-foreground" /> <DialogTitle>{t('monitoring.systemStatus')}</DialogTitle>
<span className="text-sm font-medium"> </DialogHeader>
{t('monitoring.pluginRuntime')}
</span> <div className="space-y-5">
</div> {/* Plugin Runtime */}
<div className="ml-6 text-xs space-y-0.5"> <div className="space-y-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-2">
{pluginOk ? ( <Plug className="w-4 h-4 text-muted-foreground" />
<CircleCheck className="w-3.5 h-3.5 text-green-600" /> <span className="text-sm font-semibold">
) : ( {t('monitoring.pluginRuntime')}
<CircleX className="w-3.5 h-3.5 text-red-500" /> </span>
</div>
<div className="ml-6 text-sm space-y-1">
<div className="flex items-center gap-1.5">
{pluginOk ? (
<CircleCheck className="w-4 h-4 text-green-600" />
) : (
<CircleX className="w-4 h-4 text-red-500" />
)}
<span
className={
pluginOk
? 'text-green-600 font-medium'
: 'text-red-500 font-medium'
}
>
{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 text-xs">
{t('monitoring.pluginDisabled')}
</p>
)} )}
<span className={pluginOk ? 'text-green-600' : 'text-red-500'}> {pluginStatus &&
{pluginOk !pluginOk &&
? t('monitoring.connected') pluginStatus.is_enable &&
: pluginStatus && !pluginStatus.is_enable pluginStatus.plugin_connector_error &&
? t('monitoring.disabled') pluginStatus.plugin_connector_error !== 'ok' && (
<p className="text-red-400 text-xs break-all">
{pluginStatus.plugin_connector_error}
</p>
)}
</div>
</div>
<div className="border-t" />
{/* Box Runtime */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<Box className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-semibold">
{t('monitoring.boxRuntime')}
</span>
</div>
<div className="ml-6 text-sm space-y-1">
<div className="flex items-center gap-1.5">
{boxOk ? (
<CircleCheck className="w-4 h-4 text-green-600" />
) : (
<CircleX className="w-4 h-4 text-red-500" />
)}
<span
className={
boxOk
? 'text-green-600 font-medium'
: 'text-red-500 font-medium'
}
>
{boxOk
? t('monitoring.connected')
: t('monitoring.disconnected')} : t('monitoring.disconnected')}
</span> </span>
</div> </div>
{pluginStatus && !pluginStatus.is_enable && ( {boxStatus && !boxOk && boxStatus.connector_error && (
<p className="text-muted-foreground"> <p className="text-red-400 text-xs break-all">
{t('monitoring.pluginDisabled')} {boxStatus.connector_error}
</p>
)}
{pluginStatus &&
!pluginOk &&
pluginStatus.is_enable &&
pluginStatus.plugin_connector_error &&
pluginStatus.plugin_connector_error !== 'ok' && (
<p className="text-red-400 break-all">
{pluginStatus.plugin_connector_error}
</p> </p>
)} )}
</div> {boxStatus && (
</div> <div className="text-muted-foreground text-xs space-y-0.5">
{boxStatus.backend && (
<div className="border-t" /> <p>
{t('monitoring.boxBackend')}:{' '}
{/* Box Runtime */} <span className="text-foreground font-mono">
<div className="space-y-1"> {boxStatus.backend.name}
<div className="flex items-center gap-2"> </span>
<Box className="w-4 h-4 text-muted-foreground" /> </p>
<span className="text-sm font-medium"> )}
{t('monitoring.boxRuntime')} <p>
</span> {t('monitoring.boxProfile')}:{' '}
</div> <span className="text-foreground font-mono">
<div className="ml-6 text-xs space-y-0.5"> {boxStatus.profile}
<div className="flex items-center gap-1.5"> </span>
{boxOk ? ( </p>
<CircleCheck className="w-3.5 h-3.5 text-green-600" /> {boxOk && boxStatus.active_sessions !== undefined && (
) : ( <p>
<CircleX className="w-3.5 h-3.5 text-red-500" /> {t('monitoring.boxSandboxes')}:{' '}
<span className="text-foreground font-mono">
{boxStatus.active_sessions}
</span>
</p>
)}
</div>
)}
{/* Active Sandboxes */}
{boxSessions.length > 0 && (
<div className="mt-3 space-y-2">
{boxSessions.map((session) => (
<div
key={session.session_id}
className="rounded-lg border p-3 space-y-2"
>
<div className="flex items-center gap-1.5">
<Container className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<span className="font-mono font-semibold text-foreground truncate text-sm">
{session.session_id}
</span>
</div>
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 text-xs">
<div className="flex items-center gap-1.5 text-muted-foreground">
<Image className="w-3 h-3 flex-shrink-0" />
<span className="text-foreground font-mono truncate">
{session.image}
</span>
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<HardDrive className="w-3 h-3 flex-shrink-0" />
<span className="text-foreground">
{session.backend_name}
</span>
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<Cpu className="w-3 h-3 flex-shrink-0" />
<span className="text-foreground">
{session.cpus} CPU / {session.memory_mb} MB
</span>
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<Network className="w-3 h-3 flex-shrink-0" />
<span className="text-foreground">
{session.network}
</span>
</div>
{session.host_path && (
<div className="flex items-center gap-1.5 text-muted-foreground col-span-2">
<FolderOpen className="w-3 h-3 flex-shrink-0" />
<span
className="text-foreground font-mono truncate"
title={session.host_path}
>
{session.mount_path}{' '}
<span className="text-muted-foreground">
({session.host_path_mode})
</span>
</span>
</div>
)}
<div className="flex items-center gap-1.5 text-muted-foreground">
<Clock className="w-3 h-3 flex-shrink-0" />
<span>
{t('monitoring.boxSessionCreated')}:{' '}
<span className="text-foreground">
{new Date(session.created_at).toLocaleString()}
</span>
</span>
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<Clock className="w-3 h-3 flex-shrink-0" />
<span>
{t('monitoring.boxSessionLastUsed')}:{' '}
<span className="text-foreground">
{new Date(
session.last_used_at,
).toLocaleString()}
</span>
</span>
</div>
</div>
</div>
))}
</div>
)} )}
<span className={boxOk ? 'text-green-600' : 'text-red-500'}>
{boxOk
? t('monitoring.connected')
: t('monitoring.disconnected')}
</span>
</div> </div>
{boxStatus && !boxOk && boxStatus.connector_error && (
<p className="text-red-400 break-all">
{boxStatus.connector_error}
</p>
)}
{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>
)}
{boxSessions.length > 0 && (
<div className="mt-2 space-y-1.5">
{boxSessions.map((session) => (
<div
key={session.session_id}
className="rounded border p-1.5 space-y-0.5"
>
<div className="flex items-center gap-1">
<Container className="w-3 h-3 text-muted-foreground flex-shrink-0" />
<span className="font-mono text-foreground truncate">
{session.session_id}
</span>
</div>
<div className="text-muted-foreground grid grid-cols-2 gap-x-2">
<span>
{session.image.split(':').pop() || session.image}
</span>
<span>
{session.cpus} CPU / {session.memory_mb}MB
</span>
</div>
<div className="flex items-center gap-1 text-muted-foreground">
<Clock className="w-2.5 h-2.5" />
<span>
{new Date(session.last_used_at).toLocaleString()}
</span>
</div>
</div>
))}
</div>
)}
</div> </div>
</div> </div>
</div> </DialogContent>
</PopoverContent> </Dialog>
</Popover> </>
); );
} }

View File

@@ -1264,6 +1264,8 @@ const enUS = {
boxBackend: 'Backend', boxBackend: 'Backend',
boxProfile: 'Profile', boxProfile: 'Profile',
boxSandboxes: 'Sandboxes', boxSandboxes: 'Sandboxes',
boxSessionCreated: 'Created',
boxSessionLastUsed: 'Last used',
}, },
limitation: { limitation: {
maxBotsReached: maxBotsReached:

View File

@@ -1290,6 +1290,8 @@ const esES = {
boxBackend: 'Backend', boxBackend: 'Backend',
boxProfile: 'Perfil', boxProfile: 'Perfil',
boxSandboxes: 'Sandboxes', boxSandboxes: 'Sandboxes',
boxSessionCreated: 'Creado',
boxSessionLastUsed: 'Último uso',
}, },
limitation: { limitation: {
maxBotsReached: maxBotsReached:

View File

@@ -1262,6 +1262,8 @@ const jaJP = {
boxBackend: 'バックエンド', boxBackend: 'バックエンド',
boxProfile: 'プロファイル', boxProfile: 'プロファイル',
boxSandboxes: 'サンドボックス', boxSandboxes: 'サンドボックス',
boxSessionCreated: '作成日時',
boxSessionLastUsed: '最終使用',
}, },
limitation: { limitation: {
maxBotsReached: maxBotsReached:

View File

@@ -1264,6 +1264,8 @@ const ruRU = {
boxBackend: 'Бэкенд', boxBackend: 'Бэкенд',
boxProfile: 'Профиль', boxProfile: 'Профиль',
boxSandboxes: 'Песочницы', boxSandboxes: 'Песочницы',
boxSessionCreated: 'Создано',
boxSessionLastUsed: 'Последнее использование',
}, },
limitation: { limitation: {
maxBotsReached: maxBotsReached:

View File

@@ -1237,6 +1237,8 @@ const thTH = {
boxBackend: 'แบ็กเอนด์', boxBackend: 'แบ็กเอนด์',
boxProfile: 'โปรไฟล์', boxProfile: 'โปรไฟล์',
boxSandboxes: 'แซนด์บ็อกซ์', boxSandboxes: 'แซนด์บ็อกซ์',
boxSessionCreated: 'สร้างเมื่อ',
boxSessionLastUsed: 'ใช้ล่าสุด',
}, },
limitation: { limitation: {
maxBotsReached: maxBotsReached:

View File

@@ -1258,6 +1258,8 @@ const viVN = {
boxBackend: 'Backend', boxBackend: 'Backend',
boxProfile: 'Hồ sơ', boxProfile: 'Hồ sơ',
boxSandboxes: 'Sandbox', boxSandboxes: 'Sandbox',
boxSessionCreated: 'Đã tạo',
boxSessionLastUsed: 'Lần cuối sử dụng',
}, },
limitation: { limitation: {
maxBotsReached: maxBotsReached:

View File

@@ -1210,6 +1210,8 @@ const zhHans = {
boxBackend: '后端', boxBackend: '后端',
boxProfile: '配置', boxProfile: '配置',
boxSandboxes: '沙箱数', boxSandboxes: '沙箱数',
boxSessionCreated: '创建时间',
boxSessionLastUsed: '最后使用',
}, },
limitation: { limitation: {
maxBotsReached: maxBotsReached:

View File

@@ -1203,6 +1203,8 @@ const zhHant = {
boxBackend: '後端', boxBackend: '後端',
boxProfile: '設定檔', boxProfile: '設定檔',
boxSandboxes: '沙箱數', boxSandboxes: '沙箱數',
boxSessionCreated: '建立時間',
boxSessionLastUsed: '最後使用',
}, },
limitation: { limitation: {
maxBotsReached: maxBotsReached: