diff --git a/packages/common/src/channels.ts b/packages/common/src/channels.ts index 9ad2dd8e..56686644 100644 --- a/packages/common/src/channels.ts +++ b/packages/common/src/channels.ts @@ -3,3 +3,14 @@ import { createRpcChannel } from './rpc'; // RPC channels (used by the webview to send requests to the extension) export const API_DASHBOARD = createRpcChannel('DashboardApi'); + +export interface KubernetesUpdateResourceInfo { + resourceName: string; +} + +// Broadcast events (sent by extension and received by the webview) +export const ACTIVE_RESOURCES_COUNT = createRpcChannel('ActiveResourcesCount'); +export const KUBERNETES_CONTEXTS_HEALTHS = createRpcChannel('KubernetesContextsHealths'); +export const KUBERNETES_CONTEXTS_PERMISSIONS = createRpcChannel('KubernetesContextsPermissions'); +export const KUBERNETES_RESOURCES_COUNT = createRpcChannel('KubernetesResourcesCount'); +export const KUBERNETES_UPDATE_RESOURCE = createRpcChannel('KubernetesUpdateResource'); diff --git a/packages/common/src/interface/dashboard-api.ts b/packages/common/src/interface/dashboard-api.ts index 24cc5730..b0e101c8 100644 --- a/packages/common/src/interface/dashboard-api.ts +++ b/packages/common/src/interface/dashboard-api.ts @@ -16,7 +16,9 @@ * SPDX-License-Identifier: Apache-2.0 ***********************************************************************/ +import type { ResourceCount } from '../model/kubernetes-resource-count'; + export const DashboardApi = Symbol.for('DashboardApi'); export interface DashboardApi { - ping(): Promise; + getActiveResourcesCount(): Promise; } diff --git a/packages/extension/src/types/disposable.ts b/packages/common/src/model/disposable.ts similarity index 100% rename from packages/extension/src/types/disposable.ts rename to packages/common/src/model/disposable.ts diff --git a/packages/extension/src/controller/dashboard-impl.ts b/packages/extension/src/controller/dashboard-impl.ts new file mode 100644 index 00000000..9e66992b --- /dev/null +++ b/packages/extension/src/controller/dashboard-impl.ts @@ -0,0 +1,35 @@ +/********************************************************************** + * Copyright (C) 2025 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 { ContextsStatesDispatcher } from '/@/manager/contexts-states-dispatcher'; +import { API_DASHBOARD } from '/@common/channels'; +import type { DashboardApi } from '/@common/interface/dashboard-api'; +import type { ResourceCount } from '/@common/model/kubernetes-resource-count'; +import type { RpcChannel } from '/@common/rpc'; + +export class DashboardImpl implements DashboardApi { + constructor(private contextsStatesDispatcher: ContextsStatesDispatcher) {} + + getChannel(): RpcChannel { + return API_DASHBOARD; + } + + async getActiveResourcesCount(): Promise { + return this.contextsStatesDispatcher.getActiveResourcesCount(); + } +} diff --git a/packages/extension/src/dashboard-extension.spec.ts b/packages/extension/src/dashboard-extension.spec.ts index 5bf544e5..40bedf5f 100644 --- a/packages/extension/src/dashboard-extension.spec.ts +++ b/packages/extension/src/dashboard-extension.spec.ts @@ -19,20 +19,22 @@ import type { WebviewPanel, ExtensionContext } from '@podman-desktop/api'; import { kubernetes, Uri, window } from '@podman-desktop/api'; import { assert, beforeEach, describe, expect, test, vi } from 'vitest'; -import { DashboardExtension } from './dashboard-extension'; +import { DashboardExtension } from '/@/dashboard-extension'; import { vol } from 'memfs'; -import type { ContextsManager } from './manager/contexts-manager'; -import type { ContextsStatesDispatcher } from './manager/contexts-states-dispatcher'; +import { ContextsManager } from '/@/manager/contexts-manager'; +import { ContextsStatesDispatcher } from '/@/manager/contexts-states-dispatcher'; let extensionContextMock: ExtensionContext; let dashboardExtension: DashboardExtension; let contextsManagerMock: ContextsManager; -let contextsStatesDispatcher: ContextsStatesDispatcher; +let contextsStatesDispatcherMock: ContextsStatesDispatcher; vi.mock(import('node:fs')); vi.mock(import('node:fs/promises')); vi.mock(import('@kubernetes/client-node')); +vi.mock(import('./manager/contexts-manager')); +vi.mock(import('./manager/contexts-states-dispatcher')); beforeEach(() => { vi.restoreAllMocks(); @@ -50,14 +52,18 @@ beforeEach(() => { extensionContextMock = { subscriptions: [], } as unknown as ExtensionContext; - // Create a mock for the contextsManager + contextsManagerMock = { update: vi.fn(), } as unknown as ContextsManager; - contextsStatesDispatcher = { + vi.mocked(ContextsManager).mockReturnValue(contextsManagerMock); + + contextsStatesDispatcherMock = { init: vi.fn(), } as unknown as ContextsStatesDispatcher; - dashboardExtension = new DashboardExtension(extensionContextMock, contextsManagerMock, contextsStatesDispatcher); + vi.mocked(ContextsStatesDispatcher).mockReturnValue(contextsStatesDispatcherMock); + + dashboardExtension = new DashboardExtension(extensionContextMock); vi.mocked(kubernetes.getKubeconfig).mockReturnValue({ path: '/path/to/kube/config', } as Uri); @@ -80,7 +86,7 @@ describe('a kubeconfig file is not present', () => { callback({ type: 'UPDATE', location: { path: '/path/to/kube/config' } as Uri }); expect(contextsManagerMock.update).toHaveBeenCalledOnce(); - expect(contextsStatesDispatcher.init).toHaveBeenCalledOnce(); + expect(contextsStatesDispatcherMock.init).toHaveBeenCalledOnce(); }); test('should deactivate correctly', async () => { @@ -107,7 +113,7 @@ describe('a kubeconfig file is present', () => { callback({ type: 'UPDATE', location: { path: '/path/to/kube/config' } as Uri }); expect(contextsManagerMock.update).toHaveBeenCalledOnce(); - expect(contextsStatesDispatcher.init).toHaveBeenCalledOnce(); + expect(contextsStatesDispatcherMock.init).toHaveBeenCalledOnce(); }); test('should deactivate correctly', async () => { diff --git a/packages/extension/src/dashboard-extension.ts b/packages/extension/src/dashboard-extension.ts index b04cc8e4..f039714e 100644 --- a/packages/extension/src/dashboard-extension.ts +++ b/packages/extension/src/dashboard-extension.ts @@ -22,24 +22,19 @@ import { kubernetes, Uri, window } from '@podman-desktop/api'; import { RpcExtension } from '/@common/rpc/rpc'; import { readFile } from 'node:fs/promises'; -import type { ContextsManager } from './manager/contexts-manager'; +import { ContextsManager } from '/@/manager/contexts-manager'; import { existsSync } from 'node:fs'; import { KubeConfig } from '@kubernetes/client-node'; -import type { ContextsStatesDispatcher } from './manager/contexts-states-dispatcher'; +import { ContextsStatesDispatcher } from '/@/manager/contexts-states-dispatcher'; +import { DashboardImpl } from '/@/controller/dashboard-impl'; export class DashboardExtension { #extensionContext: ExtensionContext; #contextsManager: ContextsManager; #contextsStatesDispatcher: ContextsStatesDispatcher; - constructor( - readonly extensionContext: ExtensionContext, - readonly contextManager: ContextsManager, - readonly contextsStatesDispatcher: ContextsStatesDispatcher, - ) { + constructor(readonly extensionContext: ExtensionContext) { this.#extensionContext = extensionContext; - this.#contextsManager = contextManager; - this.#contextsStatesDispatcher = contextsStatesDispatcher; } async activate(): Promise { @@ -50,12 +45,19 @@ export class DashboardExtension { rpcExtension.init(); this.#extensionContext.subscriptions.push(rpcExtension); + this.#contextsManager = new ContextsManager(); + this.#contextsStatesDispatcher = new ContextsStatesDispatcher(this.#contextsManager, rpcExtension); + const now = performance.now(); const afterFirst = performance.now(); console.log('activation time:', afterFirst - now); + // Register all controllers + const dashboardImpl = new DashboardImpl(this.#contextsStatesDispatcher); + rpcExtension.registerInstance(dashboardImpl.getChannel(), dashboardImpl); + await this.listenMonitoring(); await this.startMonitoring(); } diff --git a/packages/extension/src/main.spec.ts b/packages/extension/src/main.spec.ts index fee79471..75ee5170 100644 --- a/packages/extension/src/main.spec.ts +++ b/packages/extension/src/main.spec.ts @@ -1,11 +1,11 @@ import type { ExtensionContext } from '@podman-desktop/api'; import { beforeEach, expect, test, vi } from 'vitest'; -import { activate, deactivate } from './main'; -import { DashboardExtension } from './dashboard-extension'; +import { activate, deactivate } from '/@/main'; +import { DashboardExtension } from '/@/dashboard-extension'; let extensionContextMock: ExtensionContext; -vi.mock(import('./dashboard-extension')); +vi.mock(import('/@/dashboard-extension')); beforeEach(() => { vi.restoreAllMocks(); diff --git a/packages/extension/src/main.ts b/packages/extension/src/main.ts index b264fff5..66480b4e 100644 --- a/packages/extension/src/main.ts +++ b/packages/extension/src/main.ts @@ -18,22 +18,13 @@ import type { ExtensionContext } from '@podman-desktop/api'; -import { DashboardExtension } from './dashboard-extension'; -import { ContextsManager } from './manager/contexts-manager'; -import { ContextsStatesDispatcher } from './manager/contexts-states-dispatcher'; +import { DashboardExtension } from '/@/dashboard-extension'; let dashboardExtension: DashboardExtension | undefined; // Initialize the activation of the extension. export async function activate(extensionContext: ExtensionContext): Promise { - const contextsManager = new ContextsManager(); - const apiSender = { - send: (channel: string, data?: unknown): void => { - console.log(`==> recv data "${data}" on channel ${channel}`); - }, - }; - const contextsStatesDispatcher = new ContextsStatesDispatcher(contextsManager, apiSender); - dashboardExtension ??= new DashboardExtension(extensionContext, contextsManager, contextsStatesDispatcher); + dashboardExtension ??= new DashboardExtension(extensionContext); await dashboardExtension.activate(); } diff --git a/packages/extension/src/manager/context-health-checker.spec.ts b/packages/extension/src/manager/context-health-checker.spec.ts index 8eb2dabf..115c9204 100644 --- a/packages/extension/src/manager/context-health-checker.spec.ts +++ b/packages/extension/src/manager/context-health-checker.spec.ts @@ -20,8 +20,8 @@ import type { Cluster, Context, User } from '@kubernetes/client-node'; import { Health } from '@kubernetes/client-node'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { ContextHealthChecker } from './context-health-checker.js'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; +import { ContextHealthChecker } from '/@/manager/context-health-checker.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; vi.mock('@kubernetes/client-node'); diff --git a/packages/extension/src/manager/context-health-checker.ts b/packages/extension/src/manager/context-health-checker.ts index a4d9b77e..660862dd 100644 --- a/packages/extension/src/manager/context-health-checker.ts +++ b/packages/extension/src/manager/context-health-checker.ts @@ -19,9 +19,9 @@ import { Health } from '@kubernetes/client-node'; import type { Disposable } from '@podman-desktop/api'; -import type { Event } from '../types/emitter.js'; -import { Emitter } from '../types/emitter.js'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; +import type { Event } from '/@/types/emitter.js'; +import { Emitter } from '/@/types/emitter.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; export interface ContextHealthState { kubeConfig: KubeConfigSingleContext; diff --git a/packages/extension/src/manager/context-permissions-checker.spec.ts b/packages/extension/src/manager/context-permissions-checker.spec.ts index 72383cfe..65ec9653 100644 --- a/packages/extension/src/manager/context-permissions-checker.spec.ts +++ b/packages/extension/src/manager/context-permissions-checker.spec.ts @@ -20,9 +20,9 @@ import util from 'node:util'; import { beforeEach, describe, expect, test, vi } from 'vitest'; -import type { ContextPermissionResult, ContextResourcePermission } from './context-permissions-checker.js'; -import { ContextPermissionsChecker } from './context-permissions-checker.js'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; +import type { ContextPermissionResult, ContextResourcePermission } from '/@/manager/context-permissions-checker.js'; +import { ContextPermissionsChecker } from '/@/manager/context-permissions-checker.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; describe('ContextPermissionsChecker is built with a non recursive request', async () => { let permissionsChecker: ContextPermissionsChecker; diff --git a/packages/extension/src/manager/context-permissions-checker.ts b/packages/extension/src/manager/context-permissions-checker.ts index c0b63d72..1271e29d 100644 --- a/packages/extension/src/manager/context-permissions-checker.ts +++ b/packages/extension/src/manager/context-permissions-checker.ts @@ -24,9 +24,9 @@ import type { import { AuthorizationV1Api } from '@kubernetes/client-node'; import type { Disposable } from '@podman-desktop/api'; -import type { Event } from '../types/emitter.js'; -import { Emitter } from '../types/emitter.js'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; +import type { Event } from '/@/types/emitter.js'; +import { Emitter } from '/@/types/emitter.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; export interface ContextPermissionsRequest { // the request to send diff --git a/packages/extension/src/manager/contexts-dispatcher.spec.ts b/packages/extension/src/manager/contexts-dispatcher.spec.ts index a23f54c7..8d63ed4f 100644 --- a/packages/extension/src/manager/contexts-dispatcher.spec.ts +++ b/packages/extension/src/manager/contexts-dispatcher.spec.ts @@ -20,8 +20,8 @@ import type { Cluster, Context, User } from '@kubernetes/client-node'; import { KubeConfig } from '@kubernetes/client-node'; import { beforeEach, expect, test, vi } from 'vitest'; -import { ContextsDispatcher } from './contexts-dispatcher.js'; -import { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; +import { ContextsDispatcher } from '/@/manager/contexts-dispatcher.js'; +import { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; const contexts = [ { diff --git a/packages/extension/src/manager/contexts-dispatcher.ts b/packages/extension/src/manager/contexts-dispatcher.ts index 3f24573a..03dbaf87 100644 --- a/packages/extension/src/manager/contexts-dispatcher.ts +++ b/packages/extension/src/manager/contexts-dispatcher.ts @@ -18,9 +18,9 @@ import type { KubeConfig } from '@kubernetes/client-node'; -import type { Event } from '../types/emitter.js'; -import { Emitter } from '../types/emitter.js'; -import { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; +import type { Event } from '/@/types/emitter.js'; +import { Emitter } from '/@/types/emitter.js'; +import { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; export interface DispatcherEvent { contextName: string; diff --git a/packages/extension/src/manager/contexts-manager.spec.ts b/packages/extension/src/manager/contexts-manager.spec.ts index b9587eed..f07293ed 100644 --- a/packages/extension/src/manager/contexts-manager.spec.ts +++ b/packages/extension/src/manager/contexts-manager.spec.ts @@ -21,18 +21,18 @@ import { KubeConfig } from '@kubernetes/client-node'; import type { Event } from '@podman-desktop/api'; import { assert, beforeEach, describe, expect, test, vi } from 'vitest'; -import type { ContextHealthState } from './context-health-checker.js'; -import { ContextHealthChecker } from './context-health-checker.js'; +import type { ContextHealthState } from '/@/manager/context-health-checker.js'; +import { ContextHealthChecker } from '/@/manager/context-health-checker.js'; import { ContextPermissionsChecker, type ContextPermissionsRequest, type ContextResourcePermission, -} from './context-permissions-checker.js'; -import { ContextsManager } from './contexts-manager.js'; -import { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from '../resources/resource-factory.js'; -import { ResourceFactoryBase } from '../resources/resource-factory.js'; -import type { CacheUpdatedEvent, OfflineEvent, ResourceInformer } from '../types/resource-informer.js'; +} from '/@/manager/context-permissions-checker.js'; +import { ContextsManager } from '/@/manager/contexts-manager.js'; +import { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import type { CacheUpdatedEvent, OfflineEvent, ResourceInformer } from '/@/types/resource-informer.js'; const onCacheUpdatedMock = vi.fn>(); const onOfflineMock = vi.fn>(); diff --git a/packages/extension/src/manager/contexts-manager.ts b/packages/extension/src/manager/contexts-manager.ts index b21096db..35f532cd 100644 --- a/packages/extension/src/manager/contexts-manager.ts +++ b/packages/extension/src/manager/contexts-manager.ts @@ -24,31 +24,31 @@ import type { ResourceCount } from '/@common/model/kubernetes-resource-count.js' import type { KubernetesContextResources } from '/@common/model/kubernetes-resources.js'; import type { KubernetesTroubleshootingInformation } from '/@common/model/kubernetes-troubleshooting.js'; -import type { Event } from '../types/emitter.js'; -import { Emitter } from '../types/emitter.js'; -import { ConfigmapsResourceFactory } from '../resources/configmaps-resource-factory.js'; -import type { ContextHealthState } from './context-health-checker.js'; -import { ContextHealthChecker } from './context-health-checker.js'; -import type { ContextPermissionResult } from './context-permissions-checker.js'; -import { ContextPermissionsChecker } from './context-permissions-checker.js'; -import { ContextResourceRegistry } from '../registry/context-resource-registry.js'; -import type { CurrentChangeEvent, DispatcherEvent } from './contexts-dispatcher.js'; -import { ContextsDispatcher } from './contexts-dispatcher.js'; -import { CronjobsResourceFactory } from '../resources/cronjobs-resource-factory.js'; -import { DeploymentsResourceFactory } from '../resources/deployments-resource-factory.js'; -import { EventsResourceFactory } from '../resources/events-resource-factory.js'; -import { IngressesResourceFactory } from '../resources/ingresses-resource-factory.js'; -import { JobsResourceFactory } from '../resources/jobs-resource-factory.js'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import { NodesResourceFactory } from '../resources/nodes-resource-factory.js'; -import { PodsResourceFactory } from '../resources/pods-resource-factory.js'; -import { PVCsResourceFactory } from '../resources/pvcs-resource-factory.js'; -import type { ResourceFactory } from '../resources/resource-factory.js'; -import { ResourceFactoryHandler } from './resource-factory-handler.js'; -import type { CacheUpdatedEvent, OfflineEvent, ResourceInformer } from '../types/resource-informer.js'; -import { RoutesResourceFactory } from '../resources/routes-resource-factory.js'; -import { SecretsResourceFactory } from '../resources/secrets-resource-factory.js'; -import { ServicesResourceFactory } from '../resources/services-resource-factory.js'; +import type { Event } from '/@/types/emitter.js'; +import { Emitter } from '/@/types/emitter.js'; +import { ConfigmapsResourceFactory } from '/@/resources/configmaps-resource-factory.js'; +import type { ContextHealthState } from '/@/manager/context-health-checker.js'; +import { ContextHealthChecker } from '/@/manager/context-health-checker.js'; +import type { ContextPermissionResult } from '/@/manager/context-permissions-checker.js'; +import { ContextPermissionsChecker } from '/@/manager/context-permissions-checker.js'; +import { ContextResourceRegistry } from '/@/registry/context-resource-registry.js'; +import type { CurrentChangeEvent, DispatcherEvent } from '/@/manager/contexts-dispatcher.js'; +import { ContextsDispatcher } from '/@/manager/contexts-dispatcher.js'; +import { CronjobsResourceFactory } from '/@/resources/cronjobs-resource-factory.js'; +import { DeploymentsResourceFactory } from '/@/resources/deployments-resource-factory.js'; +import { EventsResourceFactory } from '/@/resources/events-resource-factory.js'; +import { IngressesResourceFactory } from '/@/resources/ingresses-resource-factory.js'; +import { JobsResourceFactory } from '/@/resources/jobs-resource-factory.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import { NodesResourceFactory } from '/@/resources/nodes-resource-factory.js'; +import { PodsResourceFactory } from '/@/resources/pods-resource-factory.js'; +import { PVCsResourceFactory } from '/@/resources/pvcs-resource-factory.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryHandler } from '/@/manager/resource-factory-handler.js'; +import type { CacheUpdatedEvent, OfflineEvent, ResourceInformer } from '/@/types/resource-informer.js'; +import { RoutesResourceFactory } from '/@/resources/routes-resource-factory.js'; +import { SecretsResourceFactory } from '/@/resources/secrets-resource-factory.js'; +import { ServicesResourceFactory } from '/@/resources/services-resource-factory.js'; const HEALTH_CHECK_TIMEOUT_MS = 5_000; diff --git a/packages/extension/src/manager/contexts-states-dispatcher.spec.ts b/packages/extension/src/manager/contexts-states-dispatcher.spec.ts index 801f9a56..6526e236 100644 --- a/packages/extension/src/manager/contexts-states-dispatcher.spec.ts +++ b/packages/extension/src/manager/contexts-states-dispatcher.spec.ts @@ -18,16 +18,22 @@ import { expect, test, vi } from 'vitest'; -import type { IDisposable } from '../types/disposable.js'; import type { ContextPermission } from '/@common/model/kubernetes-contexts-permissions.js'; -import type { ApiSenderType } from '/@common/model/api-sender.js'; -import type { ContextHealthState } from './context-health-checker.js'; -import type { ContextPermissionResult } from './context-permissions-checker.js'; -import type { DispatcherEvent } from './contexts-dispatcher.js'; -import type { ContextsManager } from './contexts-manager.js'; -import { ContextsStatesDispatcher } from './contexts-states-dispatcher.js'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; +import type { ContextHealthState } from '/@/manager/context-health-checker.js'; +import type { ContextPermissionResult } from '/@/manager/context-permissions-checker.js'; +import type { DispatcherEvent } from '/@/manager/contexts-dispatcher.js'; +import type { ContextsManager } from '/@/manager/contexts-manager.js'; +import { ContextsStatesDispatcher } from '/@/manager/contexts-states-dispatcher.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { IDisposable } from '/@common/model/disposable.js'; +import type { RpcExtension } from '/@common/rpc/rpc.js'; +import { + KUBERNETES_CONTEXTS_HEALTHS, + KUBERNETES_CONTEXTS_PERMISSIONS, + KUBERNETES_RESOURCES_COUNT, + KUBERNETES_UPDATE_RESOURCE, +} from '/@common/channels.js'; test('ContextsStatesDispatcher should call updateHealthStates when onContextHealthStateChange event is fired', () => { const manager: ContextsManager = { @@ -41,10 +47,10 @@ test('ContextsStatesDispatcher should call updateHealthStates when onContextHeal onResourceUpdated: vi.fn(), isContextOffline: vi.fn(), } as unknown as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates'); const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions'); dispatcher.init(); @@ -58,7 +64,7 @@ test('ContextsStatesDispatcher should call updateHealthStates when onContextHeal expect(updatePermissionsSpy).not.toHaveBeenCalled(); }); -test('ContextsStatesDispatcher should call updateHealthStates, updateResourcesCount and updateActiveResourcesCount when onOfflineChange event is fired', () => { +test('ContextsStatesDispatcher should call updateHealthStates, updateResourcesCount and updateActiveResourcesCount when onOfflineChange event is fired', async () => { const manager: ContextsManager = { onContextHealthStateChange: vi.fn(), onOfflineChange: vi.fn(), @@ -70,10 +76,10 @@ test('ContextsStatesDispatcher should call updateHealthStates, updateResourcesCo onResourceUpdated: vi.fn(), isContextOffline: vi.fn(), } as unknown as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates'); const updateResourcesCountSpy = vi.spyOn(dispatcher, 'updateResourcesCount'); const updateActiveResourcesCountSpy = vi.spyOn(dispatcher, 'updateActiveResourcesCount'); @@ -85,9 +91,11 @@ test('ContextsStatesDispatcher should call updateHealthStates, updateResourcesCo vi.mocked(manager.onOfflineChange).mockImplementation(f => f() as IDisposable); vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map()); dispatcher.init(); - expect(updateHealthStatesSpy).toHaveBeenCalled(); - expect(updateResourcesCountSpy).toHaveBeenCalled(); - expect(updateActiveResourcesCountSpy).toHaveBeenCalled(); + await vi.waitFor(() => { + expect(updateHealthStatesSpy).toHaveBeenCalled(); + expect(updateResourcesCountSpy).toHaveBeenCalled(); + expect(updateActiveResourcesCountSpy).toHaveBeenCalled(); + }); }); test('ContextsStatesDispatcher should call updatePermissions when onContextPermissionResult event is fired', () => { @@ -102,11 +110,11 @@ test('ContextsStatesDispatcher should call updatePermissions when onContextPermi onResourceUpdated: vi.fn(), isContextOffline: vi.fn(), } as unknown as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; vi.mocked(manager.getPermissions).mockReturnValue([]); - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates'); const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions'); dispatcher.init(); @@ -119,7 +127,7 @@ test('ContextsStatesDispatcher should call updatePermissions when onContextPermi expect(updatePermissionsSpy).toHaveBeenCalled(); }); -test('ContextsStatesDispatcher should call updateHealthStates and updatePermissions when onContextDelete event is fired', () => { +test('ContextsStatesDispatcher should call updateHealthStates and updatePermissions when onContextDelete event is fired', async () => { const manager: ContextsManager = { onContextHealthStateChange: vi.fn(), onOfflineChange: vi.fn(), @@ -131,11 +139,13 @@ test('ContextsStatesDispatcher should call updateHealthStates and updatePermissi onResourceUpdated: vi.fn(), isContextOffline: vi.fn(), } as unknown as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; + + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + vi.mocked(manager.getPermissions).mockReturnValue([]); - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates'); const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions'); vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map()); @@ -146,10 +156,12 @@ test('ContextsStatesDispatcher should call updateHealthStates and updatePermissi vi.mocked(manager.onContextDelete).mockImplementation(f => f({} as DispatcherEvent) as IDisposable); dispatcher.init(); expect(updateHealthStatesSpy).toHaveBeenCalled(); - expect(updatePermissionsSpy).toHaveBeenCalled(); + await vi.waitFor(() => { + expect(updatePermissionsSpy).toHaveBeenCalled(); + }); }); -test('ContextsStatesDispatcher should call updateResource and updateActiveResourcesCount when onResourceUpdated event is fired', () => { +test('ContextsStatesDispatcher should call updateResource and updateActiveResourcesCount when onResourceUpdated event is fired', async () => { const manager: ContextsManager = { onContextHealthStateChange: vi.fn(), onOfflineChange: vi.fn(), @@ -161,11 +173,11 @@ test('ContextsStatesDispatcher should call updateResource and updateActiveResour onResourceUpdated: vi.fn(), isContextOffline: vi.fn(), } as unknown as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; vi.mocked(manager.getPermissions).mockReturnValue([]); - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); const updateResourceSpy = vi.spyOn(dispatcher, 'updateResource'); const updateActiveResourcesCountSpy = vi.spyOn(dispatcher, 'updateActiveResourcesCount'); vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map()); @@ -178,7 +190,9 @@ test('ContextsStatesDispatcher should call updateResource and updateActiveResour ); dispatcher.init(); expect(updateResourceSpy).toHaveBeenCalled(); - expect(updateActiveResourcesCountSpy).toHaveBeenCalled(); + await vi.waitFor(() => { + expect(updateActiveResourcesCountSpy).toHaveBeenCalled(); + }); }); test('getContextsHealths should return the values of the map returned by manager.getHealthCheckersStates without kubeConfig', () => { @@ -191,10 +205,10 @@ test('getContextsHealths should return the values of the map returned by manager getPermissions: vi.fn(), isContextOffline: vi.fn(), } as unknown as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); const context1State = { contextName: 'context1', checking: true, @@ -221,7 +235,7 @@ test('getContextsHealths should return the values of the map returned by manager expect(result).toEqual([context1State, context2State, context3State]); }); -test('updateHealthStates should call apiSender.send with kubernetes-contexts-healths', () => { +test('updateHealthStates should call rpcExtension with kubernetes-contexts-healths', async () => { const manager: ContextsManager = { onContextHealthStateChange: vi.fn(), onContextPermissionResult: vi.fn(), @@ -229,23 +243,23 @@ test('updateHealthStates should call apiSender.send with kubernetes-contexts-hea getHealthCheckersStates: vi.fn(), getPermissions: vi.fn(), } as unknown as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); vi.spyOn(dispatcher, 'getContextsHealths').mockReturnValue([]); - dispatcher.updateHealthStates(); - expect(apiSender.send).toHaveBeenCalledWith('kubernetes-contexts-healths'); + await dispatcher.updateHealthStates(); + expect(rpcExtension.fire).toHaveBeenCalledWith(KUBERNETES_CONTEXTS_HEALTHS, undefined); }); test('getContextsPermissions should return the values as an array', () => { const manager: ContextsManager = { getPermissions: vi.fn(), } as unknown as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); const value: ContextPermission[] = [ { contextName: 'context1', @@ -277,32 +291,32 @@ test('getContextsPermissions should return the values as an array', () => { expect(result).toEqual(value); }); -test('updatePermissions should call apiSender.send with kubernetes-contexts-permissions', () => { +test('updatePermissions should call rpcExtension with kubernetes-contexts-permissions', async () => { const manager: ContextsManager = {} as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); - dispatcher.updatePermissions(); - expect(vi.mocked(apiSender.send)).toHaveBeenCalledWith('kubernetes-contexts-permissions'); + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); + await dispatcher.updatePermissions(); + expect(vi.mocked(rpcExtension.fire)).toHaveBeenCalledWith(KUBERNETES_CONTEXTS_PERMISSIONS, undefined); }); -test('updateResourcesCount should call apiSender.send with kubernetes-resources-count', () => { +test('updateResourcesCount should call rpcExtension with kubernetes-resources-count', async () => { const manager: ContextsManager = {} as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); - dispatcher.updateResourcesCount(); - expect(vi.mocked(apiSender.send)).toHaveBeenCalledWith('kubernetes-resources-count'); + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); + await dispatcher.updateResourcesCount(); + expect(vi.mocked(rpcExtension.fire)).toHaveBeenCalledWith(KUBERNETES_RESOURCES_COUNT, undefined); }); -test('updateResource should call apiSender.send with kubernetes-`resource-name`', () => { +test('updateResource should call rpcExtension with kubernetes-`resource-name`', async () => { const manager: ContextsManager = {} as ContextsManager; - const apiSender: ApiSenderType = { - send: vi.fn(), - } as unknown as ApiSenderType; - const dispatcher = new ContextsStatesDispatcher(manager, apiSender); - dispatcher.updateResource('resource1'); - expect(vi.mocked(apiSender.send)).toHaveBeenCalledWith('kubernetes-update-resource1'); + const rpcExtension: RpcExtension = { + fire: vi.fn(), + } as unknown as RpcExtension; + const dispatcher = new ContextsStatesDispatcher(manager, rpcExtension); + await dispatcher.updateResource('resource1'); + expect(vi.mocked(rpcExtension.fire)).toHaveBeenCalledWith(KUBERNETES_UPDATE_RESOURCE, { resourceName: 'resource1' }); }); diff --git a/packages/extension/src/manager/contexts-states-dispatcher.ts b/packages/extension/src/manager/contexts-states-dispatcher.ts index 21753b8e..f57c27ce 100644 --- a/packages/extension/src/manager/contexts-states-dispatcher.ts +++ b/packages/extension/src/manager/contexts-states-dispatcher.ts @@ -22,39 +22,46 @@ import type { ResourceCount } from '/@common/model/kubernetes-resource-count.js' import type { KubernetesContextResources } from '/@common/model/kubernetes-resources.js'; import type { KubernetesTroubleshootingInformation } from '/@common/model/kubernetes-troubleshooting.js'; -import type { ApiSenderType } from '/@common/model/api-sender.js'; -import type { ContextHealthState } from './context-health-checker.js'; -import type { ContextPermissionResult } from './context-permissions-checker.js'; -import type { DispatcherEvent } from './contexts-dispatcher.js'; -import type { ContextsManager } from './contexts-manager.js'; +import type { ContextHealthState } from '/@/manager/context-health-checker.js'; +import type { ContextPermissionResult } from '/@/manager/context-permissions-checker.js'; +import type { DispatcherEvent } from '/@/manager/contexts-dispatcher.js'; +import type { ContextsManager } from '/@/manager/contexts-manager.js'; +import type { RpcExtension } from '/@common/rpc/rpc.js'; +import { + ACTIVE_RESOURCES_COUNT, + KUBERNETES_CONTEXTS_HEALTHS, + KUBERNETES_CONTEXTS_PERMISSIONS, + KUBERNETES_RESOURCES_COUNT, + KUBERNETES_UPDATE_RESOURCE, +} from '/@common/channels.js'; export class ContextsStatesDispatcher { constructor( private manager: ContextsManager, - private apiSender: ApiSenderType, + private rpcExtension: RpcExtension, ) {} init(): void { this.manager.onContextHealthStateChange((_state: ContextHealthState) => this.updateHealthStates()); - this.manager.onOfflineChange(() => { - this.updateHealthStates(); - this.updateResourcesCount(); - this.updateActiveResourcesCount(); + this.manager.onOfflineChange(async () => { + await this.updateHealthStates(); + await this.updateResourcesCount(); + await this.updateActiveResourcesCount(); }); this.manager.onContextPermissionResult((_permissions: ContextPermissionResult) => this.updatePermissions()); - this.manager.onContextDelete((_state: DispatcherEvent) => { - this.updateHealthStates(); - this.updatePermissions(); + this.manager.onContextDelete(async (_state: DispatcherEvent) => { + await this.updateHealthStates(); + await this.updatePermissions(); }); this.manager.onResourceCountUpdated(() => this.updateResourcesCount()); - this.manager.onResourceUpdated(event => { - this.updateResource(event.resourceName); - this.updateActiveResourcesCount(); + this.manager.onResourceUpdated(async event => { + await this.updateResource(event.resourceName); + await this.updateActiveResourcesCount(); }); } - updateHealthStates(): void { - this.apiSender.send('kubernetes-contexts-healths'); + async updateHealthStates(): Promise { + await this.rpcExtension.fire(KUBERNETES_CONTEXTS_HEALTHS, undefined); } getContextsHealths(): ContextHealth[] { @@ -71,20 +78,20 @@ export class ContextsStatesDispatcher { return value; } - updatePermissions(): void { - this.apiSender.send('kubernetes-contexts-permissions'); + async updatePermissions(): Promise { + await this.rpcExtension.fire(KUBERNETES_CONTEXTS_PERMISSIONS, undefined); } getContextsPermissions(): ContextPermission[] { return this.manager.getPermissions(); } - updateResourcesCount(): void { - this.apiSender.send(`kubernetes-resources-count`); + async updateResourcesCount(): Promise { + await this.rpcExtension.fire(KUBERNETES_RESOURCES_COUNT, undefined); } - updateActiveResourcesCount(): void { - this.apiSender.send(`kubernetes-active-resources-count`); + async updateActiveResourcesCount(): Promise { + await this.rpcExtension.fire(ACTIVE_RESOURCES_COUNT, undefined); } getResourcesCount(): ResourceCount[] { @@ -95,8 +102,8 @@ export class ContextsStatesDispatcher { return this.manager.getActiveResourcesCount(); } - updateResource(resourceName: string): void { - this.apiSender.send(`kubernetes-update-${resourceName}`); + async updateResource(resourceName: string): Promise { + await this.rpcExtension.fire(KUBERNETES_UPDATE_RESOURCE, { resourceName }); } getResources(contextNames: string[], resourceName: string): KubernetesContextResources[] { diff --git a/packages/extension/src/manager/resource-factory-handler.ts b/packages/extension/src/manager/resource-factory-handler.ts index 383255d8..ab43fbfb 100644 --- a/packages/extension/src/manager/resource-factory-handler.ts +++ b/packages/extension/src/manager/resource-factory-handler.ts @@ -18,9 +18,9 @@ import util from 'node:util'; -import type { ContextPermissionsRequest } from './context-permissions-checker.js'; -import type { ResourceFactory } from '../resources/resource-factory.js'; -import { isResourceFactoryWithPermissions } from '../resources/resource-factory.js'; +import type { ContextPermissionsRequest } from '/@/manager/context-permissions-checker.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { isResourceFactoryWithPermissions } from '/@/resources/resource-factory.js'; export class ResourceFactoryHandler { #resourceFactories: ResourceFactory[] = []; diff --git a/packages/extension/src/registry/context-resource-registry.spec.ts b/packages/extension/src/registry/context-resource-registry.spec.ts index 4288d865..5bfb4189 100644 --- a/packages/extension/src/registry/context-resource-registry.spec.ts +++ b/packages/extension/src/registry/context-resource-registry.spec.ts @@ -18,7 +18,7 @@ import { beforeEach, expect, test } from 'vitest'; -import { ContextResourceRegistry } from './context-resource-registry.js'; +import { ContextResourceRegistry } from '/@/registry/context-resource-registry.js'; let registry: ContextResourceRegistry; diff --git a/packages/extension/src/resources/configmaps-resource-factory.ts b/packages/extension/src/resources/configmaps-resource-factory.ts index 7d7d797d..630ec497 100644 --- a/packages/extension/src/resources/configmaps-resource-factory.ts +++ b/packages/extension/src/resources/configmaps-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1ConfigMap, V1ConfigMapList } from '@kubernetes/client-node'; import { CoreV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class ConfigmapsResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/cronjobs-resource-factory.ts b/packages/extension/src/resources/cronjobs-resource-factory.ts index e1351bb6..281bcd39 100644 --- a/packages/extension/src/resources/cronjobs-resource-factory.ts +++ b/packages/extension/src/resources/cronjobs-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1CronJob, V1CronJobList } from '@kubernetes/client-node'; import { BatchV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class CronjobsResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/deployments-resource-factory.spec.ts b/packages/extension/src/resources/deployments-resource-factory.spec.ts index 4cc2b19a..14f998b3 100644 --- a/packages/extension/src/resources/deployments-resource-factory.spec.ts +++ b/packages/extension/src/resources/deployments-resource-factory.spec.ts @@ -19,7 +19,7 @@ import type { V1Deployment } from '@kubernetes/client-node'; import { expect, test } from 'vitest'; -import { DeploymentsResourceFactory } from './deployments-resource-factory.js'; +import { DeploymentsResourceFactory } from '/@/resources/deployments-resource-factory.js'; test('deployment with replica=0 is not active', () => { const factory = new DeploymentsResourceFactory(); diff --git a/packages/extension/src/resources/deployments-resource-factory.ts b/packages/extension/src/resources/deployments-resource-factory.ts index fd6cedc5..3c0c59a8 100644 --- a/packages/extension/src/resources/deployments-resource-factory.ts +++ b/packages/extension/src/resources/deployments-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1Deployment, V1DeploymentList } from '@kubernetes/client-node'; import { AppsV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class DeploymentsResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/events-resource-factory.ts b/packages/extension/src/resources/events-resource-factory.ts index f6a03783..607364b1 100644 --- a/packages/extension/src/resources/events-resource-factory.ts +++ b/packages/extension/src/resources/events-resource-factory.ts @@ -19,10 +19,10 @@ import type { CoreV1Event, CoreV1EventList } from '@kubernetes/client-node'; import { CoreV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class EventsResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/ingresses-resource-factory.ts b/packages/extension/src/resources/ingresses-resource-factory.ts index bc100f87..0abcafee 100644 --- a/packages/extension/src/resources/ingresses-resource-factory.ts +++ b/packages/extension/src/resources/ingresses-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1Ingress, V1IngressList } from '@kubernetes/client-node'; import { NetworkingV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class IngressesResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/jobs-resource-factory.ts b/packages/extension/src/resources/jobs-resource-factory.ts index 610f2676..b0319471 100644 --- a/packages/extension/src/resources/jobs-resource-factory.ts +++ b/packages/extension/src/resources/jobs-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1Job, V1JobList } from '@kubernetes/client-node'; import { BatchV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class JobsResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/nodes-resource-factory.spec.ts b/packages/extension/src/resources/nodes-resource-factory.spec.ts index 3ba01c8c..bc8ddbaf 100644 --- a/packages/extension/src/resources/nodes-resource-factory.spec.ts +++ b/packages/extension/src/resources/nodes-resource-factory.spec.ts @@ -19,7 +19,7 @@ import type { V1Node } from '@kubernetes/client-node'; import { expect, test } from 'vitest'; -import { NodesResourceFactory } from './nodes-resource-factory.js'; +import { NodesResourceFactory } from '/@/resources/nodes-resource-factory.js'; test('node with no status is not active', () => { const factory = new NodesResourceFactory(); diff --git a/packages/extension/src/resources/nodes-resource-factory.ts b/packages/extension/src/resources/nodes-resource-factory.ts index 1bc0a2ef..5c8dc171 100644 --- a/packages/extension/src/resources/nodes-resource-factory.ts +++ b/packages/extension/src/resources/nodes-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1Node, V1NodeList } from '@kubernetes/client-node'; import { CoreV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class NodesResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/pods-resource-factory.ts b/packages/extension/src/resources/pods-resource-factory.ts index 802b4368..96ca9e99 100644 --- a/packages/extension/src/resources/pods-resource-factory.ts +++ b/packages/extension/src/resources/pods-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1Pod, V1PodList } from '@kubernetes/client-node'; import { CoreV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class PodsResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/pvcs-resource-factory.ts b/packages/extension/src/resources/pvcs-resource-factory.ts index 8a4addfb..5d35e733 100644 --- a/packages/extension/src/resources/pvcs-resource-factory.ts +++ b/packages/extension/src/resources/pvcs-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1PersistentVolumeClaim, V1PersistentVolumeClaimList } from '@kubernetes/client-node'; import { CoreV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class PVCsResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/resource-factory-handler.spec.ts b/packages/extension/src/resources/resource-factory-handler.spec.ts index a7aee8de..bdd8e98a 100644 --- a/packages/extension/src/resources/resource-factory-handler.spec.ts +++ b/packages/extension/src/resources/resource-factory-handler.spec.ts @@ -18,10 +18,10 @@ import { expect, test } from 'vitest'; -import { DeploymentsResourceFactory } from './deployments-resource-factory.js'; -import { PodsResourceFactory } from './pods-resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceFactoryHandler } from '../manager/resource-factory-handler.js'; +import { DeploymentsResourceFactory } from '/@/resources/deployments-resource-factory.js'; +import { PodsResourceFactory } from '/@/resources/pods-resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceFactoryHandler } from '/@/manager/resource-factory-handler.js'; test('with 1 level and same request', () => { const factoryHandler = new ResourceFactoryHandler(); diff --git a/packages/extension/src/resources/resource-factory.spec.ts b/packages/extension/src/resources/resource-factory.spec.ts index 86759ab1..b35d17bd 100644 --- a/packages/extension/src/resources/resource-factory.spec.ts +++ b/packages/extension/src/resources/resource-factory.spec.ts @@ -18,9 +18,9 @@ import type { V1Pod } from '@kubernetes/client-node'; import { expect, test } from 'vitest'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import { isResourceFactoryWithPermissions, ResourceFactoryBase } from './resource-factory.js'; -import type { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import { isResourceFactoryWithPermissions, ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import type { ResourceInformer } from '/@/types/resource-informer.js'; test('ResourceFactoryBase set permissions', () => { const factory = new ResourceFactoryBase({ resource: 'resource1' }); diff --git a/packages/extension/src/resources/resource-factory.ts b/packages/extension/src/resources/resource-factory.ts index baed3024..99486d8f 100644 --- a/packages/extension/src/resources/resource-factory.ts +++ b/packages/extension/src/resources/resource-factory.ts @@ -17,8 +17,8 @@ ***********************************************************************/ import type { KubernetesObject, V1ResourceAttributes } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceInformer } from '/@/types/resource-informer.js'; export interface ResourcePermissionsFactory { get permissionsRequests(): V1ResourceAttributes[]; diff --git a/packages/extension/src/resources/routes-resource-factory.ts b/packages/extension/src/resources/routes-resource-factory.ts index 310eb080..9ba98943 100644 --- a/packages/extension/src/resources/routes-resource-factory.ts +++ b/packages/extension/src/resources/routes-resource-factory.ts @@ -21,10 +21,10 @@ import { CustomObjectsApi } from '@kubernetes/client-node'; import type { V1Route } from '/@common/model/openshift-types.js'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class RoutesResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/secrets-resource-factory.ts b/packages/extension/src/resources/secrets-resource-factory.ts index dd5ffe80..25b7bc98 100644 --- a/packages/extension/src/resources/secrets-resource-factory.ts +++ b/packages/extension/src/resources/secrets-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1Secret, V1SecretList } from '@kubernetes/client-node'; import { CoreV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class SecretsResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/resources/services-resource-factory.ts b/packages/extension/src/resources/services-resource-factory.ts index 080a0481..940c3fd1 100644 --- a/packages/extension/src/resources/services-resource-factory.ts +++ b/packages/extension/src/resources/services-resource-factory.ts @@ -19,10 +19,10 @@ import type { V1Service, V1ServiceList } from '@kubernetes/client-node'; import { CoreV1Api } from '@kubernetes/client-node'; -import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js'; -import type { ResourceFactory } from './resource-factory.js'; -import { ResourceFactoryBase } from './resource-factory.js'; -import { ResourceInformer } from '../types/resource-informer.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import type { ResourceFactory } from '/@/resources/resource-factory.js'; +import { ResourceFactoryBase } from '/@/resources/resource-factory.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; export class ServicesResourceFactory extends ResourceFactoryBase implements ResourceFactory { constructor() { diff --git a/packages/extension/src/types/emitter.ts b/packages/extension/src/types/emitter.ts index 48b49a41..b6e8d107 100644 --- a/packages/extension/src/types/emitter.ts +++ b/packages/extension/src/types/emitter.ts @@ -17,8 +17,7 @@ ***********************************************************************/ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ - -import type { IDisposable } from './disposable.js'; +import type { IDisposable } from '/@common/model/disposable'; export type DisposableGroup = { push(disposable: IDisposable): void } | { add(disposable: IDisposable): void }; diff --git a/packages/extension/src/types/kubeconfig-single-context.spec.ts b/packages/extension/src/types/kubeconfig-single-context.spec.ts index a02a561a..86d62d0f 100644 --- a/packages/extension/src/types/kubeconfig-single-context.spec.ts +++ b/packages/extension/src/types/kubeconfig-single-context.spec.ts @@ -20,7 +20,7 @@ import type { Cluster, Context, User } from '@kubernetes/client-node'; import { KubeConfig } from '@kubernetes/client-node'; import { expect, test } from 'vitest'; -import { KubeConfigSingleContext } from './kubeconfig-single-context.js'; +import { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; const contexts = [ { diff --git a/packages/extension/src/types/resource-informer.spec.ts b/packages/extension/src/types/resource-informer.spec.ts index 2a4dfdde..08ab7dc9 100644 --- a/packages/extension/src/types/resource-informer.spec.ts +++ b/packages/extension/src/types/resource-informer.spec.ts @@ -28,8 +28,8 @@ import type { import { ApiException, DELETE, ERROR, KubeConfig, UPDATE } from '@kubernetes/client-node'; import { expect, test, vi } from 'vitest'; -import { KubeConfigSingleContext } from './kubeconfig-single-context.js'; -import { ResourceInformer } from './resource-informer.js'; +import { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; +import { ResourceInformer } from '/@/types/resource-informer.js'; interface MyResource { apiVersion?: string; diff --git a/packages/extension/src/types/resource-informer.ts b/packages/extension/src/types/resource-informer.ts index f42b209d..836cf834 100644 --- a/packages/extension/src/types/resource-informer.ts +++ b/packages/extension/src/types/resource-informer.ts @@ -26,9 +26,9 @@ import type { import { ADD, ApiException, DELETE, ERROR, ListWatch, UPDATE, Watch } from '@kubernetes/client-node'; import type { Disposable } from '@podman-desktop/api'; -import type { Event } from './emitter.js'; -import { Emitter } from './emitter.js'; -import type { KubeConfigSingleContext } from './kubeconfig-single-context.js'; +import type { Event } from '/@/types/emitter.js'; +import { Emitter } from '/@/types/emitter.js'; +import type { KubeConfigSingleContext } from '/@/types/kubeconfig-single-context.js'; interface BaseEvent { kubeconfig: KubeConfigSingleContext; diff --git a/packages/webview/src/Main.svelte b/packages/webview/src/Main.svelte index ed8f5649..a3aa6324 100644 --- a/packages/webview/src/Main.svelte +++ b/packages/webview/src/Main.svelte @@ -1,6 +1,28 @@ -Kubernetes Dashboard +{#if mainContext} + +{/if} diff --git a/packages/webview/src/MainContextAware.svelte b/packages/webview/src/MainContextAware.svelte new file mode 100644 index 00000000..bdce327b --- /dev/null +++ b/packages/webview/src/MainContextAware.svelte @@ -0,0 +1,23 @@ + + +{#if initialized} +

Kubernetes Dashboard

+ + +{/if} diff --git a/packages/webview/src/component/dashboard/ActiveResourceCount.svelte b/packages/webview/src/component/dashboard/ActiveResourceCount.svelte new file mode 100644 index 00000000..446698dc --- /dev/null +++ b/packages/webview/src/component/dashboard/ActiveResourceCount.svelte @@ -0,0 +1,21 @@ + + +{#if activeResourcesCounts} +
    + {#each activeResourcesCounts as count, index (index)} +
  • {count.contextName}/{count.resourceName}: {count.count}
  • + {/each} +
+{/if} diff --git a/packages/webview/src/listener/active-resources-count-listen.spec.ts b/packages/webview/src/listener/active-resources-count-listen.spec.ts new file mode 100644 index 00000000..395f52ec --- /dev/null +++ b/packages/webview/src/listener/active-resources-count-listen.spec.ts @@ -0,0 +1,79 @@ +/********************************************************************** + * Copyright (C) 2025 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 { assert, expect, test, vi } from 'vitest'; + +import { listenActiveResourcesCount } from './active-resources-count-listen'; +import { getContext } from 'svelte'; +import type { RpcBrowser } from '/@common/rpc/rpc'; +import type { DashboardApi } from '/@common/interface/dashboard-api'; + +vi.mock('svelte'); + +test('get initial and updated values', async () => { + const counts = [ + { + contextName: 'ctx1', + resourceName: 'resource1', + count: 1, + }, + { + contextName: 'ctx2', + resourceName: 'resource1', + count: 2, + }, + { + contextName: 'ctx2', + resourceName: 'resource2', + count: 3, + }, + ]; + const rpcBrowserMock = { + on: vi.fn(), + getProxy: vi.fn(), + } as unknown as RpcBrowser; + vi.mocked(getContext).mockReturnValue(rpcBrowserMock); + const proxy: DashboardApi = { + getActiveResourcesCount: vi.fn(), + }; + vi.mocked(rpcBrowserMock.getProxy).mockReturnValue(proxy); + vi.mocked(proxy.getActiveResourcesCount).mockResolvedValueOnce(counts); + + const callback = vi.fn(); + const result = await listenActiveResourcesCount(callback); + expect(result).not.toBeUndefined(); + expect(callback).toHaveBeenCalledWith(counts); + + const newCounts = [ + { + contextName: 'ctx1', + resourceName: 'resource1', + count: 1, + }, + ]; + vi.mocked(proxy.getActiveResourcesCount).mockResolvedValueOnce(newCounts); + + callback.mockClear(); + + const cb = vi.mocked(rpcBrowserMock.on).mock.lastCall?.[1]; + assert(cb); + cb(undefined); + await vi.waitFor(() => { + expect(callback).toHaveBeenCalledWith(newCounts); + }); +}); diff --git a/packages/webview/src/listener/active-resources-count-listen.ts b/packages/webview/src/listener/active-resources-count-listen.ts new file mode 100644 index 00000000..1c94abe0 --- /dev/null +++ b/packages/webview/src/listener/active-resources-count-listen.ts @@ -0,0 +1,53 @@ +/********************************************************************** + * Copyright (C) 2025 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 { ResourceCount } from '/@common/model/kubernetes-resource-count'; +import type { IDisposable } from '/@common/model/disposable'; +import { getContext } from 'svelte'; +import type { RpcBrowser } from '/@common/rpc/rpc'; +import { ACTIVE_RESOURCES_COUNT, API_DASHBOARD } from '/@common/channels'; + +// listenActiveResourcesCount listens the count of active resources +export async function listenActiveResourcesCount( + callback: (activeResourcesCounts: ResourceCount[]) => void, +): Promise { + const rpcBrowser = getContext('RpcBrowser'); + const disposable = rpcBrowser.on(ACTIVE_RESOURCES_COUNT, () => { + collectAndSendCount(rpcBrowser, callback); + }); + + collectAndSendCount(rpcBrowser, callback); + + return { + dispose: (): void => { + disposable.dispose(); + }, + }; +} + +function collectAndSendCount(rpcBrowser: RpcBrowser, callback: (activeResourcesCount: ResourceCount[]) => void): void { + const rpcDashboardClient = rpcBrowser.getProxy(API_DASHBOARD); + rpcDashboardClient + .getActiveResourcesCount() + .then(result => { + callback(result); + }) + .catch(() => { + console.error(`error getting active resources counts`); + }); +} diff --git a/packages/webview/src/main.ts b/packages/webview/src/main.ts new file mode 100644 index 00000000..87b8bb46 --- /dev/null +++ b/packages/webview/src/main.ts @@ -0,0 +1,42 @@ +/********************************************************************** + * Copyright (C) 2025 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 { IDisposable } from '/@common/model/disposable'; +import { RpcBrowser } from '/@common/rpc/rpc'; + +export interface MainContext { + rpcBrowser: RpcBrowser; +} + +export class Main implements IDisposable { + private disposables: IDisposable[] = []; + + async init(): Promise { + const webViewApi = acquirePodmanDesktopApi(); + const rpcBrowser: RpcBrowser = new RpcBrowser(window, webViewApi); + return { + rpcBrowser, + }; + } + + dispose(): void { + for (const disposable of this.disposables) { + disposable.dispose(); + } + } +} diff --git a/packages/webview/vitest.config.ts b/packages/webview/vitest.config.ts index 3f1138df..84c790b0 100644 --- a/packages/webview/vitest.config.ts +++ b/packages/webview/vitest.config.ts @@ -28,7 +28,7 @@ export default defineProject({ resolve: { alias: { '/@/': join(PACKAGE_ROOT, 'src') + '/', - '/@common/': join(PACKAGE_ROOT, '../common') + '/', + '/@common/': join(PACKAGE_ROOT, '../common/src') + '/', }, }, plugins: [svelte({ hot: !process.env.VITEST }), svelteTesting()],