Files
cameleer-saas/ui/src/pages/tenant/TenantDashboardPage.tsx
hsiegeln dee1f39554
All checks were successful
CI / build (push) Successful in 2m8s
CI / docker (push) Successful in 1m41s
fix: align button icons and polish vendor sidebar
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>
2026-04-25 21:30:37 +02:00

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>
);
}