diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx index 7ef070661..7e47904f7 100644 --- a/apps/frontend/src/renderer/App.tsx +++ b/apps/frontend/src/renderer/App.tsx @@ -76,6 +76,7 @@ import { GitHubIssuesPilotView } from './components/GitHubIssuesPilotView'; import { PatternsPilotView } from './components/PatternsPilotView'; import { AnalyticsPilotView } from './components/AnalyticsPilotView'; import { ProductivityPilotView } from './components/ProductivityPilotView'; +import { MergeAnalyticsPilotView } from './components/MergeAnalyticsPilotView'; import { useSettingsStore, loadSettings, loadProfiles, saveSettings } from './stores/settings-store'; import { useClaudeProfileStore } from './stores/claude-profile-store'; import { useTerminalStore, restoreTerminalSessions } from './stores/terminal-store'; @@ -1153,6 +1154,9 @@ export function App() { {activeView === 'merge-analytics' && (activeProjectId || selectedProjectId) && ( )} + {activeView === 'merge-analytics-next' && (activeProjectId || selectedProjectId) && ( + + )} {activeView === 'feedback' && (activeProjectId || selectedProjectId) && ( )} diff --git a/apps/frontend/src/renderer/__tests__/merge-analytics-ui.test.ts b/apps/frontend/src/renderer/__tests__/merge-analytics-ui.test.ts new file mode 100644 index 000000000..b4aa3adb5 --- /dev/null +++ b/apps/frontend/src/renderer/__tests__/merge-analytics-ui.test.ts @@ -0,0 +1,125 @@ +/** + * Tests for the MergeAnalytics / ConflictPattern → AnalyticsDashboard mappers. + */ + +import { describe, expect, it } from 'vitest'; +import { + buildMergeBarLists, + buildMergeKpis, + buildMergeSections, + formatDuration, + shortenPath, +} from '../lib/merge-analytics-ui'; +import type { ConflictPattern, MergeAnalytics } from '../../shared/types'; + +function makeAnalytics(overrides: Partial = {}): MergeAnalytics { + return { + total_operations: 84, + total_files_merged: 312, + total_conflicts: 12, + successful_operations: 80, + failed_operations: 4, + total_ai_calls: 47, + total_tokens_used: 1_240_000, + average_duration_seconds: 125, + success_rate: 0.95, + auto_merge_rate: 0.72, + conflict_patterns: [], + ...overrides, + }; +} + +function makePattern(overrides: Partial = {}): ConflictPattern { + return { + file_path: 'apps/frontend/src/renderer/App.tsx', + location: 'L120', + occurrence_count: 6, + severity: 'high', + tasks_involved: ['001', '002'], + last_seen: '2026-05-22T00:00:00Z', + ...overrides, + }; +} + +describe('shortenPath', () => { + it('keeps short paths and truncates deep ones to the trailing segments', () => { + expect(shortenPath('App.tsx')).toBe('App.tsx'); + expect(shortenPath('a/b')).toBe('a/b'); + expect(shortenPath('apps/frontend/src/App.tsx')).toBe('…/src/App.tsx'); + }); +}); + +describe('formatDuration', () => { + it('renders seconds and minute+second forms', () => { + expect(formatDuration(45)).toBe('45s'); + expect(formatDuration(125)).toBe('2m 05s'); + expect(formatDuration(-1)).toBe('0s'); + }); +}); + +describe('buildMergeKpis', () => { + const labels = { + operations: 'ops', + operationsSub: 'all', + successRate: 'success', + autoMerge: 'auto', + conflicts: 'conflicts', + conflictsSub: 'total', + }; + + it('maps the analytics onto four tiles, converting 0-1 rates to percents', () => { + const kpis = buildMergeKpis(makeAnalytics(), labels); + expect(kpis.map((k) => k.value)).toEqual(['84', '95%', '72%', '12']); + expect(kpis[1].tone).toBe('good'); + expect(kpis[3].tone).toBe('warn'); + }); + + it('warns a low success rate and drops the conflict tone at zero', () => { + const kpis = buildMergeKpis( + makeAnalytics({ success_rate: 0.6, total_conflicts: 0 }), + labels, + ); + expect(kpis[1].tone).toBe('warn'); + expect(kpis[3].tone).toBeUndefined(); + }); +}); + +describe('buildMergeBarLists', () => { + it('maps conflict patterns to a distribution with severity-toned bars', () => { + const lists = buildMergeBarLists( + [ + makePattern({ occurrence_count: 6, severity: 'high' }), + makePattern({ file_path: 'x/y/z/util.ts', occurrence_count: 3, severity: 'medium' }), + ], + 'Top conflicts', + 'aria', + ); + expect(lists).toHaveLength(1); + expect(lists[0].items).toEqual([ + { label: '…/renderer/App.tsx', value: 6, tone: 'bad' }, + { label: '…/z/util.ts', value: 3, tone: 'warn' }, + ]); + }); + + it('returns no card when there are no patterns', () => { + expect(buildMergeBarLists([], 't', 'a')).toEqual([]); + }); +}); + +describe('buildMergeSections', () => { + it('builds outcome + efficiency cards', () => { + const sections = buildMergeSections(makeAnalytics(), { + outcomesTitle: 'Outcomes', + successful: 'Successful', + failed: 'Failed', + filesMerged: 'Files merged', + efficiencyTitle: 'Efficiency', + avgDuration: 'Avg duration', + aiCalls: 'AI calls', + tokens: 'Tokens', + }); + expect(sections[0].rows[0]).toEqual({ label: 'Successful', value: '80' }); + expect(sections[1].rows[0]).toEqual({ label: 'Avg duration', value: '2m 05s' }); + expect(sections[1].rows[2]).toEqual({ label: 'Tokens', value: '1.2M' }); + }); +}); diff --git a/apps/frontend/src/renderer/components/MergeAnalyticsPilotView.tsx b/apps/frontend/src/renderer/components/MergeAnalyticsPilotView.tsx new file mode 100644 index 000000000..0cdfee553 --- /dev/null +++ b/apps/frontend/src/renderer/components/MergeAnalyticsPilotView.tsx @@ -0,0 +1,148 @@ +/** + * Merge Analytics pilot on the shared design system (U5 B2). + * + * Renders `libs/ui`'s AnalyticsDashboard from the existing `getMergeSummary` + * + `getConflictPatterns` IPC, mapped with the pure `merge-analytics-ui` + * helpers. The conflict patterns render as a BarList distribution. Read-only + * — the legacy Merge Analytics view keeps its export flow. Reachable via the + * "Merge Analytics (new UI)" sidebar item. + */ + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AnalyticsDashboard } from '@auto-code/ui'; +import type { UiBarListCard, UiKpi, UiMetaSection } from '@auto-code/ui'; +import type { ConflictPattern, MergeAnalytics } from '../../shared/types'; +import { + buildMergeBarLists, + buildMergeKpis, + buildMergeSections, +} from '../lib/merge-analytics-ui'; + +interface MergeAnalyticsPilotViewProps { + projectId: string; +} + +interface PilotState { + analytics: MergeAnalytics | null; + patterns: ConflictPattern[]; + loading: boolean; + error: Error | null; +} + +export function MergeAnalyticsPilotView({ + projectId, +}: Readonly) { + const { t } = useTranslation(['analytics']); + const [state, setState] = useState({ + analytics: null, + patterns: [], + loading: true, + error: null, + }); + const [reloadKey, setReloadKey] = useState(0); + + useEffect(() => { + let active = true; + setState({ analytics: null, patterns: [], loading: true, error: null }); + Promise.all([ + window.electronAPI.getMergeSummary(projectId), + window.electronAPI.getConflictPatterns(projectId, 10), + ]) + .then(([summaryResult, patternsResult]) => { + if (!active) return; + if (!summaryResult.success || summaryResult.data == null) { + console.error( + '[MergeAnalyticsPilotView] Failed to load summary:', + summaryResult.success ? 'no data' : summaryResult.error, + ); + setState({ + analytics: null, + patterns: [], + loading: false, + error: new Error(t('analytics:mergePilot.error')), + }); + return; + } + setState({ + analytics: summaryResult.data, + patterns: patternsResult.success ? (patternsResult.data ?? []) : [], + loading: false, + error: null, + }); + }) + .catch((err: unknown) => { + if (!active) return; + console.error('[MergeAnalyticsPilotView] Failed to load merge analytics:', err); + setState({ + analytics: null, + patterns: [], + loading: false, + error: new Error(t('analytics:mergePilot.error')), + }); + }); + return () => { + active = false; + }; + }, [projectId, reloadKey, t]); + + const reload = useCallback(() => setReloadKey((key) => key + 1), []); + + const kpis = useMemo(() => { + if (state.analytics == null) return null; + return buildMergeKpis(state.analytics, { + operations: t('analytics:mergePilot.kpis.operations'), + operationsSub: t('analytics:mergePilot.kpis.operationsSub'), + successRate: t('analytics:mergePilot.kpis.successRate'), + autoMerge: t('analytics:mergePilot.kpis.autoMerge'), + conflicts: t('analytics:mergePilot.kpis.conflicts'), + conflictsSub: t('analytics:mergePilot.kpis.conflictsSub'), + }); + }, [state.analytics, t]); + + const barLists = useMemo(() => { + if (state.analytics == null) return undefined; + return buildMergeBarLists( + state.patterns, + t('analytics:mergePilot.conflicts.title'), + t('analytics:mergePilot.conflicts.aria'), + ); + }, [state.analytics, state.patterns, t]); + + const sections = useMemo(() => { + if (state.analytics == null) return undefined; + return buildMergeSections(state.analytics, { + outcomesTitle: t('analytics:mergePilot.sections.outcomesTitle'), + successful: t('analytics:mergePilot.sections.successful'), + failed: t('analytics:mergePilot.sections.failed'), + filesMerged: t('analytics:mergePilot.sections.filesMerged'), + efficiencyTitle: t('analytics:mergePilot.sections.efficiencyTitle'), + avgDuration: t('analytics:mergePilot.sections.avgDuration'), + aiCalls: t('analytics:mergePilot.sections.aiCalls'), + tokens: t('analytics:mergePilot.sections.tokens'), + }); + }, [state.analytics, t]); + + const stateLabels = useMemo( + () => ({ + loading: t('analytics:mergePilot.states.loading'), + retry: t('analytics:mergePilot.states.retry'), + empty: t('analytics:mergePilot.states.empty'), + }), + [t], + ); + + return ( +
+ +
+ ); +} diff --git a/apps/frontend/src/renderer/components/Sidebar.tsx b/apps/frontend/src/renderer/components/Sidebar.tsx index 771156d71..ac230e314 100644 --- a/apps/frontend/src/renderer/components/Sidebar.tsx +++ b/apps/frontend/src/renderer/components/Sidebar.tsx @@ -71,7 +71,7 @@ import { SessionContextIndicator } from './SessionContextIndicator'; import { NavIndicator } from './NavIndicator'; import type { Project, AutoBuildVersionInfo, GitStatus, ProjectEnvConfig } from '../../shared/types'; -export type SidebarView = 'kanban' | 'kanban-next' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'webhooks' | 'github-issues' | 'github-issues-next' | 'gitlab-issues' | 'github-prs' | 'github-prs-next' | 'gitlab-merge-requests' | 'changelog' | 'changelog-next' | 'insights' | 'worktrees' | 'agent-tools' | 'plugins' | 'analytics' | 'analytics-next' | 'productivity' | 'productivity-next' | 'merge-analytics' | 'sessions' | 'scheduler' | 'feedback' | 'patterns' | 'patterns-next' | 'model-usage' | 'agent-inspector'; +export type SidebarView = 'kanban' | 'kanban-next' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'webhooks' | 'github-issues' | 'github-issues-next' | 'gitlab-issues' | 'github-prs' | 'github-prs-next' | 'gitlab-merge-requests' | 'changelog' | 'changelog-next' | 'insights' | 'worktrees' | 'agent-tools' | 'plugins' | 'analytics' | 'analytics-next' | 'productivity' | 'productivity-next' | 'merge-analytics' | 'merge-analytics-next' | 'sessions' | 'scheduler' | 'feedback' | 'patterns' | 'patterns-next' | 'model-usage' | 'agent-inspector'; interface SidebarProps { onSettingsClick: () => void; @@ -110,6 +110,7 @@ const baseNavItems: NavItem[] = [ { id: 'plugins', labelKey: 'navigation:items.plugins', icon: Puzzle, shortcut: 'U' }, { id: 'worktrees', labelKey: 'navigation:items.worktrees', icon: GitBranch, shortcut: 'W' }, { id: 'merge-analytics', labelKey: 'navigation:items.mergeAnalytics', icon: Activity, shortcut: 'T' }, + { id: 'merge-analytics-next', labelKey: 'navigation:items.mergeAnalyticsNext', icon: GitMerge }, { id: 'sessions', labelKey: 'navigation:items.sessions', icon: Play }, { id: 'agent-inspector', labelKey: 'navigation:items.agentInspector', icon: Brain }, { id: 'feedback', labelKey: 'navigation:items.feedback', icon: MessageSquare, shortcut: 'F' }, diff --git a/apps/frontend/src/renderer/lib/merge-analytics-ui.ts b/apps/frontend/src/renderer/lib/merge-analytics-ui.ts new file mode 100644 index 000000000..6ea0569aa --- /dev/null +++ b/apps/frontend/src/renderer/lib/merge-analytics-ui.ts @@ -0,0 +1,138 @@ +/** + * Pure mapping helpers between the MergeAnalytics / ConflictPattern IPC shapes + * and the shared AnalyticsDashboard. There is no time series in the merge + * report, so the KPI tiles carry no sparklines; the conflict patterns map to a + * BarList distribution and the rest to summary cards. + */ + +import type { UiBarListCard, UiKpi, UiMetaSection } from '@auto-code/ui'; +import type { ChartTone } from '@auto-code/ui'; +import type { + ConflictPattern, + MergeAnalytics, + MergeConflictSeverity, +} from '../../shared/types'; +import { formatCount, formatPercent } from './analytics-ui'; + +const SEVERITY_TONES: Record = { + none: 'neutral', + low: 'info', + medium: 'warn', + high: 'bad', + critical: 'bad', +}; + +/** Trailing path segment for a compact bar label; keeps a leading dir hint. */ +export function shortenPath(path: string, maxSegments = 2): string { + const parts = path.split('/').filter(Boolean); + if (parts.length <= maxSegments) return path; + return `…/${parts.slice(-maxSegments).join('/')}`; +} + +export interface MergeKpiLabels { + operations: string; + operationsSub: string; + successRate: string; + autoMerge: string; + conflicts: string; + conflictsSub: string; +} + +export function buildMergeKpis( + analytics: MergeAnalytics, + labels: MergeKpiLabels, +): UiKpi[] { + // Backend rates are 0.0–1.0. + const successPct = analytics.success_rate * 100; + const autoPct = analytics.auto_merge_rate * 100; + const successTone = successPct >= 80 ? 'good' : 'warn'; + return [ + { + value: String(analytics.total_operations), + label: labels.operations, + sub: labels.operationsSub, + }, + { + value: formatPercent(successPct), + label: labels.successRate, + tone: successTone, + }, + { + value: formatPercent(autoPct), + label: labels.autoMerge, + }, + { + value: String(analytics.total_conflicts), + label: labels.conflicts, + sub: labels.conflictsSub, + tone: analytics.total_conflicts > 0 ? 'warn' : undefined, + }, + ]; +} + +export function buildMergeBarLists( + patterns: readonly ConflictPattern[], + title: string, + ariaLabel: string, +): UiBarListCard[] { + if (patterns.length === 0) return []; + return [ + { + title, + ariaLabel, + items: patterns.map((pattern) => ({ + label: shortenPath(pattern.file_path), + value: pattern.occurrence_count, + tone: SEVERITY_TONES[pattern.severity] ?? 'info', + })), + }, + ]; +} + +export interface MergeSectionLabels { + outcomesTitle: string; + successful: string; + failed: string; + filesMerged: string; + efficiencyTitle: string; + avgDuration: string; + aiCalls: string; + tokens: string; +} + +/** Seconds → "45s" or "2m 05s". */ +export function formatDuration(seconds: number): string { + if (!Number.isFinite(seconds) || seconds < 0) return '0s'; + const whole = Math.round(seconds); + if (whole < 60) return `${whole}s`; + const minutes = Math.floor(whole / 60); + const rest = whole % 60; + return `${minutes}m ${String(rest).padStart(2, '0')}s`; +} + +export function buildMergeSections( + analytics: MergeAnalytics, + labels: MergeSectionLabels, +): UiMetaSection[] { + return [ + { + title: labels.outcomesTitle, + rows: [ + { label: labels.successful, value: String(analytics.successful_operations) }, + { label: labels.failed, value: String(analytics.failed_operations) }, + { label: labels.filesMerged, value: String(analytics.total_files_merged) }, + ], + }, + { + title: labels.efficiencyTitle, + rows: [ + { + label: labels.avgDuration, + value: formatDuration(analytics.average_duration_seconds), + }, + { label: labels.aiCalls, value: String(analytics.total_ai_calls) }, + { label: labels.tokens, value: formatCount(analytics.total_tokens_used) }, + ], + }, + ]; +} diff --git a/apps/frontend/src/shared/i18n/locales/en/analytics.json b/apps/frontend/src/shared/i18n/locales/en/analytics.json index ae50298d0..d3e95fbf1 100644 --- a/apps/frontend/src/shared/i18n/locales/en/analytics.json +++ b/apps/frontend/src/shared/i18n/locales/en/analytics.json @@ -140,5 +140,35 @@ "qaIterations": "QA iterations", "firstAttempt": "First-attempt rate" } + }, + "mergePilot": { + "error": "Failed to load merge analytics.", + "states": { + "loading": "Loading merge analytics…", + "retry": "Retry", + "empty": "No merge analytics yet." + }, + "kpis": { + "operations": "merge operations", + "operationsSub": "all time", + "successRate": "success rate", + "autoMerge": "auto-merged", + "conflicts": "conflicts", + "conflictsSub": "total resolved" + }, + "conflicts": { + "title": "Top conflict files", + "aria": "Files by conflict occurrence count" + }, + "sections": { + "outcomesTitle": "Outcomes", + "successful": "Successful", + "failed": "Failed", + "filesMerged": "Files merged", + "efficiencyTitle": "Efficiency", + "avgDuration": "Avg duration", + "aiCalls": "AI calls", + "tokens": "Tokens used" + } } } diff --git a/apps/frontend/src/shared/i18n/locales/en/navigation.json b/apps/frontend/src/shared/i18n/locales/en/navigation.json index 00f28413d..07b3b66ac 100644 --- a/apps/frontend/src/shared/i18n/locales/en/navigation.json +++ b/apps/frontend/src/shared/i18n/locales/en/navigation.json @@ -35,7 +35,8 @@ "githubIssuesNext": "GitHub Issues (New UI)", "patternsNext": "Patterns (New UI)", "analyticsNext": "Analytics (New UI)", - "productivityNext": "Productivity (New UI)" + "productivityNext": "Productivity (New UI)", + "mergeAnalyticsNext": "Merge Analytics (New UI)" }, "actions": { "settings": "Settings", diff --git a/apps/frontend/src/shared/i18n/locales/fr/analytics.json b/apps/frontend/src/shared/i18n/locales/fr/analytics.json index a10d9190f..5da4b9bbe 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/analytics.json +++ b/apps/frontend/src/shared/i18n/locales/fr/analytics.json @@ -140,5 +140,35 @@ "qaIterations": "Itérations QA", "firstAttempt": "Taux au 1er essai" } + }, + "mergePilot": { + "error": "Échec du chargement de l'analytique de fusion.", + "states": { + "loading": "Chargement de l'analytique de fusion…", + "retry": "Réessayer", + "empty": "Aucune analytique de fusion pour le moment." + }, + "kpis": { + "operations": "fusions", + "operationsSub": "depuis le début", + "successRate": "taux de réussite", + "autoMerge": "fusion auto", + "conflicts": "conflits", + "conflictsSub": "total résolus" + }, + "conflicts": { + "title": "Fichiers les plus conflictuels", + "aria": "Fichiers par nombre d'occurrences de conflit" + }, + "sections": { + "outcomesTitle": "Résultats", + "successful": "Réussies", + "failed": "Échouées", + "filesMerged": "Fichiers fusionnés", + "efficiencyTitle": "Efficacité", + "avgDuration": "Durée moyenne", + "aiCalls": "Appels IA", + "tokens": "Tokens utilisés" + } } } diff --git a/apps/frontend/src/shared/i18n/locales/fr/navigation.json b/apps/frontend/src/shared/i18n/locales/fr/navigation.json index ea7a36f05..113970652 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/navigation.json +++ b/apps/frontend/src/shared/i18n/locales/fr/navigation.json @@ -35,7 +35,8 @@ "githubIssuesNext": "Issues GitHub (nouvelle interface)", "patternsNext": "Patterns (nouvelle interface)", "analyticsNext": "Analytique (nouvelle interface)", - "productivityNext": "Productivité (nouvelle interface)" + "productivityNext": "Productivité (nouvelle interface)", + "mergeAnalyticsNext": "Analytique de fusion (nouvelle interface)" }, "actions": { "settings": "Paramètres", diff --git a/libs/ui/src/client/types.ts b/libs/ui/src/client/types.ts index 1105b163e..243bf3a08 100644 --- a/libs/ui/src/client/types.ts +++ b/libs/ui/src/client/types.ts @@ -279,3 +279,16 @@ export interface UiChartCard { /** Accessible description of the chart. */ ariaLabel: string; } + +/** One titled bar-list card (a labeled distribution) in a dashboard. */ +export interface UiBarListCard { + title: string; + items: readonly { + label: string; + value: number; + valueLabel?: string; + tone?: 'info' | 'good' | 'warn' | 'bad' | 'neutral'; + }[]; + /** Accessible description of the distribution. */ + ariaLabel: string; +} diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index 5287c681c..673caad82 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -72,6 +72,7 @@ export type { UiPrStat, UiKpi, UiChartCard, + UiBarListCard, CreateTaskInput, TaskStatus, BadgeTone, diff --git a/libs/ui/src/screens/AnalyticsDashboard.stories.tsx b/libs/ui/src/screens/AnalyticsDashboard.stories.tsx index ee0c50347..9122247c1 100644 --- a/libs/ui/src/screens/AnalyticsDashboard.stories.tsx +++ b/libs/ui/src/screens/AnalyticsDashboard.stories.tsx @@ -67,6 +67,24 @@ type Story = StoryObj; export const Dashboard: Story = {}; +export const WithBarList: Story = { + args: { + charts: undefined, + barLists: [ + { + title: 'Top conflict files', + ariaLabel: 'Files by conflict occurrence count', + items: [ + { label: '…/renderer/App.tsx', value: 6, tone: 'bad' }, + { label: '…/shell/Sidebar.tsx', value: 4, tone: 'warn' }, + { label: '…/i18n/index.ts', value: 3, tone: 'info' }, + { label: '…/lib/analytics-ui.ts', value: 1, tone: 'neutral' }, + ], + }, + ], + }, +}; + export const NoRail: Story = { args: { sections: undefined }, }; diff --git a/libs/ui/src/screens/AnalyticsDashboard.tsx b/libs/ui/src/screens/AnalyticsDashboard.tsx index a0b2ac8d4..df9c061ca 100644 --- a/libs/ui/src/screens/AnalyticsDashboard.tsx +++ b/libs/ui/src/screens/AnalyticsDashboard.tsx @@ -1,7 +1,9 @@ +import { BarList } from '../primitives/BarList'; import { LineChart } from '../primitives/LineChart'; import { Sparkline } from '../primitives/Sparkline'; import { StatTile } from '../primitives/StatTile'; import type { + UiBarListCard, UiChartCard, UiKpi, UiMetaSection, @@ -19,6 +21,8 @@ export interface AnalyticsDashboardProps { kpis: UiKpi[] | null; /** Titled chart cards (line charts) in the main column. */ charts?: UiChartCard[]; + /** Titled bar-list cards (distributions) in the main column. */ + barLists?: UiBarListCard[]; /** Summary meta cards (outcomes, QA, agents …) in the right rail. */ sections?: UiMetaSection[]; loading?: boolean; @@ -38,6 +42,7 @@ export interface AnalyticsDashboardProps { export function AnalyticsDashboard({ kpis, charts, + barLists, sections, loading = false, error = null, @@ -84,6 +89,7 @@ export function AnalyticsDashboard({ } const chartCards = charts ?? []; + const barListCards = barLists ?? []; const metaCards = sections ?? []; return ( @@ -126,6 +132,12 @@ export function AnalyticsDashboard({ /> ))} + {barListCards.map((barList) => ( +
+

{barList.title}

+ +
+ ))} {metaCards.length > 0 && (