mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-20 17:30:59 +00:00
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:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -1251,6 +1251,7 @@ const jaJP = {
|
|||||||
sessions: 'セッション',
|
sessions: 'セッション',
|
||||||
feedback: 'ユーザーフィードバック',
|
feedback: 'ユーザーフィードバック',
|
||||||
},
|
},
|
||||||
|
systemStatus: 'システム状態',
|
||||||
pluginRuntime: 'プラグインランタイム',
|
pluginRuntime: 'プラグインランタイム',
|
||||||
boxRuntime: 'Box ランタイム',
|
boxRuntime: 'Box ランタイム',
|
||||||
connected: '接続済み',
|
connected: '接続済み',
|
||||||
|
|||||||
@@ -1253,6 +1253,7 @@ const ruRU = {
|
|||||||
sessions: 'Сессии',
|
sessions: 'Сессии',
|
||||||
feedback: 'Отзывы пользователей',
|
feedback: 'Отзывы пользователей',
|
||||||
},
|
},
|
||||||
|
systemStatus: 'Состояние системы',
|
||||||
pluginRuntime: 'Среда плагинов',
|
pluginRuntime: 'Среда плагинов',
|
||||||
boxRuntime: 'Среда Box',
|
boxRuntime: 'Среда Box',
|
||||||
connected: 'Подключено',
|
connected: 'Подключено',
|
||||||
|
|||||||
@@ -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: 'เชื่อมต่อแล้ว',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -1199,6 +1199,7 @@ const zhHans = {
|
|||||||
sessions: '会话记录',
|
sessions: '会话记录',
|
||||||
feedback: '用户反馈',
|
feedback: '用户反馈',
|
||||||
},
|
},
|
||||||
|
systemStatus: '系统状态',
|
||||||
pluginRuntime: '插件运行时',
|
pluginRuntime: '插件运行时',
|
||||||
boxRuntime: 'Box 运行时',
|
boxRuntime: 'Box 运行时',
|
||||||
connected: '已连接',
|
connected: '已连接',
|
||||||
|
|||||||
@@ -1192,6 +1192,7 @@ const zhHant = {
|
|||||||
sessions: '會話記錄',
|
sessions: '會話記錄',
|
||||||
feedback: '使用者回饋',
|
feedback: '使用者回饋',
|
||||||
},
|
},
|
||||||
|
systemStatus: '系統狀態',
|
||||||
pluginRuntime: '外掛執行時',
|
pluginRuntime: '外掛執行時',
|
||||||
boxRuntime: 'Box 執行時',
|
boxRuntime: 'Box 執行時',
|
||||||
connected: '已連線',
|
connected: '已連線',
|
||||||
|
|||||||
Reference in New Issue
Block a user