From ca3261848af16ff7af70fd1b267aaa29d77cbd5f Mon Sep 17 00:00:00 2001 From: Emanuele <106186915+OneStepAt4time@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:35:05 +0200 Subject: [PATCH] feat(dashboard): consistent empty states across all pages - Reusable EmptyState component (icon, title, description, optional CTA) - Applied to SessionHistoryPage, AuditPage, PipelinesPage, UsersPage - Accessible with role=status and aria-label - Uses design tokens for consistent styling - Fixed SessionHistoryPage test to match new text Closes #1788 --- .../src/__tests__/SessionHistoryPage.test.tsx | 2 +- .../src/components/shared/EmptyState.tsx | 40 +++++++++++++++++++ dashboard/src/pages/AuditPage.tsx | 8 +++- dashboard/src/pages/PipelinesPage.tsx | 9 ++++- dashboard/src/pages/SessionHistoryPage.tsx | 8 +++- dashboard/src/pages/UsersPage.tsx | 11 ++++- 6 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 dashboard/src/components/shared/EmptyState.tsx diff --git a/dashboard/src/__tests__/SessionHistoryPage.test.tsx b/dashboard/src/__tests__/SessionHistoryPage.test.tsx index 728a4845..5f512126 100644 --- a/dashboard/src/__tests__/SessionHistoryPage.test.tsx +++ b/dashboard/src/__tests__/SessionHistoryPage.test.tsx @@ -53,7 +53,7 @@ describe('SessionHistoryPage', () => { render(); - await screen.findByText('No session history records found.'); + await screen.findByText('No session history records found'); fireEvent.change(screen.getByLabelText('Owner key ID'), { target: { value: 'owner-1' } }); fireEvent.change(screen.getByLabelText('Status'), { target: { value: 'active' } }); diff --git a/dashboard/src/components/shared/EmptyState.tsx b/dashboard/src/components/shared/EmptyState.tsx new file mode 100644 index 00000000..f2719451 --- /dev/null +++ b/dashboard/src/components/shared/EmptyState.tsx @@ -0,0 +1,40 @@ +/** + * components/shared/EmptyState.tsx — Reusable empty state with icon, title, description, optional CTA. + */ + +import type { ReactNode } from 'react'; + +interface EmptyStateProps { + icon: ReactNode; + title: string; + description?: string; + action?: { + label: string; + onClick: () => void; + }; + className?: string; +} + +export default function EmptyState({ icon, title, description, action, className = '' }: EmptyStateProps) { + return ( +
+
{icon}
+

{title}

+ {description && ( +

{description}

+ )} + {action && ( + + )} +
+ ); +} diff --git a/dashboard/src/pages/AuditPage.tsx b/dashboard/src/pages/AuditPage.tsx index 286c3a3f..700b706a 100644 --- a/dashboard/src/pages/AuditPage.tsx +++ b/dashboard/src/pages/AuditPage.tsx @@ -12,6 +12,7 @@ import { SearchX, AlertCircle, } from 'lucide-react'; +import EmptyState from '../components/shared/EmptyState'; import { fetchAuditLogs, type FetchAuditLogsParams } from '../api/client'; import type { AuditRecord } from '../types'; @@ -278,8 +279,11 @@ export default function AuditPage() { ) : records.length === 0 ? (
- -

No audit records found

+ } + title="No audit records found" + description="Audit logs will appear here when actions are performed." + />

{(appliedActor || appliedAction || appliedSessionId) ? 'Try adjusting your filters.' diff --git a/dashboard/src/pages/PipelinesPage.tsx b/dashboard/src/pages/PipelinesPage.tsx index cc95ec04..2238311f 100644 --- a/dashboard/src/pages/PipelinesPage.tsx +++ b/dashboard/src/pages/PipelinesPage.tsx @@ -4,7 +4,8 @@ import { useState, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom'; -import { Plus } from 'lucide-react'; +import { Plus, GitBranch } from 'lucide-react'; +import EmptyState from '../components/shared/EmptyState'; import { getPipelines } from '../api/client'; import type { PipelineInfo } from '../api/client'; import { useStore } from '../store/useStore'; @@ -133,7 +134,11 @@ export default function PipelinesPage() {

) : pipelines.length === 0 ? (
-

No pipelines yet

+ } + title="No pipelines yet" + description="Create a pipeline to automate session workflows." + />

Create a pipeline to run sessions in sequence

) : ( diff --git a/dashboard/src/pages/SessionHistoryPage.tsx b/dashboard/src/pages/SessionHistoryPage.tsx index 4879a360..2f9501f2 100644 --- a/dashboard/src/pages/SessionHistoryPage.tsx +++ b/dashboard/src/pages/SessionHistoryPage.tsx @@ -17,6 +17,7 @@ import { type SessionHistoryRecord, } from '../api/client'; import { formatTimeAgo } from '../utils/format'; +import EmptyState from '../components/shared/EmptyState'; const STATUS_OPTIONS = [ { value: '', label: 'All statuses' }, @@ -322,8 +323,11 @@ export default function SessionHistoryPage() { ) : records.length === 0 ? ( - - No session history records found. + } + title="No session history records found" + description="Try adjusting your filters or date range." + /> ) : ( diff --git a/dashboard/src/pages/UsersPage.tsx b/dashboard/src/pages/UsersPage.tsx index d3f511f1..ce0a412f 100644 --- a/dashboard/src/pages/UsersPage.tsx +++ b/dashboard/src/pages/UsersPage.tsx @@ -3,7 +3,8 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { AlertCircle, RefreshCw, UsersRound } from 'lucide-react'; +import { AlertCircle, RefreshCw, UsersRound, Users } from 'lucide-react'; +import EmptyState from '../components/shared/EmptyState'; import { fetchUsers, type UserSummary } from '../api/client'; import { formatTimeAgo } from '../utils/format'; @@ -155,7 +156,13 @@ export default function UsersPage() { ) : filtered.length === 0 ? ( - No users match the current filter. + + } + title="No users match the current filter" + description="Try adjusting your search or filter criteria." + /> + ) : ( filtered.map((user) => (