Skip to content

Commit 5762c56

Browse files
authored
feat: select container for terminal (#293)
* feat: select container for terminal Signed-off-by: Philippe Martin <[email protected]> * docs: doc on readme Signed-off-by: Philippe Martin <[email protected]> * test: add unit tests Signed-off-by: Philippe Martin <[email protected]> --------- Signed-off-by: Philippe Martin <[email protected]>
1 parent 9ebdb91 commit 5762c56

File tree

5 files changed

+137
-6
lines changed

5 files changed

+137
-6
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ From this list, you have access to the details for a specifc resource, including
2020
- a utility to patch the resource definition, using [strategic merge patch](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/update-api-object-kubectl-patch/).
2121

2222
For Pods, you also have access to:
23-
- the logs of the containers running in the pod.
23+
- the logs of the containers running in the pod,
24+
- a terminal on each container running in the pod.
2425

2526
You can switch the namespace you want to explore from the Dashboard page or from any list of namespaced resources. The extension will disconnect from the previous namespace and connect to the new one.
2627

packages/webview/src/component/pods/PodDetails.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { PodUI } from './PodUI';
88
import { PodHelper } from './pod-helper';
99
import PodDetailsSummary from './PodDetailsSummary.svelte';
1010
import PodLogs from './PodLogs.svelte';
11-
import PodTerminal from './PodTerminal.svelte';
11+
import PodTerminalBrowser from './PodTerminalBrowser.svelte';
1212
1313
interface Props {
1414
name: string;
@@ -38,7 +38,7 @@ const podHelper = dependencyAccessor.get<PodHelper>(PodHelper);
3838
{
3939
title: 'Terminal',
4040
url: 'terminal',
41-
component: PodTerminal,
41+
component: PodTerminalBrowser,
4242
},
4343
]}
4444
ActionsComponent={Actions}

packages/webview/src/component/pods/PodTerminal.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import { Disposable } from '/@common/types/disposable';
1313
1414
interface Props {
1515
object: V1Pod;
16+
containerName: string;
1617
}
17-
let { object }: Props = $props();
18+
let { object, containerName }: Props = $props();
1819
1920
const streams = getContext<Streams>(Streams);
2021
const remote = getContext<Remote>(Remote);
@@ -30,7 +31,6 @@ let fitAddon: FitAddon;
3031
onMount(async () => {
3132
const podName = object.metadata?.name ?? '';
3233
const namespace = object.metadata?.namespace ?? '';
33-
const containerName = object.spec?.containers?.[0]?.name ?? '';
3434
const savedState = await podTerminalsApi.getState(podName, namespace, containerName);
3535
3636
disposables.push(await initializeNewTerminal(terminalXtermDiv, podName, namespace, containerName));
@@ -106,4 +106,4 @@ onDestroy(() => {
106106
});
107107
</script>
108108

109-
<div class="h-full p-[5px] pr-0 bg-[var(--pd-terminal-background)]" bind:this={terminalXtermDiv}></div>
109+
<div class="h-full w-full p-[5px] pr-0 bg-[var(--pd-terminal-background)]" bind:this={terminalXtermDiv}></div>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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 '@testing-library/jest-dom/vitest';
20+
21+
import { render, screen, within } from '@testing-library/svelte';
22+
import PodTerminalBrowser from './PodTerminalBrowser.svelte';
23+
import type { V1Pod } from '@kubernetes/client-node';
24+
import PodTerminal from './PodTerminal.svelte';
25+
import userEvent from '@testing-library/user-event';
26+
27+
vi.mock(import('./PodTerminal.svelte'));
28+
29+
const fakePod2containersRunning: V1Pod = {
30+
status: {
31+
containerStatuses: [
32+
{ name: 'container1', state: { running: {} } },
33+
{ name: 'container2', state: { running: {} } },
34+
],
35+
},
36+
} as V1Pod;
37+
38+
const fakePod1containerRunning: V1Pod = {
39+
status: {
40+
containerStatuses: [
41+
{ name: 'container1', state: { running: {} } },
42+
{ name: 'container2', state: { terminated: {} } },
43+
],
44+
},
45+
} as V1Pod;
46+
47+
const fakePodNoContainerRunning: V1Pod = {
48+
status: {
49+
containerStatuses: [
50+
{ name: 'container1', state: { terminated: {} } },
51+
{ name: 'container2', state: { terminated: {} } },
52+
],
53+
},
54+
} as V1Pod;
55+
56+
test('renders with no container running', () => {
57+
render(PodTerminalBrowser, { object: fakePodNoContainerRunning });
58+
screen.getByText('No container running');
59+
});
60+
61+
test('renders with 2 containers running', async () => {
62+
render(PodTerminalBrowser, { object: fakePod2containersRunning });
63+
const dropdown = screen.getByLabelText('Select container');
64+
const dropdownButton = within(dropdown).getByText('container1');
65+
expect(vi.mocked(PodTerminal)).toHaveBeenCalledWith(expect.anything(), {
66+
object: fakePod2containersRunning,
67+
containerName: 'container1',
68+
});
69+
await userEvent.click(dropdownButton);
70+
const container2 = within(dropdown).getByText('container2');
71+
await userEvent.click(container2);
72+
expect(vi.mocked(PodTerminal)).toHaveBeenCalledWith(expect.anything(), {
73+
object: fakePod2containersRunning,
74+
containerName: 'container2',
75+
});
76+
});
77+
78+
test('renders with 1 container running', () => {
79+
render(PodTerminalBrowser, { object: fakePod1containerRunning });
80+
expect(screen.queryByText('container1')).not.toBeInTheDocument();
81+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script lang="ts">
2+
import type { V1Pod } from '@kubernetes/client-node';
3+
import { Dropdown, EmptyScreen } from '@podman-desktop/ui-svelte';
4+
import NoLogIcon from '../icons/NoLogIcon.svelte';
5+
import PodTerminal from './PodTerminal.svelte';
6+
import { onMount } from 'svelte';
7+
8+
interface Props {
9+
object: V1Pod;
10+
}
11+
let { object }: Props = $props();
12+
13+
const runningContainers = $derived(object.status?.containerStatuses?.filter(status => status.state?.running) ?? []);
14+
15+
let selectedContainerName = $state('');
16+
17+
onMount(() => {
18+
selectedContainerName = runningContainers?.[0]?.name ?? '';
19+
});
20+
</script>
21+
22+
{#if runningContainers.length > 0}
23+
<div class="flex grow flex-col h-full w-full">
24+
{#if runningContainers.length > 1}
25+
<div class="flex p-2 h-[40px] w-full">
26+
<div class="w-full">
27+
<Dropdown
28+
ariaLabel="Select container"
29+
class="w-48"
30+
name="container"
31+
bind:value={selectedContainerName}
32+
options={runningContainers.map(container => ({
33+
label: container.name,
34+
value: container.name,
35+
}))}>
36+
</Dropdown>
37+
</div>
38+
</div>
39+
{/if}
40+
41+
<div class="flex w-full h-full min-h-0">
42+
{#key selectedContainerName}
43+
<PodTerminal object={object} containerName={selectedContainerName} />
44+
{/key}
45+
</div>
46+
</div>
47+
{:else}
48+
<EmptyScreen icon={NoLogIcon} title="No Terminal" message="No container running" />
49+
{/if}

0 commit comments

Comments
 (0)