Skip to content

Commit

Permalink
feat(UI): task indicator (podman-desktop#9186)
Browse files Browse the repository at this point in the history
* feat(UI): task indicator

Signed-off-by: axel7083 <[email protected]>

* feat: applying comments from UX/UI call

Signed-off-by: axel7083 <[email protected]>

* fix: apply code review suggestion from @deboer-tim

Signed-off-by: axel7083 <[email protected]>

---------

Signed-off-by: axel7083 <[email protected]>
  • Loading branch information
axel7083 authored Oct 11, 2024
1 parent 4dfdbf4 commit d7acec1
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 19 deletions.
22 changes: 22 additions & 0 deletions packages/api/src/tasks-preferences.ts
Original file line number Diff line number Diff line change
@@ -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',
}
2 changes: 1 addition & 1 deletion packages/main/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
41 changes: 26 additions & 15 deletions packages/main/src/plugin/tasks/task-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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',
Expand All @@ -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',
});
Expand All @@ -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',
Expand All @@ -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',
});
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -273,15 +284,15 @@ 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');
}).toThrowError(`task with id fake-id does not exist.`);
});

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(() => {
Expand All @@ -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 = {
Expand All @@ -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());
Expand All @@ -322,15 +333,15 @@ 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();
expect(mocks.setEntryMock).toHaveBeenCalledOnce();
});

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' });

Expand All @@ -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',
Expand Down
18 changes: 18 additions & 0 deletions packages/main/src/plugin/tasks/task-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -37,6 +39,7 @@ export class TaskManager {
private apiSender: ApiSenderType,
private statusBarRegistry: StatusBarRegistry,
private commandRegistry: CommandRegistry,
private configurationRegistry: ConfigurationRegistry,
) {}

public init(): void {
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/renderer/src/App.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
69 changes: 69 additions & 0 deletions packages/renderer/src/lib/statusbar/StatusBar.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
19 changes: 16 additions & 3 deletions packages/renderer/src/lib/statusbar/StatusBar.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
<script lang="ts">
import { onMount } from 'svelte';
import TaskIndicator from '/@/lib/statusbar/TaskIndicator.svelte';
import { statusBarEntries } from '/@/stores/statusbar';
import { ExperimentalTasksSettings } from '/@api/tasks-preferences';
import type { StatusBarEntry } from '../../../../main/src/plugin/statusbar/statusbar-registry';
import { statusBarEntries } from '../../stores/statusbar';
import StatusBarItem from './StatusBarItem.svelte';
let leftEntries: StatusBarEntry[] = [];
let rightEntries: StatusBarEntry[] = [];
let leftEntries: StatusBarEntry[] = $state([]);
let rightEntries: StatusBarEntry[] = $state([]);
let experimentalTaskStatusBar: boolean = $state(false);
onMount(async () => {
statusBarEntries.subscribe(value => {
Expand Down Expand Up @@ -44,6 +49,11 @@ onMount(async () => {
return descriptor.entry;
});
});
experimentalTaskStatusBar =
(await window.getConfigurationValue<boolean>(
`${ExperimentalTasksSettings.SectionName}.${ExperimentalTasksSettings.StatusBar}`,
)) ?? false;
});
</script>

Expand All @@ -60,5 +70,8 @@ onMount(async () => {
{#each rightEntries as entry}
<StatusBarItem entry={entry} />
{/each}
{#if experimentalTaskStatusBar}
<TaskIndicator />
{/if}
</div>
</div>
Loading

0 comments on commit d7acec1

Please sign in to comment.