mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat(scheduler): flag one-shot tasks in the Scheduled Operations table (#1433)
A one-shot task (delete after successful run) self-deletes only on a successful run; a failed one-shot is kept so it can be retried or debugged. The table gave no signal for this, so a lingering failed one-shot was indistinguishable from a stuck recurring task. Add a "One-shot" chip to the status cell for every delete-after-run task, with a tooltip that explains the keep-on-failure behavior when the task has failed.
This commit is contained in:
@@ -619,13 +619,28 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter, p
|
||||
<div className="text-xs text-muted-foreground font-mono">{task.cron_expression}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{task.last_status === 'success' ? (
|
||||
<Badge className="bg-success-muted text-success border-success/20">Success</Badge>
|
||||
) : task.last_status === 'failure' ? (
|
||||
<Badge variant="destructive">Failed</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">Never run</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{task.last_status === 'success' ? (
|
||||
<Badge className="bg-success-muted text-success border-success/20">Success</Badge>
|
||||
) : task.last_status === 'failure' ? (
|
||||
<Badge variant="destructive">Failed</Badge>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">Never run</span>
|
||||
)}
|
||||
{task.delete_after_run === 1 && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] text-muted-foreground"
|
||||
title={
|
||||
task.last_status === 'failure'
|
||||
? 'One-shot task kept after a failed run so you can retry or debug. Deletes itself after a successful run.'
|
||||
: 'One-shot task. Deletes itself after a successful run.'
|
||||
}
|
||||
>
|
||||
One-shot
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{formatTimestamp(task.next_run_at)}
|
||||
|
||||
@@ -79,6 +79,37 @@ describe('ScheduledOperationsView', () => {
|
||||
expect(await screen.findByText('nightly-prune')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('marks a one-shot task with a chip and leaves recurring tasks unmarked', async () => {
|
||||
tasksFixture = [
|
||||
makeTask({ id: 1, name: 'recurring-task' }),
|
||||
makeTask({ id: 2, name: 'one-shot-task', delete_after_run: 1 }),
|
||||
];
|
||||
render(<ScheduledOperationsView />);
|
||||
|
||||
await userEvent.click(await screen.findByRole('button', { name: /All tasks/ }));
|
||||
await screen.findByText('recurring-task');
|
||||
|
||||
// A single One-shot chip, scoped to the one-shot row.
|
||||
const chips = screen.getAllByText('One-shot');
|
||||
expect(chips).toHaveLength(1);
|
||||
// A pending one-shot carries the plain lifecycle tooltip, not the failure copy.
|
||||
expect(chips[0]).toHaveAttribute('title', expect.stringContaining('Deletes itself after a successful run'));
|
||||
expect(chips[0]).toHaveAttribute('title', expect.not.stringContaining('kept after a failed run'));
|
||||
const oneShotRow = screen.getByText('one-shot-task').closest('tr');
|
||||
expect(oneShotRow).toContainElement(chips[0]);
|
||||
const recurringRow = screen.getByText('recurring-task').closest('tr');
|
||||
expect(recurringRow?.textContent).not.toContain('One-shot');
|
||||
});
|
||||
|
||||
it('explains a failed one-shot was kept to debug', async () => {
|
||||
tasksFixture = [makeTask({ id: 1, name: 'failed-one-shot', delete_after_run: 1, last_status: 'failure' })];
|
||||
render(<ScheduledOperationsView />);
|
||||
|
||||
await userEvent.click(await screen.findByRole('button', { name: /All tasks/ }));
|
||||
const chip = await screen.findByText('One-shot');
|
||||
expect(chip).toHaveAttribute('title', expect.stringContaining('kept after a failed run'));
|
||||
});
|
||||
|
||||
it('opens the create modal from a prefill and consumes it once', async () => {
|
||||
const onPrefillConsumed = vi.fn();
|
||||
render(
|
||||
|
||||
Reference in New Issue
Block a user