Skip to content

Commit db8e487

Browse files
committed
feat: use kubernetes dashboard api
Signed-off-by: Philippe Martin <phmartin@redhat.com>
1 parent ba411e0 commit db8e487

File tree

7 files changed

+190
-5
lines changed

7 files changed

+190
-5
lines changed

__mocks__/@podman-desktop/api.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ const plugin = {
8484
Disposable: {
8585
create: (func) => ({ dispose: func }),
8686
},
87+
extensions: {
88+
onDidChange: vi.fn(),
89+
getExtension: vi.fn(),
90+
},
8791
};
8892

8993
module.exports = plugin;

packages/extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@kubernetes-contexts/rpc": "workspace:*",
2525
"@kubernetes-contexts/channels": "workspace:*",
2626
"@podman-desktop/api": "1.24.2",
27+
"@podman-desktop/kubernetes-dashboard-extension-api": "0.2.0-next.202512131506-f6c0d06",
2728
"@types/node": "^24",
2829
"@typescript-eslint/eslint-plugin": "^8.49.0",
2930
"@typescript-eslint/parser": "^8.30.1",

packages/extension/src/contexts-extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ChannelSubscriber } from '/@/manager/channel-subscriber';
3030
import { Dispatcher } from '/@/manager/dispatcher';
3131
import { existsSync } from 'node:fs';
3232
import { KubeConfig } from '@kubernetes/client-node';
33+
import { DashboardStatesManager } from './manager/dashboard-states-manager';
3334

3435
export class ContextsExtension {
3536
#container: Container | undefined;
@@ -40,6 +41,7 @@ export class ContextsExtension {
4041
#contextsManager: ContextsManager;
4142
#channelSubscriber: ChannelSubscriber;
4243
#dispatcher: Dispatcher;
44+
#dashboardStatesManager: DashboardStatesManager;
4345

4446
constructor(readonly extensionContext: ExtensionContext) {
4547
this.#extensionContext = extensionContext;
@@ -62,6 +64,10 @@ export class ContextsExtension {
6264
this.#contextsManager = await this.#container.getAsync(ContextsManager);
6365
this.#channelSubscriber = await this.#container.getAsync(ChannelSubscriber);
6466
this.#dispatcher = await this.#container.getAsync(Dispatcher);
67+
this.#dashboardStatesManager = await this.#container.getAsync(DashboardStatesManager);
68+
this.#dashboardStatesManager.init();
69+
this.#extensionContext.subscriptions.push(this.#dashboardStatesManager);
70+
6571
this.#dispatcher.init();
6672

6773
const afterFirst = performance.now();

packages/extension/src/manager/_manager-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import { ContainerModule } from 'inversify';
2121
import { ContextsManager } from './contexts-manager';
2222
import { ChannelSubscriber } from '/@/manager/channel-subscriber';
2323
import { Dispatcher } from '/@/manager/dispatcher';
24+
import { DashboardStatesManager } from './dashboard-states-manager';
2425

2526
const managersModule = new ContainerModule(options => {
2627
options.bind<ContextsManager>(ContextsManager).toSelf().inSingletonScope();
2728
options.bind<ContextsManager>(ChannelSubscriber).toSelf().inSingletonScope();
2829
options.bind<Dispatcher>(Dispatcher).toSelf().inSingletonScope();
30+
options.bind<DashboardStatesManager>(DashboardStatesManager).toSelf().inSingletonScope();
2931

3032
// Bind IDisposable to services which need to clear data/stop connection/etc when the panel is left
3133
// (the onDestroy are not called from components when the panel is left, which may introduce memory leaks if not disposed here)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**********************************************************************
2+
* Copyright (C) 2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
20+
import { DashboardStatesManager } from './dashboard-states-manager';
21+
import { type Disposable, type Extension, extensions } from '@podman-desktop/api';
22+
import {
23+
type KubernetesDashboardExtensionApi,
24+
type KubernetesDashboardSubscriber,
25+
} from '@podman-desktop/kubernetes-dashboard-extension-api';
26+
27+
describe('dashboard extension is not installed', () => {
28+
let manager: DashboardStatesManager;
29+
const onDidChangeDisposable: () => void = vi.fn();
30+
31+
beforeEach(() => {
32+
vi.mocked(extensions.onDidChange).mockReturnValue({
33+
dispose: onDidChangeDisposable,
34+
} as unknown as Disposable);
35+
});
36+
37+
afterEach(() => {
38+
manager?.dispose();
39+
});
40+
41+
test('subscriber is undefined', () => {
42+
manager = new DashboardStatesManager();
43+
manager.init();
44+
expect(manager.getSubscriber()).toBeUndefined();
45+
});
46+
47+
test('onDidChangeDisposable is called', () => {
48+
manager = new DashboardStatesManager();
49+
manager.init();
50+
manager.dispose();
51+
expect(onDidChangeDisposable).toHaveBeenCalled();
52+
});
53+
});
54+
55+
describe('dashboard extension is installed', () => {
56+
let manager: DashboardStatesManager;
57+
const onDidChangeDisposable: () => void = vi.fn();
58+
const subscriber: () => KubernetesDashboardSubscriber = vi.fn();
59+
const disposeSubscriber: () => void = vi.fn();
60+
61+
beforeEach(() => {
62+
vi.mocked(extensions.onDidChange).mockImplementation(f => {
63+
setTimeout(() => {
64+
f();
65+
}, 0);
66+
return {
67+
dispose: onDidChangeDisposable,
68+
} as unknown as Disposable;
69+
});
70+
vi.mocked(extensions.getExtension).mockImplementation(id => {
71+
vi.mocked(subscriber).mockReturnValue({
72+
onContextsHealth: vi.fn(),
73+
onContextsPermissions: vi.fn(),
74+
onResourcesCount: vi.fn(),
75+
dispose: disposeSubscriber,
76+
} as unknown as KubernetesDashboardSubscriber);
77+
if (id === 'redhat.kubernetes-dashboard') {
78+
return {
79+
exports: {
80+
getSubscriber: subscriber,
81+
},
82+
} as unknown as Extension<KubernetesDashboardExtensionApi>;
83+
}
84+
return undefined;
85+
});
86+
});
87+
88+
afterEach(() => {
89+
manager?.dispose();
90+
});
91+
92+
test('subscriber is eventually defined', async () => {
93+
manager = new DashboardStatesManager();
94+
manager.init();
95+
await vi.waitFor(() => {
96+
expect(manager.getSubscriber()).toBeDefined();
97+
});
98+
});
99+
100+
test('onDidChangeDisposable is called', () => {
101+
manager = new DashboardStatesManager();
102+
manager.init();
103+
manager.dispose();
104+
expect(onDidChangeDisposable).toHaveBeenCalled();
105+
});
106+
107+
test('subscriber is disposed on dispose', () => {
108+
manager = new DashboardStatesManager();
109+
manager.init();
110+
manager.dispose();
111+
expect(disposeSubscriber).toHaveBeenCalled();
112+
});
113+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**********************************************************************
2+
* Copyright (C) 2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import { Disposable, extensions } from '@podman-desktop/api';
20+
import {
21+
KubernetesDashboardExtensionApi,
22+
KubernetesDashboardSubscriber,
23+
} from '@podman-desktop/kubernetes-dashboard-extension-api';
24+
import { injectable } from 'inversify';
25+
26+
@injectable()
27+
export class DashboardStatesManager implements Disposable {
28+
#subscriptions: Disposable[] = [];
29+
#subscriber: KubernetesDashboardSubscriber | undefined;
30+
31+
init(): void {
32+
const didChangeSubscription = extensions.onDidChange(() => {
33+
const api = extensions.getExtension<KubernetesDashboardExtensionApi>('redhat.kubernetes-dashboard')?.exports;
34+
if (api) {
35+
this.#subscriber = api.getSubscriber();
36+
// dispose the subscriber when the extension is deactivated
37+
this.#subscriptions.push(this.#subscriber);
38+
// stop being notified when the extension is changed
39+
didChangeSubscription.dispose();
40+
}
41+
});
42+
// stop being notified when the extension is deactivated
43+
this.#subscriptions.push(didChangeSubscription);
44+
}
45+
46+
dispose(): void {
47+
for (const subscription of this.#subscriptions) {
48+
subscription.dispose();
49+
}
50+
this.#subscriptions = [];
51+
}
52+
53+
getSubscriber(): KubernetesDashboardSubscriber | undefined {
54+
return this.#subscriber;
55+
}
56+
}

pnpm-lock.yaml

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)