diff --git a/packages/api/src/tasks-preferences.ts b/packages/api/src/tasks-preferences.ts new file mode 100644 index 0000000000000..94be17724775c --- /dev/null +++ b/packages/api/src/tasks-preferences.ts @@ -0,0 +1,22 @@ +/********************************************************************** + * Copyright (C) 2024 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 + ***********************************************************************/ + +export enum ExperimentalTasksSettings { + SectionName = 'tasks', + StatusBar = 'StatusBar', +} diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index c82df06fd9677..305a8ecccfddb 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -472,7 +472,7 @@ export class PluginSystem { const exec = new Exec(proxy); const commandRegistry = new CommandRegistry(apiSender, telemetry); - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); taskManager.init(); const notificationRegistry = new NotificationRegistry(apiSender, taskManager); diff --git a/packages/main/src/plugin/tasks/task-manager.spec.ts b/packages/main/src/plugin/tasks/task-manager.spec.ts index e2a807fd08600..75ce9e4c0f035 100644 --- a/packages/main/src/plugin/tasks/task-manager.spec.ts +++ b/packages/main/src/plugin/tasks/task-manager.spec.ts @@ -19,6 +19,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { CommandRegistry } from '/@/plugin/command-registry.js'; +import type { ConfigurationRegistry } from '/@/plugin/configuration-registry.js'; import type { StatusBarRegistry } from '/@/plugin/statusbar/statusbar-registry.js'; import type { ApiSenderType } from '../api.js'; @@ -43,12 +44,22 @@ const commandRegistry: CommandRegistry = { registerCommand: mocks.registerCommandMock, } as unknown as CommandRegistry; +const configurationRegistry: ConfigurationRegistry = { + registerConfigurations: vi.fn(), +} as unknown as ConfigurationRegistry; + beforeEach(() => { vi.resetAllMocks(); }); +test('task manager init should register a configuration option', async () => { + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); + taskManager.init(); + expect(configurationRegistry.registerConfigurations).toHaveBeenCalledOnce(); +}); + test('create task with title', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createTask({ title: 'title' }); expect(task.id).equal('task-1'); expect(task.name).equal('title'); @@ -64,7 +75,7 @@ test('create task with title', async () => { }); test('create task without title', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createTask(); expect(task.id).equal('task-1'); expect(task.name).equal('Task 1'); @@ -80,7 +91,7 @@ test('create task without title', async () => { }); test('create multiple tasks with title', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createTask({ title: 'title' }); expect(task.id).equal('task-1'); expect(task.name).equal('title'); @@ -122,7 +133,7 @@ test('create multiple tasks with title', async () => { }); test('create notification task with body', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createNotificationTask({ title: 'title', body: 'body', @@ -143,7 +154,7 @@ test('create notification task with body', async () => { }); test('create task without body', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createNotificationTask({ title: 'title', }); @@ -163,7 +174,7 @@ test('create task without body', async () => { }); test('create task with markdown actions', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createNotificationTask({ title: 'title', markdownActions: 'action', @@ -184,7 +195,7 @@ test('create task with markdown actions', async () => { }); test('create multiple stateful tasks with title', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createNotificationTask({ title: 'title', }); @@ -238,7 +249,7 @@ test('create multiple stateful tasks with title', async () => { }); test('clear tasks should clear task not in running state', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task1 = taskManager.createTask({ title: 'Task 1' }); task1.status = 'success'; @@ -273,7 +284,7 @@ test('clear tasks should clear task not in running state', async () => { describe('execute', () => { test('execute should throw an error if the task does not exist', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); expect(() => { taskManager.execute('fake-id'); @@ -281,7 +292,7 @@ describe('execute', () => { }); test('execute should throw an error if the task has no action', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createTask({ title: 'Task 1' }); expect(() => { @@ -290,7 +301,7 @@ describe('execute', () => { }); test('execute should execute the task execute function', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createTask({ title: 'Task 1' }); task.action = { @@ -304,7 +315,7 @@ describe('execute', () => { }); test('updating a task should notify apiSender', () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createTask({ title: 'Task 1' }); expect(apiSenderSendMock).toHaveBeenCalledWith('task-created', expect.anything()); @@ -322,7 +333,7 @@ test('updating a task should notify apiSender', () => { }); test('Ensure init setup command and statusbar registry', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); taskManager.init(); expect(mocks.registerCommandMock).toHaveBeenCalledOnce(); @@ -330,7 +341,7 @@ test('Ensure init setup command and statusbar registry', async () => { }); test('Ensure statusbar registry', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); taskManager.createTask({ title: 'Dummy Task' }); @@ -349,7 +360,7 @@ test('Ensure statusbar registry', async () => { }); test('task dispose should send `task-removed` message', async () => { - const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry); + const taskManager = new TaskManager(apiSender, statusBarRegistry, commandRegistry, configurationRegistry); const task = taskManager.createNotificationTask({ title: 'title', body: 'body', diff --git a/packages/main/src/plugin/tasks/task-manager.ts b/packages/main/src/plugin/tasks/task-manager.ts index a7d61a3dec6b4..71493fe6c1660 100644 --- a/packages/main/src/plugin/tasks/task-manager.ts +++ b/packages/main/src/plugin/tasks/task-manager.ts @@ -18,11 +18,13 @@ import type { NotificationOptions } from '@podman-desktop/api'; +import type { ConfigurationRegistry } from '/@/plugin/configuration-registry.js'; import { NotificationImpl } from '/@/plugin/tasks/notification-impl.js'; import type { NotificationTask } from '/@/plugin/tasks/notifications.js'; import { TaskImpl } from '/@/plugin/tasks/task-impl.js'; import type { Task, TaskAction, TaskUpdateEvent } from '/@/plugin/tasks/tasks.js'; import type { NotificationTaskInfo, TaskInfo } from '/@api/taskInfo.js'; +import { ExperimentalTasksSettings } from '/@api/tasks-preferences.js'; import type { ApiSenderType } from '../api.js'; import type { CommandRegistry } from '../command-registry.js'; @@ -37,6 +39,7 @@ export class TaskManager { private apiSender: ApiSenderType, private statusBarRegistry: StatusBarRegistry, private commandRegistry: CommandRegistry, + private configurationRegistry: ConfigurationRegistry, ) {} public init(): void { @@ -47,6 +50,21 @@ export class TaskManager { this.apiSender.send('toggle-task-manager', ''); this.setStatusBarEntry(false); }); + + this.configurationRegistry.registerConfigurations([ + { + id: 'preferences.experimental.tasks', + title: 'Tasks', + type: 'object', + properties: { + [`${ExperimentalTasksSettings.SectionName}.${ExperimentalTasksSettings.StatusBar}`]: { + description: 'Show running tasks in the status bar', + type: 'boolean', + default: false, + }, + }, + }, + ]); } private setStatusBarEntry(highlight: boolean): void { diff --git a/packages/renderer/src/App.spec.ts b/packages/renderer/src/App.spec.ts index c2f0f1ef6a2a9..92bdc1ee1f020 100644 --- a/packages/renderer/src/App.spec.ts +++ b/packages/renderer/src/App.spec.ts @@ -91,6 +91,7 @@ beforeEach(() => { }), }; (window as any).dispatchEvent = dispatchEventMock; + (window.getConfigurationValue as unknown) = vi.fn(); vi.mocked(kubeContextStore).kubernetesCurrentContextState = readable({ reachable: false, error: 'initializing', diff --git a/packages/renderer/src/lib/statusbar/StatusBar.spec.ts b/packages/renderer/src/lib/statusbar/StatusBar.spec.ts new file mode 100644 index 0000000000000..b314be3fb5829 --- /dev/null +++ b/packages/renderer/src/lib/statusbar/StatusBar.spec.ts @@ -0,0 +1,69 @@ +/********************************************************************** + * Copyright (C) 2024 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 { render } from '@testing-library/svelte'; +import { beforeEach, expect, test, vi } from 'vitest'; + +import StatusBar from '/@/lib/statusbar/StatusBar.svelte'; +import { statusBarEntries } from '/@/stores/statusbar'; +import { tasksInfo } from '/@/stores/tasks'; +import { ExperimentalTasksSettings } from '/@api/tasks-preferences'; + +beforeEach(() => { + (window.getConfigurationValue as unknown) = vi.fn(); + + // reset stores + statusBarEntries.set([]); + tasksInfo.set([ + { + name: 'Dummy Task', + state: 'running', + status: 'in-progress', + started: 0, + id: 'dummy-task', + }, + ]); +}); + +test('onMount should call getConfigurationValue', () => { + render(StatusBar); + + expect(window.getConfigurationValue).toHaveBeenCalledWith( + `${ExperimentalTasksSettings.SectionName}.${ExperimentalTasksSettings.StatusBar}`, + ); +}); + +test('tasks should be visible when getConfigurationValue is true', async () => { + vi.mocked(window.getConfigurationValue).mockResolvedValue(true); + + const { getByRole } = render(StatusBar); + + await vi.waitFor(() => { + const status = getByRole('status'); + expect(status).toBeDefined(); + expect(status.textContent).toBe('Dummy Task'); + }); +}); + +test('tasks should not be visible when getConfigurationValue is false', () => { + vi.mocked(window.getConfigurationValue).mockResolvedValue(false); + + const { queryByRole } = render(StatusBar); + const status = queryByRole('status'); + expect(status).toBeNull(); +}); diff --git a/packages/renderer/src/lib/statusbar/StatusBar.svelte b/packages/renderer/src/lib/statusbar/StatusBar.svelte index 2960b245786bc..daf9fc6b63118 100644 --- a/packages/renderer/src/lib/statusbar/StatusBar.svelte +++ b/packages/renderer/src/lib/statusbar/StatusBar.svelte @@ -1,12 +1,17 @@ @@ -60,5 +70,8 @@ onMount(async () => { {#each rightEntries as entry} {/each} + {#if experimentalTaskStatusBar} + + {/if} diff --git a/packages/renderer/src/lib/statusbar/TaskIndicator.spec.ts b/packages/renderer/src/lib/statusbar/TaskIndicator.spec.ts new file mode 100644 index 0000000000000..ba2b2c27286ad --- /dev/null +++ b/packages/renderer/src/lib/statusbar/TaskIndicator.spec.ts @@ -0,0 +1,100 @@ +/********************************************************************** + * Copyright (C) 2024 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 { fireEvent, render } from '@testing-library/svelte'; +import { beforeAll, expect, test, vi } from 'vitest'; + +import TaskIndicator from '/@/lib/statusbar/TaskIndicator.svelte'; +import { tasksInfo } from '/@/stores/tasks'; + +beforeAll(() => { + (window.events as unknown) = { + send: vi.fn(), + }; + // reset store + tasksInfo.set([]); +}); + +test('should not be visible when no running tasks', async () => { + const { queryByRole } = render(TaskIndicator); + const status = queryByRole('status'); + expect(status).toBeNull(); +}); + +test('clicking on task should send task manager toggle event', async () => { + tasksInfo.set([ + { + name: 'Dummy Task', + state: 'running', + status: 'in-progress', + started: 0, + id: 'dummy-task', + }, + ]); + + const { getByRole } = render(TaskIndicator); + const button = getByRole('button', { name: 'Toggle Task Manager' }); + expect(button).toBeDefined(); + + await fireEvent.click(button); + + await vi.waitFor(() => { + expect(window.events.send).toHaveBeenCalledWith('toggle-task-manager', ''); + }); +}); + +test('one task running should display it', async () => { + tasksInfo.set([ + { + name: 'Dummy Task', + state: 'running', + status: 'in-progress', + started: 0, + id: 'dummy-task', + }, + ]); + + const { getByRole } = render(TaskIndicator); + const status = getByRole('status'); + expect(status).toBeDefined(); + expect(status.textContent).toBe('Dummy Task'); +}); + +test('multiple tasks running should display them', async () => { + tasksInfo.set([ + { + name: 'Foo Task', + state: 'running', + status: 'in-progress', + started: 0, + id: 'foo-task', + }, + { + name: 'Bar Task', + state: 'running', + status: 'in-progress', + started: 0, + id: 'foo-task', + }, + ]); + + const { getByRole } = render(TaskIndicator); + const status = getByRole('status'); + expect(status).toBeDefined(); + expect(status.textContent).toBe('2 tasks running'); +}); diff --git a/packages/renderer/src/lib/statusbar/TaskIndicator.svelte b/packages/renderer/src/lib/statusbar/TaskIndicator.svelte new file mode 100644 index 0000000000000..b74dcc7ebf3f6 --- /dev/null +++ b/packages/renderer/src/lib/statusbar/TaskIndicator.svelte @@ -0,0 +1,36 @@ + + +{#if runningTasks.length > 0} + + + + {title} + {#if (progress ?? 0) >= 0} + + {/if} + + + +{/if}