diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index be7eaf5a0208f..6a2801407339c 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -155,6 +155,8 @@ import { NavigationManager } from '/@/plugin/navigation/navigation-manager.js'; import { WebviewRegistry } from './webview/webview-registry.js'; import type { IDisposable } from './types/disposable.js'; +import { KubernetesUtils } from './kubernetes-util.js'; + type LogType = 'log' | 'warn' | 'trace' | 'debug' | 'error'; export const UPDATER_UPDATE_AVAILABLE_ICON = 'fa fa-exclamation-triangle'; @@ -746,6 +748,10 @@ export class PluginSystem { const webviewRegistry = new WebviewRegistry(apiSender); await webviewRegistry.start(); + // init kubernetes configuration + const kubernetesUtils = new KubernetesUtils(configurationRegistry); + kubernetesUtils.init(); + this.extensionLoader = new ExtensionLoader( commandRegistry, menuRegistry, diff --git a/packages/main/src/plugin/kubernetes-util.ts b/packages/main/src/plugin/kubernetes-util.ts new file mode 100644 index 0000000000000..042f398de4f54 --- /dev/null +++ b/packages/main/src/plugin/kubernetes-util.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 KubernetesUtils { + constructor(private configurationRegistry: IConfigurationRegistry) {} + + init() { + const kubernetesConfiguration: IConfigurationNode = { + id: 'preferences.kubernetes', + title: 'Kubernetes', + type: 'object', + properties: { + ['kubernetes.experimental']: { + description: 'Experimental extended Kubernetes support.', + type: 'boolean', + default: false, + hidden: false, + }, + }, + }; + + this.configurationRegistry.registerConfigurations([kubernetesConfiguration]); + } +} diff --git a/packages/renderer/src/AppNavigation.spec.ts b/packages/renderer/src/AppNavigation.spec.ts index f0cce6af9c1ee..201f6954c6404 100644 --- a/packages/renderer/src/AppNavigation.spec.ts +++ b/packages/renderer/src/AppNavigation.spec.ts @@ -17,19 +17,19 @@ ***********************************************************************/ import '@testing-library/jest-dom/vitest'; -import { beforeAll, test, expect } from 'vitest'; +import { beforeAll, test, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import AppNavigation from './AppNavigation.svelte'; import type { TinroRouteMeta } from 'tinro'; +import { kubernetesContexts } from './stores/kubernetes-contexts'; -// fake the window.events object +const eventsMock = vi.fn(); +const getConfigurationValueMock = vi.fn(); + +// fake the window object beforeAll(() => { - (window.events as unknown) = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - receive: (_channel: string, func: any) => { - func(); - }, - }; + (window as any).events = eventsMock; + (window as any).getConfigurationValue = getConfigurationValueMock; }); test('Test rendering of the navigation bar with empty items', () => { @@ -57,4 +57,79 @@ test('Test rendering of the navigation bar with empty items', () => { expect(volumes).toBeInTheDocument(); const settings = screen.getByRole('link', { name: 'Settings' }); expect(settings).toBeInTheDocument(); + + const deployments = screen.queryByRole('link', { name: 'Deployments' }); + expect(deployments).not.toBeInTheDocument(); + const services = screen.queryByRole('link', { name: 'Services' }); + expect(services).not.toBeInTheDocument(); +}); + +test('Test Kubernetes experimental hidden with valid context', async () => { + kubernetesContexts.set([ + { + name: 'context-name', + cluster: 'cluster-name', + user: 'user-name', + clusterInfo: { + name: 'cluster-name', + server: 'https://server-name', + }, + }, + ]); + getConfigurationValueMock.mockResolvedValue(false); + + const meta = { + url: '/', + } as unknown as TinroRouteMeta; + + render(AppNavigation, { + meta, + exitSettingsCallback: () => {}, + }); + + const navigationBar = screen.getByRole('navigation', { name: 'AppNavigation' }); + expect(navigationBar).toBeInTheDocument(); + + // wait 100ms for stores to initialize + await new Promise(resolve => setTimeout(resolve, 100)); + + const deployments = screen.queryByRole('link', { name: 'Deployments' }); + expect(deployments).not.toBeInTheDocument(); + const services = screen.queryByRole('link', { name: 'Services' }); + expect(services).not.toBeInTheDocument(); +}); + +test('Test Kubernetes experimental enablement', async () => { + kubernetesContexts.set([ + { + name: 'context-name', + cluster: 'cluster-name', + user: 'user-name', + clusterInfo: { + name: 'cluster-name', + server: 'https://server-name', + }, + }, + ]); + getConfigurationValueMock.mockResolvedValue(true); + + const meta = { + url: '/', + } as unknown as TinroRouteMeta; + + render(AppNavigation, { + meta, + exitSettingsCallback: () => {}, + }); + + const navigationBar = screen.getByRole('navigation', { name: 'AppNavigation' }); + expect(navigationBar).toBeInTheDocument(); + + // wait 100ms for stores to initialize + await new Promise(resolve => setTimeout(resolve, 100)); + + const deployments = screen.getByRole('link', { name: 'Deployments' }); + expect(deployments).toBeInTheDocument(); + const services = screen.getByRole('link', { name: 'Services' }); + expect(services).toBeInTheDocument(); }); diff --git a/packages/renderer/src/AppNavigation.svelte b/packages/renderer/src/AppNavigation.svelte index 26c180d3d300f..d3748cabedcbd 100644 --- a/packages/renderer/src/AppNavigation.svelte +++ b/packages/renderer/src/AppNavigation.svelte @@ -29,6 +29,7 @@ import { ingresses } from './stores/ingresses'; import { routes } from './stores/routes'; import Webviews from '/@/lib/webview/Webviews.svelte'; import PuzzleIcon from './lib/images/PuzzleIcon.svelte'; +import { onDidChangeConfiguration } from './stores/configurationProperties'; let podInfoSubscribe: Unsubscriber; let containerInfoSubscribe: Unsubscriber; @@ -54,6 +55,17 @@ let ingressesRoutesCount = ''; const imageUtils = new ImageUtils(); export let exitSettingsCallback: () => void; +const KUBERNETES_EXPERIMENTAL_CONFIGURATION_KEY = 'kubernetes.experimental'; +let showKubernetesNav = false; + +async function updateKubernetesNav(): Promise { + showKubernetesNav = (await window.getConfigurationValue(KUBERNETES_EXPERIMENTAL_CONFIGURATION_KEY)) || false; +} + +let configurationChangeCallback: EventListenerOrEventListenerObject = () => { + updateKubernetesNav(); +}; + onMount(async () => { const commandRegistry = new CommandRegistry(); commandRegistry.init(); @@ -112,6 +124,10 @@ onMount(async () => { contextsSubscribe = kubernetesContexts.subscribe(value => { contextCount = value.length; }); + + // set initial Kubernetes experimental state, and listen for changes + await updateKubernetesNav(); + onDidChangeConfiguration.addEventListener(KUBERNETES_EXPERIMENTAL_CONFIGURATION_KEY, configurationChangeCallback); }); onDestroy(() => { @@ -138,6 +154,7 @@ onDestroy(() => { } ingressesSubscribe?.(); routesSubscribe?.(); + onDidChangeConfiguration.removeEventListener(KUBERNETES_EXPERIMENTAL_CONFIGURATION_KEY, configurationChangeCallback); }); function updateIngressesRoutesCount(count: number) { @@ -181,7 +198,7 @@ export let meta: TinroRouteMeta; - {#if contextCount > 0} + {#if contextCount > 0 && showKubernetesNav}