239 lines
14 KiB
HTML
239 lines
14 KiB
HTML
|
|
<!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>
|