feat(I): I-ONB/I-UI/I-DASH frontend — SetupWizard, WorkstreamBlockRenderer, Dashboard platform section, API hooks, i18n, tsc clean, 11/11 dashboard tests
This commit is contained in:
@@ -1,10 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Bot,
|
||||
Workflow,
|
||||
Search,
|
||||
BookOpen,
|
||||
DollarSign,
|
||||
HeartPulse,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { StatCard } from '@/components/shared/StatCard';
|
||||
import { ActivityFeed, ActivityItem } from '@/components/shared/ActivityFeed';
|
||||
import { DashboardGrid } from '@/components/dashboard/DashboardGrid';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { useUnifiedContacts, useAuditLog } from '@/api/hooks';
|
||||
import { useDashboardWidgets } from '@/api/dashboard';
|
||||
import {
|
||||
usePlatformDashboard,
|
||||
useCostDashboard,
|
||||
useUsageAnalytics,
|
||||
} from '@/api/platform';
|
||||
import { formatDateTime } from '@/utils/date';
|
||||
|
||||
export function DashboardPage() {
|
||||
@@ -14,6 +30,22 @@ export function DashboardPage() {
|
||||
const { data: auditData, isError: auditError } = useAuditLog(1, 5);
|
||||
const { data: widgetsData, isError: widgetsError } = useDashboardWidgets();
|
||||
|
||||
const {
|
||||
data: platformData,
|
||||
isLoading: platformLoading,
|
||||
isError: platformError,
|
||||
} = usePlatformDashboard();
|
||||
const {
|
||||
data: costData,
|
||||
isLoading: costLoading,
|
||||
isError: costError,
|
||||
} = useCostDashboard(30);
|
||||
const {
|
||||
data: usageData,
|
||||
isLoading: usageLoading,
|
||||
isError: usageError,
|
||||
} = useUsageAnalytics(30);
|
||||
|
||||
const totalCompanies = companiesData?.total ?? 0;
|
||||
const totalContacts = contactsData?.total ?? 0;
|
||||
|
||||
@@ -46,6 +78,20 @@ export function DashboardPage() {
|
||||
|
||||
const widgets = widgetsData?.items ?? [];
|
||||
|
||||
const agents = platformData?.agents;
|
||||
const workflows = platformData?.workflows;
|
||||
const search = platformData?.search;
|
||||
const knowledge = platformData?.knowledge;
|
||||
const systemHealth = platformData?.system_health;
|
||||
const searchQueries = usageData?.search_queries;
|
||||
|
||||
const costUsd = costData?.total_cost_usd ?? 0;
|
||||
const budget = costData?.budget;
|
||||
const budgetUtilization = budget?.utilization_pct ?? 0;
|
||||
|
||||
const healthLabel = systemHealth?.status ?? 'unknown';
|
||||
const healthVariant = healthStatusVariant(healthLabel);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto" data-testid="dashboard-page">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-6">{t('dashboard.title')}</h1>
|
||||
@@ -73,6 +119,155 @@ export function DashboardPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Platform Dashboard — Phase I.7 (I-DASH) */}
|
||||
<section className="mb-8" aria-label={t('dashboard.platform.title')}>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mb-4">
|
||||
{t('dashboard.platform.title')}
|
||||
</h2>
|
||||
|
||||
{platformLoading || costLoading || usageLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="animate-spin h-6 w-6 text-primary-500" aria-hidden="true" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* Agent status */}
|
||||
<Card title={t('dashboard.platform.agents')}>
|
||||
{agents?.error || platformError ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<Bot className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.activeAgents')}
|
||||
value={agents?.active_agents ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Bot className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.totalRuns')}
|
||||
value={agents?.total_runs ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Bot className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.recentRuns7d')}
|
||||
value={agents?.recent_runs_7d ?? 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Workflow stats */}
|
||||
<Card title={t('dashboard.platform.workflows')}>
|
||||
{workflows?.error || platformError ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<Workflow className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.activeWorkflows')}
|
||||
value={workflows?.active_workflows ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Workflow className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.runningInstances')}
|
||||
value={workflows?.running_instances ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Workflow className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.completedInstances')}
|
||||
value={workflows?.completed_instances ?? 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Search metrics */}
|
||||
<Card title={t('dashboard.platform.search')}>
|
||||
{usageError || searchQueries?.error ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<Search className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.totalQueries')}
|
||||
value={searchQueries?.total ?? search?.total_queries ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Search className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.avgLatency')}
|
||||
value={
|
||||
typeof search?.avg_latency_ms === 'number'
|
||||
? `${search.avg_latency_ms} ms`
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Knowledge coverage */}
|
||||
<Card title={t('dashboard.platform.knowledge')}>
|
||||
{platformError || knowledge?.error ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<BookOpen className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.wikiArticles')}
|
||||
value={knowledge?.wiki_articles ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<BookOpen className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.coverage')}
|
||||
value={
|
||||
typeof knowledge?.coverage_pct === 'number'
|
||||
? `${knowledge.coverage_pct}%`
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Cost tracking */}
|
||||
<Card title={t('dashboard.platform.cost')}>
|
||||
{costError || costData?.error ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<DollarSign className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.totalCost')}
|
||||
value={`$${costUsd.toFixed(2)}`}
|
||||
/>
|
||||
{budget?.monthly_limit_usd ? (
|
||||
<MetricRow
|
||||
icon={<DollarSign className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.budgetUtilization')}
|
||||
value={`${budgetUtilization.toFixed(1)}%`}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* System health */}
|
||||
<Card title={t('dashboard.platform.systemHealth')}>
|
||||
{platformError ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeartPulse className="h-4 w-4 text-primary-500" aria-hidden="true" />
|
||||
<Badge variant={healthVariant} dot>
|
||||
{t(`dashboard.platform.health.${healthLabel}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Dynamic Plugin Widgets */}
|
||||
{widgetsError ? (
|
||||
<p className="text-sm text-secondary-500 mb-6" data-testid="dashboard-widgets-unavailable">
|
||||
@@ -95,3 +290,32 @@ export function DashboardPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricRow({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-sm text-secondary-600">
|
||||
<span className="text-primary-500" aria-hidden="true">{icon}</span>
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-secondary-900">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function healthStatusVariant(status: string): 'success' | 'warning' | 'danger' | 'secondary' {
|
||||
if (status === 'healthy') return 'success';
|
||||
if (status === 'degraded') return 'warning';
|
||||
if (status === 'down') return 'danger';
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
|
||||
Reference in New Issue
Block a user