From bc635dcdd931d155d078b767719c051d29acc558 Mon Sep 17 00:00:00 2001 From: Tim deBoer Date: Mon, 15 Jan 2024 16:16:37 -0500 Subject: [PATCH] feat: make Kubernetes support experimental Adds a new experimental flag (off by default) that hides the Kubernetes options on the left navbar. When you enable it, the options automatically appear. Tests added to make sure that the Kubernetes options don't appear if the preference is not set, and do appear if it is (and have at least one context). Fixes #5525. Signed-off-by: Tim deBoer --- packages/main/src/plugin/index.ts | 6 ++ packages/main/src/plugin/kubernetes-util.ts | 41 ++++++++++ packages/renderer/src/AppNavigation.spec.ts | 91 +++++++++++++++++++-- packages/renderer/src/AppNavigation.svelte | 19 ++++- 4 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 packages/main/src/plugin/kubernetes-util.ts 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}