Fix vertical alignment of Lucide icons inside Button children across all pages by adding verticalAlign offsets (-3px for 16px icons, -2px for 14px icons). The design system Button wraps children in an inline span, so SVG icons defaulted to baseline alignment. Hide the redundant top-right "Create Tenant" button on VendorTenantsPage when no tenants exist — the EmptyState already provides that action. Add icons to all vendor sidebar sub-items for consistency (previously only Email Connector had one). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
171 lines
6.1 KiB
TypeScript
171 lines
6.1 KiB
TypeScript
import { useNavigate } from 'react-router';
|
|
import {
|
|
Alert,
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
KpiStrip,
|
|
Spinner,
|
|
useToast,
|
|
} from '@cameleer/design-system';
|
|
import { ArrowUpCircle, ExternalLink, Key, RefreshCw, Settings } from 'lucide-react';
|
|
import { useTenantDashboard, useRestartServer, useUpgradeServer } from '../../api/tenant-hooks';
|
|
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
|
|
import { UsageIndicator } from '../../components/UsageIndicator';
|
|
import { tierColor } from '../../utils/tier';
|
|
import styles from '../../styles/platform.module.css';
|
|
|
|
function statusColor(status: string): 'success' | 'error' | 'warning' | 'auto' {
|
|
switch (status?.toUpperCase()) {
|
|
case 'ACTIVE': return 'success';
|
|
case 'SUSPENDED': return 'warning';
|
|
case 'PROVISIONING': return 'auto';
|
|
case 'ERROR': return 'error';
|
|
default: return 'auto';
|
|
}
|
|
}
|
|
|
|
export function TenantDashboardPage() {
|
|
const navigate = useNavigate();
|
|
const { toast } = useToast();
|
|
const { data, isLoading, isError } = useTenantDashboard();
|
|
const restartServer = useRestartServer();
|
|
const upgradeServer = useUpgradeServer();
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 64 }}>
|
|
<Spinner />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isError || !data) {
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<Alert variant="error" title="Failed to load dashboard">
|
|
Could not fetch dashboard data. Please refresh.
|
|
</Alert>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const serverDown = !data.serverHealthy;
|
|
const daysRemaining = data.licenseDaysRemaining ?? 0;
|
|
|
|
const agentLimit = data.limits?.['agents'] ?? -1;
|
|
const envLimit = data.limits?.['environments'] ?? -1;
|
|
const agentUsed = data.agentCount ?? 0;
|
|
const envUsed = data.environmentCount ?? 0;
|
|
|
|
return (
|
|
<div style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20 }}>
|
|
{/* Header */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<h1 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 600 }}>{data.name}</h1>
|
|
<Badge label={data.tier} color={tierColor(data.tier)} />
|
|
<Badge label={data.status} color={statusColor(data.status)} />
|
|
</div>
|
|
|
|
{/* Server-down alert */}
|
|
{serverDown && (
|
|
<Alert variant="error" title="Server is not responding">
|
|
Your Cameleer server is currently unreachable. Agent data collection is paused.
|
|
</Alert>
|
|
)}
|
|
|
|
{/* KPI strip */}
|
|
<KpiStrip
|
|
items={[
|
|
{ label: 'Tier', value: data.tier },
|
|
{ label: 'Status', value: data.status },
|
|
{ label: 'Server', value: data.serverHealthy ? 'Healthy' : 'Down' },
|
|
{ label: 'License', value: daysRemaining > 0 ? `${daysRemaining}d remaining` : 'Expired' },
|
|
]}
|
|
/>
|
|
|
|
{/* Cards */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 }}>
|
|
{/* Server card */}
|
|
<Card title="Server">
|
|
<div className={styles.dividerList}>
|
|
<div className={styles.kvRow}>
|
|
<span className={styles.kvLabel}>State</span>
|
|
<ServerStatusBadge state={data.serverStatus} />
|
|
</div>
|
|
{data.serverEndpoint && (
|
|
<div className={styles.kvRow}>
|
|
<span className={styles.kvLabel}>Endpoint</span>
|
|
<span className={styles.kvValueMono}>{data.serverEndpoint}</span>
|
|
</div>
|
|
)}
|
|
<div style={{ paddingTop: 8, display: 'flex', gap: 8 }}>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={async () => {
|
|
try {
|
|
await restartServer.mutateAsync();
|
|
toast({ title: 'Server restarted', variant: 'success' });
|
|
} catch (err) {
|
|
toast({ title: 'Restart failed', description: String(err), variant: 'error' });
|
|
}
|
|
}}
|
|
loading={restartServer.isPending}
|
|
>
|
|
<RefreshCw size={14} style={{ marginRight: 6, verticalAlign: -2 }} />
|
|
Restart
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
onClick={async () => {
|
|
try {
|
|
await upgradeServer.mutateAsync();
|
|
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
|
|
} catch (err) {
|
|
toast({ title: 'Upgrade failed', description: String(err), variant: 'error' });
|
|
}
|
|
}}
|
|
loading={upgradeServer.isPending}
|
|
>
|
|
<ArrowUpCircle size={14} style={{ marginRight: 6, verticalAlign: -2 }} />
|
|
Upgrade
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Usage card */}
|
|
<Card title="Usage">
|
|
<div className={styles.dividerList}>
|
|
<UsageIndicator used={agentUsed} limit={agentLimit} label="Agents" />
|
|
<UsageIndicator used={envUsed} limit={envLimit} label="Environments" />
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Quick links card */}
|
|
<Card title="Quick Links">
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{data.serverEndpoint && (
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => window.open(`/t/${data.slug}/`, '_blank')}
|
|
>
|
|
<ExternalLink size={14} style={{ marginRight: 6, verticalAlign: -2 }} />
|
|
Open Server Dashboard
|
|
</Button>
|
|
)}
|
|
<Button variant="secondary" onClick={() => navigate('../license')}>
|
|
<Key size={14} style={{ marginRight: 6, verticalAlign: -2 }} />
|
|
View License
|
|
</Button>
|
|
<Button variant="secondary" onClick={() => navigate('../oidc')}>
|
|
<Settings size={14} style={{ marginRight: 6, verticalAlign: -2 }} />
|
|
Configure OIDC
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|