Redesign UI into command center layout
This commit is contained in:
1 parent
8942d41354
commit
b9da742b3d
17 files changed
+1431
-441
No files matched your search
+72
-100
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { formatLocalTime } from './utils/timeUtils';
|
||||
import './Observability.css';
|
||||
|
||||
function Observability() {
|
||||
const [activeTab, setActiveTab] = useState('metrics');
|
||||
@@ -45,49 +46,25 @@ function Observability() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container" style={{ paddingTop: '2rem' }}>
|
||||
<div className="container observability-page">
|
||||
<h2>System Observability</h2>
|
||||
|
||||
<div style={{ marginBottom: '2rem', borderBottom: '1px solid #ddd', display: 'flex', gap: '1rem' }}>
|
||||
<div className="obs-tabs">
|
||||
<button
|
||||
onClick={() => setActiveTab('metrics')}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'metrics' ? '3px solid #007bff' : 'none',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontWeight: activeTab === 'metrics' ? 'bold' : 'normal',
|
||||
color: activeTab === 'metrics' ? '#007bff' : '#666'
|
||||
}}
|
||||
className={`obs-tab ${activeTab === 'metrics' ? 'is-active' : ''}`}
|
||||
>
|
||||
API Metrics
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('errors')}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'errors' ? '3px solid #007bff' : 'none',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontWeight: activeTab === 'errors' ? 'bold' : 'normal',
|
||||
color: activeTab === 'errors' ? '#007bff' : '#666'
|
||||
}}
|
||||
className={`obs-tab ${activeTab === 'errors' ? 'is-active' : ''}`}
|
||||
>
|
||||
Errors
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('audit')}
|
||||
style={{
|
||||
padding: '0.75rem 1.5rem',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === 'audit' ? '3px solid #007bff' : 'none',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontWeight: activeTab === 'audit' ? 'bold' : 'normal',
|
||||
color: activeTab === 'audit' ? '#007bff' : '#666'
|
||||
}}
|
||||
className={`obs-tab ${activeTab === 'audit' ? 'is-active' : ''}`}
|
||||
>
|
||||
Audit Logs
|
||||
</button>
|
||||
@@ -96,64 +73,59 @@ function Observability() {
|
||||
{/* Metrics Tab */}
|
||||
{activeTab === 'metrics' && metrics && (
|
||||
<div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '1rem', marginBottom: '2rem' }}>
|
||||
<div style={{ padding: '1rem', border: '1px solid #ddd', borderRadius: '4px', backgroundColor: '#f9f9f9' }}>
|
||||
<div style={{ fontSize: '0.9rem', color: '#666' }}>Server Uptime</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', marginTop: '0.5rem' }}>
|
||||
<div className="obs-summary-grid">
|
||||
<div className="obs-summary-card">
|
||||
<div className="obs-card-label">Server Uptime</div>
|
||||
<div className="obs-card-value">
|
||||
{metrics.uptimeFormatted || `${Math.floor(metrics.uptime / 3600)}h`}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '1rem', border: '1px solid #ddd', borderRadius: '4px', backgroundColor: '#f9f9f9' }}>
|
||||
<div style={{ fontSize: '0.9rem', color: '#666' }}>Total API Requests</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', marginTop: '0.5rem' }}>
|
||||
<div className="obs-summary-card">
|
||||
<div className="obs-card-label">Total API Requests</div>
|
||||
<div className="obs-card-value">
|
||||
{metrics.apiStats.reduce((sum, s) => sum + s.totalRequests, 0)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '1rem', border: '1px solid #ddd', borderRadius: '4px', backgroundColor: '#f9f9f9' }}>
|
||||
<div style={{ fontSize: '0.9rem', color: '#666' }}>Errors Logged</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', marginTop: '0.5rem', color: metrics.errorCount > 0 ? '#dc3545' : '#28a745' }}>
|
||||
<div className="obs-summary-card">
|
||||
<div className="obs-card-label">Errors Logged</div>
|
||||
<div className={`obs-card-value ${metrics.errorCount > 0 ? 'is-danger' : 'is-ok'}`}>
|
||||
{metrics.errorCount}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '1rem', border: '1px solid #ddd', borderRadius: '4px', backgroundColor: '#f9f9f9' }}>
|
||||
<div style={{ fontSize: '0.9rem', color: '#666' }}>Unique Errors</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', marginTop: '0.5rem' }}>
|
||||
<div className="obs-summary-card">
|
||||
<div className="obs-card-label">Unique Errors</div>
|
||||
<div className="obs-card-value">
|
||||
{metrics.uniqueErrors}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>API Endpoint Performance</h3>
|
||||
<div style={{ overflowX: 'auto', marginBottom: '2rem' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<div className="obs-table-wrap obs-spacing-bottom">
|
||||
<table className="obs-table">
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Endpoint</th>
|
||||
<th style={{ textAlign: 'right', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Requests</th>
|
||||
<th style={{ textAlign: 'right', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Success Rate</th>
|
||||
<th style={{ textAlign: 'right', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Avg Response</th>
|
||||
<th style={{ textAlign: 'right', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Min/Max</th>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th className="obs-right">Requests</th>
|
||||
<th className="obs-right">Success Rate</th>
|
||||
<th className="obs-right">Avg Response</th>
|
||||
<th className="obs-right">Min/Max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{metrics.apiStats.map((stat, i) => (
|
||||
<tr key={i} style={{ borderBottom: '1px solid #ddd' }}>
|
||||
<td style={{ padding: '0.75rem' }}>
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<code>{stat.method} {stat.endpoint}</code>
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '0.75rem' }}>{stat.totalRequests}</td>
|
||||
<td style={{ textAlign: 'right', padding: '0.75rem' }}>
|
||||
<span style={{
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '3px',
|
||||
backgroundColor: stat.successRate >= 95 ? '#d4edda' : '#fff3cd',
|
||||
color: stat.successRate >= 95 ? '#155724' : '#856404'
|
||||
}}>
|
||||
<td className="obs-right">{stat.totalRequests}</td>
|
||||
<td className="obs-right">
|
||||
<span className={`obs-rate-pill ${stat.successRate >= 95 ? 'is-good' : 'is-warn'}`}>
|
||||
{stat.successRate}%
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ textAlign: 'right', padding: '0.75rem' }}>{stat.avgResponseTime}ms</td>
|
||||
<td style={{ textAlign: 'right', padding: '0.75rem' }}>
|
||||
<td className="obs-right">{stat.avgResponseTime}ms</td>
|
||||
<td className="obs-right">
|
||||
{stat.minResponseTime}ms / {stat.maxResponseTime}ms
|
||||
</td>
|
||||
</tr>
|
||||
@@ -168,27 +140,27 @@ function Observability() {
|
||||
{activeTab === 'errors' && (
|
||||
<div>
|
||||
{errors.length === 0 ? (
|
||||
<p style={{ color: '#999', textAlign: 'center', padding: '2rem' }}>No errors logged</p>
|
||||
<p className="obs-empty-copy">No errors logged</p>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<div className="obs-table-wrap">
|
||||
<table className="obs-table">
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Error Message</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Context</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Occurred At</th>
|
||||
<tr>
|
||||
<th>Error Message</th>
|
||||
<th>Context</th>
|
||||
<th>Occurred At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{errors.map((error, i) => (
|
||||
<tr key={i} style={{ borderBottom: '1px solid #ddd', backgroundColor: i % 2 ? '#f9f9f9' : 'white' }}>
|
||||
<td style={{ padding: '0.75rem' }}>
|
||||
<code style={{ fontSize: '0.85rem', color: '#d9534f' }}>{error.error_message}</code>
|
||||
<tr key={i} className={i % 2 ? 'obs-zebra' : ''}>
|
||||
<td>
|
||||
<code className="obs-error-code">{error.error_message}</code>
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.85rem', color: '#666' }}>
|
||||
<td className="obs-muted-sm">
|
||||
{error.context ? JSON.stringify(JSON.parse(error.context)).substring(0, 50) : '-'}
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.85rem' }}>
|
||||
<td className="obs-small">
|
||||
{formatLocalTime(error.occurred_at)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -204,84 +176,84 @@ function Observability() {
|
||||
{activeTab === 'audit' && (
|
||||
<div>
|
||||
{auditSummary && (
|
||||
<div style={{ marginBottom: '2rem', padding: '1rem', backgroundColor: '#f9f9f9', borderRadius: '4px' }}>
|
||||
<div className="obs-audit-summary">
|
||||
<p><strong>Summary (Last 7 Days):</strong> {auditSummary.totalEvents} events</p>
|
||||
{auditSummary.actionBreakdown.map(action => (
|
||||
<p key={action.action} style={{ margin: '0.25rem 0', fontSize: '0.9rem', color: '#666' }}>
|
||||
<p key={action.action} className="obs-audit-summary-line">
|
||||
{action.action}: {action.count}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '1rem', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '0.5rem' }}>
|
||||
<div className="obs-filters-grid">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Entity Type"
|
||||
value={filters.entityType}
|
||||
onChange={(e) => setFilters({ ...filters, entityType: e.target.value })}
|
||||
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
className="obs-filter-input"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Entity ID"
|
||||
value={filters.entityId}
|
||||
onChange={(e) => setFilters({ ...filters, entityId: e.target.value })}
|
||||
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
className="obs-filter-input"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="User ID"
|
||||
value={filters.userId}
|
||||
onChange={(e) => setFilters({ ...filters, userId: e.target.value })}
|
||||
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
className="obs-filter-input"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Action"
|
||||
value={filters.action}
|
||||
onChange={(e) => setFilters({ ...filters, action: e.target.value })}
|
||||
style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px' }}
|
||||
className="obs-filter-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{auditLogs.length === 0 ? (
|
||||
<p style={{ color: '#999', textAlign: 'center', padding: '2rem' }}>No audit logs found</p>
|
||||
<p className="obs-empty-copy">No audit logs found</p>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<div className="obs-table-wrap">
|
||||
<table className="obs-table">
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid #ddd' }}>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Action</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Entity</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>User</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Changes</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', backgroundColor: '#f5f5f5' }}>Time</th>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>Entity</th>
|
||||
<th>User</th>
|
||||
<th>Changes</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{auditLogs.map((log, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<tr style={{ borderBottom: '1px solid #ddd', backgroundColor: i % 2 ? '#f9f9f9' : 'white', cursor: 'pointer' }} onClick={() => setExpandedAuditRow(expandedAuditRow === i ? null : i)}>
|
||||
<td style={{ padding: '0.75rem' }}><strong>{log.action}</strong></td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.85rem', color: '#666' }}>
|
||||
<tr className={`obs-click-row ${i % 2 ? 'obs-zebra' : ''}`} onClick={() => setExpandedAuditRow(expandedAuditRow === i ? null : i)}>
|
||||
<td><strong>{log.action}</strong></td>
|
||||
<td className="obs-muted-sm">
|
||||
{log.entity_type} #{log.entity_id}
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.85rem' }}>{log.user_id}</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.85rem' }}>
|
||||
<code style={{ cursor: 'pointer', color: '#007bff' }}>
|
||||
<td className="obs-small">{log.user_id}</td>
|
||||
<td className="obs-small">
|
||||
<code className="obs-toggle-code">
|
||||
{expandedAuditRow === i ? '▼ Hide' : '▶ Show'} Changes
|
||||
</code>
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.85rem' }}>
|
||||
<td className="obs-small">
|
||||
{formatLocalTime(log.created_at)}
|
||||
</td>
|
||||
</tr>
|
||||
{expandedAuditRow === i && (
|
||||
<tr style={{ backgroundColor: '#f0f8ff', borderBottom: '1px solid #ddd' }}>
|
||||
<td colSpan="5" style={{ padding: '1rem' }}>
|
||||
<tr className="obs-expanded-row">
|
||||
<td colSpan="5" className="obs-expanded-cell">
|
||||
<strong>Full Changes:</strong>
|
||||
<pre style={{ marginTop: '0.5rem', backgroundColor: 'white', padding: '0.75rem', borderRadius: '4px', overflowX: 'auto', border: '1px solid #ddd' }}>
|
||||
<pre className="obs-pre">
|
||||
{JSON.stringify(log.changes, null, 2)}
|
||||
</pre>
|
||||
</td>
|
||||
|
||||
Reference in new issue
Block a user