Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/common/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>('DashboardApi');

export interface KubernetesUpdateResourceInfo {
resourceName: string;
}

// Broadcast events (sent by extension and received by the webview)
export const ACTIVE_RESOURCES_COUNT = createRpcChannel<undefined>('ActiveResourcesCount');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK it should not be <undefined>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think? Should it be some 'empty' value (void?), or do you think it is expecting some real data? (I would prefer to not pass data from here, but let the frontend query the data when it wants, with some throttling/debouncing)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RPC channel is used by the webview to call methods on the extension so I would not expect an undefined type at this level. It's providing at the end a Proxy.

I expect here to have an API (an interface) and then this interface can have Promise methods.

webview then say: Give me a proxy to counters API

and then I can ask: startCounting(): Promise<void> (it's a dummy example)

Copy link
Contributor Author

@feloy feloy Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the lightspeed extension (https://github.com/redhat-developer/podman-desktop-redhat-lightspeed-ext/blob/main/packages/common/src/channels.ts), I understand that the pattern you are describing is used for the RPC channels (first section), but not for the Broadcast events (second section), where the type is only the data the extension wants to send to the webview along with the event. Am I wrong?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for broadcast sections, then it's broadcasting the value to be displayed

so for the case of counts for example it should be ActiveResourcesCountInfo interface with for example count: number field

the idea is that the frontend is never asking directly a data

frontend ask for a data, backend broadcast the value of the data, frontend receive the value and can display it.

Copy link
Contributor Author

@feloy feloy Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My argument for doing the other way (webview asking for the data), is that the data provided by the extension will be massive (for some events, it will be the complete description of all the pods, for example). We will need to introduce some debouncing/throttling to limit how frequently we really want to transfer this data. Do you think that this should be done in the extension side instead of the webview?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my idea is that we don't broadcast every 5s for example (or on every change) or we don't need to display the value

but the logic is: frontend ask when it's interesting, then extension/backend will send the data. Debouncing/throttling being done on the extension side, webview only taking care of displaying values.

webview side:

I want to monitor cluster 2 for "pod resource"

and then it receives value for "pods"

you change the page, then it's sending "I'm no longer interested" and the data are no longer sent.

overall idea is:
only quick calls from frontend to backend (we're almost never waiting for a result)
and sending data is only done asynchronously from backend to frontend.

logic = backend, display = frontend

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or in short: we ask for a value but we don't get it directly, it's received through another channel.

it avoids stores /states that follow the logic: if not intialized, asked for the data.

initEvents(): {
 if (!events) {
    events = await window.grabLatestEvents();
}

onEvents(events: Events) {
events = events)
}

in fact, we don't have anymore grabLatestEvents methods at all that return the value
values are always received through the events

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I'll move all the logic to the extension. Thanks

export const KUBERNETES_CONTEXTS_HEALTHS = createRpcChannel<undefined>('KubernetesContextsHealths');
export const KUBERNETES_CONTEXTS_PERMISSIONS = createRpcChannel<undefined>('KubernetesContextsPermissions');
export const KUBERNETES_RESOURCES_COUNT = createRpcChannel<undefined>('KubernetesResourcesCount');
export const KUBERNETES_UPDATE_RESOURCE = createRpcChannel<KubernetesUpdateResourceInfo>('KubernetesUpdateResource');
4 changes: 3 additions & 1 deletion packages/common/src/interface/dashboard-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
getActiveResourcesCount(): Promise<ResourceCount[]>;
}
35 changes: 35 additions & 0 deletions packages/extension/src/controller/dashboard-impl.ts
Original file line number Diff line number Diff line change
@@ -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<DashboardApi> {
return API_DASHBOARD;
}

async getActiveResourcesCount(): Promise<ResourceCount[]> {
return this.contextsStatesDispatcher.getActiveResourcesCount();
}
}
24 changes: 15 additions & 9 deletions packages/extension/src/dashboard-extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
20 changes: 11 additions & 9 deletions packages/extension/src/dashboard-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
Expand All @@ -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();
}
Expand Down
6 changes: 3 additions & 3 deletions packages/extension/src/main.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
13 changes: 2 additions & 11 deletions packages/extension/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
6 changes: 3 additions & 3 deletions packages/extension/src/manager/context-health-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions packages/extension/src/manager/contexts-dispatcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down
6 changes: 3 additions & 3 deletions packages/extension/src/manager/contexts-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 8 additions & 8 deletions packages/extension/src/manager/contexts-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Event<CacheUpdatedEvent>>();
const onOfflineMock = vi.fn<Event<OfflineEvent>>();
Expand Down
50 changes: 25 additions & 25 deletions packages/extension/src/manager/contexts-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading