Skip to content

Commit

Permalink
feat: allow to cancel a task from the status bar (#10209)
Browse files Browse the repository at this point in the history
* feat: allow to cancel a task from the status bar

fixes podman-desktop/podman-desktop#10162

Signed-off-by: Florent Benoit <[email protected]>
  • Loading branch information
benoitf authored Dec 4, 2024
1 parent cac1d19 commit 0552417
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 4 deletions.
87 changes: 85 additions & 2 deletions packages/renderer/src/lib/statusbar/TaskIndicator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@

import '@testing-library/jest-dom/vitest';

import { fireEvent, render } from '@testing-library/svelte';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { beforeAll, beforeEach, expect, test, vi } from 'vitest';

import TaskIndicator from '/@/lib/statusbar/TaskIndicator.svelte';
import { tasksInfo } from '/@/stores/tasks';
import { type TaskInfoUI, tasksInfo } from '/@/stores/tasks';

beforeAll(() => {
Object.defineProperty(global, 'window', {
value: {
executeCommand: vi.fn(),
cancelToken: vi.fn(),
events: {
send: vi.fn(),
},
Expand Down Expand Up @@ -152,3 +153,85 @@ test('task with undefined progress value should show indeterminate progress', as
const progressBar = getByRole('progressbar');
expect(progressBar).toHaveClass('progress-bar-indeterminate');
});

test('cancellable task should display cancel button', async () => {
const cancellableTask: TaskInfoUI = {
name: 'Dummy Task',
state: 'running',
status: 'in-progress',
started: 0,
id: 'dummy-task',
progress: undefined, // indeterminate
cancellable: true,
cancellationTokenSourceId: 1234,
};

tasksInfo.set([cancellableTask]);

const { getByRole } = render(TaskIndicator);

// expect the cancel button to be there
const cancelButton = getByRole('button', { name: 'Cancel task Dummy Task' });
expect(cancelButton).toBeDefined();

// click on the cancel button
await fireEvent.click(cancelButton);

// expect the cancel token to be called
expect(window.cancelToken).toHaveBeenCalledWith(cancellableTask.cancellationTokenSourceId);
});

test('non cancellable task should not display cancel', async () => {
const nonCancellableTask: TaskInfoUI = {
name: 'Dummy Task',
state: 'running',
status: 'in-progress',
started: 0,
id: 'dummy-task',
progress: undefined, // indeterminate
cancellable: false,
};

tasksInfo.set([nonCancellableTask]);

render(TaskIndicator);

// expect the cancel button not to be there
const cancelButton = screen.queryByRole('button', { name: 'Cancel task Dummy Task' });
expect(cancelButton).toBeNull();
});

test('if multiple tasks in progress, no cancel button is displayed', async () => {
const cancellableTask1: TaskInfoUI = {
name: 'Dummy Task 1',
state: 'running',
status: 'in-progress',
started: 0,
id: 'dummy-task1',
progress: undefined,
cancellable: true,
cancellationTokenSourceId: 1234,
};

const cancellableTask2: TaskInfoUI = {
name: 'Dummy Task 2',
state: 'running',
status: 'in-progress',
started: 0,
id: 'dummy-task2',
progress: undefined,
cancellable: true,
cancellationTokenSourceId: 2345,
};

tasksInfo.set([cancellableTask1, cancellableTask2]);

render(TaskIndicator);

// expect no cancel button not to be there
const allButtons = screen.queryAllByRole('button');
// filter all buttons starting with "Cancel task"
const cancelButtons = allButtons.filter(button => button.ariaLabel?.startsWith('Cancel task'));
// no button at all
expect(cancelButtons).toHaveLength(0);
});
33 changes: 31 additions & 2 deletions packages/renderer/src/lib/statusbar/TaskIndicator.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script lang="ts">
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons';
import { Tooltip } from '@podman-desktop/ui-svelte';
import Fa from 'svelte-fa';
import ProgressBar from '/@/lib/task-manager/ProgressBar.svelte';
import { tasksInfo } from '/@/stores/tasks';
Expand All @@ -12,14 +14,31 @@ let title: string | undefined = $derived.by(() => {
return `${runningTasks.length} tasks running`;
});
// single task (if only one task is running)
let singleCurrentTask = $derived.by(() => {
if (runningTasks.length !== 1) return undefined;
return runningTasks[0];
});
let progress: number | undefined = $derived.by(() => {
if (runningTasks.length !== 1) return undefined; // return indeterminate
return runningTasks[0].progress; // return task's progress value
return singleCurrentTask?.progress; // return task's progress value (if there is one)
});
let cancellableToken = $derived.by(() => {
return singleCurrentTask?.cancellationTokenSourceId && singleCurrentTask?.cancellable
? singleCurrentTask.cancellationTokenSourceId
: undefined;
});
async function toggleTaskManager(): Promise<void> {
await window.executeCommand('show-task-manager');
}
async function cancelTask() {
if (cancellableToken) {
await window.cancelToken(cancellableToken);
}
}
</script>

{#if runningTasks.length > 0}
Expand All @@ -32,5 +51,15 @@ async function toggleTaskManager(): Promise<void> {
</div>
</button>
</Tooltip>
{#if cancellableToken}
<div class="flex items-center ml-0.5">
<Tooltip top tip="Cancel task {title}">
<button class="cursor-pointer" onclick={cancelTask} aria-label="Cancel task {title}">
<Fa size="0.750x" icon={faTimesCircle} />
</button>
</Tooltip>
</div>
{/if}
</div>
{/if}

0 comments on commit 0552417

Please sign in to comment.