Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ dist
output
packages/extension/media

/.vs
15 changes: 9 additions & 6 deletions packages/channels/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import { createRpcChannel } from '@kubernetes-dashboard/rpc';
import type {
ContextsHealthsInfo,
ContextsPermissionsInfo,
ResourcesCountInfo,
} from '@podman-desktop/kubernetes-dashboard-extension-api';
import type { ContextsApi } from './interface/contexts-api';
import type { NavigationApi } from './interface/navigation-api';
import type { PodLogsApi } from './interface/pod-logs-api';
Expand All @@ -33,13 +39,8 @@ import type { PodTerminalChunk } from './model/pod-terminal-chunk';
import type { PortForwardsInfo } from './model/port-forward-info';
import type { ResourceDetailsInfo } from './model/resource-details-info';
import type { ResourceEventsInfo } from './model/resource-events-info';
import type { TerminalSettings } from './model/terminal-settings';
import type { UpdateResourceInfo } from './model/update-resource-info';
import { createRpcChannel } from '@kubernetes-dashboard/rpc';
import type {
ContextsHealthsInfo,
ContextsPermissionsInfo,
ResourcesCountInfo,
} from '@podman-desktop/kubernetes-dashboard-extension-api';

// RPC channels (used by the webview to send requests to the extension)
export const API_CONTEXTS = createRpcChannel<ContextsApi>('ContextsApi');
Expand Down Expand Up @@ -68,3 +69,5 @@ export const POD_LOGS = createRpcChannel<PodLogsChunk>('PodLogs');

export const API_POD_TERMINALS = createRpcChannel<PodTerminalsApi>('PodTerminalsApi');
export const POD_TERMINAL_DATA = createRpcChannel<PodTerminalChunk>('PodTerminalData');

export const TERMINAL_SETTINGS = createRpcChannel<TerminalSettings>('TerminalSettings');
4 changes: 3 additions & 1 deletion packages/channels/src/interface/pod-logs-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import type { PodLogsOptions } from '/@/model/pod-logs-options';

export const PodLogsApi = Symbol.for('PodLogsApi');

export interface PodLogsApi {
streamPodLogs(podName: string, namespace: string, containerName: string): Promise<void>;
streamPodLogs(podName: string, namespace: string, containerName: string, options?: PodLogsOptions): Promise<void>;
stopStreamPodLogs(podName: string, namespace: string, containerName: string): Promise<void>;
}
4 changes: 3 additions & 1 deletion packages/channels/src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ export * from './kubernetes-providers-info';
export * from './kubernetes-troubleshooting';
export * from './openshift-types';
export * from './pod-logs-chunk';
export * from './pod-logs-options';
export * from './pod-terminal-chunk';
export * from './port-forward-info';
export * from './port-forward';
export * from './port-forward-info';
export * from './resource-details-info';
export * from './resource-details-options';
export * from './resource-events-info';
export * from './resource-events-options';
export * from './target-ref';
export * from './terminal-settings';
export * from './update-resource-info';
export * from './update-resource-options';
25 changes: 25 additions & 0 deletions packages/channels/src/model/pod-logs-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**********************************************************************
* 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
***********************************************************************/

export interface PodLogsOptions {
stream?: boolean;
previous?: boolean;
tailLines?: number;
sinceSeconds?: number;
timestamps?: boolean;
}
23 changes: 23 additions & 0 deletions packages/channels/src/model/terminal-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**********************************************************************
* 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
***********************************************************************/

export interface TerminalSettings {
fontSize: number;
lineHeight: number;
scrollback: number;
}
18 changes: 11 additions & 7 deletions packages/extension/src/dispatcher/_dispatcher-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,19 @@

import { ContainerModule } from 'inversify';
import { ActiveResourcesCountDispatcher } from './active-resources-count-dispatcher';
import { AvailableContextsDispatcher } from './available-contexts-dispatcher';
import { ContextsHealthsDispatcher } from './contexts-healths-dispatcher';
import { ContextsPermissionsDispatcher } from './contexts-permissions-dispatcher';
import { ResourcesCountDispatcher } from './resources-count-dispatcher';
import { DispatcherObject } from './util/dispatcher-object';
import { CurrentContextDispatcher } from './current-context-dispatcher';
import { UpdateResourceDispatcher } from './update-resource-dispatcher';
import { ResourceDetailsDispatcher } from './resource-details-dispatcher';
import { ResourceEventsDispatcher } from './resource-events-dispatcher';
import { PortForwardsDispatcher } from './port-forwards-dispatcher';
import { TerminalSettingsDispatcher } from './terminal-settings-dispatcher';
import { EndpointsDispatcher } from './endpoints-dispatcher';
import { AvailableContextsDispatcher } from './available-contexts-dispatcher';
import { KubernetesProvidersDispatcher } from './kubernetes-providers-dispatcher';
import { PortForwardsDispatcher } from './port-forwards-dispatcher';
import { ResourceDetailsDispatcher } from './resource-details-dispatcher';
import { ResourceEventsDispatcher } from './resource-events-dispatcher';
import { ResourcesCountDispatcher } from './resources-count-dispatcher';
import { UpdateResourceDispatcher } from './update-resource-dispatcher';
import { DispatcherObject } from './util/dispatcher-object';

const dispatchersModule = new ContainerModule(options => {
options.bind<ActiveResourcesCountDispatcher>(ActiveResourcesCountDispatcher).toSelf().inSingletonScope();
Expand All @@ -50,6 +51,9 @@ const dispatchersModule = new ContainerModule(options => {
options.bind<AvailableContextsDispatcher>(AvailableContextsDispatcher).toSelf().inSingletonScope();
options.bind(DispatcherObject).toService(AvailableContextsDispatcher);

options.bind<TerminalSettingsDispatcher>(TerminalSettingsDispatcher).toSelf().inSingletonScope();
options.bind(DispatcherObject).toService(TerminalSettingsDispatcher);

options.bind<UpdateResourceDispatcher>(UpdateResourceDispatcher).toSelf().inSingletonScope();
options.bind(DispatcherObject).toService(UpdateResourceDispatcher);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**********************************************************************
* 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 { TERMINAL_SETTINGS, type TerminalSettings } from '@kubernetes-dashboard/channels';
import { configuration } from '@podman-desktop/api';
import { injectable } from 'inversify';
import type { DispatcherObject } from '/@/dispatcher/util/dispatcher-object';
import { AbsDispatcherObjectImpl } from '/@/dispatcher/util/dispatcher-object';

@injectable()
export class TerminalSettingsDispatcher
extends AbsDispatcherObjectImpl<void, TerminalSettings>
implements DispatcherObject<void>
{
constructor() {
super(TERMINAL_SETTINGS);
}

getData(): TerminalSettings {
//TODO probably would be nice to expose these keys in the podman-desktop api spec
const terminalSettings = configuration.getConfiguration('terminal');
const fontSize = terminalSettings.get<number>('integrated.fontSize') ?? 10;
const lineHeight = terminalSettings.get<number>('integrated.lineHeight') ?? 1;
const scrollback = terminalSettings.get<number>('integrated.scrollback') ?? 1000;
return {
fontSize,
lineHeight,
scrollback,
};
}
}
74 changes: 43 additions & 31 deletions packages/extension/src/manager/contexts-states-dispatcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,6 @@ import { beforeAll, beforeEach, expect, test, vi } from 'vitest';

import type { IDisposable } from '@kubernetes-dashboard/channels';

import type { ContextHealthState } from './context-health-checker.js';
import type { ContextPermissionResult } from './context-permissions-checker.js';
import type { DispatcherEvent } from './contexts-dispatcher.js';
import { ContextsManager } from './contexts-manager.js';
import { ContextsStatesDispatcher } from './contexts-states-dispatcher.js';
import type { RpcExtension } from '@kubernetes-dashboard/rpc';
import type { ExtensionContext, TelemetryLogger } from '@podman-desktop/api';
import type { Container } from 'inversify';
import { InversifyBinding } from '/@/inject/inversify-binding.js';
import { ResourcesCountDispatcher } from '/@/dispatcher/resources-count-dispatcher.js';
import { ActiveResourcesCountDispatcher } from '/@/dispatcher/active-resources-count-dispatcher.js';
import { ContextsHealthsDispatcher } from '/@/dispatcher/contexts-healths-dispatcher.js';
import { ContextsPermissionsDispatcher } from '/@/dispatcher/contexts-permissions-dispatcher.js';
import {
ACTIVE_RESOURCES_COUNT,
AVAILABLE_CONTEXTS,
Expand All @@ -46,6 +33,19 @@ import {
RESOURCES_COUNT,
UPDATE_RESOURCE,
} from '@kubernetes-dashboard/channels';
import type { RpcExtension } from '@kubernetes-dashboard/rpc';
import type { ExtensionContext, TelemetryLogger } from '@podman-desktop/api';
import type { Container } from 'inversify';
import type { ContextHealthState } from './context-health-checker.js';
import type { ContextPermissionResult } from './context-permissions-checker.js';
import type { DispatcherEvent } from './contexts-dispatcher.js';
import { ContextsManager } from './contexts-manager.js';
import { ContextsStatesDispatcher } from './contexts-states-dispatcher.js';
import { ActiveResourcesCountDispatcher } from '/@/dispatcher/active-resources-count-dispatcher.js';
import { ContextsHealthsDispatcher } from '/@/dispatcher/contexts-healths-dispatcher.js';
import { ContextsPermissionsDispatcher } from '/@/dispatcher/contexts-permissions-dispatcher.js';
import { ResourcesCountDispatcher } from '/@/dispatcher/resources-count-dispatcher.js';
import { InversifyBinding } from '/@/inject/inversify-binding.js';
import { KubernetesProvidersManager } from '/@/manager/kubernetes-providers.js';
import { ChannelSubscriber } from '/@/subscriber/channel-subscriber.js';

Expand Down Expand Up @@ -109,28 +109,33 @@ beforeEach(() => {
dispatcher = container.get<ContextsStatesDispatcher>(ContextsStatesDispatcher);
});

// Number of times TERMINAL_SETTINGS is dispatched when init() is called twice (once per init)
const terminalSettingsCount = 2;

test('ContextsStatesDispatcher should call updateHealthStates when onContextHealthStateChange event is fired', async () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(contextsManagerMock.onContextHealthStateChange).mockImplementation(
f => f({} as ContextHealthState) as IDisposable,
);
dispatcher.init();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 1); // TERMINAL_SETTINGS from both inits + CONTEXTS_HEALTHS
expect(dispatcherSpy).toHaveBeenCalledWith(CONTEXTS_HEALTHS);
});

test('ContextsStatesDispatcher should call updateHealthStates, updateResourcesCount and updateActiveResourcesCount when onOfflineChange event is fired', async () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(contextsManagerMock.onOfflineChange).mockImplementation(f => f() as IDisposable);
dispatcher.init();
await vi.waitFor(() => {
expect(dispatcherSpy).toHaveBeenCalledTimes(3);
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 3); // 2x TERMINAL_SETTINGS + 3 from offline change
});
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining(CONTEXTS_HEALTHS));
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining(RESOURCES_COUNT));
Expand All @@ -140,25 +145,27 @@ test('ContextsStatesDispatcher should call updateHealthStates, updateResourcesCo
test('ContextsStatesDispatcher should call updatePermissions when onContextPermissionResult event is fired', () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(contextsManagerMock.onContextPermissionResult).mockImplementation(
f => f({} as ContextPermissionResult) as IDisposable,
);
dispatcher.init();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 1); // 2x TERMINAL_SETTINGS + CONTEXTS_PERMISSIONS
expect(dispatcherSpy).toHaveBeenCalledWith(CONTEXTS_PERMISSIONS);
});

test('ContextsStatesDispatcher should call updateHealthStates and updatePermissions when onContextDelete event is fired', async () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(contextsManagerMock.onContextDelete).mockImplementation(f => f({} as DispatcherEvent) as IDisposable);
dispatcher.init();
await vi.waitFor(() => {
expect(dispatcherSpy).toHaveBeenCalledTimes(3);
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 3); // 2x TERMINAL_SETTINGS + 3 from context delete
});
expect(dispatcherSpy).toHaveBeenCalledWith(CONTEXTS_HEALTHS);
expect(dispatcherSpy).toHaveBeenCalledWith(CONTEXTS_PERMISSIONS);
Expand All @@ -168,27 +175,29 @@ test('ContextsStatesDispatcher should call updateHealthStates and updatePermissi
test('ContextsStatesDispatcher should dispatchavailable contexts when onContextAdd event is fired', async () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(contextsManagerMock.onContextAdd).mockImplementation(f => f({} as DispatcherEvent) as IDisposable);
dispatcher.init();
await vi.waitFor(() => {
expect(dispatcherSpy).toHaveBeenCalledTimes(1);
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 1); // 2x TERMINAL_SETTINGS + AVAILABLE_CONTEXTS
});
expect(dispatcherSpy).toHaveBeenCalledWith(AVAILABLE_CONTEXTS);
});

test('ContextsStatesDispatcher should call updateResource and updateActiveResourcesCount when onResourceUpdated event is fired', async () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(contextsManagerMock.onResourceUpdated).mockImplementation(
f => f({ contextName: 'context1', resourceName: 'res1' }) as IDisposable,
);
dispatcher.init();
await vi.waitFor(() => {
expect(dispatcherSpy).toHaveBeenCalledTimes(4);
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 4); // 2x TERMINAL_SETTINGS + 4 from resource updated
});
expect(dispatcherSpy).toHaveBeenCalledWith(UPDATE_RESOURCE);
expect(dispatcherSpy).toHaveBeenCalledWith(RESOURCE_DETAILS);
Expand All @@ -199,12 +208,13 @@ test('ContextsStatesDispatcher should call updateResource and updateActiveResour
test('ContextsStatesDispatcher should dispatch CURRENT_CONTEXT when onCurrentContextChange event is fired', async () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(contextsManagerMock.onCurrentContextChange).mockImplementation(f => f() as IDisposable);
dispatcher.init();
await vi.waitFor(() => {
expect(dispatcherSpy).toHaveBeenCalledTimes(1);
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 2); // 2x TERMINAL_SETTINGS + CURRENT_CONTEXT + UPDATE_RESOURCE
});
expect(dispatcherSpy).toHaveBeenCalledWith(CURRENT_CONTEXT);
});
Expand All @@ -220,25 +230,27 @@ test('dispatchByChannelName is called when onSubscribe emits an event', async ()
test('ContextsStatesDispatcher should dispatch ENDPOINTS when onEndpointsChange event is fired', async () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(contextsManagerMock.onEndpointsChange).mockImplementation(f => f() as IDisposable);
dispatcher.init();
await vi.waitFor(() => {
expect(dispatcherSpy).toHaveBeenCalledTimes(1);
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 1); // 2x TERMINAL_SETTINGS + ENDPOINTS
});
expect(dispatcherSpy).toHaveBeenCalledWith(ENDPOINTS);
});

test('ContextsStatesDispatcher should dispatch KUBERNETES_PROVIDERS when onKubernetesProvidersChange event is fired', async () => {
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
dispatcher.init();
expect(dispatcherSpy).not.toHaveBeenCalled();
expect(dispatcherSpy).toHaveBeenCalledOnce();
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'TerminalSettings' }));

vi.mocked(kubernetesProvidersManagerMock.onKubernetesProvidersChange).mockImplementation(f => f() as IDisposable);
dispatcher.init();
await vi.waitFor(() => {
expect(dispatcherSpy).toHaveBeenCalledTimes(1);
expect(dispatcherSpy).toHaveBeenCalledTimes(terminalSettingsCount + 1); // 2x TERMINAL_SETTINGS + KUBERNETES_PROVIDERS
});
expect(dispatcherSpy).toHaveBeenCalledWith(KUBERNETES_PROVIDERS);
});
Loading