MAESTRO: Add EmptyState component for Usage Dashboard

- Create EmptyState component with theme-aware styling
- Features BarChart3 icon with decorative SVG bars
- Supports customizable title and message props
- Default message: "No usage data yet. Start using Maestro to see your stats!"
- Update UsageDashboardModal to use new component
- Add comprehensive test suite (16 tests)
This commit is contained in:
Pedram Amini
2025-12-28 16:31:55 -06:00
parent d624d71dc5
commit cec24b5dcf
3 changed files with 281 additions and 16 deletions

View File

@@ -0,0 +1,181 @@
/**
* Tests for EmptyState component
*
* Verifies:
* - Renders empty state container with correct test ID
* - Displays chart icon illustration
* - Shows default title and message
* - Supports custom title and message
* - Applies theme colors properly
* - Theme-aware styling for all elements
*/
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { EmptyState } from '../../../../renderer/components/UsageDashboard/EmptyState';
import { THEMES } from '../../../../shared/themes';
// Test themes
const darkTheme = THEMES['dracula'];
const lightTheme = THEMES['solarized-light'];
describe('EmptyState', () => {
describe('Rendering', () => {
it('renders the empty state container with correct test ID', () => {
render(<EmptyState theme={darkTheme} />);
expect(screen.getByTestId('usage-dashboard-empty')).toBeInTheDocument();
});
it('renders the chart icon illustration', () => {
render(<EmptyState theme={darkTheme} />);
// The container should have the icon (BarChart3 renders as svg)
const container = screen.getByTestId('usage-dashboard-empty');
const svgs = container.querySelectorAll('svg');
// Should have at least 2 svgs: main icon and decorative bars
expect(svgs.length).toBeGreaterThanOrEqual(2);
});
it('displays the default title', () => {
render(<EmptyState theme={darkTheme} />);
expect(screen.getByText('No usage data yet')).toBeInTheDocument();
});
it('displays the default message', () => {
render(<EmptyState theme={darkTheme} />);
expect(screen.getByText('Start using Maestro to see your stats!')).toBeInTheDocument();
});
});
describe('Custom Content', () => {
it('supports custom title', () => {
render(
<EmptyState
theme={darkTheme}
title="No data for this period"
/>
);
expect(screen.getByText('No data for this period')).toBeInTheDocument();
expect(screen.queryByText('No usage data yet')).not.toBeInTheDocument();
});
it('supports custom message', () => {
render(
<EmptyState
theme={darkTheme}
message="Try selecting a different time range."
/>
);
expect(screen.getByText('Try selecting a different time range.')).toBeInTheDocument();
expect(screen.queryByText('Start using Maestro to see your stats!')).not.toBeInTheDocument();
});
it('supports both custom title and message', () => {
render(
<EmptyState
theme={darkTheme}
title="Custom Title"
message="Custom message for testing."
/>
);
expect(screen.getByText('Custom Title')).toBeInTheDocument();
expect(screen.getByText('Custom message for testing.')).toBeInTheDocument();
});
});
describe('Theme Styling', () => {
it('applies theme textDim color to container', () => {
render(<EmptyState theme={darkTheme} />);
const container = screen.getByTestId('usage-dashboard-empty');
expect(container).toHaveStyle({ color: darkTheme.colors.textDim });
});
it('applies theme textMain color to title', () => {
render(<EmptyState theme={darkTheme} />);
const title = screen.getByText('No usage data yet');
expect(title).toHaveStyle({ color: darkTheme.colors.textMain });
});
it('works with light theme', () => {
render(<EmptyState theme={lightTheme} />);
const container = screen.getByTestId('usage-dashboard-empty');
expect(container).toHaveStyle({ color: lightTheme.colors.textDim });
const title = screen.getByText('No usage data yet');
expect(title).toHaveStyle({ color: lightTheme.colors.textMain });
});
});
describe('Layout', () => {
it('uses flexbox centering layout', () => {
render(<EmptyState theme={darkTheme} />);
const container = screen.getByTestId('usage-dashboard-empty');
expect(container).toHaveClass('flex');
expect(container).toHaveClass('flex-col');
expect(container).toHaveClass('items-center');
expect(container).toHaveClass('justify-center');
});
it('has gap between icon and text', () => {
render(<EmptyState theme={darkTheme} />);
const container = screen.getByTestId('usage-dashboard-empty');
expect(container).toHaveClass('gap-4');
});
it('has full height container', () => {
render(<EmptyState theme={darkTheme} />);
const container = screen.getByTestId('usage-dashboard-empty');
expect(container).toHaveClass('h-full');
});
});
describe('Icon Styling', () => {
it('renders decorative svg with theme-aware fills', () => {
render(<EmptyState theme={darkTheme} />);
const container = screen.getByTestId('usage-dashboard-empty');
const rects = container.querySelectorAll('rect');
// The decorative bars should have rect elements
expect(rects.length).toBeGreaterThan(0);
// Check that rects use the theme color
rects.forEach((rect) => {
expect(rect.getAttribute('fill')).toBe(darkTheme.colors.textDim);
});
});
});
describe('Accessibility', () => {
it('has a data-testid for testing', () => {
render(<EmptyState theme={darkTheme} />);
expect(screen.getByTestId('usage-dashboard-empty')).toBeInTheDocument();
});
it('text is visible and readable', () => {
render(<EmptyState theme={darkTheme} />);
const title = screen.getByText('No usage data yet');
const message = screen.getByText('Start using Maestro to see your stats!');
// Text should be in the document and visible
expect(title).toBeVisible();
expect(message).toBeVisible();
});
});
});

View File

@@ -0,0 +1,97 @@
/**
* EmptyState
*
* Displays a friendly empty state message when no usage data exists.
* Used in the Usage Dashboard to indicate that the user should start
* using Maestro to generate stats.
*
* Features:
* - Theme-aware styling with inline styles
* - Subtle chart illustration/icon
* - Friendly, encouraging message
* - Reusable component with customizable message
*/
import React from 'react';
import { BarChart3 } from 'lucide-react';
import type { Theme } from '../../types';
interface EmptyStateProps {
/** Current theme for styling */
theme: Theme;
/** Optional custom title (default: "No usage data yet") */
title?: string;
/** Optional custom message (default: "Start using Maestro to see your stats!") */
message?: string;
}
export function EmptyState({
theme,
title = 'No usage data yet',
message = 'Start using Maestro to see your stats!',
}: EmptyStateProps) {
return (
<div
className="h-full flex flex-col items-center justify-center gap-4"
style={{ color: theme.colors.textDim }}
data-testid="usage-dashboard-empty"
>
{/* Subtle chart illustration */}
<div
className="relative"
style={{ opacity: 0.3 }}
>
{/* Main chart icon */}
<BarChart3 className="w-16 h-16" />
{/* Decorative subtle bars for visual interest */}
<svg
className="absolute -bottom-1 -right-2 w-6 h-6"
viewBox="0 0 24 24"
fill="none"
style={{ opacity: 0.5 }}
>
<rect
x="4"
y="12"
width="4"
height="8"
rx="1"
fill={theme.colors.textDim}
/>
<rect
x="10"
y="8"
width="4"
height="12"
rx="1"
fill={theme.colors.textDim}
/>
<rect
x="16"
y="4"
width="4"
height="16"
rx="1"
fill={theme.colors.textDim}
/>
</svg>
</div>
{/* Message text */}
<div className="text-center">
<p
className="text-lg mb-2"
style={{ color: theme.colors.textMain }}
>
{title}
</p>
<p className="text-sm">
{message}
</p>
</div>
</div>
);
}
export default EmptyState;

View File

@@ -21,6 +21,7 @@ import { AgentComparisonChart } from './AgentComparisonChart';
import { SourceDistributionChart } from './SourceDistributionChart';
import { DurationTrendsChart } from './DurationTrendsChart';
import { AutoRunStats } from './AutoRunStats';
import { EmptyState } from './EmptyState';
import type { Theme } from '../../types';
import { useLayerStack } from '../../contexts/LayerStackContext';
import { MODAL_PRIORITIES } from '../../constants/modalPriorities';
@@ -377,22 +378,8 @@ export function UsageDashboardModal({
</button>
</div>
) : !data || (data.totalQueries === 0 && data.bySource.user === 0 && data.bySource.auto === 0) ? (
/* Empty State */
<div
className="h-full flex flex-col items-center justify-center gap-4"
style={{ color: theme.colors.textDim }}
data-testid="usage-dashboard-empty"
>
<BarChart3 className="w-16 h-16 opacity-30" />
<div className="text-center">
<p className="text-lg mb-2" style={{ color: theme.colors.textMain }}>
No usage data yet
</p>
<p className="text-sm">
Start using Maestro to see your stats!
</p>
</div>
</div>
/* Empty State Component */
<EmptyState theme={theme} />
) : (
<div className="space-y-6" data-testid="usage-dashboard-content">
{/* View-specific content based on viewMode */}