mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-16 15:30:59 +00:00
96c84740db
* feat(mcp): add logs tab to MCP server detail panel - Add MCPLogs component mirroring PluginLogs functionality - Add getMcpServerLogs API method to BackendClient - Add logs tab to MCPForm detail panel (edit mode only) - Add i18n keys (tabLogs, logsLevelAll, logsRefresh, logsAutoRefresh, logsEmpty) to all 8 locales - Auto-refresh logs every 3s with toggle - Filter logs by level (ALL/DEBUG/INFO/WARNING/ERROR) * style: fix prettier formatting in MCPForm.tsx logs tab * style: fix prettier in i18n locale files --------- Co-authored-by: dadachann <dadachann@users.noreply.github.com> Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
150 lines
4.3 KiB
TypeScript
150 lines
4.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { PluginLogEntry } from '@/app/infra/entities/plugin';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { RefreshCw } from 'lucide-react';
|
|
|
|
const LEVEL_OPTIONS = ['ALL', 'DEBUG', 'INFO', 'WARNING', 'ERROR'] as const;
|
|
|
|
function levelClassName(level: string): string {
|
|
switch (level) {
|
|
case 'ERROR':
|
|
case 'CRITICAL':
|
|
return 'text-red-500';
|
|
case 'WARNING':
|
|
return 'text-amber-500';
|
|
case 'DEBUG':
|
|
return 'text-gray-400 dark:text-gray-500';
|
|
default:
|
|
return 'text-gray-700 dark:text-gray-300';
|
|
}
|
|
}
|
|
|
|
export default function MCPLogs({ serverName }: { serverName: string }) {
|
|
const { t } = useTranslation();
|
|
const [logs, setLogs] = useState<PluginLogEntry[]>([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [level, setLevel] = useState<string>('ALL');
|
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const atBottomRef = useRef(true);
|
|
|
|
const fetchLogs = useCallback(() => {
|
|
setIsLoading(true);
|
|
httpClient
|
|
.getMcpServerLogs(serverName, 500, level === 'ALL' ? undefined : level)
|
|
.then((res) => {
|
|
setLogs(res.logs ?? []);
|
|
})
|
|
.catch(() => {
|
|
setLogs([]);
|
|
})
|
|
.finally(() => {
|
|
setIsLoading(false);
|
|
});
|
|
}, [serverName, level]);
|
|
|
|
useEffect(() => {
|
|
fetchLogs();
|
|
}, [fetchLogs]);
|
|
|
|
// Auto-refresh poll loop.
|
|
useEffect(() => {
|
|
if (!autoRefresh) return;
|
|
const timer = setInterval(fetchLogs, 3000);
|
|
return () => clearInterval(timer);
|
|
}, [autoRefresh, fetchLogs]);
|
|
|
|
// Keep view pinned to bottom when the user is already at the bottom.
|
|
useEffect(() => {
|
|
const el = scrollRef.current;
|
|
if (el && atBottomRef.current) {
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
}, [logs]);
|
|
|
|
function handleScroll() {
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
atBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
<div className="flex shrink-0 flex-wrap items-center gap-2 px-1 pb-3 sm:px-6">
|
|
<Select value={level} onValueChange={setLevel}>
|
|
<SelectTrigger className="h-8 w-[130px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{LEVEL_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt} value={opt}>
|
|
{opt === 'ALL' ? t('mcp.logsLevelAll') : opt}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-8"
|
|
onClick={fetchLogs}
|
|
disabled={isLoading}
|
|
>
|
|
<RefreshCw
|
|
className={`mr-1.5 size-3.5 ${isLoading ? 'animate-spin' : ''}`}
|
|
/>
|
|
{t('mcp.logsRefresh')}
|
|
</Button>
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
id="mcp-logs-auto-refresh"
|
|
checked={autoRefresh}
|
|
onCheckedChange={setAutoRefresh}
|
|
/>
|
|
<Label
|
|
htmlFor="mcp-logs-auto-refresh"
|
|
className="cursor-pointer text-sm font-normal text-muted-foreground"
|
|
>
|
|
{t('mcp.logsAutoRefresh')}
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
ref={scrollRef}
|
|
onScroll={handleScroll}
|
|
className="min-h-0 flex-1 overflow-auto bg-gray-50 px-3 py-3 font-mono text-xs leading-relaxed dark:bg-gray-900/40 sm:px-6"
|
|
>
|
|
{logs.length === 0 ? (
|
|
<div className="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
{t('mcp.logsEmpty')}
|
|
</div>
|
|
) : (
|
|
logs.map((entry, idx) => (
|
|
<div
|
|
key={`${entry.ts}-${idx}`}
|
|
className={`whitespace-pre-wrap break-all ${levelClassName(
|
|
entry.level,
|
|
)}`}
|
|
>
|
|
{entry.text}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|