diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index 9b2d5ad0528..12a8041e9a0 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -295,6 +295,21 @@ export class PluginSystem { statusBarRegistry.setEntry('help', false, 0, undefined, 'Help', 'fa fa-question-circle', true, 'help', undefined); + statusBarRegistry.setEntry( + 'tasks', + false, + 0, + undefined, + 'tasks', + 'fa fa-bell', + true, + 'show-task-manager', + undefined, + ); + commandRegistry.registerCommand('show-task-manager', () => { + apiSender.send('toggle-task-manager', ''); + }); + statusBarRegistry.setEntry( 'feedback', false, diff --git a/packages/renderer/src/App.svelte b/packages/renderer/src/App.svelte index dae30f32e6f..b7143123e57 100644 --- a/packages/renderer/src/App.svelte +++ b/packages/renderer/src/App.svelte @@ -35,6 +35,7 @@ import RunImage from './lib/image/RunImage.svelte'; import SendFeedback from './lib/feedback/SendFeedback.svelte'; import ToastHandler from './lib/toast/ToastHandler.svelte'; import QuickPickInput from './lib/dialogs/QuickPickInput.svelte'; +import TaskManager from './lib/task-manager/TaskManager.svelte'; router.mode.hash(); @@ -87,6 +88,7 @@ window.events?.receive('display-help', () => { {/if}
+ diff --git a/packages/renderer/src/lib/images/BellSlashIcon.svelte b/packages/renderer/src/lib/images/BellSlashIcon.svelte new file mode 100644 index 00000000000..f7898bc89f2 --- /dev/null +++ b/packages/renderer/src/lib/images/BellSlashIcon.svelte @@ -0,0 +1,26 @@ + + + diff --git a/packages/renderer/src/lib/images/TaskIcon.svelte b/packages/renderer/src/lib/images/TaskIcon.svelte new file mode 100644 index 00000000000..cb51dcb75cb --- /dev/null +++ b/packages/renderer/src/lib/images/TaskIcon.svelte @@ -0,0 +1,33 @@ + + + diff --git a/packages/renderer/src/lib/task-manager/TaskManager.spec.ts b/packages/renderer/src/lib/task-manager/TaskManager.spec.ts new file mode 100644 index 00000000000..17a24d431f7 --- /dev/null +++ b/packages/renderer/src/lib/task-manager/TaskManager.spec.ts @@ -0,0 +1,145 @@ +/********************************************************************** + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import '@testing-library/jest-dom'; +import { beforeAll, test, expect, vi } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/svelte'; +import TaskManager from './TaskManager.svelte'; +import userEvent from '@testing-library/user-event'; +import type { Task } from '/@/stores/tasks'; +import { tasksInfo } from '/@/stores/tasks'; + +// fake the window.events object +beforeAll(() => { + (window.events as unknown) = { + receive: vi.fn(), + }; + // reset store + tasksInfo.set([]); +}); + +const started = new Date().getTime(); +const IN_PROGRESS_TASK: Task = { id: '1', name: 'Running Task 1', state: 'running', started, status: 'in-progress' }; +const SUCCEED_TASK: Task = { id: '1', name: 'Running Task 1', state: 'completed', started, status: 'success' }; + +test('Expect that the tasks manager is hidden by default', async () => { + render(TaskManager, {}); + // expect the tasks manager is not visible by default + const tasksManager = screen.queryByTitle('Tasks manager'); + expect(tasksManager).not.toBeInTheDocument(); +}); + +test('Expect that the tasks manager is visible by property', async () => { + render(TaskManager, { showTaskManager: true }); + + // expect the tasks manager is visible + const tasksManager = screen.queryByTitle('Tasks manager'); + expect(tasksManager).toBeInTheDocument(); +}); + +test('Expect that the tasks manager is hidden if user press the ESC key', async () => { + render(TaskManager, { showTaskManager: true }); + + // expect the tasks manager is visible + let tasksManager = screen.queryByTitle('Tasks manager'); + expect(tasksManager).toBeInTheDocument(); + + // now, press the ESC key + await userEvent.keyboard('{Escape}'); + + // expect the tasks manager has been hidden + tasksManager = screen.queryByTitle('Tasks manager'); + expect(tasksManager).not.toBeInTheDocument(); +}); + +test('Expect that the tasks manager is hidden if user click on the hide button', async () => { + render(TaskManager, { showTaskManager: true }); + + // expect the tasks manager is visible + let tasksManager = screen.queryByTitle('Tasks manager'); + expect(tasksManager).toBeInTheDocument(); + + // now, click on the Hide (Escape) button + const hideButton = screen.getByRole('button', { name: 'Hide (Escape)' }); + await fireEvent.click(hideButton); + + // expect the tasks manager has been hidden + tasksManager = screen.queryByTitle('Tasks manager'); + expect(tasksManager).not.toBeInTheDocument(); +}); + +test('Expect no tasks', async () => { + render(TaskManager, { showTaskManager: true }); + + // expect the "You have no tasks" is visible + const noTaskField = screen.queryByText('You have no tasks.'); + expect(noTaskField).toBeInTheDocument(); +}); + +test('Expect tasks', async () => { + tasksInfo.set([IN_PROGRESS_TASK]); + render(TaskManager, { showTaskManager: true }); + + // expect the "You Have no Tasks" is not visible + const noTaskField = screen.queryByText('You have no tasks.'); + expect(noTaskField).not.toBeInTheDocument(); + + // expect the task is visible + const task = screen.queryByText('Running Task 1'); + expect(task).toBeInTheDocument(); +}); + +test('Expect delete completed tasks remove tasks', async () => { + tasksInfo.set([SUCCEED_TASK]); + render(TaskManager, { showTaskManager: true }); + + // expect the task name is visible + const task = screen.queryByText(SUCCEED_TASK.name); + expect(task).toBeInTheDocument(); + + // click on the button "Clear completed" + const clearCompletedButton = screen.getByRole('button', { name: 'Clear completed' }); + expect(clearCompletedButton).toBeInTheDocument(); + await fireEvent.click(clearCompletedButton); + + // expect the task name is not visible + const afterTask = screen.queryByText(SUCCEED_TASK.name); + expect(afterTask).not.toBeInTheDocument(); + + // button is also gone + const afterClearCompletedButton = screen.queryByRole('button', { name: 'Clear completed' }); + expect(afterClearCompletedButton).not.toBeInTheDocument(); +}); + +test('Expect click on faClose icon remove the task', async () => { + tasksInfo.set([SUCCEED_TASK]); + render(TaskManager, { showTaskManager: true }); + + // expect the task name is visible + const task = screen.queryByText(SUCCEED_TASK.name); + expect(task).toBeInTheDocument(); + + // click on the button with title "Clear notification" + const clearCompletedButton = screen.getByRole('button', { name: 'Clear notification' }); + expect(clearCompletedButton).toBeInTheDocument(); + await fireEvent.click(clearCompletedButton); + + // expect the task name is not visible + const afterTask = screen.queryByText(SUCCEED_TASK.name); + expect(afterTask).not.toBeInTheDocument(); +}); diff --git a/packages/renderer/src/lib/task-manager/TaskManager.svelte b/packages/renderer/src/lib/task-manager/TaskManager.svelte new file mode 100644 index 00000000000..2347305c038 --- /dev/null +++ b/packages/renderer/src/lib/task-manager/TaskManager.svelte @@ -0,0 +1,126 @@ + + + + + +{#if showTaskManager} +
+ +
+
+ +
+ +
+ +
+ +
tasks
+
+ + +
+
+ + {#if $tasksInfo.length > 0} +
+ + {#if runningTasks.length > 0} +
+ +
+ {/if} + + + {#if completedTasks.length > 0} +
+ +
+ {/if} +
+ {/if} + + + + {#if completedTasks.length > 0} +
+
+ + +
+
+ {/if} + + {#if $tasksInfo.length === 0} + + {/if} +
+
+{/if} diff --git a/packages/renderer/src/lib/task-manager/TaskManagerEmptyScreen.svelte b/packages/renderer/src/lib/task-manager/TaskManagerEmptyScreen.svelte new file mode 100644 index 00000000000..74e75049e45 --- /dev/null +++ b/packages/renderer/src/lib/task-manager/TaskManagerEmptyScreen.svelte @@ -0,0 +1,11 @@ + + +
+
+
+ +
You have no tasks.
+
+
diff --git a/packages/renderer/src/lib/task-manager/TaskManagerGroup.svelte b/packages/renderer/src/lib/task-manager/TaskManagerGroup.svelte new file mode 100644 index 00000000000..c5f86d2b73a --- /dev/null +++ b/packages/renderer/src/lib/task-manager/TaskManagerGroup.svelte @@ -0,0 +1,34 @@ + + + +
+
+
+
+ +
{title} ({tasks.length})
+
+
+
+
+ {#each tasks as task, index} + + + {#if !lastItem(tasks, index)} +
+ {/if} + {/each} +
+
diff --git a/packages/renderer/src/lib/task-manager/TaskManagerItem.svelte b/packages/renderer/src/lib/task-manager/TaskManagerItem.svelte new file mode 100644 index 00000000000..80777cd3e57 --- /dev/null +++ b/packages/renderer/src/lib/task-manager/TaskManagerItem.svelte @@ -0,0 +1,94 @@ + + + +
+ +
+ +
+ +
+
+
{taskUI.name}
+ +
+ + {#if taskUI.state === 'completed'} + + {/if} +
+
+ +
{taskUI.age}
+ + + {#if taskUI.status === 'in-progress'} +
+ {#if taskUI.progress >= 0} +
+
+
+
+
+
{taskUI.progress}%
+ {/if} +
+ {#if taskUI.hasGotoTask} + + {/if} +
+
+ {/if} + + + {#if taskUI.status === 'failure'} +
+
View Error >
+
+ {/if} +
+
diff --git a/packages/renderer/src/lib/task-manager/task-manager.ts b/packages/renderer/src/lib/task-manager/task-manager.ts new file mode 100644 index 00000000000..e3bef24d9e4 --- /dev/null +++ b/packages/renderer/src/lib/task-manager/task-manager.ts @@ -0,0 +1,53 @@ +/********************************************************************** + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import type { Task } from '/@/stores/tasks'; + +import humanizeDuration from 'humanize-duration'; + +export interface TaskUI extends Task { + age: string; + progress?: number; + hasGotoTask: boolean; + gotoTask?: () => void; +} + +export class TaskManager { + toTaskUi(task: Task): TaskUI { + const taskUI: TaskUI = { + id: task.id, + name: task.name, + started: task.started, + state: task.state, + status: task.status, + hasGotoTask: false, + age: `${humanizeDuration(new Date().getTime() - task.started, { round: true, largest: 1 })} ago`, + }; + + if (task.status === 'in-progress') { + taskUI.progress = task.progress; + if (task.gotoTask) { + taskUI.hasGotoTask = true; + taskUI.gotoTask = task.gotoTask; + } else { + taskUI.hasGotoTask = false; + } + } + return taskUI; + } +} diff --git a/packages/renderer/src/stores/tasks.ts b/packages/renderer/src/stores/tasks.ts new file mode 100644 index 00000000000..e56f330ac0f --- /dev/null +++ b/packages/renderer/src/stores/tasks.ts @@ -0,0 +1,66 @@ +/********************************************************************** + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import type { Writable } from 'svelte/store'; +import { writable } from 'svelte/store'; + +export interface Task { + id: string; + name: string; + started: number; + state: 'running' | 'completed'; + status: 'in-progress' | 'success' | 'failure'; + progress?: number; + gotoTask?: () => void; +} + +/** + * Defines the store used to define the tasks. + */ +export const tasksInfo: Writable = writable([]); + +// refresh the array every second +setInterval(() => { + tasksInfo.update(tasks => [...tasks]); +}, 1000); + +// remove element from the store +export function removeTask(id: string) { + tasksInfo.update(tasks => tasks.filter(task => task.id !== id)); +} + +// remove element from the store that are completed +export function clearCompletedTasks() { + tasksInfo.update(tasks => tasks.filter(task => task.state !== 'completed')); +} + +let taskId = 0; + +// create a new task +export function createTask(name: string): Task { + taskId++; + const task: Task = { + id: `${taskId}`, + name, + started: new Date().getTime(), + state: 'running', + status: 'in-progress', + }; + tasksInfo.update(tasks => [...tasks, task]); + return task; +}