Files
ALwrity/frontend/src/components/billing/UsageLogsTable.tsx

427 lines
16 KiB
TypeScript

/**
* Usage Logs Table Component
* Displays API usage logs in a table with pagination and filtering.
* Terminal-themed UI matching scheduler dashboard style.
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Table,
TableBody,
TableContainer,
TableHead,
TableRow,
TablePagination,
IconButton,
Tooltip,
Select,
MenuItem,
FormControl,
InputLabel,
CircularProgress
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Refresh as RefreshIcon,
Receipt as ReceiptIcon
} from '@mui/icons-material';
import { billingService } from '../../services/billingService';
import { UsageLog, UsageLogsResponse } from '../../types/billing';
import {
TerminalPaper,
TerminalTypography,
TerminalChipSuccess,
TerminalChipError,
TerminalTableCell,
TerminalTableRow,
TerminalAlert,
terminalColors
} from '../SchedulerDashboard/terminalTheme';
import { formatCurrency } from '../../services/billingService';
interface UsageLogsTableProps {
initialLimit?: number;
}
const UsageLogsTable: React.FC<UsageLogsTableProps> = ({ initialLimit = 50 }) => {
const [logs, setLogs] = useState<UsageLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(initialLimit);
const [totalCount, setTotalCount] = useState(0);
const [statusFilter, setStatusFilter] = useState<'success' | 'failed' | 'all'>('all');
const [providerFilter, setProviderFilter] = useState<string>('all');
const fetchLogs = async () => {
try {
setLoading(true);
setError(null);
const statusCode = statusFilter === 'all' ? undefined : (statusFilter === 'success' ? 200 : 400);
const provider = providerFilter === 'all' ? undefined : providerFilter;
const response: UsageLogsResponse = await billingService.getUsageLogs(
rowsPerPage,
page * rowsPerPage,
provider,
statusCode
);
setLogs(response.logs || []);
setTotalCount(response.total_count || 0);
} catch (err: any) {
setError(err.message || 'Failed to fetch usage logs');
console.error('Error fetching usage logs:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchLogs();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, rowsPerPage, statusFilter, providerFilter]);
const handleChangePage = (_event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const getStatusIcon = (status: string): React.ReactElement | undefined => {
switch (status) {
case 'success':
return <CheckCircleIcon fontSize="small" sx={{ color: terminalColors.success }} />;
case 'failed':
return <ErrorIcon fontSize="small" sx={{ color: terminalColors.error }} />;
default:
return undefined;
}
};
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
return date.toLocaleString();
} catch {
return dateString;
}
};
const formatResponseTime = (seconds: number) => {
if (!seconds) return 'N/A';
const ms = seconds * 1000;
if (ms < 1000) return `${ms.toFixed(0)}ms`;
return `${seconds.toFixed(2)}s`;
};
const formatTokens = (tokens: number) => {
return new Intl.NumberFormat('en-US').format(tokens);
};
return (
<TerminalPaper sx={{ p: 3 }}>
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
<Box display="flex" alignItems="center" gap={1}>
<ReceiptIcon sx={{ color: terminalColors.primary }} />
<TerminalTypography variant="h6" component="h2" sx={{ fontSize: '1.25rem', fontWeight: 'bold' }}>
API Usage Logs
</TerminalTypography>
</Box>
<Box display="flex" alignItems="center" gap={2}>
<FormControl
size="small"
sx={{
minWidth: 120,
'& .MuiOutlinedInput-root': {
color: terminalColors.primary,
'& fieldset': {
borderColor: terminalColors.primary,
},
'&:hover fieldset': {
borderColor: terminalColors.secondary,
},
},
'& .MuiInputLabel-root': {
color: terminalColors.textSecondary,
},
'& .MuiSelect-icon': {
color: terminalColors.primary,
}
}}
>
<InputLabel>Provider</InputLabel>
<Select
value={providerFilter}
label="Provider"
onChange={(e) => {
setProviderFilter(e.target.value);
setPage(0);
}}
MenuProps={{
PaperProps: {
sx: {
backgroundColor: terminalColors.backgroundLight,
border: `1px solid ${terminalColors.primary}`,
'& .MuiMenuItem-root': {
color: terminalColors.primary,
fontFamily: 'monospace',
'&:hover': {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
},
'&.Mui-selected': {
backgroundColor: 'rgba(0, 255, 0, 0.15)',
}
}
}
}
}}
>
<MenuItem value="all">All</MenuItem>
<MenuItem value="gemini">Gemini</MenuItem>
<MenuItem value="huggingface">HuggingFace</MenuItem>
</Select>
</FormControl>
<FormControl
size="small"
sx={{
minWidth: 120,
'& .MuiOutlinedInput-root': {
color: terminalColors.primary,
'& fieldset': {
borderColor: terminalColors.primary,
},
'&:hover fieldset': {
borderColor: terminalColors.secondary,
},
},
'& .MuiInputLabel-root': {
color: terminalColors.textSecondary,
},
'& .MuiSelect-icon': {
color: terminalColors.primary,
}
}}
>
<InputLabel>Status</InputLabel>
<Select
value={statusFilter}
label="Status"
onChange={(e) => {
setStatusFilter(e.target.value as any);
setPage(0);
}}
MenuProps={{
PaperProps: {
sx: {
backgroundColor: terminalColors.backgroundLight,
border: `1px solid ${terminalColors.primary}`,
'& .MuiMenuItem-root': {
color: terminalColors.primary,
fontFamily: 'monospace',
'&:hover': {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
},
'&.Mui-selected': {
backgroundColor: 'rgba(0, 255, 0, 0.15)',
}
}
}
}
}}
>
<MenuItem value="all">All</MenuItem>
<MenuItem value="success">Success</MenuItem>
<MenuItem value="failed">Failed</MenuItem>
</Select>
</FormControl>
<Tooltip title="Refresh logs">
<IconButton
onClick={fetchLogs}
size="small"
sx={{
color: terminalColors.primary,
border: `1px solid ${terminalColors.primary}`,
'&:hover': {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
}
}}
>
<RefreshIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
{error && (
<TerminalAlert severity="error" sx={{ mb: 2 }}>
{error}
</TerminalAlert>
)}
{loading ? (
<Box display="flex" justifyContent="center" p={3}>
<CircularProgress sx={{ color: terminalColors.primary }} />
</Box>
) : (
<>
<TableContainer
sx={{
backgroundColor: terminalColors.background,
maxHeight: '600px',
overflow: 'auto'
}}
>
<Table size="small" sx={{ minWidth: 800 }}>
<TableHead>
<TerminalTableRow>
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Timestamp</TerminalTableCell>
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Provider</TerminalTableCell>
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Model</TerminalTableCell>
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Tokens</TerminalTableCell>
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Cost</TerminalTableCell>
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Status</TerminalTableCell>
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Response Time</TerminalTableCell>
<TerminalTableCell sx={{ fontWeight: 'bold', color: terminalColors.primary }}>Endpoint</TerminalTableCell>
</TerminalTableRow>
</TableHead>
<TableBody>
{logs.length === 0 ? (
<TerminalTableRow>
<TerminalTableCell colSpan={8} align="center">
<Box sx={{ py: 4, textAlign: 'center' }}>
<ReceiptIcon sx={{ color: terminalColors.textSecondary, fontSize: 48, mb: 2, opacity: 0.5 }} />
<TerminalTypography variant="body2" sx={{ color: terminalColors.primary, mb: 1, fontWeight: 'bold' }}>
No Usage Logs Yet
</TerminalTypography>
<TerminalTypography variant="body2" sx={{ color: terminalColors.textSecondary }}>
API usage logs will appear here once you start making API calls.
</TerminalTypography>
</Box>
</TerminalTableCell>
</TerminalTableRow>
) : (
logs.map((log) => (
<TerminalTableRow
key={log.id}
sx={{
backgroundColor: terminalColors.background,
color: terminalColors.primary,
'&:hover': {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
}
}}
>
<TerminalTableCell>
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem' }}>
{formatDate(log.timestamp)}
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
<TerminalTypography variant="body2" sx={{ textTransform: 'capitalize', fontWeight: 'bold' }}>
{log.provider}
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem' }}>
{log.model_used || 'N/A'}
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
<TerminalTypography variant="body2">
{formatTokens(log.tokens_total)}
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
<TerminalTypography variant="body2">
{formatCurrency(log.cost_total)}
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
{log.status === 'success' ? (
<TerminalChipSuccess
icon={getStatusIcon(log.status) || undefined}
label={`${log.status_code}`}
size="small"
/>
) : (
<TerminalChipError
icon={getStatusIcon(log.status) || undefined}
label={`${log.status_code}`}
size="small"
/>
)}
</TerminalTableCell>
<TerminalTableCell>
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem' }}>
{formatResponseTime(log.response_time)}
</TerminalTypography>
</TerminalTableCell>
<TerminalTableCell>
<Tooltip title={log.endpoint || ''}>
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{log.is_aggregated ? (
<span style={{ color: terminalColors.warning, fontStyle: 'italic' }}>
[AGGREGATED] {log.error_message || 'Historical data'}
</span>
) : (
`${log.method} ${log.endpoint?.substring(0, 30)}...`
)}
</TerminalTypography>
</Tooltip>
</TerminalTableCell>
</TerminalTableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
component="div"
count={totalCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[10, 25, 50, 100]}
sx={{
color: terminalColors.primary,
borderTop: `1px solid ${terminalColors.primary}`,
'& .MuiTablePagination-toolbar': {
color: terminalColors.primary,
},
'& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': {
color: terminalColors.primary,
fontFamily: 'monospace',
},
'& .MuiIconButton-root': {
color: terminalColors.primary,
'&:hover': {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
},
'&.Mui-disabled': {
color: terminalColors.textSecondary,
}
},
'& .MuiSelect-root': {
color: terminalColors.primary,
borderColor: terminalColors.primary,
}
}}
/>
</>
)}
</TerminalPaper>
);
};
export default UsageLogsTable;