Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dashboard/src/__tests__/SessionHistoryPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('SessionHistoryPage', () => {

render(<SessionHistoryPage />);

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' } });
Expand Down
40 changes: 40 additions & 0 deletions dashboard/src/components/shared/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
role="status"
aria-label={title}
className={`flex flex-col items-center justify-center py-16 px-6 text-center ${className}`}
>
<div className="mb-4 text-zinc-600">{icon}</div>
<h3 className="text-lg font-medium text-zinc-300">{title}</h3>
{description && (
<p className="mt-1.5 max-w-sm text-sm text-zinc-500">{description}</p>
)}
{action && (
<button
onClick={action.onClick}
className="mt-4 rounded border border-[var(--color-accent-cyan)]/30 bg-[var(--color-accent-cyan)]/10 px-4 py-2 text-sm font-medium text-[var(--color-accent-cyan)] transition-colors hover:bg-[var(--color-accent-cyan)]/20"
>
{action.label}
</button>
)}
</div>
);
}
8 changes: 6 additions & 2 deletions dashboard/src/pages/AuditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -278,8 +279,11 @@ export default function AuditPage() {
</div>
) : records.length === 0 ? (
<div className="rounded-lg border border-zinc-800 bg-[var(--color-surface)]] p-12 text-center">
<SearchX className="mx-auto h-10 w-10 text-zinc-600 mb-3" />
<p className="text-zinc-400 font-medium">No audit records found</p>
<EmptyState
icon={<SearchX className="h-10 w-10" />}
title="No audit records found"
description="Audit logs will appear here when actions are performed."
/>
<p className="mt-1 text-xs text-zinc-600">
{(appliedActor || appliedAction || appliedSessionId)
? 'Try adjusting your filters.'
Expand Down
9 changes: 7 additions & 2 deletions dashboard/src/pages/PipelinesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -133,7 +134,11 @@ export default function PipelinesPage() {
</div>
) : pipelines.length === 0 ? (
<div className="rounded-lg border border-void-lighter bg-[var(--color-surface)]] p-12 text-center">
<p className="text-gray-500">No pipelines yet</p>
<EmptyState
icon={<GitBranch className="h-8 w-8" />}
title="No pipelines yet"
description="Create a pipeline to automate session workflows."
/>
<p className="mt-1 text-xs text-gray-600">Create a pipeline to run sessions in sequence</p>
</div>
) : (
Expand Down
8 changes: 6 additions & 2 deletions dashboard/src/pages/SessionHistoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -322,8 +323,11 @@ export default function SessionHistoryPage() {
) : records.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-16 text-center text-zinc-500">
<SearchX className="mx-auto mb-2 h-5 w-5 text-zinc-600" />
No session history records found.
<EmptyState
icon={<SearchX className="h-8 w-8" />}
title="No session history records found"
description="Try adjusting your filters or date range."
/>
</td>
</tr>
) : (
Expand Down
11 changes: 9 additions & 2 deletions dashboard/src/pages/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -155,7 +156,13 @@ export default function UsersPage() {
<SkeletonRows count={8} />
) : filtered.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-14 text-center text-zinc-500">No users match the current filter.</td>
<td colSpan={6} className="px-4 py-0">
<EmptyState
icon={<Users className="h-8 w-8" />}
title="No users match the current filter"
description="Try adjusting your search or filter criteria."
/>
</td>
</tr>
) : (
filtered.map((user) => (
Expand Down
Loading