Skip to content
Open
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
37 changes: 28 additions & 9 deletions frontend/src/pages/Scans.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -482,15 +482,34 @@ export default function Scans() {
{formatLocaleTime(createDate)}
</p>
</div>
{task.duration_seconds && (
<div className="bg-charcoal-dark border-2 border-black px-4 py-2 shadow-[3px_3px_0px_0px_rgba(0,0,0,1)]">
<p className="text-[10px] font-black font-mono text-rag-blue leading-none">
{formatDuration(
task.duration_seconds,
)?.toUpperCase()}
</p>
</div>
)}
<div className="flex items-center gap-3">
{task.duration_seconds && (
<div className="bg-charcoal-dark border-2 border-black px-4 py-2 shadow-[3px_3px_0px_0px_rgba(0,0,0,1)]">
<p className="text-[10px] font-black font-mono text-rag-blue leading-none">
{formatDuration(
task.duration_seconds,
)?.toUpperCase()}
</p>
</div>
)}
{(task.status === "completed" ||
task.status === "failed" ||
task.status === "cancelled") && (
<button
type="button"
aria-label={`Re-run ${task.tool} scan`}
onClick={(e) => {
e.stopPropagation();
handleRescan(task);
}}
className="w-10 h-10 border-4 border-black bg-rag-blue text-black flex items-center justify-center transition-all hover:bg-rag-blue/80 active:translate-x-0.5 active:translate-y-0.5 shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] active:shadow-none"
>
<span className="material-symbols-outlined text-sm font-black">
replay
</span>
</button>
)}
</div>
</div>
</div>

Expand Down
25 changes: 25 additions & 0 deletions frontend/testing/unit/pages/Scans.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ vi.mock('../../../src/api', () => ({
deleteTask: vi.fn().mockResolvedValue({}),
clearAllTasks: vi.fn().mockResolvedValue({}),
bulkDeleteTasks: vi.fn().mockResolvedValue({}),
startTask: vi.fn().mockResolvedValue({ task_id: 'new-task-123' }),
}))

vi.mock('../../../src/routes', () => ({
Expand Down Expand Up @@ -213,4 +214,28 @@ describe('Scans — task list', () => {

await waitFor(() => expect(screen.getByText('Delete Scan Record')).toBeInTheDocument())
})

it('renders quick re-run button for completed tasks and triggers handleRescan', async () => {
const tasks = [makeTask({ task_id: 'task-123', status: 'completed', tool: 'nmap' })]
mockFetch(tasks)
renderScans()

await waitFor(() => expect(screen.getByText('nmap')).toBeInTheDocument())

const rerunBtn = screen.getByRole('button', { name: /Re-run nmap scan/i })
expect(rerunBtn).toBeInTheDocument()

const { startTask } = await import('../../../src/api')
await userEvent.click(rerunBtn)

await waitFor(() => {
expect(startTask).toHaveBeenCalledWith(
'nmap',
expect.any(Object),
true,
undefined,
undefined
)
})
})
})
Loading