diff --git a/__mocks__/@podman-desktop/api.js b/__mocks__/@podman-desktop/api.js index 5f703e4..527b3ed 100644 --- a/__mocks__/@podman-desktop/api.js +++ b/__mocks__/@podman-desktop/api.js @@ -84,6 +84,10 @@ const plugin = { Disposable: { create: (func) => ({ dispose: func }), }, + extensions: { + onDidChange: vi.fn(), + getExtension: vi.fn(), + }, }; module.exports = plugin; diff --git a/packages/extension/package.json b/packages/extension/package.json index 52b1c83..033d1d5 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -24,6 +24,7 @@ "@kubernetes-contexts/rpc": "workspace:*", "@kubernetes-contexts/channels": "workspace:*", "@podman-desktop/api": "1.24.2", + "@podman-desktop/kubernetes-dashboard-extension-api": "0.2.0-next.202512131506-f6c0d06", "@types/node": "^24", "@typescript-eslint/eslint-plugin": "^8.49.0", "@typescript-eslint/parser": "^8.30.1", diff --git a/packages/extension/src/contexts-extension.ts b/packages/extension/src/contexts-extension.ts index b8e4e54..cb6dcdd 100644 --- a/packages/extension/src/contexts-extension.ts +++ b/packages/extension/src/contexts-extension.ts @@ -30,6 +30,7 @@ import { ChannelSubscriber } from '/@/manager/channel-subscriber'; import { Dispatcher } from '/@/manager/dispatcher'; import { existsSync } from 'node:fs'; import { KubeConfig } from '@kubernetes/client-node'; +import { DashboardStatesManager } from './manager/dashboard-states-manager'; export class ContextsExtension { #container: Container | undefined; @@ -40,6 +41,7 @@ export class ContextsExtension { #contextsManager: ContextsManager; #channelSubscriber: ChannelSubscriber; #dispatcher: Dispatcher; + #dashboardStatesManager: DashboardStatesManager; constructor(readonly extensionContext: ExtensionContext) { this.#extensionContext = extensionContext; @@ -62,6 +64,10 @@ export class ContextsExtension { this.#contextsManager = await this.#container.getAsync(ContextsManager); this.#channelSubscriber = await this.#container.getAsync(ChannelSubscriber); this.#dispatcher = await this.#container.getAsync(Dispatcher); + this.#dashboardStatesManager = await this.#container.getAsync(DashboardStatesManager); + this.#dashboardStatesManager.init(); + this.#extensionContext.subscriptions.push(this.#dashboardStatesManager); + this.#dispatcher.init(); const afterFirst = performance.now(); diff --git a/packages/extension/src/manager/_manager-module.ts b/packages/extension/src/manager/_manager-module.ts index 6ecfb89..d0d8813 100644 --- a/packages/extension/src/manager/_manager-module.ts +++ b/packages/extension/src/manager/_manager-module.ts @@ -21,11 +21,13 @@ import { ContainerModule } from 'inversify'; import { ContextsManager } from './contexts-manager'; import { ChannelSubscriber } from '/@/manager/channel-subscriber'; import { Dispatcher } from '/@/manager/dispatcher'; +import { DashboardStatesManager } from './dashboard-states-manager'; const managersModule = new ContainerModule(options => { options.bind(ContextsManager).toSelf().inSingletonScope(); options.bind(ChannelSubscriber).toSelf().inSingletonScope(); options.bind(Dispatcher).toSelf().inSingletonScope(); + options.bind(DashboardStatesManager).toSelf().inSingletonScope(); // Bind IDisposable to services which need to clear data/stop connection/etc when the panel is left // (the onDestroy are not called from components when the panel is left, which may introduce memory leaks if not disposed here) diff --git a/packages/extension/src/manager/dashboard-states-manager.spec.ts b/packages/extension/src/manager/dashboard-states-manager.spec.ts new file mode 100644 index 0000000..29e29c8 --- /dev/null +++ b/packages/extension/src/manager/dashboard-states-manager.spec.ts @@ -0,0 +1,113 @@ +/********************************************************************** + * 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 { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { DashboardStatesManager } from './dashboard-states-manager'; +import { type Disposable, type Extension, extensions } from '@podman-desktop/api'; +import { + type KubernetesDashboardExtensionApi, + type KubernetesDashboardSubscriber, +} from '@podman-desktop/kubernetes-dashboard-extension-api'; + +describe('dashboard extension is not installed', () => { + let manager: DashboardStatesManager; + const onDidChangeDisposable: () => void = vi.fn(); + + beforeEach(() => { + vi.mocked(extensions.onDidChange).mockReturnValue({ + dispose: onDidChangeDisposable, + } as unknown as Disposable); + }); + + afterEach(() => { + manager?.dispose(); + }); + + test('subscriber is undefined', () => { + manager = new DashboardStatesManager(); + manager.init(); + expect(manager.getSubscriber()).toBeUndefined(); + }); + + test('onDidChangeDisposable is called', () => { + manager = new DashboardStatesManager(); + manager.init(); + manager.dispose(); + expect(onDidChangeDisposable).toHaveBeenCalled(); + }); +}); + +describe('dashboard extension is installed', () => { + let manager: DashboardStatesManager; + const onDidChangeDisposable: () => void = vi.fn(); + const subscriber: () => KubernetesDashboardSubscriber = vi.fn(); + const disposeSubscriber: () => void = vi.fn(); + + beforeEach(() => { + vi.mocked(extensions.onDidChange).mockImplementation(f => { + setTimeout(() => { + f(); + }, 0); + return { + dispose: onDidChangeDisposable, + } as unknown as Disposable; + }); + vi.mocked(extensions.getExtension).mockImplementation(id => { + vi.mocked(subscriber).mockReturnValue({ + onContextsHealth: vi.fn(), + onContextsPermissions: vi.fn(), + onResourcesCount: vi.fn(), + dispose: disposeSubscriber, + } as unknown as KubernetesDashboardSubscriber); + if (id === 'redhat.kubernetes-dashboard') { + return { + exports: { + getSubscriber: subscriber, + }, + } as unknown as Extension; + } + return undefined; + }); + }); + + afterEach(() => { + manager?.dispose(); + }); + + test('subscriber is eventually defined', async () => { + manager = new DashboardStatesManager(); + manager.init(); + await vi.waitFor(() => { + expect(manager.getSubscriber()).toBeDefined(); + }); + }); + + test('onDidChangeDisposable is called', () => { + manager = new DashboardStatesManager(); + manager.init(); + manager.dispose(); + expect(onDidChangeDisposable).toHaveBeenCalled(); + }); + + test('subscriber is disposed on dispose', () => { + manager = new DashboardStatesManager(); + manager.init(); + manager.dispose(); + expect(disposeSubscriber).toHaveBeenCalled(); + }); +}); diff --git a/packages/extension/src/manager/dashboard-states-manager.ts b/packages/extension/src/manager/dashboard-states-manager.ts new file mode 100644 index 0000000..33b5c64 --- /dev/null +++ b/packages/extension/src/manager/dashboard-states-manager.ts @@ -0,0 +1,56 @@ +/********************************************************************** + * 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 { Disposable, extensions } from '@podman-desktop/api'; +import { + KubernetesDashboardExtensionApi, + KubernetesDashboardSubscriber, +} from '@podman-desktop/kubernetes-dashboard-extension-api'; +import { injectable } from 'inversify'; + +@injectable() +export class DashboardStatesManager implements Disposable { + #subscriptions: Disposable[] = []; + #subscriber: KubernetesDashboardSubscriber | undefined; + + init(): void { + const didChangeSubscription = extensions.onDidChange(() => { + const api = extensions.getExtension('redhat.kubernetes-dashboard')?.exports; + if (api) { + this.#subscriber = api.getSubscriber(); + // dispose the subscriber when the extension is deactivated + this.#subscriptions.push(this.#subscriber); + // stop being notified when the extension is changed + didChangeSubscription.dispose(); + } + }); + // stop being notified when the extension is deactivated + this.#subscriptions.push(didChangeSubscription); + } + + dispose(): void { + for (const subscription of this.#subscriptions) { + subscription.dispose(); + } + this.#subscriptions = []; + } + + getSubscriber(): KubernetesDashboardSubscriber | undefined { + return this.#subscriber; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85df916..2857705 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,9 @@ importers: '@podman-desktop/api': specifier: 1.24.2 version: 1.24.2 + '@podman-desktop/kubernetes-dashboard-extension-api': + specifier: 0.2.0-next.202512131506-f6c0d06 + version: 0.2.0-next.202512131506-f6c0d06 '@types/node': specifier: ^24 version: 24.10.4 @@ -817,6 +820,9 @@ packages: '@podman-desktop/api@1.24.2': resolution: {integrity: sha512-k1q+FpLtuotjsOkiXUlk7EO2FmRsSAehnyyFTZj/9fHUtYVkit7vy4eAHRvt9KqF3Xujm7jqHFNkuJtfdF01kw==} + '@podman-desktop/kubernetes-dashboard-extension-api@0.2.0-next.202512131506-f6c0d06': + resolution: {integrity: sha512-JzpHXswZzsRzBDP3hMVgMQVwDuT2Ge0m7qbjQoHx3HI86hJHRG1AjzWRrhMHGOPa84MU3sDYGbvdjV/TMx0aIQ==} + '@podman-desktop/ui-svelte@1.24.2': resolution: {integrity: sha512-HsDc0VVCbwHZGlgTScCThLVdRQaYt/coL/+PrTjsbhwb4ECr41f4xfR+QZBuxbjowJurrUzpxgagfO1//U008Q==} peerDependencies: @@ -4570,6 +4576,8 @@ snapshots: '@podman-desktop/api@1.24.2': {} + '@podman-desktop/kubernetes-dashboard-extension-api@0.2.0-next.202512131506-f6c0d06': {} + '@podman-desktop/ui-svelte@1.24.2(svelte-fa@4.0.4(svelte@5.46.0))(svelte@5.46.0)': dependencies: '@floating-ui/dom': 1.7.4