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
3 changes: 0 additions & 3 deletions .jules/palette.md

This file was deleted.

25 changes: 25 additions & 0 deletions frontend/src/components/Spinner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Spinner } from './Spinner';
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest';

describe('Spinner', () => {
it('renders correctly', () => {
const { container } = render(<Spinner />);
const svg = container.querySelector('svg');
expect(svg).toBeInTheDocument();
expect(svg).toHaveClass('animate-spin');
expect(svg).toHaveClass('w-4'); // Default size sm
});

it('applies custom className', () => {
const { container } = render(<Spinner className="text-red-500" />);
const svg = container.querySelector('svg');
expect(svg).toHaveClass('text-red-500');
});

it('applies size classes', () => {
const { container } = render(<Spinner size="lg" />);
const svg = container.querySelector('svg');
expect(svg).toHaveClass('w-8');
});
});
36 changes: 36 additions & 0 deletions frontend/src/components/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
interface SpinnerProps {
className?: string;
size?: 'sm' | 'md' | 'lg';
}

export function Spinner({ className = '', size = 'sm' }: SpinnerProps) {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8',
};

return (
<svg
className={`animate-spin text-current ${sizeClasses[size]}${className ? ` ${className}` : ''}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);
}
7 changes: 5 additions & 2 deletions frontend/src/pages/Browse.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { getApiBase, fetchWithAuth, deleteJobs } from '../api';
import { useAuth } from '../contexts/AuthContext';
import { WorkerStatusBanner } from '../components/WorkerStatusBanner';
import { Spinner } from '../components/Spinner';
import { useWorkerStatus } from '../hooks/useWorkerStatus';
import type { JobStatus, JobSummary } from '@shared/types/job';

Expand Down Expand Up @@ -186,7 +187,8 @@ export default function Browse() {

{/* Jobs List */}
{isLoading && (
<div className="bg-gray-800 rounded-lg p-6 text-gray-400 text-center">
<div className="bg-gray-800 rounded-lg p-6 text-gray-400 text-center flex justify-center items-center gap-2">
<Spinner />
Loading simulations...
</div>
)}
Expand Down Expand Up @@ -220,8 +222,9 @@ export default function Browse() {
type="button"
onClick={handleBulkDelete}
disabled={isDeleting}
className="ml-auto px-3 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="ml-auto px-3 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
>
{isDeleting && <Spinner size="sm" />}
{isDeleting ? 'Deleting...' : `Delete Selected (${selectedJobs.size})`}
</button>
</>
Expand Down
13 changes: 9 additions & 4 deletions frontend/src/pages/JobStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getApiBase, fetchWithAuth, deleteJob } from '../api';
import { ColorIdentity } from '../components/ColorIdentity';
import { DeckShowcase } from '../components/DeckShowcase';
import { SimulationGrid } from '../components/SimulationGrid';
import { Spinner } from '../components/Spinner';
import { useJobStream } from '../hooks/useJobStream';
import { useWinData } from '../hooks/useWinData';
import { useJobLogs } from '../hooks/useJobLogs';
Expand Down Expand Up @@ -184,8 +185,9 @@ export default function JobStatusPage() {
type="button"
onClick={handleRunAgain}
disabled={isResubmitting}
className="bg-blue-600 hover:bg-blue-700 text-white text-sm rounded px-3 py-1 disabled:opacity-50 disabled:cursor-not-allowed"
className="bg-blue-600 hover:bg-blue-700 text-white text-sm rounded px-3 py-1 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
>
{isResubmitting && <Spinner size="sm" />}
{isResubmitting ? 'Submitting...' : 'Run Again'}
</button>
)}
Expand Down Expand Up @@ -237,8 +239,9 @@ export default function JobStatusPage() {
type="button"
onClick={handleCancel}
disabled={isCancelling}
className="ml-auto px-3 py-1 text-xs rounded bg-orange-600 text-white hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="ml-auto px-3 py-1 text-xs rounded bg-orange-600 text-white hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
>
{isCancelling && <Spinner size="sm" />}
{isCancelling ? 'Cancelling...' : 'Cancel Job'}
</button>
</div>
Expand Down Expand Up @@ -324,8 +327,9 @@ export default function JobStatusPage() {
type="button"
onClick={handleCancel}
disabled={isCancelling}
className="ml-4 px-3 py-1 text-xs rounded bg-orange-600 text-white hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="ml-4 px-3 py-1 text-xs rounded bg-orange-600 text-white hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
>
{isCancelling && <Spinner size="sm" />}
{isCancelling ? 'Cancelling...' : 'Cancel Job'}
</button>
)}
Expand Down Expand Up @@ -780,8 +784,9 @@ export default function JobStatusPage() {
type="button"
onClick={handleDelete}
disabled={isDeletingJob}
className="ml-auto px-3 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="ml-auto px-3 py-1 text-sm rounded bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
>
{isDeletingJob && <Spinner size="sm" />}
{isDeletingJob ? 'Deleting...' : 'Delete Job'}
</button>
)}
Expand Down