Skip to main content
Glama
intecrel

Industrial MCP Server

by intecrel
AuditDashboard.tsx16.4 kB
'use client' /** * Audit Dashboard Component * Provides real-time monitoring and querying of audit events */ import React, { useState, useEffect } from 'react' // Types interface AuditEvent { id: number timestamp: string event_type: string user_id?: string user_email?: string action: string result: 'success' | 'failure' | 'warning' risk_level: 'low' | 'medium' | 'high' | 'critical' database_type?: 'neo4j' | 'mysql' operation_type?: 'CREATE' | 'MERGE' | 'SET' | 'READ' execution_time_ms?: number affected_nodes?: number affected_relationships?: number } interface AuditFilters { startDate?: string endDate?: string eventType?: string userEmail?: string riskLevel?: string result?: string databaseType?: string search?: string } interface AuditResponse { events: AuditEvent[] pagination: { page: number limit: number total: number totalPages: number hasNext: boolean hasPrevious: boolean } } export default function AuditDashboard() { const [events, setEvents] = useState<AuditEvent[]>([]) const [loading, setLoading] = useState(false) const [error, setError] = useState<string | null>(null) const [filters, setFilters] = useState<AuditFilters>({}) const [pagination, setPagination] = useState({ page: 1, limit: 50, total: 0, totalPages: 0, hasNext: false, hasPrevious: false }) // Fetch audit events const fetchAuditEvents = async (newFilters: AuditFilters = {}, page: number = 1) => { setLoading(true) setError(null) try { const params = new URLSearchParams({ page: page.toString(), limit: pagination.limit.toString(), ...Object.fromEntries( Object.entries({ ...filters, ...newFilters }).filter(([_, value]) => value !== '') ) }) const response = await fetch(`/api/audit?${params}`, { headers: { 'x-api-key': process.env.NEXT_PUBLIC_API_KEY || '' } }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const data: AuditResponse = await response.json() setEvents(data.events) setPagination(data.pagination) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch audit events') console.error('Error fetching audit events:', err) } finally { setLoading(false) } } // Export events to CSV const exportToCSV = async () => { try { const params = new URLSearchParams({ format: 'csv', ...Object.fromEntries( Object.entries(filters).filter(([_, value]) => value !== '') ) }) const response = await fetch(`/api/audit?${params}`, { headers: { 'x-api-key': process.env.NEXT_PUBLIC_API_KEY || '' } }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const blob = await response.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `audit-events-${new Date().toISOString().split('T')[0]}.csv` document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to export audit events') } } // Format timestamp const formatTimestamp = (timestamp: string) => { return new Date(timestamp).toLocaleString() } // Get risk level color const getRiskLevelColor = (level: string) => { switch (level) { case 'critical': return 'text-red-600 bg-red-50' case 'high': return 'text-orange-600 bg-orange-50' case 'medium': return 'text-yellow-600 bg-yellow-50' case 'low': return 'text-green-600 bg-green-50' default: return 'text-gray-600 bg-gray-50' } } // Get result color const getResultColor = (result: string) => { switch (result) { case 'success': return 'text-green-600 bg-green-50' case 'failure': return 'text-red-600 bg-red-50' case 'warning': return 'text-yellow-600 bg-yellow-50' default: return 'text-gray-600 bg-gray-50' } } // Initial load useEffect(() => { fetchAuditEvents() }, []) return ( <div className="p-6 max-w-7xl mx-auto"> {/* Header */} <div className="mb-6"> <h1 className="text-2xl font-bold text-gray-900">Audit Dashboard</h1> <p className="text-gray-600 mt-2"> Monitor and analyze security and database audit events </p> </div> {/* Filters */} <div className="bg-white rounded-lg shadow p-6 mb-6"> <h2 className="text-lg font-semibold mb-4">Filters</h2> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4"> {/* Time Range */} <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Start Date </label> <input type="datetime-local" value={filters.startDate || ''} onChange={(e) => setFilters({ ...filters, startDate: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> End Date </label> <input type="datetime-local" value={filters.endDate || ''} onChange={(e) => setFilters({ ...filters, endDate: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> {/* Risk Level */} <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Risk Level </label> <select value={filters.riskLevel || ''} onChange={(e) => setFilters({ ...filters, riskLevel: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="">All Risk Levels</option> <option value="low">Low</option> <option value="medium">Medium</option> <option value="high">High</option> <option value="critical">Critical</option> </select> </div> {/* Result */} <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Result </label> <select value={filters.result || ''} onChange={(e) => setFilters({ ...filters, result: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="">All Results</option> <option value="success">Success</option> <option value="failure">Failure</option> <option value="warning">Warning</option> </select> </div> {/* Event Type */} <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Event Type </label> <input type="text" placeholder="e.g., database.neo4j.%" value={filters.eventType || ''} onChange={(e) => setFilters({ ...filters, eventType: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> {/* Database Type */} <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Database Type </label> <select value={filters.databaseType || ''} onChange={(e) => setFilters({ ...filters, databaseType: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="">All Databases</option> <option value="neo4j">Neo4j</option> <option value="mysql">MySQL</option> </select> </div> {/* User Email */} <div> <label className="block text-sm font-medium text-gray-700 mb-1"> User Email </label> <input type="email" placeholder="user@example.com" value={filters.userEmail || ''} onChange={(e) => setFilters({ ...filters, userEmail: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> {/* Search */} <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Search </label> <input type="text" placeholder="Search actions, events..." value={filters.search || ''} onChange={(e) => setFilters({ ...filters, search: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> </div> {/* Filter Actions */} <div className="flex space-x-3"> <button onClick={() => fetchAuditEvents(filters, 1)} disabled={loading} className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" > {loading ? 'Loading...' : 'Apply Filters'} </button> <button onClick={() => { setFilters({}) fetchAuditEvents({}, 1) }} className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700" > Clear Filters </button> <button onClick={exportToCSV} className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700" > Export CSV </button> </div> </div> {/* Error Display */} {error && ( <div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6"> <div className="text-red-800"> <strong>Error:</strong> {error} </div> </div> )} {/* Results */} <div className="bg-white rounded-lg shadow"> {/* Results Header */} <div className="px-6 py-4 border-b border-gray-200"> <div className="flex justify-between items-center"> <h2 className="text-lg font-semibold"> Audit Events ({pagination.total.toLocaleString()}) </h2> <div className="text-sm text-gray-600"> Page {pagination.page} of {pagination.totalPages} </div> </div> </div> {/* Events Table */} <div className="overflow-x-auto"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Timestamp </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Event Type </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Action </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> User </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Result </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Risk Level </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Database </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {events.map((event) => ( <tr key={event.id} className="hover:bg-gray-50"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {formatTimestamp(event.timestamp)} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 font-mono"> {event.event_type} </td> <td className="px-6 py-4 text-sm text-gray-900 max-w-xs truncate"> {event.action} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {event.user_email || event.user_id || 'System'} </td> <td className="px-6 py-4 whitespace-nowrap"> <span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${getResultColor(event.result)}`}> {event.result} </span> </td> <td className="px-6 py-4 whitespace-nowrap"> <span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${getRiskLevelColor(event.risk_level)}`}> {event.risk_level} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> {event.database_type ? ( <div> <div className="font-medium">{event.database_type}</div> {event.operation_type && ( <div className="text-gray-500 text-xs">{event.operation_type}</div> )} {event.execution_time_ms && ( <div className="text-gray-500 text-xs">{event.execution_time_ms}ms</div> )} </div> ) : ( '-' )} </td> </tr> ))} </tbody> </table> {events.length === 0 && !loading && ( <div className="text-center py-12 text-gray-500"> No audit events found. Try adjusting your filters. </div> )} {loading && ( <div className="text-center py-12 text-gray-500"> Loading audit events... </div> )} </div> {/* Pagination */} {pagination.totalPages > 1 && ( <div className="px-6 py-4 border-t border-gray-200"> <div className="flex justify-between items-center"> <button onClick={() => fetchAuditEvents(filters, pagination.page - 1)} disabled={!pagination.hasPrevious || loading} className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed" > Previous </button> <span className="text-sm text-gray-600"> Page {pagination.page} of {pagination.totalPages} </span> <button onClick={() => fetchAuditEvents(filters, pagination.page + 1)} disabled={!pagination.hasNext || loading} className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed" > Next </button> </div> </div> )} </div> </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/intecrel/industrial-mcp'

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