mcp-servers/dashboard/dashboard.html

239 lines
14 KiB
HTML
Raw Permalink Normal View History

2026-03-31 15:33:28 -04:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MCP Gateway Dashboard</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/@tabler/icons@latest"></script>
<style>
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; }
.animate-spin { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div id="root"></div>
<script>
// Lucide React Icons (simplified versions)
const CheckCircle = ({ className = '' }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
);
const AlertCircle = ({ className = '' }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
);
const RotateCcw = ({ className = '' }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
);
const Clock = ({ className = '' }) => (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 2m6-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
const Zap = ({ className = '' }) => (
<svg className={className} fill="currentColor" viewBox="0 0 24 24">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
);
// Dashboard Component
const Dashboard = () => {
const [services, setServices] = React.useState([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const [lastUpdated, setLastUpdated] = React.useState(null);
const fetchServiceStatus = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/mcp/status');
if (!response.ok) {
throw new Error('Failed to fetch service status');
}
const data = await response.json();
setServices(data.services || []);
setLastUpdated(new Date());
} catch (err) {
console.error('Error fetching status:', err);
setError(err.message);
} finally {
setLoading(false);
}
};
React.useEffect(() => {
fetchServiceStatus();
const interval = setInterval(fetchServiceStatus, 30000);
return () => clearInterval(interval);
}, []);
const getStatusIcon = (status) => {
if (status === 'healthy') {
return React.createElement(CheckCircle, { className: 'w-5 h-5 text-green-500' });
} else if (status === 'warning') {
return React.createElement(AlertCircle, { className: 'w-5 h-5 text-yellow-500' });
} else {
return React.createElement(AlertCircle, { className: 'w-5 h-5 text-red-500' });
}
};
const getStatusBgColor = (status) => {
if (status === 'healthy') return 'bg-green-50 border-green-200';
if (status === 'warning') return 'bg-yellow-50 border-yellow-200';
return 'bg-red-50 border-red-200';
};
const getStatusTextColor = (status) => {
if (status === 'healthy') return 'text-green-700';
if (status === 'warning') return 'text-yellow-700';
return 'text-red-700';
};
const totalTools = services.reduce((sum, service) => sum + (service.toolCount || 0), 0);
const healthyCount = services.filter(s => s.status === 'healthy').length;
return React.createElement('div', { className: 'min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-8' },
React.createElement('div', { className: 'max-w-7xl mx-auto' },
// Header
React.createElement('div', { className: 'flex items-center justify-between mb-8' },
React.createElement('div', null,
React.createElement('h1', { className: 'text-4xl font-bold text-slate-900 mb-2' }, 'MCP Gateway Dashboard'),
React.createElement('p', { className: 'text-slate-600' }, 'Real-time status and monitoring of all MCP services')
),
React.createElement('button', {
onClick: fetchServiceStatus,
disabled: loading,
className: 'flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-blue-400 transition-colors'
},
React.createElement(RotateCcw, { className: `w-4 h-4 ${loading ? 'animate-spin' : ''}` }),
'Refresh'
)
),
// Summary Stats
React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-3 gap-4 mb-8' },
React.createElement('div', { className: 'bg-white rounded-lg shadow p-6' },
React.createElement('div', { className: 'flex items-center justify-between' },
React.createElement('div', null,
React.createElement('p', { className: 'text-slate-600 text-sm font-medium' }, 'Total Services'),
React.createElement('p', { className: 'text-3xl font-bold text-slate-900 mt-1' }, services.length)
),
React.createElement(Zap, { className: 'w-8 h-8 text-blue-500' })
)
),
React.createElement('div', { className: 'bg-white rounded-lg shadow p-6' },
React.createElement('div', { className: 'flex items-center justify-between' },
React.createElement('div', null,
React.createElement('p', { className: 'text-slate-600 text-sm font-medium' }, 'Total Tools'),
React.createElement('p', { className: 'text-3xl font-bold text-slate-900 mt-1' }, totalTools)
),
React.createElement(Zap, { className: 'w-8 h-8 text-green-500' })
)
),
React.createElement('div', { className: 'bg-white rounded-lg shadow p-6' },
React.createElement('div', { className: 'flex items-center justify-between' },
React.createElement('div', null,
React.createElement('p', { className: 'text-slate-600 text-sm font-medium' }, 'Healthy Services'),
React.createElement('p', { className: 'text-3xl font-bold text-slate-900 mt-1' }, healthyCount)
),
React.createElement(CheckCircle, { className: 'w-8 h-8 text-green-500' })
)
)
),
// Last Updated
lastUpdated && React.createElement('div', { className: 'text-sm text-slate-600 mb-6 flex items-center gap-2' },
React.createElement(Clock, { className: 'w-4 h-4' }),
`Last updated: ${lastUpdated.toLocaleTimeString()}`
),
// Error State
error && React.createElement('div', { className: 'bg-red-50 border border-red-200 rounded-lg p-4 mb-8 text-red-700' },
React.createElement('p', { className: 'font-medium' }, 'Error loading service status'),
React.createElement('p', { className: 'text-sm' }, error)
),
// Services Grid
React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6' },
services.map((service) =>
React.createElement('div', {
key: service.name,
className: `rounded-lg border-2 p-6 transition-all hover:shadow-lg ${getStatusBgColor(service.status)}`
},
// Service Header
React.createElement('div', { className: 'flex items-start justify-between mb-4' },
React.createElement('div', null,
React.createElement('h3', { className: 'text-lg font-bold text-slate-900 capitalize' }, service.name),
React.createElement('p', { className: `text-sm font-medium ${getStatusTextColor(service.status)}` },
service.status.charAt(0).toUpperCase() + service.status.slice(1)
)
),
getStatusIcon(service.status)
),
// Service Details
React.createElement('div', { className: 'space-y-3' },
// Tool Count
React.createElement('div', { className: 'flex items-center justify-between p-3 bg-white rounded border border-slate-200' },
React.createElement('span', { className: 'text-sm text-slate-600' }, 'Tools Available'),
React.createElement('span', { className: 'text-xl font-bold text-slate-900' }, service.toolCount || 0)
),
// Response Time
service.responseTime !== undefined && React.createElement('div', { className: 'flex items-center justify-between p-3 bg-white rounded border border-slate-200' },
React.createElement('span', { className: 'text-sm text-slate-600' }, 'Response Time'),
React.createElement('span', { className: 'text-sm font-mono font-bold text-slate-900' }, `${service.responseTime}ms`)
),
// URL
React.createElement('div', { className: 'p-3 bg-white rounded border border-slate-200 text-xs' },
React.createElement('p', { className: 'text-slate-600 mb-1' }, 'Service URL'),
React.createElement('p', { className: 'text-slate-900 font-mono break-all' }, service.url)
),
// Last Check
service.lastCheck && React.createElement('div', { className: 'text-xs text-slate-600 px-3' },
`Last health check: ${new Date(service.lastCheck).toLocaleTimeString()}`
)
)
)
)
),
// Empty State
!loading && services.length === 0 && !error && React.createElement('div', { className: 'text-center py-12' },
React.createElement('p', { className: 'text-slate-600' }, 'No services available yet. Check your configuration.')
),
// Loading State
loading && services.length === 0 && React.createElement('div', { className: 'text-center py-12' },
React.createElement('div', { className: 'inline-block' },
React.createElement('div', { className: 'w-8 h-8 border-4 border-slate-300 border-t-blue-600 rounded-full animate-spin' })
),
React.createElement('p', { className: 'text-slate-600 mt-4' }, 'Loading service status...')
)
)
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(Dashboard));
</script>
</body>
</html>