Skip to main content
Glama
Dashboard.tsx11.5 kB
import { EditServerModal } from "@/components/dashboard/EditServerModal"; import { MetricsPanel } from "@/components/dashboard/MetricsPanel"; import { ConnectivityDiagram } from "@/components/dashboard/SystemConnectivity/ConnectivityDiagram"; import { Card, CardContent } from "@/components/ui/card"; import { useToast } from "@/components/ui/use-toast"; import { useDashboardStore, useModalsStore, useSocketStore } from "@/store"; import { Agent, McpServer, McpServerStatus } from "@/types"; import { isActive } from "@/utils"; import { SystemState } from "@mcpx/shared-model"; import { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; type TransformedState = { agents: Agent[]; lastUpdated?: Date; servers: McpServer[]; systemUsage?: { callCount: number; lastCalledAt?: Date; }; }; // Helper function to create default access config for agents const createDefaultAccessConfig = (servers: McpServer[]) => { return servers.map((server) => ({ serverId: server.id, serverName: server.name, allowServer: true, tools: server.tools.map((tool) => ({ toolName: tool.name, allowTool: true, })), })); }; // Transform JSON configuration data to our internal format // TODO: This should be moved to a separate utility file const transformConfigurationData = (config: SystemState): TransformedState => { // Transform targetServers_new to mcpServers format - keep original order const transformedServers: McpServer[] = (config.targetServers_new || []).map( (server) => { // Determine status based on connection state let status: McpServerStatus = "connected_stopped"; let connectionError = null; switch (server.state.type) { case "connected": status = isActive(server.usage?.lastCalledAt) ? "connected_running" : "connected_stopped"; break; case "connection-failed": status = "connection_failed"; connectionError = server.state.error?.name === "McpError" ? "Failed to initiate server: inspect logs for more details" : server.state.error?.message || "Connection failed"; break; case "pending-auth": status = "pending_auth"; break; default: status = "connected_stopped"; } return { args: (server._type === "stdio" && server.args) || [], command: (server._type === "stdio" && server.command) || "", env: (server._type === "stdio" && server.env) || {}, icon: server.icon, id: `server-${server.name}`, name: server.name, status, connectionError, tools: server.tools.map((tool) => ({ name: tool.name, description: tool.description || "", invocations: tool.usage.callCount, lastCalledAt: tool.usage.lastCalledAt, })), configuration: {}, usage: server.usage, type: server._type || "stdio", url: ("url" in server && server.url) || "", headers: ("headers" in server && server.headers) || {}, }; }, ); // Transform agents using clusters (backend always provides clusters now) const defaultAccessConfig = createDefaultAccessConfig(transformedServers); const transformedAgents: Agent[] = (config.connectedClientClusters || []).map( (cluster, index) => { const firstClient = config.connectedClients.find((client) => cluster.sessionIds.includes(client.sessionId), ); return { id: `agent-cluster-${index}`, identifier: cluster.name, status: "connected", lastActivity: cluster.usage.lastCalledAt, sessionIds: cluster.sessionIds, llm: firstClient?.llm || { provider: "unknown", model: "unknown" }, usage: cluster.usage, accessConfig: defaultAccessConfig, }; }, ); return { servers: transformedServers, agents: transformedAgents, systemUsage: config.usage, lastUpdated: config.lastUpdatedAt, }; }; // TODO: Split this component into smaller pieces for better maintainability export default function Dashboard() { // Use separate selectors to get stable references - Zustand will only re-render when these specific values change const configurationData = useSocketStore((s) => s.systemState); // Use individual selectors to prevent re-renders from object creation const closeEditServerModal = useModalsStore((s) => s.closeEditServerModal); const isEditServerModalOpen = useModalsStore((s) => s.isEditServerModalOpen); const [mcpServers, setMcpServers] = useState<Array<McpServer>>([]); const [aiAgents, setAiAgents] = useState<Agent[]>([]); const [mcpxSystemActualStatus, setMcpxSystemActualStatus] = useState("stopped"); const [shouldOpenAddServerModal, setShouldOpenAddServerModal] = useState(false); const [searchParams] = useSearchParams(); const navigate = useNavigate(); const { dismiss } = useToast(); // Use individual selectors to prevent re-renders from object creation const isDiagramExpanded = useDashboardStore((s) => s.isDiagramExpanded); const reset = useDashboardStore((s) => s.reset); const toggleDiagramExpansion = useDashboardStore( (s) => s.toggleDiagramExpansion, ); // Reset the state when the dashboard unmounts useEffect(() => reset, [reset]); const catalogHandledRef = useRef(false); const prevTabRef = useRef<string | null>(null); useEffect(() => { const tab = searchParams.get("tab"); if (tab === "catalog") { const tabChanged = prevTabRef.current !== "catalog"; if (tabChanged) { catalogHandledRef.current = false; } if (!catalogHandledRef.current) { catalogHandledRef.current = true; dismiss(); if (!isDiagramExpanded) { toggleDiagramExpansion(); } setShouldOpenAddServerModal(true); const timer = setTimeout(() => { navigate("/dashboard", { replace: true }); setTimeout(() => { setShouldOpenAddServerModal(false); }, 300); }, 100); prevTabRef.current = tab; return () => clearTimeout(timer); } } else { catalogHandledRef.current = false; if (shouldOpenAddServerModal) { setShouldOpenAddServerModal(false); } } prevTabRef.current = tab; return undefined; // eslint-disable-next-line react-hooks/exhaustive-deps -- shouldOpenAddServerModal intentionally excluded to avoid loops }, [ searchParams, dismiss, navigate, isDiagramExpanded, toggleDiagramExpansion, ]); // Memoized data processing - recalculate when configurationData reference changes // The useEffect below will prevent state updates if data hasn't actually changed const processedData = useMemo(() => { if (!configurationData) { return { servers: [], agents: [], status: "stopped", systemUsage: undefined, }; } const transformed = transformConfigurationData(configurationData); return { servers: transformed.servers, agents: transformed.agents, status: isActive(configurationData?.usage?.lastCalledAt) ? "running" : "stopped", systemUsage: transformed.systemUsage, }; }, [configurationData]); // Update state only when processed data actually changes (not just reference) const prevProcessedDataRef = useRef<typeof processedData | null>(null); useEffect(() => { // Only update if the actual data changed, not just the reference const prev = prevProcessedDataRef.current; if (!prev) { // First render - always update setMcpServers(processedData.servers); setAiAgents(processedData.agents); setMcpxSystemActualStatus(processedData.status); prevProcessedDataRef.current = processedData; return; } // Check if servers actually changed - compare by ID to handle order changes const prevServerIds = new Set(prev.servers.map((s) => s.id)); const newServerIds = new Set(processedData.servers.map((s) => s.id)); const serversChanged = prev.servers.length !== processedData.servers.length || prevServerIds.size !== newServerIds.size || Array.from(prevServerIds).some((id) => !newServerIds.has(id)) || prev.servers.some((s) => { const newServer = processedData.servers.find((ns) => ns.id === s.id); return ( !newServer || s.name !== newServer.name || s.status !== newServer.status ); }); // Check if agents actually changed - compare by ID to handle order changes const prevAgentIds = new Set(prev.agents.map((a) => a.id)); const newAgentIds = new Set(processedData.agents.map((a) => a.id)); const agentsChanged = prev.agents.length !== processedData.agents.length || prevAgentIds.size !== newAgentIds.size || Array.from(prevAgentIds).some((id) => !newAgentIds.has(id)) || prev.agents.some((a) => { const newAgent = processedData.agents.find((na) => na.id === a.id); return !newAgent || a.identifier !== newAgent.identifier; }); // Check if status changed const statusChanged = prev.status !== processedData.status; if (serversChanged || agentsChanged || statusChanged) { setMcpServers(processedData.servers); setAiAgents(processedData.agents); setMcpxSystemActualStatus(processedData.status); prevProcessedDataRef.current = processedData; } }, [processedData]); // Get MCPX version from system state const mcpxVersionString = (configurationData as SystemState)?.mcpxVersion; const parseVersion = (versionStr: string) => { if (!versionStr) return "1.0.0"; const cleanVersion = versionStr.replace(/^v/, ""); const versionPart = cleanVersion.split("-")[0]; return versionPart; }; const mcpxVersion = mcpxVersionString ? parseVersion(mcpxVersionString) : "1.0.0"; // Reset state when no configuration data useEffect(() => { if (!configurationData) { setMcpServers([]); setAiAgents([]); } }, [configurationData]); return ( <div className="p-4 md:p-6 bg-gray-100 text-[var(--color-text-primary)] flex flex-col h-screen max-h-screen"> <div className="flex flex-col flex-grow space-y-4 overflow-hidden"> {/* Metrics Panel */} <MetricsPanel agents={aiAgents} servers={mcpServers} systemUsage={processedData.systemUsage} /> <Card className={ "flex-1 shadow-sm border-[var(--color-border-primary)] bg-white flex flex-col overflow-hidden" + (isDiagramExpanded ? " h-full rounded-md" : " flex-0 h-[50px]") } > <CardContent className="p-0 flex-grow overflow-hidden"> {isDiagramExpanded && ( <ConnectivityDiagram agents={aiAgents} mcpServersData={mcpServers} mcpxStatus={mcpxSystemActualStatus} version={mcpxVersion} initialOpenAddServerModal={shouldOpenAddServerModal} /> )} </CardContent> </Card> </div> {isEditServerModalOpen && ( <EditServerModal isOpen={isEditServerModalOpen} onClose={closeEditServerModal} /> )} </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheLunarCompany/lunar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server