story writer backend migration complete, Blog writer SEO and story writer backend migration complete, Blog writer SEO and story writer frontend migration complete
This commit is contained in:
@@ -28,15 +28,33 @@ import {
|
||||
calculateUsagePercentage
|
||||
} from '../../services/billingService';
|
||||
|
||||
// Terminal Theme
|
||||
import {
|
||||
TerminalCard,
|
||||
TerminalCardContent,
|
||||
TerminalTypography,
|
||||
TerminalChip,
|
||||
TerminalChipSuccess,
|
||||
TerminalChipError,
|
||||
TerminalChipWarning,
|
||||
terminalColors
|
||||
} from '../SchedulerDashboard/terminalTheme';
|
||||
|
||||
interface BillingOverviewProps {
|
||||
usageStats: UsageStats;
|
||||
onRefresh: () => void;
|
||||
terminalTheme?: boolean;
|
||||
}
|
||||
|
||||
const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
usageStats,
|
||||
onRefresh
|
||||
onRefresh,
|
||||
terminalTheme = false
|
||||
}) => {
|
||||
// Conditional component selection based on terminal theme
|
||||
const CardComponent = terminalTheme ? TerminalCard : Card;
|
||||
const CardContentComponent = terminalTheme ? TerminalCardContent : CardContent;
|
||||
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
|
||||
// Debug logs removed to reduce console noise
|
||||
|
||||
const costUsagePercentage = calculateUsagePercentage(
|
||||
@@ -47,9 +65,53 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
// Debug logs removed to reduce console noise
|
||||
|
||||
const getStatusChip = () => {
|
||||
const status = usageStats.usage_status;
|
||||
const status: string = usageStats.usage_status;
|
||||
const icon = getUsageStatusIcon(status);
|
||||
|
||||
// Helper function to format status label
|
||||
const formatStatusLabel = (statusStr: string): string => {
|
||||
return statusStr.charAt(0).toUpperCase() + statusStr.slice(1).replace('_', ' ');
|
||||
};
|
||||
|
||||
if (terminalTheme) {
|
||||
if (status === 'active') {
|
||||
return (
|
||||
<TerminalChipSuccess
|
||||
icon={<span>{icon}</span>}
|
||||
label={formatStatusLabel(status)}
|
||||
size="small"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
/>
|
||||
);
|
||||
} else if (status === 'warning') {
|
||||
return (
|
||||
<TerminalChipWarning
|
||||
icon={<span>{icon}</span>}
|
||||
label={formatStatusLabel(status)}
|
||||
size="small"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
/>
|
||||
);
|
||||
} else if (status === 'limit_reached') {
|
||||
return (
|
||||
<TerminalChipError
|
||||
icon={<span>{icon}</span>}
|
||||
label={formatStatusLabel(status)}
|
||||
size="small"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TerminalChip
|
||||
icon={<span>{icon}</span>}
|
||||
label={formatStatusLabel(status)}
|
||||
size="small"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let chipColor: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
|
||||
if (status === 'active') chipColor = 'success';
|
||||
else if (status === 'warning') chipColor = 'warning';
|
||||
@@ -58,7 +120,7 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
return (
|
||||
<Chip
|
||||
icon={<span>{icon}</span>}
|
||||
label={status.charAt(0).toUpperCase() + status.slice(1).replace('_', ' ')}
|
||||
label={formatStatusLabel(status)}
|
||||
color={chipColor}
|
||||
size="small"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
@@ -66,6 +128,25 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const cardStyles = terminalTheme
|
||||
? {
|
||||
height: '100%',
|
||||
backgroundColor: terminalColors.background,
|
||||
border: `1px solid ${terminalColors.border}`,
|
||||
borderRadius: 3,
|
||||
position: 'relative' as const,
|
||||
overflow: 'hidden' as const
|
||||
}
|
||||
: {
|
||||
height: '100%',
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 3,
|
||||
position: 'relative' as const,
|
||||
overflow: 'hidden' as const
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -73,34 +154,24 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
transition={{ duration: 0.4 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
height: '100%',
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 3,
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<CardComponent sx={cardStyles}>
|
||||
{/* Header */}
|
||||
<CardContent sx={{ pb: 1 }}>
|
||||
<CardContentComponent sx={{ pb: 1 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold' }}>
|
||||
<DollarSign size={20} />
|
||||
<TypographyComponent variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold' }}>
|
||||
<DollarSign size={20} color={terminalTheme ? terminalColors.text : undefined} />
|
||||
Billing Overview
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
<Tooltip title="View your current billing status, usage metrics, and subscription plan details">
|
||||
<Info size={16} color="rgba(255,255,255,0.7)" />
|
||||
<Info size={16} color={terminalTheme ? terminalColors.textSecondary : "rgba(255,255,255,0.7)"} />
|
||||
</Tooltip>
|
||||
<Tooltip title="Refresh data">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onRefresh}
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
'&:hover': { color: 'primary.main' }
|
||||
color: terminalTheme ? terminalColors.text : 'text.secondary',
|
||||
'&:hover': { color: terminalTheme ? terminalColors.secondary : 'primary.main' }
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
@@ -112,9 +183,9 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{getStatusChip()}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardContentComponent>
|
||||
|
||||
<CardContent sx={{ pt: 0 }}>
|
||||
<CardContentComponent sx={{ pt: 0 }}>
|
||||
{/* Current Cost */}
|
||||
<Box sx={{ mb: 3, textAlign: 'center' }}>
|
||||
<motion.div
|
||||
@@ -122,20 +193,20 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
>
|
||||
<Typography
|
||||
<TypographyComponent
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
color: '#ffffff',
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.3)',
|
||||
color: terminalTheme ? terminalColors.text : '#ffffff',
|
||||
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)',
|
||||
mb: 1
|
||||
}}
|
||||
>
|
||||
{formatCurrency(usageStats.total_cost)}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="body2" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.8)' }}>
|
||||
Total Cost This Month
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</motion.div>
|
||||
</Box>
|
||||
|
||||
@@ -143,34 +214,34 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Tooltip title="Total number of API requests made this billing period">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
<TypographyComponent variant="body2" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
|
||||
API Calls
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="body2" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors.text : '#ffffff' }}>
|
||||
{formatNumber(usageStats.total_calls)}
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Total tokens processed across all API providers (input + output tokens)">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
<TypographyComponent variant="body2" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
|
||||
Tokens Used
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="body2" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors.text : '#ffffff' }}>
|
||||
{formatNumber(usageStats.total_tokens)}
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Average response time for API requests in the last 24 hours">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
<TypographyComponent variant="body2" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
|
||||
Avg Response Time
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="body2" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors.text : '#ffffff' }}>
|
||||
{usageStats.avg_response_time.toFixed(0)}ms
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@@ -179,12 +250,12 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
{usageStats.limits.limits.monthly_cost > 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<TypographyComponent variant="body2" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
|
||||
Monthly Cost Limit
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="bold">
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="body2" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors.text : '#ffffff' }}>
|
||||
{formatPercentage(costUsagePercentage)}
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
@@ -192,17 +263,20 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
backgroundColor: terminalTheme ? terminalColors.backgroundLight : 'rgba(255,255,255,0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: costUsagePercentage > 80 ? '#ef4444' :
|
||||
costUsagePercentage > 60 ? '#f59e0b' : '#22c55e',
|
||||
backgroundColor: terminalTheme
|
||||
? (costUsagePercentage > 80 ? terminalColors.error :
|
||||
costUsagePercentage > 60 ? terminalColors.warning : terminalColors.success)
|
||||
: (costUsagePercentage > 80 ? '#ef4444' :
|
||||
costUsagePercentage > 60 ? '#f59e0b' : '#22c55e'),
|
||||
borderRadius: 4,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
|
||||
<TypographyComponent variant="caption" sx={{ mt: 0.5, display: 'block', color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
|
||||
{formatCurrency(usageStats.total_cost)} of {formatCurrency(usageStats.limits.limits.monthly_cost)} limit
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -210,69 +284,73 @@ const BillingOverview: React.FC<BillingOverviewProps> = ({
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
backgroundColor: terminalTheme ? terminalColors.backgroundLight : 'rgba(255,255,255,0.05)',
|
||||
borderRadius: 2,
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
border: terminalTheme ? `1px solid ${terminalColors.border}` : '1px solid rgba(255,255,255,0.1)'
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ mb: 1, color: 'rgba(255,255,255,0.8)' }}>
|
||||
<TypographyComponent variant="body2" sx={{ mb: 1, color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.8)' }}>
|
||||
Current Plan
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', mb: 1, color: '#ffffff' }}>
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="h6" sx={{ fontWeight: 'bold', mb: 1, color: terminalTheme ? terminalColors.text : '#ffffff' }}>
|
||||
{usageStats.limits.plan_name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="caption" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
|
||||
{usageStats.limits.tier.charAt(0).toUpperCase() + usageStats.limits.tier.slice(1)} Tier
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'space-around' }}>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
|
||||
<TypographyComponent variant="h6" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors.text : 'primary.main' }}>
|
||||
{usageStats.usage_percentages.gemini_calls.toFixed(0)}%
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="caption" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
|
||||
Gemini Usage
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'secondary.main' }}>
|
||||
<TypographyComponent variant="h6" sx={{ fontWeight: 'bold', color: terminalTheme ? terminalColors.text : 'secondary.main' }}>
|
||||
{usageStats.error_rate.toFixed(1)}%
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="caption" sx={{ color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)' }}>
|
||||
Error Rate
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardContentComponent>
|
||||
|
||||
{/* Decorative Elements */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -50,
|
||||
right: -50,
|
||||
width: 100,
|
||||
height: 100,
|
||||
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -30,
|
||||
left: -30,
|
||||
width: 60,
|
||||
height: 60,
|
||||
background: 'radial-gradient(circle, rgba(118, 75, 162, 0.1) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
{/* Decorative Elements - only show in non-terminal theme */}
|
||||
{!terminalTheme && (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -50,
|
||||
right: -50,
|
||||
width: 100,
|
||||
height: 100,
|
||||
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -30,
|
||||
left: -30,
|
||||
width: 60,
|
||||
height: 60,
|
||||
background: 'radial-gradient(circle, rgba(118, 75, 162, 0.1) 0%, transparent 70%)',
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CardComponent>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,18 +25,36 @@ import { SystemHealth } from '../../types/monitoring';
|
||||
import { billingService } from '../../services/billingService';
|
||||
import { monitoringService } from '../../services/monitoringService';
|
||||
import { onApiEvent } from '../../utils/apiEvents';
|
||||
import { showToastNotification } from '../../utils/toastNotifications';
|
||||
|
||||
// Terminal Theme
|
||||
import {
|
||||
TerminalCard,
|
||||
TerminalCardContent,
|
||||
TerminalTypography,
|
||||
TerminalChip,
|
||||
TerminalChipError,
|
||||
TerminalChipWarning,
|
||||
terminalColors
|
||||
} from '../SchedulerDashboard/terminalTheme';
|
||||
|
||||
interface CompactBillingDashboardProps {
|
||||
userId?: string;
|
||||
terminalTheme?: boolean;
|
||||
}
|
||||
|
||||
const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userId }) => {
|
||||
const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userId, terminalTheme = false }) => {
|
||||
// Conditional component selection based on terminal theme
|
||||
const CardComponent = terminalTheme ? TerminalCard : Card;
|
||||
const CardContentComponent = terminalTheme ? TerminalCardContent : CardContent;
|
||||
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
|
||||
const ChipComponent = terminalTheme ? TerminalChip : Chip;
|
||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
||||
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
const fetchData = async (showSuccessToast: boolean = false) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -48,8 +66,21 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
|
||||
setDashboardData(billingData);
|
||||
setSystemHealth(healthData);
|
||||
|
||||
// Show success toast only if explicitly requested (user-initiated refresh)
|
||||
if (showSuccessToast && billingData && healthData) {
|
||||
showToastNotification(
|
||||
'Billing data refreshed successfully',
|
||||
'success',
|
||||
{ duration: 3000 }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch data');
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch data';
|
||||
setError(errorMessage);
|
||||
|
||||
// Always show error toast for failures
|
||||
showToastNotification(errorMessage, 'error', { duration: 5000 });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -80,35 +111,55 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
const formatNumber = (num: number) => num.toLocaleString();
|
||||
|
||||
if (loading && !dashboardData) {
|
||||
const loadingCardStyles = terminalTheme
|
||||
? {
|
||||
backgroundColor: terminalColors.background,
|
||||
border: `1px solid ${terminalColors.border}`,
|
||||
borderRadius: 3
|
||||
}
|
||||
: {
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 3
|
||||
};
|
||||
|
||||
return (
|
||||
<Card sx={{
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 3
|
||||
}}>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.8)' }}>Loading billing data...</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<CardComponent sx={loadingCardStyles}>
|
||||
<CardContentComponent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<TypographyComponent sx={{ color: terminalTheme ? terminalColors.text : 'rgba(255,255,255,0.8)' }}>
|
||||
Loading billing data...
|
||||
</TypographyComponent>
|
||||
</CardContentComponent>
|
||||
</CardComponent>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorCardStyles = terminalTheme
|
||||
? {
|
||||
backgroundColor: terminalColors.background,
|
||||
border: `1px solid ${terminalColors.error}`,
|
||||
borderRadius: 3
|
||||
}
|
||||
: {
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 3
|
||||
};
|
||||
|
||||
return (
|
||||
<Card sx={{
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 3
|
||||
}}>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography sx={{ color: '#ff6b6b' }}>Error: {error}</Typography>
|
||||
<IconButton onClick={fetchData} sx={{ mt: 1 }}>
|
||||
<CardComponent sx={errorCardStyles}>
|
||||
<CardContentComponent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<TypographyComponent sx={{ color: terminalTheme ? terminalColors.error : '#ff6b6b' }}>
|
||||
Error: {error}
|
||||
</TypographyComponent>
|
||||
<IconButton onClick={() => fetchData(true)} sx={{ mt: 1, color: terminalTheme ? terminalColors.text : 'inherit' }}>
|
||||
<RefreshCw size={16} />
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContentComponent>
|
||||
</CardComponent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,36 +167,55 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
|
||||
const { current_usage, limits, alerts } = dashboardData;
|
||||
|
||||
const mainCardStyles = terminalTheme
|
||||
? {
|
||||
backgroundColor: terminalColors.background,
|
||||
border: `1px solid ${terminalColors.border}`,
|
||||
borderRadius: 4,
|
||||
position: 'relative' as const,
|
||||
overflow: 'hidden' as const,
|
||||
boxShadow: '0 0 15px rgba(0, 255, 0, 0.2)',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '1px',
|
||||
background: `linear-gradient(90deg, transparent, ${terminalColors.border}, transparent)`,
|
||||
zIndex: 1
|
||||
}
|
||||
}
|
||||
: {
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.08) 100%)',
|
||||
backdropFilter: 'blur(15px)',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
borderRadius: 4,
|
||||
position: 'relative' as const,
|
||||
overflow: 'hidden' as const,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.1)',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '1px',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent)',
|
||||
zIndex: 1
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.08) 100%)',
|
||||
backdropFilter: 'blur(15px)',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
borderRadius: 4,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.1)',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '1px',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent)',
|
||||
zIndex: 1
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardComponent sx={mainCardStyles}>
|
||||
{/* Header - Removed to save space */}
|
||||
|
||||
<CardContent sx={{ pt: 2 }}>
|
||||
<CardContentComponent sx={{ pt: 2 }}>
|
||||
{/* Compact Overview */}
|
||||
<Grid container spacing={2} sx={{ mb: 2 }}>
|
||||
{/* Total Cost */}
|
||||
@@ -170,46 +240,71 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
<Box sx={{
|
||||
textAlign: 'center',
|
||||
p: 2.5,
|
||||
background: 'linear-gradient(135deg, rgba(74, 222, 128, 0.12) 0%, rgba(34, 197, 94, 0.08) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(74, 222, 128, 0.25)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 25px rgba(74, 222, 128, 0.2)',
|
||||
border: '1px solid rgba(74, 222, 128, 0.4)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #4ade80, #22c55e)',
|
||||
zIndex: 1
|
||||
}
|
||||
...(terminalTheme ? {
|
||||
backgroundColor: terminalColors.backgroundLight,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${terminalColors.border}`,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: `0 0 15px ${terminalColors.border}40`,
|
||||
borderColor: terminalColors.secondary
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: terminalColors.border,
|
||||
zIndex: 1
|
||||
}
|
||||
} : {
|
||||
background: 'linear-gradient(135deg, rgba(74, 222, 128, 0.12) 0%, rgba(34, 197, 94, 0.08) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(74, 222, 128, 0.25)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 25px rgba(74, 222, 128, 0.2)',
|
||||
border: '1px solid rgba(74, 222, 128, 0.4)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #4ade80, #22c55e)',
|
||||
zIndex: 1
|
||||
}
|
||||
})
|
||||
}}>
|
||||
<Typography variant="h5" sx={{
|
||||
<TypographyComponent variant="h5" sx={{
|
||||
fontWeight: 800,
|
||||
color: '#ffffff',
|
||||
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
||||
color: terminalTheme ? terminalColors.text : '#ffffff',
|
||||
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.4)',
|
||||
mb: 0.5
|
||||
}}>
|
||||
{formatCurrency(current_usage.total_cost)}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="body2" sx={{
|
||||
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
fontSize: '0.75rem'
|
||||
}}>
|
||||
Total Cost
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
@@ -236,46 +331,71 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
<Box sx={{
|
||||
textAlign: 'center',
|
||||
p: 2.5,
|
||||
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(37, 99, 235, 0.08) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(59, 130, 246, 0.25)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 25px rgba(59, 130, 246, 0.2)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.4)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #3b82f6, #2563eb)',
|
||||
zIndex: 1
|
||||
}
|
||||
...(terminalTheme ? {
|
||||
backgroundColor: terminalColors.backgroundLight,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${terminalColors.border}`,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: `0 0 15px ${terminalColors.border}40`,
|
||||
borderColor: terminalColors.secondary
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: terminalColors.border,
|
||||
zIndex: 1
|
||||
}
|
||||
} : {
|
||||
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(37, 99, 235, 0.08) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(59, 130, 246, 0.25)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 25px rgba(59, 130, 246, 0.2)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.4)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #3b82f6, #2563eb)',
|
||||
zIndex: 1
|
||||
}
|
||||
})
|
||||
}}>
|
||||
<Typography variant="h5" sx={{
|
||||
<TypographyComponent variant="h5" sx={{
|
||||
fontWeight: 800,
|
||||
color: '#ffffff',
|
||||
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
||||
color: terminalTheme ? terminalColors.text : '#ffffff',
|
||||
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.4)',
|
||||
mb: 0.5
|
||||
}}>
|
||||
{formatNumber(current_usage.total_calls)}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="body2" sx={{
|
||||
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
fontSize: '0.75rem'
|
||||
}}>
|
||||
API Calls
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
@@ -302,46 +422,71 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
<Box sx={{
|
||||
textAlign: 'center',
|
||||
p: 2.5,
|
||||
background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.12) 0%, rgba(147, 51, 234, 0.08) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(168, 85, 247, 0.25)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 25px rgba(168, 85, 247, 0.2)',
|
||||
border: '1px solid rgba(168, 85, 247, 0.4)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #a855f7, #9333ea)',
|
||||
zIndex: 1
|
||||
}
|
||||
...(terminalTheme ? {
|
||||
backgroundColor: terminalColors.backgroundLight,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${terminalColors.border}`,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: `0 0 15px ${terminalColors.border}40`,
|
||||
borderColor: terminalColors.secondary
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: terminalColors.border,
|
||||
zIndex: 1
|
||||
}
|
||||
} : {
|
||||
background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.12) 0%, rgba(147, 51, 234, 0.08) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(168, 85, 247, 0.25)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 25px rgba(168, 85, 247, 0.2)',
|
||||
border: '1px solid rgba(168, 85, 247, 0.4)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #a855f7, #9333ea)',
|
||||
zIndex: 1
|
||||
}
|
||||
})
|
||||
}}>
|
||||
<Typography variant="h5" sx={{
|
||||
<TypographyComponent variant="h5" sx={{
|
||||
fontWeight: 800,
|
||||
color: '#ffffff',
|
||||
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
||||
color: terminalTheme ? terminalColors.text : '#ffffff',
|
||||
textShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.4)',
|
||||
mb: 0.5
|
||||
}}>
|
||||
{(current_usage.total_tokens / 1000).toFixed(1)}k
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="body2" sx={{
|
||||
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
fontSize: '0.75rem'
|
||||
}}>
|
||||
Tokens
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
@@ -371,59 +516,89 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
<Box sx={{
|
||||
textAlign: 'center',
|
||||
p: 2.5,
|
||||
background: systemHealth?.status === 'healthy'
|
||||
? 'linear-gradient(135deg, rgba(34, 197, 94, 0.12) 0%, rgba(22, 163, 74, 0.08) 100%)'
|
||||
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.12) 0%, rgba(220, 38, 38, 0.08) 100%)',
|
||||
borderRadius: 3,
|
||||
border: systemHealth?.status === 'healthy'
|
||||
? '1px solid rgba(34, 197, 94, 0.25)'
|
||||
: '1px solid rgba(239, 68, 68, 0.25)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: systemHealth?.status === 'healthy'
|
||||
? '0 8px 25px rgba(34, 197, 94, 0.2)'
|
||||
: '0 8px 25px rgba(239, 68, 68, 0.2)',
|
||||
border: systemHealth?.status === 'healthy'
|
||||
? '1px solid rgba(34, 197, 94, 0.4)'
|
||||
: '1px solid rgba(239, 68, 68, 0.4)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
...(terminalTheme ? {
|
||||
backgroundColor: terminalColors.backgroundLight,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error}`,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: `0 0 15px ${systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error}40`,
|
||||
borderColor: systemHealth?.status === 'healthy' ? terminalColors.secondary : terminalColors.error
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error,
|
||||
zIndex: 1
|
||||
}
|
||||
} : {
|
||||
background: systemHealth?.status === 'healthy'
|
||||
? 'linear-gradient(90deg, #22c55e, #16a34a)'
|
||||
: 'linear-gradient(90deg, #ef4444, #dc2626)',
|
||||
zIndex: 1
|
||||
}
|
||||
? 'linear-gradient(135deg, rgba(34, 197, 94, 0.12) 0%, rgba(22, 163, 74, 0.08) 100%)'
|
||||
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.12) 0%, rgba(220, 38, 38, 0.08) 100%)',
|
||||
borderRadius: 3,
|
||||
border: systemHealth?.status === 'healthy'
|
||||
? '1px solid rgba(34, 197, 94, 0.25)'
|
||||
: '1px solid rgba(239, 68, 68, 0.25)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'help',
|
||||
transition: 'all 0.3s ease',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: systemHealth?.status === 'healthy'
|
||||
? '0 8px 25px rgba(34, 197, 94, 0.2)'
|
||||
: '0 8px 25px rgba(239, 68, 68, 0.2)',
|
||||
border: systemHealth?.status === 'healthy'
|
||||
? '1px solid rgba(34, 197, 94, 0.4)'
|
||||
: '1px solid rgba(239, 68, 68, 0.4)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: systemHealth?.status === 'healthy'
|
||||
? 'linear-gradient(90deg, #22c55e, #16a34a)'
|
||||
: 'linear-gradient(90deg, #ef4444, #dc2626)',
|
||||
zIndex: 1
|
||||
}
|
||||
})
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, mb: 0.5 }}>
|
||||
<CheckCircle size={18} color={systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b'} />
|
||||
<Typography variant="body1" sx={{
|
||||
color: systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b',
|
||||
<CheckCircle size={18} color={terminalTheme
|
||||
? (systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error)
|
||||
: (systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b')
|
||||
} />
|
||||
<TypographyComponent variant="body1" sx={{
|
||||
color: terminalTheme
|
||||
? (systemHealth?.status === 'healthy' ? terminalColors.success : terminalColors.error)
|
||||
: (systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b'),
|
||||
fontWeight: 700,
|
||||
textTransform: 'capitalize',
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)'
|
||||
}}>
|
||||
{systemHealth?.status || 'Unknown'}
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
<TypographyComponent variant="body2" sx={{
|
||||
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.9)',
|
||||
fontWeight: 500,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px',
|
||||
fontSize: '0.75rem'
|
||||
}}>
|
||||
System Health
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
@@ -435,40 +610,46 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
<Box sx={{
|
||||
mb: 3,
|
||||
p: 2.5,
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
...(terminalTheme ? {
|
||||
backgroundColor: terminalColors.backgroundLight,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${terminalColors.border}`
|
||||
} : {
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
})
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{
|
||||
color: '#ffffff',
|
||||
<TypographyComponent variant="subtitle2" sx={{
|
||||
color: terminalTheme ? terminalColors.text : '#ffffff',
|
||||
fontWeight: 600,
|
||||
mb: 0.5
|
||||
}}>
|
||||
Monthly Budget Usage
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="caption" sx={{
|
||||
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
|
||||
display: 'block'
|
||||
}}>
|
||||
Track your AI spending against monthly limits
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
<Box sx={{ textAlign: 'right' }}>
|
||||
<Typography variant="h6" sx={{
|
||||
color: '#ffffff',
|
||||
<TypographyComponent variant="h6" sx={{
|
||||
color: terminalTheme ? terminalColors.text : '#ffffff',
|
||||
fontWeight: 'bold',
|
||||
textShadow: '0 2px 4px rgba(0,0,0,0.3)'
|
||||
textShadow: terminalTheme ? 'none' : '0 2px 4px rgba(0,0,0,0.3)'
|
||||
}}>
|
||||
{formatCurrency(current_usage.total_cost)}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="caption" sx={{
|
||||
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
|
||||
display: 'block'
|
||||
}}>
|
||||
of {formatCurrency(limits.limits.monthly_cost)}
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
@@ -477,21 +658,29 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
backgroundColor: terminalTheme ? terminalColors.backgroundLight : 'rgba(255,255,255,0.1)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
background: current_usage.total_cost / limits.limits.monthly_cost > 0.8
|
||||
? 'linear-gradient(90deg, #ff6b6b, #ff5252)'
|
||||
: current_usage.total_cost / limits.limits.monthly_cost > 0.6
|
||||
? 'linear-gradient(90deg, #ffa726, #ff9800)'
|
||||
: 'linear-gradient(90deg, #4ade80, #22c55e)',
|
||||
background: terminalTheme
|
||||
? (current_usage.total_cost / limits.limits.monthly_cost > 0.8
|
||||
? terminalColors.error
|
||||
: current_usage.total_cost / limits.limits.monthly_cost > 0.6
|
||||
? terminalColors.warning
|
||||
: terminalColors.success)
|
||||
: (current_usage.total_cost / limits.limits.monthly_cost > 0.8
|
||||
? 'linear-gradient(90deg, #ff6b6b, #ff5252)'
|
||||
: current_usage.total_cost / limits.limits.monthly_cost > 0.6
|
||||
? 'linear-gradient(90deg, #ffa726, #ff9800)'
|
||||
: 'linear-gradient(90deg, #4ade80, #22c55e)'),
|
||||
borderRadius: 4,
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.2)'
|
||||
boxShadow: terminalTheme ? 'none' : '0 2px 8px rgba(0,0,0,0.2)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
|
||||
<Typography variant="caption" sx={{
|
||||
color: current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? '#ff6b6b' : 'rgba(255,255,255,0.7)',
|
||||
<TypographyComponent variant="caption" sx={{
|
||||
color: terminalTheme
|
||||
? (current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? terminalColors.error : terminalColors.textSecondary)
|
||||
: (current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? '#ff6b6b' : 'rgba(255,255,255,0.7)'),
|
||||
fontWeight: current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? 600 : 400
|
||||
}}>
|
||||
{current_usage.total_cost / limits.limits.monthly_cost > 0.8
|
||||
@@ -500,13 +689,13 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
? '⚡ Moderate usage'
|
||||
: '✅ Within budget'
|
||||
}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
</TypographyComponent>
|
||||
<TypographyComponent variant="caption" sx={{
|
||||
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.7)',
|
||||
fontWeight: 500
|
||||
}}>
|
||||
{((current_usage.total_cost / limits.limits.monthly_cost) * 100).toFixed(1)}% used
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
@@ -516,38 +705,55 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
<Box sx={{
|
||||
mb: 3,
|
||||
p: 2.5,
|
||||
background: 'linear-gradient(135deg, rgba(255, 107, 107, 0.1) 0%, rgba(239, 68, 68, 0.05) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(255, 107, 107, 0.2)',
|
||||
position: 'relative',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #ff6b6b, #ef4444)',
|
||||
borderRadius: '3px 3px 0 0'
|
||||
}
|
||||
...(terminalTheme ? {
|
||||
backgroundColor: terminalColors.backgroundLight,
|
||||
borderRadius: 3,
|
||||
border: `1px solid ${terminalColors.error}`,
|
||||
position: 'relative',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: terminalColors.error,
|
||||
borderRadius: '3px 3px 0 0'
|
||||
}
|
||||
} : {
|
||||
background: 'linear-gradient(135deg, rgba(255, 107, 107, 0.1) 0%, rgba(239, 68, 68, 0.05) 100%)',
|
||||
borderRadius: 3,
|
||||
border: '1px solid rgba(255, 107, 107, 0.2)',
|
||||
position: 'relative',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: 'linear-gradient(90deg, #ff6b6b, #ef4444)',
|
||||
borderRadius: '3px 3px 0 0'
|
||||
}
|
||||
})
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<AlertTriangle size={18} color="#ff6b6b" />
|
||||
<Typography variant="subtitle2" sx={{
|
||||
<AlertTriangle size={18} color={terminalTheme ? terminalColors.error : "#ff6b6b"} />
|
||||
<TypographyComponent variant="subtitle2" sx={{
|
||||
fontWeight: 700,
|
||||
color: '#ff6b6b',
|
||||
textShadow: '0 1px 2px rgba(0,0,0,0.3)'
|
||||
color: terminalTheme ? terminalColors.error : '#ff6b6b',
|
||||
textShadow: terminalTheme ? 'none' : '0 1px 2px rgba(0,0,0,0.3)'
|
||||
}}>
|
||||
System Alerts ({alerts.length})
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{
|
||||
color: 'rgba(255,255,255,0.8)',
|
||||
<TypographyComponent variant="caption" sx={{
|
||||
color: terminalTheme ? terminalColors.textSecondary : 'rgba(255,255,255,0.8)',
|
||||
display: 'block',
|
||||
mb: 2
|
||||
}}>
|
||||
Important notifications requiring your attention
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{alerts.slice(0, 3).map((alert) => (
|
||||
<Tooltip
|
||||
@@ -565,42 +771,65 @@ const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userI
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Chip
|
||||
label={alert.title}
|
||||
size="small"
|
||||
icon={<AlertTriangle size={14} />}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 107, 107, 0.2)',
|
||||
color: '#ff6b6b',
|
||||
border: '1px solid rgba(255, 107, 107, 0.3)',
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 107, 107, 0.3)',
|
||||
transform: 'translateY(-1px)'
|
||||
},
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
/>
|
||||
{terminalTheme ? (
|
||||
<TerminalChipError
|
||||
label={alert.title}
|
||||
size="small"
|
||||
icon={<AlertTriangle size={14} />}
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
transform: 'translateY(-1px)'
|
||||
},
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Chip
|
||||
label={alert.title}
|
||||
size="small"
|
||||
icon={<AlertTriangle size={14} />}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 107, 107, 0.2)',
|
||||
color: '#ff6b6b',
|
||||
border: '1px solid rgba(255, 107, 107, 0.3)',
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 107, 107, 0.3)',
|
||||
transform: 'translateY(-1px)'
|
||||
},
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
))}
|
||||
{alerts.length > 3 && (
|
||||
<Chip
|
||||
label={`+${alerts.length - 3} more`}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
color: 'rgba(255,255,255,0.8)',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
fontWeight: 500
|
||||
}}
|
||||
/>
|
||||
terminalTheme ? (
|
||||
<TerminalChip
|
||||
label={`+${alerts.length - 3} more`}
|
||||
size="small"
|
||||
sx={{ fontWeight: 500 }}
|
||||
/>
|
||||
) : (
|
||||
<Chip
|
||||
label={`+${alerts.length - 3} more`}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255,255,255,0.1)',
|
||||
color: 'rgba(255,255,255,0.8)',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
fontWeight: 500
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContentComponent>
|
||||
</CardComponent>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { billingService } from '../../services/billingService';
|
||||
import { monitoringService } from '../../services/monitoringService';
|
||||
import { onApiEvent } from '../../utils/apiEvents';
|
||||
import { showToastNotification } from '../../utils/toastNotifications';
|
||||
|
||||
// Types
|
||||
import { DashboardData } from '../../types/billing';
|
||||
@@ -38,20 +39,31 @@ import UsageTrends from './UsageTrends';
|
||||
import UsageAlerts from './UsageAlerts';
|
||||
import ComprehensiveAPIBreakdown from './ComprehensiveAPIBreakdown';
|
||||
|
||||
// Terminal Theme
|
||||
import {
|
||||
TerminalTypography,
|
||||
TerminalAlert,
|
||||
terminalColors
|
||||
} from '../SchedulerDashboard/terminalTheme';
|
||||
|
||||
interface EnhancedBillingDashboardProps {
|
||||
userId?: string;
|
||||
terminalTheme?: boolean;
|
||||
}
|
||||
|
||||
type ViewMode = 'compact' | 'detailed';
|
||||
|
||||
const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ userId }) => {
|
||||
const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ userId, terminalTheme = false }) => {
|
||||
// Conditional component selection based on terminal theme
|
||||
const TypographyComponent = terminalTheme ? TerminalTypography : Typography;
|
||||
const AlertComponent = terminalTheme ? TerminalAlert : Alert;
|
||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
||||
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('compact');
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
const fetchDashboardData = async (showSuccessToast: boolean = false) => {
|
||||
try {
|
||||
const [billingData, healthData] = await Promise.all([
|
||||
billingService.getDashboardData(),
|
||||
@@ -59,8 +71,21 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
||||
]);
|
||||
setDashboardData(billingData);
|
||||
setSystemHealth(healthData);
|
||||
|
||||
// Show success toast only if explicitly requested (user-initiated refresh)
|
||||
if (showSuccessToast && billingData && healthData) {
|
||||
showToastNotification(
|
||||
'Billing data refreshed successfully',
|
||||
'success',
|
||||
{ duration: 3000 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to fetch dashboard data');
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch dashboard data';
|
||||
setError(errorMessage);
|
||||
|
||||
// Always show error toast for failures
|
||||
showToastNotification(errorMessage, 'error', { duration: 5000 });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -95,6 +120,29 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
||||
return () => document.removeEventListener('visibilitychange', onVisible);
|
||||
}, []);
|
||||
|
||||
// Listen for billing refresh requests (e.g., when subscription limits are exceeded)
|
||||
useEffect(() => {
|
||||
const handleBillingRefresh = () => {
|
||||
console.log('EnhancedBillingDashboard: Billing refresh requested, refreshing data...');
|
||||
// Use a fresh call to fetchDashboardData to ensure we get latest data
|
||||
Promise.all([billingService.getDashboardData(), monitoringService.getSystemHealth()])
|
||||
.then(([billingData, healthData]) => {
|
||||
setDashboardData(billingData);
|
||||
setSystemHealth(healthData);
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to refresh billing data';
|
||||
setError(errorMessage);
|
||||
console.error('Error refreshing billing data:', error);
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('billing-refresh-requested', handleBillingRefresh);
|
||||
return () => {
|
||||
window.removeEventListener('billing-refresh-requested', handleBillingRefresh);
|
||||
};
|
||||
}, []); // Empty deps - handler doesn't depend on component state
|
||||
|
||||
const handleViewModeChange = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
newViewMode: ViewMode | null,
|
||||
@@ -117,9 +165,9 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
||||
if (error) {
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ py: 4 }}>
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
<AlertComponent severity="error" sx={{ mb: 3 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
</AlertComponent>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -127,9 +175,9 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
||||
if (!dashboardData) {
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ py: 4 }}>
|
||||
<Alert severity="warning">
|
||||
<AlertComponent severity="warning">
|
||||
No billing data available. Please check your subscription status.
|
||||
</Alert>
|
||||
</AlertComponent>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -146,17 +194,17 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flex: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography
|
||||
<TypographyComponent
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
mb: 1.5,
|
||||
fontSize: '1.1rem',
|
||||
color: 'rgba(255,255,255,0.95)',
|
||||
color: terminalTheme ? terminalColors.text : 'rgba(255,255,255,0.95)',
|
||||
}}
|
||||
>
|
||||
Billing & Usage Dashboard
|
||||
</Typography>
|
||||
</TypographyComponent>
|
||||
<Tooltip
|
||||
title={
|
||||
<Box>
|
||||
@@ -231,7 +279,7 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
||||
<Tooltip title="Refresh billing data">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={fetchDashboardData}
|
||||
onClick={() => fetchDashboardData(true)}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
@@ -305,7 +353,7 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<CompactBillingDashboard userId={userId} />
|
||||
<CompactBillingDashboard userId={userId} terminalTheme={terminalTheme} />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
@@ -321,6 +369,7 @@ const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ use
|
||||
<BillingOverview
|
||||
usageStats={dashboardData.current_usage}
|
||||
onRefresh={fetchDashboardData}
|
||||
terminalTheme={terminalTheme}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
|
||||
467
frontend/src/components/billing/SubscriptionRenewalHistory.tsx
Normal file
467
frontend/src/components/billing/SubscriptionRenewalHistory.tsx
Normal file
@@ -0,0 +1,467 @@
|
||||
/**
|
||||
* Subscription Renewal History Component
|
||||
* Displays historical subscription renewals with details about plan changes, renewals, and usage.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TablePagination,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
Chip,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Refresh as RefreshIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
TrendingDown as TrendingDownIcon,
|
||||
Add as AddIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { RefreshCw } from 'lucide-react'; // Use lucide-react for header icon
|
||||
import { billingService } from '../../services/billingService';
|
||||
import { SubscriptionRenewal, RenewalHistoryResponse } from '../../types/billing';
|
||||
import {
|
||||
TerminalPaper,
|
||||
TerminalTypography,
|
||||
TerminalTableCell,
|
||||
TerminalTableRow,
|
||||
TerminalAlert,
|
||||
TerminalChip,
|
||||
TerminalChipSuccess,
|
||||
TerminalChipWarning,
|
||||
TerminalChipError,
|
||||
terminalColors
|
||||
} from '../SchedulerDashboard/terminalTheme';
|
||||
import { showToastNotification } from '../../utils/toastNotifications';
|
||||
|
||||
interface SubscriptionRenewalHistoryProps {
|
||||
userId?: string;
|
||||
terminalTheme?: boolean;
|
||||
initialLimit?: number;
|
||||
}
|
||||
|
||||
const SubscriptionRenewalHistory: React.FC<SubscriptionRenewalHistoryProps> = ({
|
||||
userId,
|
||||
terminalTheme = false,
|
||||
initialLimit = 20
|
||||
}) => {
|
||||
const [renewals, setRenewals] = useState<SubscriptionRenewal[]>([]);
|
||||
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 fetchRenewals = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response: RenewalHistoryResponse = await billingService.getRenewalHistory(
|
||||
userId,
|
||||
rowsPerPage,
|
||||
page * rowsPerPage
|
||||
);
|
||||
|
||||
setRenewals(response.renewals || []);
|
||||
setTotalCount(response.total_count || 0);
|
||||
|
||||
// Don't show success toast on automatic refresh - only show errors
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.message || 'Failed to fetch renewal history';
|
||||
setError(errorMessage);
|
||||
console.error('Error fetching renewal history:', err);
|
||||
|
||||
// Show error toast
|
||||
showToastNotification(errorMessage, 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRenewals();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, rowsPerPage, userId]);
|
||||
|
||||
const handleChangePage = (_event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string | null) => {
|
||||
if (!dateString) return 'N/A';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return 'Invalid Date';
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const getRenewalTypeIcon = (type: string): React.ReactElement | undefined => {
|
||||
switch (type) {
|
||||
case 'upgrade':
|
||||
return <TrendingUpIcon fontSize="small" sx={{ color: terminalTheme ? terminalColors.success : '#4ade80' }} />;
|
||||
case 'downgrade':
|
||||
return <TrendingDownIcon fontSize="small" sx={{ color: terminalTheme ? terminalColors.warning : '#f59e0b' }} />;
|
||||
case 'renewal':
|
||||
return <RefreshIcon fontSize="small" sx={{ color: terminalTheme ? terminalColors.primary : '#3b82f6' }} />;
|
||||
case 'new':
|
||||
return <AddIcon fontSize="small" sx={{ color: terminalTheme ? terminalColors.primary : '#3b82f6' }} />;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getRenewalTypeChip = (type: string) => {
|
||||
const ChipComponent = terminalTheme ? TerminalChip : Chip;
|
||||
const chipStyles = {
|
||||
upgrade: { backgroundColor: 'rgba(34, 197, 94, 0.2)', color: '#22c55e' },
|
||||
downgrade: { backgroundColor: 'rgba(245, 158, 11, 0.2)', color: '#f59e0b' },
|
||||
renewal: { backgroundColor: 'rgba(59, 130, 246, 0.2)', color: '#3b82f6' },
|
||||
new: { backgroundColor: 'rgba(59, 130, 246, 0.2)', color: '#3b82f6' },
|
||||
};
|
||||
|
||||
const chipStyle = chipStyles[type as keyof typeof chipStyles] || chipStyles.renewal;
|
||||
const label = type.charAt(0).toUpperCase() + type.slice(1);
|
||||
const icon = getRenewalTypeIcon(type);
|
||||
|
||||
if (terminalTheme) {
|
||||
const TerminalChipComponent = type === 'upgrade' ? TerminalChipSuccess :
|
||||
type === 'downgrade' ? TerminalChipWarning :
|
||||
TerminalChip;
|
||||
return (
|
||||
<TerminalChipComponent
|
||||
label={label}
|
||||
size="small"
|
||||
icon={icon}
|
||||
sx={{ fontWeight: 500 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={label}
|
||||
size="small"
|
||||
icon={icon}
|
||||
sx={{
|
||||
...chipStyle,
|
||||
fontWeight: 500,
|
||||
border: `1px solid ${chipStyle.color}40`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Conditional component selection based on terminal theme
|
||||
const PaperComponent = terminalTheme ? TerminalPaper : Box;
|
||||
|
||||
// Alert component wrapper for terminal theme
|
||||
const AlertWrapper: React.FC<{ children: React.ReactNode; severity?: 'error' | 'info' | 'warning'; sx?: any }> = ({ children, severity = 'info', sx }) => {
|
||||
if (terminalTheme) {
|
||||
return (
|
||||
<TerminalAlert severity={severity} sx={sx}>
|
||||
{children}
|
||||
</TerminalAlert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
backgroundColor: severity === 'error' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(59, 130, 246, 0.1)',
|
||||
borderRadius: 2,
|
||||
color: severity === 'error' ? '#ef4444' : '#3b82f6',
|
||||
...sx
|
||||
}}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PaperComponent sx={{ p: 3, mt: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{terminalTheme ? (
|
||||
<RefreshCw size={20} color={terminalColors.primary} />
|
||||
) : (
|
||||
<RefreshCw size={20} />
|
||||
)}
|
||||
{terminalTheme ? (
|
||||
<TerminalTypography variant="h6" sx={{ fontWeight: 'bold' }}>
|
||||
Subscription Renewal History
|
||||
</TerminalTypography>
|
||||
) : (
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
|
||||
Subscription Renewal History
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Tooltip title="Refresh Renewal History">
|
||||
<IconButton
|
||||
onClick={fetchRenewals}
|
||||
disabled={loading}
|
||||
sx={{ color: terminalTheme ? terminalColors.primary : 'inherit' }}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{loading && renewals.length === 0 ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
|
||||
<CircularProgress sx={{ color: terminalTheme ? terminalColors.primary : 'inherit' }} />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<AlertWrapper severity="error">
|
||||
{terminalTheme ? (
|
||||
<TerminalTypography>{error}</TerminalTypography>
|
||||
) : (
|
||||
<Typography>{error}</Typography>
|
||||
)}
|
||||
</AlertWrapper>
|
||||
) : renewals.length === 0 ? (
|
||||
<AlertWrapper severity="info">
|
||||
{terminalTheme ? (
|
||||
<TerminalTypography>No renewal history found. Your subscription renewals will appear here.</TerminalTypography>
|
||||
) : (
|
||||
<Typography>No renewal history found. Your subscription renewals will appear here.</Typography>
|
||||
)}
|
||||
</AlertWrapper>
|
||||
) : (
|
||||
<>
|
||||
<TableContainer component={Box} sx={{ maxHeight: 600, overflow: 'auto' }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TerminalTableCell sx={{ width: '8%' }}>#</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ width: '12%' }}>Type</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ width: '15%' }}>Plan</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ width: '12%' }}>Previous Plan</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ width: '10%' }}>Billing Cycle</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ width: '12%' }}>Period Start</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ width: '12%' }}>Period End</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ width: '10%' }}>Amount</TerminalTableCell>
|
||||
<TerminalTableCell sx={{ width: '9%' }}>Status</TerminalTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{renewals.map((renewal) => (
|
||||
<TerminalTableRow key={renewal.id}>
|
||||
<TerminalTableCell>
|
||||
{terminalTheme ? (
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
#{renewal.renewal_count}
|
||||
</TerminalTypography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
#{renewal.renewal_count}
|
||||
</Typography>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
{getRenewalTypeChip(renewal.renewal_type)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
{terminalTheme ? (
|
||||
<>
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 600 }}>
|
||||
{renewal.plan_name}
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="caption" sx={{ fontSize: '0.7rem', display: 'block', opacity: 0.7 }}>
|
||||
{renewal.plan_tier}
|
||||
</TerminalTypography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 600 }}>
|
||||
{renewal.plan_name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', display: 'block', opacity: 0.7 }}>
|
||||
{renewal.plan_tier}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
{renewal.previous_plan_name ? (
|
||||
terminalTheme ? (
|
||||
<>
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
{renewal.previous_plan_name}
|
||||
</TerminalTypography>
|
||||
<TerminalTypography variant="caption" sx={{ fontSize: '0.7rem', display: 'block', opacity: 0.7 }}>
|
||||
{renewal.previous_plan_tier}
|
||||
</TerminalTypography>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
{renewal.previous_plan_name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.7rem', display: 'block', opacity: 0.7 }}>
|
||||
{renewal.previous_plan_tier}
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
terminalTheme ? (
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem', fontStyle: 'italic', opacity: 0.5 }}>
|
||||
N/A
|
||||
</TerminalTypography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', fontStyle: 'italic', opacity: 0.5 }}>
|
||||
N/A
|
||||
</Typography>
|
||||
)
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
{terminalTheme ? (
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem', textTransform: 'capitalize' }}>
|
||||
{renewal.billing_cycle}
|
||||
</TerminalTypography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', textTransform: 'capitalize' }}>
|
||||
{renewal.billing_cycle}
|
||||
</Typography>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
{terminalTheme ? (
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
{formatDate(renewal.new_period_start)}
|
||||
</TerminalTypography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
{formatDate(renewal.new_period_start)}
|
||||
</Typography>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
{terminalTheme ? (
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
{formatDate(renewal.new_period_end)}
|
||||
</TerminalTypography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
|
||||
{formatDate(renewal.new_period_end)}
|
||||
</Typography>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
{terminalTheme ? (
|
||||
<TerminalTypography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 600 }}>
|
||||
{formatCurrency(renewal.payment_amount)}
|
||||
</TerminalTypography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 600 }}>
|
||||
{formatCurrency(renewal.payment_amount)}
|
||||
</Typography>
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
<TerminalTableCell>
|
||||
{renewal.payment_status === 'paid' ? (
|
||||
terminalTheme ? (
|
||||
<TerminalChipSuccess label="Paid" size="small" />
|
||||
) : (
|
||||
<Chip
|
||||
label="Paid"
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.2)',
|
||||
color: '#22c55e',
|
||||
fontWeight: 500
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : renewal.payment_status === 'pending' ? (
|
||||
terminalTheme ? (
|
||||
<TerminalChipWarning label="Pending" size="small" />
|
||||
) : (
|
||||
<Chip
|
||||
label="Pending"
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.2)',
|
||||
color: '#f59e0b',
|
||||
fontWeight: 500
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
terminalTheme ? (
|
||||
<TerminalChipError label="Failed" size="small" />
|
||||
) : (
|
||||
<Chip
|
||||
label="Failed"
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.2)',
|
||||
color: '#ef4444',
|
||||
fontWeight: 500
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</TerminalTableCell>
|
||||
</TerminalTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={totalCount}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
rowsPerPageOptions={[10, 20, 50, 100]}
|
||||
sx={{
|
||||
color: terminalTheme ? terminalColors.text : 'inherit',
|
||||
'& .MuiTablePagination-selectLabel, & .MuiTablePagination-displayedRows': {
|
||||
color: terminalTheme ? terminalColors.text : 'inherit',
|
||||
fontFamily: terminalTheme ? 'monospace' : 'inherit'
|
||||
},
|
||||
'& .MuiIconButton-root': {
|
||||
color: terminalTheme ? terminalColors.primary : 'inherit'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</PaperComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionRenewalHistory;
|
||||
|
||||
426
frontend/src/components/billing/UsageLogsTable.tsx
Normal file
426
frontend/src/components/billing/UsageLogsTable.tsx
Normal file
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* 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' }}>
|
||||
{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;
|
||||
|
||||
Reference in New Issue
Block a user