diff --git a/packages/main/src/mainWindow.ts b/packages/main/src/mainWindow.ts index c4abc4738..dbe9ef375 100644 --- a/packages/main/src/mainWindow.ts +++ b/packages/main/src/mainWindow.ts @@ -1,5 +1,5 @@ /********************************************************************** - * Copyright (C) 2022 Red Hat, Inc. + * Copyright (C) 2022-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. @@ -24,11 +24,13 @@ import { app, autoUpdater, BrowserWindow, ipcMain, Menu, nativeTheme, screen } f import contextMenu from 'electron-context-menu'; import { aboutMenuItem } from 'electron-util/main'; +import { NavigationItemsMenuBuilder } from './navigation-items-menu-builder.js'; import { OpenDevTools } from './open-dev-tools.js'; import type { ConfigurationRegistry } from './plugin/configuration-registry.js'; import { isLinux, isMac, stoppedExtensions } from './util.js'; const openDevTools = new OpenDevTools(); +let navigationItemsMenuBuilder: NavigationItemsMenuBuilder; async function createWindow(): Promise { const INITIAL_APP_WIDTH = 1050; @@ -104,10 +106,17 @@ async function createWindow(): Promise { ipcMain.on('configuration-registry', (_, data) => { configurationRegistry = data; + navigationItemsMenuBuilder = new NavigationItemsMenuBuilder(configurationRegistry); + // open dev tools (if required) openDevTools.open(browserWindow, configurationRegistry); }); + // receive the navigation items + ipcMain.handle('navigation:sendNavigationItems', (_, data) => { + navigationItemsMenuBuilder?.receiveNavigationItems(data); + }); + // receive the message because an update is in progress and we need to quit the app let quitAfterUpdate = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -196,6 +205,9 @@ async function createWindow(): Promise { return []; } }, + append: (_defaultActions, parameters) => { + return navigationItemsMenuBuilder?.buildNavigationMenu(parameters) ?? []; + }, }); // Add help/about menu entry diff --git a/packages/main/src/navigation-items-menu-builder.spec.ts b/packages/main/src/navigation-items-menu-builder.spec.ts new file mode 100644 index 000000000..fbbf8844e --- /dev/null +++ b/packages/main/src/navigation-items-menu-builder.spec.ts @@ -0,0 +1,167 @@ +/********************************************************************** + * 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 type { BrowserWindow, ContextMenuParams, MenuItem, MenuItemConstructorOptions } from 'electron'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +import { NavigationItemsMenuBuilder } from './navigation-items-menu-builder.js'; +import type { ConfigurationRegistry } from './plugin/configuration-registry.js'; + +let navigationItemsMenuBuilder: TestNavigationItemsMenuBuilder; + +const getConfigurationMock = vi.fn(); +const configurationRegistryMock = { + getConfiguration: getConfigurationMock, + updateConfigurationValue: vi.fn(), +} as unknown as ConfigurationRegistry; + +const browserWindowMock = { + webContents: {}, +} as unknown as BrowserWindow; + +class TestNavigationItemsMenuBuilder extends NavigationItemsMenuBuilder { + buildHideMenuItem(linkText: string): MenuItemConstructorOptions | undefined { + return super.buildHideMenuItem(linkText); + } + buildNavigationToggleMenuItems(): MenuItemConstructorOptions[] { + return super.buildNavigationToggleMenuItems(); + } +} + +beforeEach(() => { + vi.resetAllMocks(); + navigationItemsMenuBuilder = new TestNavigationItemsMenuBuilder(configurationRegistryMock); +}); + +describe('buildHideMenuItem', async () => { + test('build hide item', async () => { + getConfigurationMock.mockReturnValue({ get: () => [] } as unknown as ConfigurationRegistry); + + const menu = navigationItemsMenuBuilder.buildHideMenuItem('Hello'); + expect(menu?.label).toBe(`Hide 'Hello'`); + expect(menu?.click).toBeDefined(); + expect(menu?.visible).toBe(true); + + // click on the menu + menu?.click?.({} as MenuItem, browserWindowMock, {} as unknown as KeyboardEvent); + + expect(getConfigurationMock).toBeCalled(); + // if clicking it should send the item to the configuration as being disabled + expect(configurationRegistryMock.updateConfigurationValue).toBeCalledWith( + 'navbar.disabledItems', + ['Hello'], + 'DEFAULT', + ); + }); + + test('should not create a menu item if in excluded list', async () => { + getConfigurationMock.mockReturnValue({ get: () => [] } as unknown as ConfigurationRegistry); + + const menu = navigationItemsMenuBuilder.buildHideMenuItem('Accounts'); + expect(menu).toBeUndefined(); + }); +}); + +describe('buildNavigationToggleMenuItems', async () => { + test('build navigation toggle menu items', async () => { + getConfigurationMock.mockReturnValue({ get: () => ['existing'] } as unknown as ConfigurationRegistry); + + // send 3 items, two being visible, one being hidden + navigationItemsMenuBuilder.receiveNavigationItems([ + { name: 'A & A', visible: true }, + { name: 'B', visible: false }, + { name: 'C', visible: true }, + ]); + + const menu = navigationItemsMenuBuilder.buildNavigationToggleMenuItems(); + + // 4 items (first one being a separator) + expect(menu.length).toBe(4); + + // check the first item is a separator + expect(menu[0].type).toBe('separator'); + + // label should be escaped as we have an & + expect(menu[1].label).toBe('A && A'); + expect(menu[1].checked).toBe(true); + expect(menu[2].label).toBe('B'); + expect(menu[2].checked).toBe(false); + expect(menu[3].label).toBe('C'); + expect(menu[3].checked).toBe(true); + + // click on the A item + menu[1].click?.({} as MenuItem, browserWindowMock, {} as unknown as KeyboardEvent); + + expect(getConfigurationMock).toBeCalled(); + // if clicking it should send the item to the configuration as being disabled + expect(configurationRegistryMock.updateConfigurationValue).toBeCalledWith( + 'navbar.disabledItems', + // item A & A should not be escaped + ['existing', 'A & A'], + 'DEFAULT', + ); + + // reset the calls + vi.mocked(configurationRegistryMock.updateConfigurationValue).mockClear(); + + // click on the B item should unhide it so disabled items should be empty + menu[2].click?.({} as MenuItem, browserWindowMock, {} as unknown as KeyboardEvent); + expect(configurationRegistryMock.updateConfigurationValue).toBeCalledWith( + 'navbar.disabledItems', + ['existing'], + 'DEFAULT', + ); + }); +}); + +describe('buildNavigationMenu', async () => { + test('no items if no linktext', async () => { + const parameters = {} as unknown as ContextMenuParams; + + const menu = navigationItemsMenuBuilder.buildNavigationMenu(parameters); + + expect(menu).toStrictEqual([]); + }); + + test('no items if outside of range of navbar', async () => { + const parameters = { + linkText: 'outside', + x: 0, + y: 0, + } as unknown as ContextMenuParams; + + const menu = navigationItemsMenuBuilder.buildNavigationMenu(parameters); + + expect(menu).toStrictEqual([]); + }); + + test('should call the build if inside range of navbar', async () => { + const spyMock = vi.spyOn(navigationItemsMenuBuilder, 'buildHideMenuItem'); + spyMock.mockReturnValue({} as MenuItemConstructorOptions); + const parameters = { + linkText: 'inside', + x: 30, + y: 100, + } as unknown as ContextMenuParams; + + const menu = navigationItemsMenuBuilder.buildNavigationMenu(parameters); + + expect(menu.length).toBe(1); + expect(spyMock).toBeCalledWith('inside'); + }); +}); diff --git a/packages/main/src/navigation-items-menu-builder.ts b/packages/main/src/navigation-items-menu-builder.ts new file mode 100644 index 000000000..5ef6821fd --- /dev/null +++ b/packages/main/src/navigation-items-menu-builder.ts @@ -0,0 +1,124 @@ +/********************************************************************** + * 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 type { ContextMenuParams, MenuItemConstructorOptions } from 'electron'; + +import type { ConfigurationRegistry } from './plugin/configuration-registry.js'; +import { CONFIGURATION_DEFAULT_SCOPE } from './plugin/configuration-registry-constants.js'; + +// items that can't be hidden +const EXCLUDED_ITEMS = ['Accounts', 'Settings']; + +// This class is responsible of creating the items to hide a given selected item of the left navigation bar +// and also display a list of all items with the ability to toggle the visibility of each item. +export class NavigationItemsMenuBuilder { + private navigationItems: { name: string; visible: boolean }[] = []; + + constructor(private configurationRegistry: ConfigurationRegistry) {} + + receiveNavigationItems(data: { name: string; visible: boolean }[]): void { + this.navigationItems = data; + } + + protected async updateNavbarHiddenItem(itemName: string, visible: boolean): Promise { + // grab the disabled items, and add the new one + const configuration = this.configurationRegistry.getConfiguration('navbar'); + let items = configuration.get('disabledItems', []); + + if (visible) { + items = items.filter(i => i !== itemName); + } else if (!items.includes(itemName)) { + items.push(itemName); + } + await this.configurationRegistry.updateConfigurationValue( + 'navbar.disabledItems', + items, + CONFIGURATION_DEFAULT_SCOPE, + ); + } + + protected escapeLabel(label: string): string { + return label.replace('&', '&&'); + } + + protected buildHideMenuItem(linkText: string): MenuItemConstructorOptions | undefined { + const rawItemName = linkText; + + // need to filter any counter from the item name + // it's at the end with parenthesis like itemName (2) + const itemName = rawItemName.replace(/\s\(\d+\)$/, ''); + + if (EXCLUDED_ITEMS.includes(itemName)) { + return undefined; + } + + // on electron, need to esccape the & character to show it + const itemDisplayName = this.escapeLabel(itemName); + + const item: MenuItemConstructorOptions = { + label: `Hide '${itemDisplayName}'`, + visible: true, + click: (): void => { + // flag the item as being disabled + this.updateNavbarHiddenItem(itemName, false).catch((e: unknown) => console.error('error disabling item', e)); + }, + }; + return item; + } + + protected buildNavigationToggleMenuItems(): MenuItemConstructorOptions[] { + const items: MenuItemConstructorOptions[] = []; + + // add all navigation items to be able to show/hide them + const menuForNavItems: Electron.MenuItemConstructorOptions[] = this.navigationItems.map(item => ({ + label: this.escapeLabel(item.name), + type: 'checkbox', + checked: item.visible, + click: (): void => { + // send the item to the frontend to show/hide it + this.updateNavbarHiddenItem(item.name, !item.visible).catch((e: unknown) => + console.error('error disabling item', e), + ); + }, + })); + if (menuForNavItems.length > 0) { + // add separator + items.push({ type: 'separator' }); + // add all items + items.push(...menuForNavItems); + } + + return items; + } + + buildNavigationMenu(parameters: ContextMenuParams): MenuItemConstructorOptions[] { + const items: MenuItemConstructorOptions[] = []; + // allow to hide the item being selected + if (parameters.linkText && parameters.x < 48 && parameters.y > 76) { + const menu = this.buildHideMenuItem(parameters.linkText); + if (menu) { + items.push(menu); + } + } + if (parameters.x < 48) { + // add all navigation items to be able to show/hide them + items.push(...this.buildNavigationToggleMenuItems()); + } + return items; + } +} diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index 59eb055b0..6b19b5de2 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -152,6 +152,7 @@ import { downloadGuideList } from './learning-center/learning-center.js'; import { LibpodApiInit } from './libpod-api-enable/libpod-api-init.js'; import type { MessageBoxOptions, MessageBoxReturnValue } from './message-box.js'; import { MessageBox } from './message-box.js'; +import { NavigationItemsInit } from './navigation-items-init.js'; import { NotificationRegistry } from './notification-registry.js'; import { OnboardingRegistry } from './onboarding-registry.js'; import { OpenDevToolsInit } from './open-devtools-init.js'; @@ -569,6 +570,9 @@ export class PluginSystem { const terminalInit = new TerminalInit(configurationRegistry); terminalInit.init(); + const navigationItems = new NavigationItemsInit(configurationRegistry); + navigationItems.init(); + // only in development mode if (import.meta.env.DEV) { const openDevToolsInit = new OpenDevToolsInit(configurationRegistry); diff --git a/packages/main/src/plugin/navigation-items-init.spec.ts b/packages/main/src/plugin/navigation-items-init.spec.ts new file mode 100644 index 000000000..6ec245abc --- /dev/null +++ b/packages/main/src/plugin/navigation-items-init.spec.ts @@ -0,0 +1,50 @@ +/********************************************************************** + * 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 { expect, test, vi } from 'vitest'; + +import type { ConfigurationRegistry } from './configuration-registry.js'; +import { NavigationItemsInit } from './navigation-items-init.js'; + +let navigationItemsInit: NavigationItemsInit; + +const registerConfigurationsMock = vi.fn(); + +const configurationRegistryMock = { + registerConfigurations: registerConfigurationsMock, +} as unknown as ConfigurationRegistry; + +test('should register a configuration', async () => { + navigationItemsInit = new NavigationItemsInit(configurationRegistryMock); + + navigationItemsInit.init(); + + expect(configurationRegistryMock.registerConfigurations).toBeCalled(); + + const configurationNode = vi.mocked(configurationRegistryMock.registerConfigurations).mock.calls[0][0][0]; + expect(configurationNode.id).toBe('preferences.navBar'); + expect(configurationNode.title).toBe('User Confirmation'); + expect(configurationNode.properties).toBeDefined(); + expect(Object.keys(configurationNode.properties ?? {}).length).toBe(1); + expect(configurationNode.properties?.['navbar.disabledItems']).toBeDefined(); + expect(configurationNode.properties?.['navbar.disabledItems'].description).toBe( + 'Items being disabled in the navigation bar', + ); + expect(configurationNode.properties?.['navbar.disabledItems'].type).toStrictEqual(['boolean']); + expect(configurationNode.properties?.['navbar.disabledItems'].default).toStrictEqual([]); +}); diff --git a/packages/main/src/plugin/navigation-items-init.ts b/packages/main/src/plugin/navigation-items-init.ts new file mode 100644 index 000000000..0a6317d33 --- /dev/null +++ b/packages/main/src/plugin/navigation-items-init.ts @@ -0,0 +1,41 @@ +/********************************************************************** + * 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 type { IConfigurationNode, IConfigurationRegistry } from './configuration-registry.js'; + +export class NavigationItemsInit { + constructor(private configurationRegistry: IConfigurationRegistry) {} + + init(): void { + const confirmationConfiguration: IConfigurationNode = { + id: 'preferences.navBar', + title: 'User Confirmation', + type: 'object', + properties: { + ['navbar.disabledItems']: { + description: 'Items being disabled in the navigation bar', + type: ['boolean'], + default: [], + hidden: true, + }, + }, + }; + + this.configurationRegistry.registerConfigurations([confirmationConfiguration]); + } +} diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index 4845e3d82..14e934fe6 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -227,6 +227,13 @@ export function initExposure(): void { }, ); + contextBridge.exposeInMainWorld( + 'sendNavigationItems', + async (items: { name: string; visible: boolean }[]): Promise => { + return ipcRenderer.invoke('navigation:sendNavigationItems', items); + }, + ); + contextBridge.exposeInMainWorld('listContainers', async (): Promise => { return ipcInvoke('container-provider-registry:listContainers'); }); diff --git a/packages/renderer/src/AppNavigation.spec.ts b/packages/renderer/src/AppNavigation.spec.ts index b8d509180..8132403b1 100644 --- a/packages/renderer/src/AppNavigation.spec.ts +++ b/packages/renderer/src/AppNavigation.spec.ts @@ -40,6 +40,8 @@ vi.mock('/@/stores/kubernetes-contexts-state', async () => { // fake the window object beforeAll(() => { (window as any).events = eventsMock; + (window as any).getConfigurationValue = vi.fn(); + (window as any).sendNavigationItems = vi.fn(); }); test('Test rendering of the navigation bar with empty items', async () => { diff --git a/packages/renderer/src/AppNavigation.svelte b/packages/renderer/src/AppNavigation.svelte index d3602a15b..f925b99d5 100644 --- a/packages/renderer/src/AppNavigation.svelte +++ b/packages/renderer/src/AppNavigation.svelte @@ -47,15 +47,18 @@ function clickSettings(b: boolean) { {#each $navigationRegistry as navigationRegistryItem} {#if navigationRegistryItem.type === 'section' && navigationRegistryItem.enabled} - - + {@const allItemsHidden = (navigationRegistryItem.items ?? []).every(item => item.hidden)} + {#if !allItemsHidden} + + - {#if navigationRegistryItem.items} - {#each navigationRegistryItem.items as item} - - {/each} - {/if} - + {#if navigationRegistryItem.items} + {#each navigationRegistryItem.items as item} + + {/each} + {/if} + + {/if} {:else if navigationRegistryItem.items && navigationRegistryItem.type === 'group'} {#each navigationRegistryItem.items as item} diff --git a/packages/renderer/src/lib/ui/NavRegistryEntry.spec.ts b/packages/renderer/src/lib/ui/NavRegistryEntry.spec.ts new file mode 100644 index 000000000..96a4881f0 --- /dev/null +++ b/packages/renderer/src/lib/ui/NavRegistryEntry.spec.ts @@ -0,0 +1,76 @@ +/********************************************************************** + * 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 + ***********************************************************************/ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import '@testing-library/jest-dom/vitest'; + +import { faPuzzlePiece } from '@fortawesome/free-solid-svg-icons'; +import { render, screen } from '@testing-library/svelte'; +import type { TinroRouteMeta } from 'tinro'; +import { beforeAll, expect, test, vi } from 'vitest'; + +import type { NavigationRegistryEntry } from '/@/stores/navigation/navigation-registry'; + +import NavRegistryEntry from './NavRegistryEntry.svelte'; + +beforeAll(() => { + // Mock the animate function + HTMLElement.prototype.animate = vi.fn().mockReturnValue({ + finished: Promise.resolve(), + cancel: vi.fn(), + }); +}); + +test('Expect entry is rendered', async () => { + const entry: NavigationRegistryEntry = { + name: 'Item1', + hidden: false, + icon: { + faIcon: { definition: faPuzzlePiece, size: 'lg' }, + }, + tooltip: 'Item tooltip', + link: '/mylink', + counter: 0, + type: 'entry', + }; + const meta = { url: '/test' } as TinroRouteMeta; + render(NavRegistryEntry, { entry, meta }); + + const content = screen.queryByLabelText('Item1'); + expect(content).toBeInTheDocument(); +}); + +test('Expect hidden entry is not rendered', async () => { + const entry: NavigationRegistryEntry = { + name: 'Item1', + hidden: true, + icon: { + faIcon: { definition: faPuzzlePiece, size: 'lg' }, + }, + tooltip: 'Item tooltip', + link: '/mylink', + counter: 0, + type: 'entry', + }; + const meta = { url: '/test' } as TinroRouteMeta; + render(NavRegistryEntry, { entry, meta }); + + const content = screen.queryByLabelText('Item1'); + expect(content).not.toBeInTheDocument(); +}); diff --git a/packages/renderer/src/lib/ui/NavRegistryEntry.svelte b/packages/renderer/src/lib/ui/NavRegistryEntry.svelte index 186650ae2..2d8d3a988 100644 --- a/packages/renderer/src/lib/ui/NavRegistryEntry.svelte +++ b/packages/renderer/src/lib/ui/NavRegistryEntry.svelte @@ -11,14 +11,16 @@ import NavItem from './NavItem.svelte'; let { entry, meta = $bindable() }: { entry: NavigationRegistryEntry; meta: TinroRouteMeta } = $props(); - - {#if entry.icon === undefined} - {entry.name} - {:else if entry.icon.faIcon} - - {:else if entry.icon.iconComponent} - - {:else if entry.icon.iconImage && typeof entry.icon.iconImage === 'string'} - {entry.name} - {/if} - +{#if !entry.hidden} + + {#if entry.icon === undefined} + {entry.name} + {:else if entry.icon.faIcon} + + {:else if entry.icon.iconComponent} + + {:else if entry.icon.iconImage && typeof entry.icon.iconImage === 'string'} + {entry.name} + {/if} + +{/if} diff --git a/packages/renderer/src/stores/navigation/navigation-registry.spec.ts b/packages/renderer/src/stores/navigation/navigation-registry.spec.ts index 7c4c9d008..7a6701882 100644 --- a/packages/renderer/src/stores/navigation/navigation-registry.spec.ts +++ b/packages/renderer/src/stores/navigation/navigation-registry.spec.ts @@ -21,13 +21,16 @@ import { get } from 'svelte/store'; import { beforeEach, expect, test, vi } from 'vitest'; +import { configurationProperties } from '../configurationProperties'; import { fetchNavigationRegistries, navigationRegistry } from './navigation-registry'; const kubernetesRegisterGetCurrentContextResourcesMock = vi.fn(); - +const getConfigurationValueMock = vi.fn(); beforeEach(() => { vi.resetAllMocks(); (window as any).kubernetesRegisterGetCurrentContextResources = kubernetesRegisterGetCurrentContextResourcesMock; + (window as any).getConfigurationValue = getConfigurationValueMock; + (window as any).sendNavigationItems = vi.fn(); }); test('check navigation registry items', async () => { @@ -37,3 +40,33 @@ test('check navigation registry items', async () => { // expect 7 items in the registry expect(registries.length).equal(7); }); + +test('check update properties', async () => { + // first, check that all items are visible + const items = get(navigationRegistry); + items.forEach(item => { + expect(item.hidden).toBeFalsy(); + }); + + // Say that Containers and Pods are hidden by the configuration + getConfigurationValueMock.mockResolvedValue(['Containers', 'Pods']); + + // do an update to force the update + configurationProperties.set([]); + + // wait that the update is done asynchronously + await new Promise(resolve => setTimeout(resolve, 500)); + + // and now check the hidden values + const hidden = get(navigationRegistry); + + const allItemsExceptContainersAndPods = hidden.filter(item => item.name !== 'Containers' && item.name !== 'Pods'); + allItemsExceptContainersAndPods.forEach(item => { + expect(item.hidden).toBeFalsy(); + }); + + const containersAndPods = hidden.filter(item => item.name === 'Containers' || item.name === 'Pods'); + containersAndPods.forEach(item => { + expect(item.hidden).toBeTruthy(); + }); +}); diff --git a/packages/renderer/src/stores/navigation/navigation-registry.ts b/packages/renderer/src/stores/navigation/navigation-registry.ts index f255f10e3..bf79fbf20 100644 --- a/packages/renderer/src/stores/navigation/navigation-registry.ts +++ b/packages/renderer/src/stores/navigation/navigation-registry.ts @@ -17,12 +17,12 @@ ***********************************************************************/ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'; -import type { Writable } from 'svelte/store'; -import { writable } from 'svelte/store'; +import { type Writable, writable } from 'svelte/store'; import type { IconSize } from 'svelte-fa'; import { EventStore } from '/@/stores/event-store'; +import { configurationProperties } from '../configurationProperties'; import { createNavigationContainerEntry } from './navigation-registry-container.svelte'; import { createNavigationExtensionEntry, createNavigationExtensionGroup } from './navigation-registry-extension.svelte'; import { createNavigationImageEntry } from './navigation-registry-image.svelte'; @@ -43,6 +43,12 @@ export interface NavigationRegistryEntry { type: 'section' | 'entry' | 'group'; enabled?: boolean; items?: NavigationRegistryEntry[]; + hidden?: boolean; +} + +interface DisplayItem { + name: string; + visible: boolean; } const windowEvents: string[] = []; @@ -50,7 +56,9 @@ const windowListeners = ['extensions-already-started', 'system-ready']; export const navigationRegistry: Writable = writable([]); -const values: NavigationRegistryEntry[] = []; +let hiddenItems: string[] = []; + +let values: NavigationRegistryEntry[] = []; let initialized = false; const init = () => { values.push(createNavigationContainerEntry()); @@ -60,14 +68,42 @@ const init = () => { values.push(createNavigationKubernetesGroup()); values.push(createNavigationExtensionEntry()); values.push(createNavigationExtensionGroup()); + hideItems(); }; +function collecItem(navigationRegistryEntry: NavigationRegistryEntry, items: DisplayItem[]) { + if ( + navigationRegistryEntry.items && + (navigationRegistryEntry.type === 'group' || navigationRegistryEntry.type === 'section') + ) { + navigationRegistryEntry.items.forEach(item => { + collecItem(item, items); + }); + } + + // add only if it does not exist + if (items.find(i => i.name === navigationRegistryEntry.name)) { + return; + } + + if (navigationRegistryEntry.type !== 'section') { + items.push({ + name: navigationRegistryEntry.name, + visible: navigationRegistryEntry.hidden ? false : true, + }); + } +} + // use helper here as window methods are initialized after the store in tests const grabList = async (): Promise => { if (!initialized) { init(); initialized = true; } + + // override hidden property + await hideItems(); + return values; }; @@ -85,3 +121,49 @@ const navigationRegistryEventStoreInfo = navigationRegistryEventStore.setup(); export const fetchNavigationRegistries = async (): Promise => { await navigationRegistryEventStoreInfo.fetch(); }; + +function hideSingleItem(navigationRegistryEntry: NavigationRegistryEntry) { + if (hiddenItems?.includes(navigationRegistryEntry.name)) { + navigationRegistryEntry.hidden = true; + } else { + navigationRegistryEntry.hidden = false; + } + + // iterate on all the items + if (navigationRegistryEntry.items) { + navigationRegistryEntry.items.forEach(item => { + hideSingleItem(item); + }); + } +} + +async function hideItems(): Promise { + // for each item, set the hidden property to true + values.forEach(item => { + hideSingleItem(item); + }); + + // send to the main side the list of all items, items being displayed or hidden + const navItems: DisplayItem[] = []; + values.forEach(item => { + collecItem(item, navItems); + }); + + await window.sendNavigationItems(navItems); + values = [...values]; + navigationRegistry.set(values); +} + +// update the items by looking at the disabled items each time we update the configuration properties +configurationProperties.subscribe(() => { + if (window.getConfigurationValue) { + window + .getConfigurationValue('navbar.disabledItems') + .then(value => { + if (value) { + hiddenItems = value; + } + }) + .then(() => hideItems()); + } +});