mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1253,6 +1253,16 @@ const enUS = {
|
||||
sessions: 'Sessions',
|
||||
feedback: 'User Feedback',
|
||||
},
|
||||
pluginRuntime: 'Plugin Runtime',
|
||||
boxRuntime: 'Box Runtime',
|
||||
connected: 'Connected',
|
||||
disconnected: 'Disconnected',
|
||||
disabled: 'Disabled',
|
||||
statusDetail: 'Status',
|
||||
pluginDisabled: 'Plugin system is disabled',
|
||||
boxBackend: 'Backend',
|
||||
boxProfile: 'Profile',
|
||||
boxSandboxes: 'Sandboxes',
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
|
||||
@@ -1279,6 +1279,16 @@ const esES = {
|
||||
sessions: 'Sesiones',
|
||||
feedback: 'Comentarios de usuarios',
|
||||
},
|
||||
pluginRuntime: 'Plugin Runtime',
|
||||
boxRuntime: 'Box Runtime',
|
||||
connected: 'Conectado',
|
||||
disconnected: 'Desconectado',
|
||||
disabled: 'Desactivado',
|
||||
statusDetail: 'Estado',
|
||||
pluginDisabled: 'El sistema de plugins está desactivado',
|
||||
boxBackend: 'Backend',
|
||||
boxProfile: 'Perfil',
|
||||
boxSandboxes: 'Sandboxes',
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
|
||||
@@ -1251,6 +1251,16 @@ const jaJP = {
|
||||
sessions: 'セッション',
|
||||
feedback: 'ユーザーフィードバック',
|
||||
},
|
||||
pluginRuntime: 'プラグインランタイム',
|
||||
boxRuntime: 'Box ランタイム',
|
||||
connected: '接続済み',
|
||||
disconnected: '未接続',
|
||||
disabled: '無効',
|
||||
statusDetail: 'ステータス',
|
||||
pluginDisabled: 'プラグインシステムが無効です',
|
||||
boxBackend: 'バックエンド',
|
||||
boxProfile: 'プロファイル',
|
||||
boxSandboxes: 'サンドボックス',
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
|
||||
@@ -1253,6 +1253,16 @@ const ruRU = {
|
||||
sessions: 'Сессии',
|
||||
feedback: 'Отзывы пользователей',
|
||||
},
|
||||
pluginRuntime: 'Среда плагинов',
|
||||
boxRuntime: 'Среда Box',
|
||||
connected: 'Подключено',
|
||||
disconnected: 'Отключено',
|
||||
disabled: 'Отключено',
|
||||
statusDetail: 'Статус',
|
||||
pluginDisabled: 'Система плагинов отключена',
|
||||
boxBackend: 'Бэкенд',
|
||||
boxProfile: 'Профиль',
|
||||
boxSandboxes: 'Песочницы',
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
|
||||
@@ -1226,6 +1226,16 @@ const thTH = {
|
||||
sessions: 'เซสชัน',
|
||||
feedback: 'ความคิดเห็นผู้ใช้',
|
||||
},
|
||||
pluginRuntime: 'Plugin Runtime',
|
||||
boxRuntime: 'Box Runtime',
|
||||
connected: 'เชื่อมต่อแล้ว',
|
||||
disconnected: 'ไม่ได้เชื่อมต่อ',
|
||||
disabled: 'ปิดใช้งาน',
|
||||
statusDetail: 'สถานะ',
|
||||
pluginDisabled: 'ระบบปลั๊กอินถูกปิดใช้งาน',
|
||||
boxBackend: 'แบ็กเอนด์',
|
||||
boxProfile: 'โปรไฟล์',
|
||||
boxSandboxes: 'แซนด์บ็อกซ์',
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
|
||||
@@ -1247,6 +1247,16 @@ const viVN = {
|
||||
sessions: 'Phiên',
|
||||
feedback: 'Phản hồi người dùng',
|
||||
},
|
||||
pluginRuntime: 'Plugin Runtime',
|
||||
boxRuntime: 'Box Runtime',
|
||||
connected: 'Đã kết nối',
|
||||
disconnected: 'Chưa kết nối',
|
||||
disabled: 'Đã tắt',
|
||||
statusDetail: 'Trạng thái',
|
||||
pluginDisabled: 'Hệ thống plugin đã tắt',
|
||||
boxBackend: 'Backend',
|
||||
boxProfile: 'Hồ sơ',
|
||||
boxSandboxes: 'Sandbox',
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
|
||||
@@ -1199,6 +1199,16 @@ const zhHans = {
|
||||
sessions: '会话记录',
|
||||
feedback: '用户反馈',
|
||||
},
|
||||
pluginRuntime: '插件运行时',
|
||||
boxRuntime: 'Box 运行时',
|
||||
connected: '已连接',
|
||||
disconnected: '未连接',
|
||||
disabled: '已禁用',
|
||||
statusDetail: '状态',
|
||||
pluginDisabled: '插件系统已禁用',
|
||||
boxBackend: '后端',
|
||||
boxProfile: '配置',
|
||||
boxSandboxes: '沙箱数',
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
|
||||
@@ -1192,6 +1192,16 @@ const zhHant = {
|
||||
sessions: '會話記錄',
|
||||
feedback: '使用者回饋',
|
||||
},
|
||||
pluginRuntime: '外掛執行時',
|
||||
boxRuntime: 'Box 執行時',
|
||||
connected: '已連線',
|
||||
disconnected: '未連線',
|
||||
disabled: '已停用',
|
||||
statusDetail: '狀態',
|
||||
pluginDisabled: '外掛系統已停用',
|
||||
boxBackend: '後端',
|
||||
boxProfile: '設定檔',
|
||||
boxSandboxes: '沙箱數',
|
||||
},
|
||||
limitation: {
|
||||
maxBotsReached:
|
||||
|
||||
Reference in New Issue
Block a user