Files
OPC/clarity.controlplane/src/pages/DashboardPage.tsx
T
2026-04-26 11:32:23 -04:00

157 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import { AnchorButton, Button, Callout, Intent, NonIdealState, Spinner, Tab, Tabs, Tag } from '@blueprintjs/core';
import DeployWizard from '../components/wizard/DeployWizard';
import { tenantUrl, CLARITY_DOMAIN } from '../config';
import { getTenants, subscribeToTenantLogs } from '../api/tenantApi';
import type { TenantRecord } from '../types/provisioning';
const ENV_INTENT: Record<string, Intent> = {
prod: Intent.DANGER,
uat: Intent.WARNING,
fdev: Intent.PRIMARY,
};
function TenantCard({ t }: { t: TenantRecord }) {
const [logs, setLogs] = useState<string[]>([]);
const [logsOpen, setLogsOpen] = useState(false);
const logRef = useRef<HTMLDivElement>(null);
const sourceRef = useRef<EventSource | null>(null);
useEffect(() => {
if (!logsOpen) return;
const src = subscribeToTenantLogs(
t.subdomain,
(line) => setLogs((prev) => [...prev.slice(-500), line]), // cap at 500 lines
() => {}
);
sourceRef.current = src;
return () => { src.close(); sourceRef.current = null; };
}, [logsOpen, t.subdomain]);
// Auto-scroll to bottom when new lines arrive
useEffect(() => {
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
}, [logs]);
return (
<div className="job-card">
<div className="job-card-header">
<div>
<strong>{t.clientName}</strong>
<span className="job-card-subdomain">{t.subdomain}</span>
</div>
<div style={{ display: 'flex', gap: '0.4rem', alignItems: 'center' }}>
<Tag intent={ENV_INTENT[t.environment] ?? Intent.NONE} round minimal>{t.environment}</Tag>
<Tag intent={t.status === 'Provisioned' ? Intent.SUCCESS : Intent.WARNING} round>{t.status}</Tag>
</div>
</div>
<Tabs onChange={(id) => setLogsOpen(id === 'logs')}>
<Tab id="info" title="Info" panel={
<div className="job-card-meta" style={{ paddingTop: '0.5rem' }}>
<span>Site: <code>{t.siteCode}</code></span>
<span>Container: <code>{t.containerName ?? '—'}{t.containerPort ? `:${t.containerPort}` : ''}</code></span>
<span>{new Date(t.provisionedAt).toLocaleString()}</span>
{t.status === 'Provisioned' && (
<AnchorButton
icon="share"
minimal
small
href={tenantUrl(t.subdomain)}
target="_blank"
rel="noopener noreferrer"
style={{ marginTop: '0.25rem' }}
>
{t.subdomain}.{CLARITY_DOMAIN}
</AnchorButton>
)}
</div>
} />
<Tab id="logs" title="Server" panel={
<div
ref={logRef}
style={{
fontFamily: 'monospace',
fontSize: '0.75rem',
background: '#1a1a1a',
color: '#d4d4d4',
padding: '0.5rem',
borderRadius: '4px',
height: '260px',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
marginTop: '0.5rem',
}}
>
{logs.length === 0
? <span style={{ color: '#666' }}>Connecting to container logs</span>
: logs.map((l, i) => <div key={i}>{l}</div>)
}
</div>
} />
</Tabs>
</div>
);
}
export default function DashboardPage() {
const [wizardOpen, setWizardOpen] = useState(false);
const [tenants, setTenants] = useState<TenantRecord[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = () => {
getTenants()
.then((data) => { setTenants(data); setError(null); })
.catch((e: Error) => setError(e.message))
.finally(() => setLoading(false));
};
useEffect(() => { load(); }, []);
const handleWizardClose = () => { setWizardOpen(false); setLoading(true); load(); };
if (wizardOpen) return <DeployWizard onClose={handleWizardClose} />;
return (
<>
<div className="page-header">
<div>
<h1>Provisioned Tenants</h1>
<p>Manage and monitor client deployments.</p>
</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<Button icon="refresh" minimal onClick={load} loading={loading} title="Refresh" />
<Button intent={Intent.PRIMARY} text="Deploy New Client" icon="plus" large onClick={() => setWizardOpen(true)} />
</div>
</div>
{loading && <NonIdealState icon={<Spinner />} title="Loading tenants..." />}
{error && (
<Callout intent={Intent.DANGER} title="Failed to load tenants" style={{ marginBottom: '1rem' }}>
{error}
</Callout>
)}
{!loading && !error && tenants.length === 0 && (
<div className="empty-state">
<div className="empty-state-icon">🏗</div>
<h3>No tenants provisioned yet</h3>
<p>Deploy your first client to get started.</p>
<Button intent={Intent.PRIMARY} text="Deploy New Client" icon="plus" onClick={() => setWizardOpen(true)} />
</div>
)}
{!loading && tenants.length > 0 && (
<div className="job-list">
{tenants.map((t) => (
<TenantCard key={t.subdomain} t={t} />
))}
</div>
)}
</>
);
}