mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-05-24 10:18:53 +00:00
feat: add task manager widget (#1724)
* feat: add task manager widget fixes https://github.com/containers/podman-desktop/issues/1411 Change-Id: Ifa90b6c9ee40d3a239f509dd8b4148bf33f084aa Signed-off-by: Florent Benoit <fbenoit@redhat.com>
This commit is contained in:
parent
d2628191b4
commit
efb0d8496c
11 changed files with 605 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
<div class="z-0 w-full h-full bg-zinc-800 flex flex-col overflow-y-scroll">
|
||||
<TaskManager />
|
||||
<SendFeedback />
|
||||
<ToastHandler />
|
||||
<QuickPickInput />
|
||||
|
|
|
|||
26
packages/renderer/src/lib/images/BellSlashIcon.svelte
Normal file
26
packages/renderer/src/lib/images/BellSlashIcon.svelte
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
export let size: string = '40';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="{size}"
|
||||
height="{size}"
|
||||
class="{$$props.class}"
|
||||
style="{$$props.style}"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="screenshot-03ba2b93-cedd-80a6-8002-080e6e29745f"
|
||||
viewBox="-0 -0 20 16"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
><g id="shape-03ba2b93-cedd-80a6-8002-080e6e29745f" rx="0" ry="0" style="opacity: 0.75; fill: rgb(0, 0, 0);"
|
||||
><g id="shape-03ba2b93-cedd-80a6-8002-080e6e29747f"
|
||||
><g class="fills" id="fills-03ba2b93-cedd-80a6-8002-080e6e29747f"
|
||||
><path
|
||||
rx="0"
|
||||
ry="0"
|
||||
d="M5.737,3.706C6.453,2.581,7.628,1.776,9.000,1.559L9.000,1.000C9.000,0.448,9.447,0.000,10.000,0.000C10.553,0.000,11.000,0.448,11.000,1.000L11.000,1.559C13.266,1.918,15.000,3.881,15.000,6.250L15.000,7.294C15.000,8.712,15.484,10.091,16.369,11.200L16.834,11.781C16.972,11.984,17.044,12.256,16.956,12.500L19.712,14.659C20.037,14.915,20.097,15.387,19.840,15.712C19.584,16.037,19.112,16.097,18.787,15.840L0.287,1.340C-0.039,1.085,-0.096,0.613,0.160,0.287C0.415,-0.039,0.887,-0.096,1.213,0.160L5.737,3.706ZZM6.928,4.637L14.084,10.222C13.700,9.319,13.500,8.316,13.500,7.294L13.500,6.250C13.500,4.456,12.044,3.000,10.250,3.000L9.750,3.000C8.541,3.000,7.487,3.659,6.928,4.637ZL6.928,4.637ZZM5.000,7.294L5.000,6.941L6.459,8.091C6.334,9.303,5.922,10.475,5.259,11.500L10.787,11.500L12.694,13.000L3.750,13.000C3.462,13.000,3.200,12.834,3.074,12.575C2.949,12.315,2.984,12.006,3.166,11.781L3.631,11.200C4.516,10.091,5.000,8.712,5.000,7.294ZL5.000,7.294ZZM12.000,14.000C12.000,14.503,11.791,15.040,11.416,15.415C11.041,15.790,10.503,16.000,10.000,16.000C9.469,16.000,8.959,15.790,8.584,15.415C8.209,15.040,8.000,14.503,8.000,14.000L12.000,14.000ZZ"
|
||||
style="fill: rgb(255, 255, 255); fill-opacity: 1;"></path
|
||||
></g
|
||||
></g
|
||||
></g
|
||||
></svg>
|
||||
33
packages/renderer/src/lib/images/TaskIcon.svelte
Normal file
33
packages/renderer/src/lib/images/TaskIcon.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
export let size: string = '40';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="{size}"
|
||||
height="{size}"
|
||||
class="{$$props.class}"
|
||||
style="{$$props.style}"
|
||||
viewBox="-0.5 -0.5 16 16"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
><g id="shape-03ba2b93-cedd-80a6-8002-080fc568c1f9" rx="0" ry="0" style="opacity: 0.75; fill: rgb(0, 0, 0);"
|
||||
><g id="shape-03ba2b93-cedd-80a6-8002-080fc56918fe"
|
||||
><g class="fills" id="fills-03ba2b93-cedd-80a6-8002-080fc56918fe"
|
||||
><path
|
||||
rx="0"
|
||||
ry="0"
|
||||
d="M13.125,4.688L9.375,4.688L9.375,3.750L13.125,3.750L13.125,4.688ZZM1.406,1.875C0.630,1.875,0.000,2.505,0.000,3.281L0.000,5.156C0.000,5.933,0.630,6.563,1.406,6.563L13.594,6.563C14.370,6.563,15.000,5.933,15.000,5.156L15.000,3.281C15.000,2.505,14.370,1.875,13.594,1.875L1.406,1.875ZZM13.125,10.313L13.125,11.250L5.625,11.250L5.625,10.313L13.125,10.313ZZM1.406,8.438C0.630,8.438,0.000,9.067,0.000,9.844L0.000,11.719C0.000,12.495,0.630,13.125,1.406,13.125L13.594,13.125C14.370,13.125,15.000,12.495,15.000,11.719L15.000,9.844C15.000,9.067,14.370,8.438,13.594,8.438L1.406,8.438ZZ"
|
||||
style="fill: rgb(177, 178, 181); fill-opacity: 0;"></path
|
||||
></g
|
||||
><g id="strokes-03ba2b93-cedd-80a6-8002-080fc56918fe" class="strokes"
|
||||
><g class="stroke-shape"
|
||||
><path
|
||||
rx="0"
|
||||
ry="0"
|
||||
d="M13.125,4.688L9.375,4.688L9.375,3.750L13.125,3.750L13.125,4.688ZZM1.406,1.875C0.630,1.875,0.000,2.505,0.000,3.281L0.000,5.156C0.000,5.933,0.630,6.563,1.406,6.563L13.594,6.563C14.370,6.563,15.000,5.933,15.000,5.156L15.000,3.281C15.000,2.505,14.370,1.875,13.594,1.875L1.406,1.875ZZM13.125,10.313L13.125,11.250L5.625,11.250L5.625,10.313L13.125,10.313ZZM1.406,8.438C0.630,8.438,0.000,9.067,0.000,9.844L0.000,11.719C0.000,12.495,0.630,13.125,1.406,13.125L13.594,13.125C14.370,13.125,15.000,12.495,15.000,11.719L15.000,9.844C15.000,9.067,14.370,8.438,13.594,8.438L1.406,8.438ZZ"
|
||||
style="fill: none; stroke-width: 1; stroke: rgb(255, 255, 255); stroke-opacity: 1;"></path
|
||||
></g
|
||||
></g
|
||||
></g
|
||||
></g
|
||||
></svg>
|
||||
145
packages/renderer/src/lib/task-manager/TaskManager.spec.ts
Normal file
145
packages/renderer/src/lib/task-manager/TaskManager.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
126
packages/renderer/src/lib/task-manager/TaskManager.svelte
Normal file
126
packages/renderer/src/lib/task-manager/TaskManager.svelte
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
faCheck,
|
||||
faCheckCircle,
|
||||
faCheckDouble,
|
||||
faCheckSquare,
|
||||
faChevronDown,
|
||||
faCircle,
|
||||
faInfoCircle,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { clearCompletedTasks, tasksInfo } from '/@/stores/tasks';
|
||||
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import BellSlashIcon from '../images/BellSlashIcon.svelte';
|
||||
import TaskIcon from '../images/TaskIcon.svelte';
|
||||
import TaskManagerEmptyScreen from './TaskManagerEmptyScreen.svelte';
|
||||
import TaskManagerGroup from './TaskManagerGroup.svelte';
|
||||
|
||||
// display or not the tasks manager (defaut is false)
|
||||
export let showTaskManager = false;
|
||||
|
||||
$: runningTasks = $tasksInfo.filter(task => task.state === 'running');
|
||||
$: completedTasks = $tasksInfo.filter(task => task.state === 'completed');
|
||||
|
||||
// hide the task manager
|
||||
function hide() {
|
||||
showTaskManager = false;
|
||||
}
|
||||
|
||||
function clearCompleted() {
|
||||
// needs to delete the task from the svelte store
|
||||
clearCompletedTasks();
|
||||
}
|
||||
|
||||
// If we hit ESC while the menu is open, close it
|
||||
function handleEscape({ key }) {
|
||||
// if the task manager is not open, do not check any keys
|
||||
if (!showTaskManager) {
|
||||
return;
|
||||
}
|
||||
if (key === 'Escape') {
|
||||
showTaskManager = false;
|
||||
}
|
||||
}
|
||||
|
||||
// listen to the event "toggle-task-manager" to toggle the task manager
|
||||
window.events?.receive('toggle-task-manager', () => {
|
||||
showTaskManager = !showTaskManager;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- track keys like "ESC" -->
|
||||
<svelte:window on:keyup="{handleEscape}" />
|
||||
|
||||
{#if showTaskManager}
|
||||
<div title="Tasks manager" class="fixed bottom-9 right-4 bg-zinc-900 h-96 w-80 z-40 ">
|
||||
<!-- Draw the arrow below the box-->
|
||||
<div
|
||||
class="absolute bottom-0 z-50 right-[17px] transform -translate-x-1/2 translate-y-1/2 rotate-45 w-4 h-4 {$tasksInfo.length >
|
||||
0
|
||||
? 'bg-zinc-900'
|
||||
: 'bg-zinc-800'} border-r border-b border-zinc-600 ">
|
||||
</div>
|
||||
|
||||
<div title="" class="flex flex-col h-full w-full border border-zinc-600">
|
||||
<!-- header of the task manager -->
|
||||
<div class="flex flex-row w-full">
|
||||
<!-- title of bars-->
|
||||
<div class="p-2 flex flex-row items-center w-full text-gray-300">
|
||||
<TaskIcon size="15" />
|
||||
<div class="text-xs uppercase ml-2">tasks</div>
|
||||
<div class="flex-1"></div>
|
||||
<!--
|
||||
<div title="Toggle Do Not Disturb Mode" class="cursor-pointer hover:bg-zinc-800 p-1">
|
||||
<BellSlashIcon size="15" />
|
||||
</div>
|
||||
-->
|
||||
<button on:click="{() => hide()}" title="Hide (Escape)" class="cursor-pointer hover:bg-zinc-800 p-1 ml-1">
|
||||
<Fa icon="{faChevronDown}" size="15" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $tasksInfo.length > 0}
|
||||
<div class="flex flex-col grow h-[100px] overflow-y-auto">
|
||||
<!-- running tasks-->
|
||||
{#if runningTasks.length > 0}
|
||||
<div class="flex bg-zinc-700 px-4">
|
||||
<TaskManagerGroup
|
||||
lineColor="bg-zinc-800"
|
||||
icon="{faCircle}"
|
||||
tasks="{runningTasks}"
|
||||
title="running tasks" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- completed tasks-->
|
||||
{#if completedTasks.length > 0}
|
||||
<div class="flex bg-zinc-800 pt-1 px-4">
|
||||
<TaskManagerGroup
|
||||
lineColor="bg-zinc-400"
|
||||
icon="{faCheck}"
|
||||
tasks="{completedTasks}"
|
||||
title="completed tasks" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- footer of the task manager -->
|
||||
<!-- only if there are tasks-->
|
||||
{#if completedTasks.length > 0}
|
||||
<div class="flex flex-row w-full">
|
||||
<div class="p-2 flex flex-row space-x-2 w-full text-gray-300">
|
||||
<button on:click="{() => clearCompleted()}" class="pf-c-button pf-m-secondary">Clear completed</button>
|
||||
<!--<button class="pf-c-button pf-m-secondary">View task history</button>-->
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- display the empty screen -->
|
||||
{#if $tasksInfo.length === 0}
|
||||
<TaskManagerEmptyScreen />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<script>
|
||||
import TaskIcon from '../images/TaskIcon.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row grow bg-zinc-800 items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class=""><TaskIcon /></div>
|
||||
|
||||
<div class="mt-4">You have no tasks.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import TaskManagerItem from './TaskManagerItem.svelte';
|
||||
import type { Task } from '/@/stores/tasks';
|
||||
|
||||
export let icon;
|
||||
export let tasks: Task[];
|
||||
export let title;
|
||||
export let lineColor;
|
||||
|
||||
// check if the item is the last one
|
||||
let lastItem = (a: unknown[], i: number) => i == a.length - 1;
|
||||
</script>
|
||||
|
||||
<!-- Display a title and then the list of the tasks -->
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-row items-center w-full flex-nowrap">
|
||||
<hr class="w-3 h-[2px] my-3 {lineColor} border-0" />
|
||||
<div class="flex mx-2 flex-row items-center">
|
||||
<Fa class="mr-1 text-purple-300" size="7" icon="{icon}" />
|
||||
<div class="flex-nowrap uppercase font-bold text-xs">{title} ({tasks.length})</div>
|
||||
</div>
|
||||
<hr class="flex-grow flex w-max h-[2px] {lineColor} border-0" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
{#each tasks as task, index}
|
||||
<TaskManagerItem task="{task}" />
|
||||
<!-- only if there are more items-->
|
||||
{#if !lastItem(tasks, index)}
|
||||
<hr class="w-full h-[1px] border-0 {lineColor}" />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts">
|
||||
import { faClose, faInfoCircle, faSquareCheck, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import Fa from 'svelte-fa/src/fa.svelte';
|
||||
import { TaskManager, TaskUI } from './task-manager';
|
||||
import { removeTask, Task } from '/@/stores/tasks';
|
||||
|
||||
export let task: Task;
|
||||
|
||||
const taskManager = new TaskManager();
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let taskUI: TaskUI;
|
||||
$: taskUI = taskManager.toTaskUi(task);
|
||||
|
||||
let icon;
|
||||
let iconColor;
|
||||
onMount(() => {
|
||||
if (task.status === 'success') {
|
||||
icon = faSquareCheck;
|
||||
iconColor = 'text-green-600';
|
||||
} else if (task.status === 'failure') {
|
||||
icon = faTriangleExclamation;
|
||||
iconColor = 'text-red-500';
|
||||
} else {
|
||||
icon = faInfoCircle;
|
||||
iconColor = 'text-purple-500';
|
||||
}
|
||||
});
|
||||
|
||||
function closeCompleted(taskUI: TaskUI) {
|
||||
// needs to delete the task from the svelte store
|
||||
removeTask(taskUI.id);
|
||||
}
|
||||
|
||||
function gotoTask(taskUI: TaskUI) {
|
||||
// hide the task manager
|
||||
window.events?.send('toggle-task-manager', '');
|
||||
// and open the task
|
||||
taskUI?.gotoTask();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Display a task item-->
|
||||
<div class="flex flew-row w-full py-2">
|
||||
<!-- first column is the icon-->
|
||||
<div class="flex w-3 {iconColor} justify-center">
|
||||
<Fa size="14" icon="{icon}" />
|
||||
</div>
|
||||
<!-- second column is about the task-->
|
||||
<div class="flex flex-col w-full pl-2">
|
||||
<div class="flex flex-row w-full">
|
||||
<div title="{taskUI.name}" class="w-60 pb-1 cursor-default truncate">{taskUI.name}</div>
|
||||
|
||||
<div class="flex flex-col flex-grow items-end">
|
||||
<!-- if completed task, display a close icon-->
|
||||
{#if taskUI.state === 'completed'}
|
||||
<button
|
||||
title="Clear notification"
|
||||
class="hover:bg-zinc-900 hover:text-purple-500"
|
||||
on:click="{() => closeCompleted(taskUI)}"><Fa size="12" icon="{faClose}" /></button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- age -->
|
||||
<div class="text-gray-400 text-xs">{taskUI.age}</div>
|
||||
|
||||
<!-- if in-progress task, display a link to resume-->
|
||||
{#if taskUI.status === 'in-progress'}
|
||||
<div class="flex flex-row w-full">
|
||||
{#if taskUI.progress >= 0}
|
||||
<div class="w-32">
|
||||
<div class="w-full h-4 mb-4 rounded-full bg-gray-600">
|
||||
<div class="h-4 bg-purple-500 rounded-full " style="width: {taskUI.progress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-2 text-xs">{taskUI.progress}%</div>
|
||||
{/if}
|
||||
<div class="flex flex-1 flex-col w-full items-end text-purple-500 text-xs">
|
||||
{#if taskUI.hasGotoTask}
|
||||
<button class="text-purple-500 cursor-pointer" on:click="{() => gotoTask(taskUI)}">Go to task ></button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- if failed task, display the error-->
|
||||
{#if taskUI.status === 'failure'}
|
||||
<div class="flex flex-col w-full items-end text-purple-300 text-xs">
|
||||
<div>View Error ></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
53
packages/renderer/src/lib/task-manager/task-manager.ts
Normal file
53
packages/renderer/src/lib/task-manager/task-manager.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
66
packages/renderer/src/stores/tasks.ts
Normal file
66
packages/renderer/src/stores/tasks.ts
Normal file
|
|
@ -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<Task[]> = 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;
|
||||
}
|
||||
Loading…
Reference in a new issue