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) => (