Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: open openshift routes #5560

Merged
merged 1 commit into from Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/main/src/plugin/api/openshift-types.ts
Expand Up @@ -23,6 +23,7 @@ export type V1Route = {
name: string;
namespace: string;
annotations?: { [key: string]: string };
labels?: { [key: string]: string };
};
spec: {
host: string;
Expand Down
96 changes: 82 additions & 14 deletions packages/renderer/src/lib/pod/PodActions.spec.ts
Expand Up @@ -23,16 +23,26 @@ import PodActions from './PodActions.svelte';
import type { PodInfoUI } from './PodInfoUI';
import type { ContainerInfo, Port } from '@podman-desktop/api';

const pod: PodInfoUI = {
const podmanPod: PodInfoUI = {
id: 'pod',
containers: [{ Id: 'pod' }],
status: 'RUNNING',
kind: 'podman',
} as PodInfoUI;

const kubernetesPod: PodInfoUI = {
id: 'pod',
name: 'name',
containers: [{ Id: 'pod' }],
status: 'RUNNING',
kind: 'kubernetes',
} as PodInfoUI;

const listContainersMock = vi.fn();
const getContributedMenusMock = vi.fn();
const updateMock = vi.fn();
const kubernetesGetCurrentNamespaceMock = vi.fn();
const kubernetesReadNamespacedPodMock = vi.fn();

beforeEach(() => {
(window as any).kubernetesDeletePod = vi.fn();
Expand All @@ -41,13 +51,18 @@ beforeEach(() => {
(window as any).stopPod = vi.fn();
(window as any).restartPod = vi.fn();
(window as any).removePod = vi.fn();
(window as any).kubernetesGetCurrentNamespace = kubernetesGetCurrentNamespaceMock;
(window as any).kubernetesReadNamespacedPod = kubernetesReadNamespacedPodMock;

listContainersMock.mockResolvedValue([
{ Id: 'pod', Ports: [{ PublicPort: 8080 } as Port] as Port[] } as ContainerInfo,
]);

kubernetesGetCurrentNamespaceMock.mockResolvedValue('ns');
kubernetesReadNamespacedPodMock.mockResolvedValue({ metadata: { labels: { app: 'foo' } } });

(window as any).getContributedMenus = getContributedMenusMock;
getContributedMenusMock.mockImplementation(() => Promise.resolve([]));
getContributedMenusMock.mockResolvedValue([]);
});

afterEach(() => {
Expand All @@ -58,59 +73,112 @@ afterEach(() => {
test('Expect no error and status starting pod', async () => {
listContainersMock.mockResolvedValue([]);

const { component } = render(PodActions, { pod });
const { component } = render(PodActions, { pod: podmanPod });
component.$on('update', updateMock);

// click on start button
const startButton = screen.getByRole('button', { name: 'Start Pod' });
await fireEvent.click(startButton);

expect(pod.status).toEqual('STARTING');
expect(pod.actionError).toEqual('');
expect(podmanPod.status).toEqual('STARTING');
expect(podmanPod.actionError).toEqual('');
expect(updateMock).toHaveBeenCalled();
});

test('Expect no error and status stopping pod', async () => {
listContainersMock.mockResolvedValue([]);

const { component } = render(PodActions, { pod });
const { component } = render(PodActions, { pod: podmanPod });
component.$on('update', updateMock);

// click on stop button
const stopButton = screen.getByRole('button', { name: 'Stop Pod' });
await fireEvent.click(stopButton);

expect(pod.status).toEqual('STOPPING');
expect(pod.actionError).toEqual('');
expect(podmanPod.status).toEqual('STOPPING');
expect(podmanPod.actionError).toEqual('');
expect(updateMock).toHaveBeenCalled();
});

test('Expect no error and status restarting pod', async () => {
listContainersMock.mockResolvedValue([]);

const { component } = render(PodActions, { pod });
const { component } = render(PodActions, { pod: podmanPod });
component.$on('update', updateMock);

// click on restart button
const restartButton = screen.getByRole('button', { name: 'Restart Pod' });
await fireEvent.click(restartButton);

expect(pod.status).toEqual('RESTARTING');
expect(pod.actionError).toEqual('');
expect(podmanPod.status).toEqual('RESTARTING');
expect(podmanPod.actionError).toEqual('');
expect(updateMock).toHaveBeenCalled();
});

test('Expect no error and status deleting pod', async () => {
listContainersMock.mockResolvedValue([]);

const { component } = render(PodActions, { pod });
const { component } = render(PodActions, { pod: podmanPod });
component.$on('update', updateMock);

// click on delete button
const deleteButton = screen.getByRole('button', { name: 'Delete Pod' });
await fireEvent.click(deleteButton);

expect(pod.status).toEqual('DELETING');
expect(pod.actionError).toEqual('');
expect(podmanPod.status).toEqual('DELETING');
expect(podmanPod.actionError).toEqual('');
expect(updateMock).toHaveBeenCalled();
});

test('Expect kubernetes route to be displayed', async () => {
const routeName = 'route.name';
const routeHost = 'host.local';
const openExternalSpy = vi.fn();

(window as any).kubernetesListRoutes = function () {
return { items: [{ metadata: { labels: { app: 'foo' }, name: routeName }, spec: { host: routeHost } }] };
};
(window as any).openExternal = openExternalSpy;

render(PodActions, { pod: kubernetesPod });

const openRouteButton = await screen.findByRole('button', { name: `Open ${routeName}` });
expect(openRouteButton).toBeVisible();

await fireEvent.click(openRouteButton);

expect(openExternalSpy).toHaveBeenCalledWith(`http://${routeHost}`);
});

test('Expect kubernetes route to be displayed but disabled', async () => {
(window as any).kubernetesListRoutes = function () {
return { items: [] };
};

render(PodActions, { pod: kubernetesPod });

const openRouteButton = await screen.findByRole('button', { name: `Open Browser` });
expect(openRouteButton).toBeVisible();
expect(openRouteButton).toBeDisabled();
});

test('Expect kubernetes routes kebab menu to be displayed', async () => {
(window as any).kubernetesListRoutes = function () {
return {
items: [
{ metadata: { labels: { app: 'foo' }, name: 'route1.name' }, spec: { host: 'host1.local' } },
{ metadata: { labels: { app: 'foo' }, name: 'route2.name' }, spec: { host: 'host2.local' } },
],
};
};

render(PodActions, { pod: kubernetesPod });

const openRouteButton = await screen.findByRole('button', { name: 'Open Kubernetes Routes' });
expect(openRouteButton).toBeVisible();

await fireEvent.click(openRouteButton);

const routesDropDownMenu = await screen.findByTitle('Drop Down Menu Items');
expect(routesDropDownMenu).toBeVisible();
});
89 changes: 74 additions & 15 deletions packages/renderer/src/lib/pod/PodActions.svelte
Expand Up @@ -18,6 +18,7 @@ import ContributionActions from '/@/lib/actions/ContributionActions.svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { MenuContext } from '../../../../main/src/plugin/menu-registry';
import { ContainerUtils } from '../container/container-utils';
import type { V1Route } from '../../../../main/src/plugin/api/openshift-types';

export let pod: PodInfoUI;
export let dropdownMenu = false;
Expand All @@ -32,24 +33,45 @@ onMount(async () => {

let urls: Array<string> = [];
$: openingUrls = urls;
$: openingKubernetesUrls = new Map();

function extractPort(urlString: string) {
const match = urlString.match(/:(\d+)/);
return match ? parseInt(match[1], 10) : undefined;
}

onMount(async () => {
const containerUtils = new ContainerUtils();
if (pod.kind === 'podman') {
const containerUtils = new ContainerUtils();

const containerIds = pod.containers.map(podContainer => podContainer.Id);
const podContainers = (await window.listContainers()).filter(
container => containerIds.findIndex(containerInfo => containerInfo === container.Id) >= 0,
);
const containerIds = pod.containers.map(podContainer => podContainer.Id);
const podContainers = (await window.listContainers()).filter(
container => containerIds.findIndex(containerInfo => containerInfo === container.Id) >= 0,
);

podContainers.forEach(container => {
const openingUrls = containerUtils.getOpeningUrls(container);
urls = [...new Set([...urls, ...openingUrls])];
});
podContainers.forEach(container => {
const openingUrls = containerUtils.getOpeningUrls(container);
urls = [...new Set([...urls, ...openingUrls])];
});
} else if (pod.kind === 'kubernetes') {
const ns = await window.kubernetesGetCurrentNamespace();
if (ns) {
const kubepod = await window.kubernetesReadNamespacedPod(pod.name, ns);
if (kubepod?.metadata?.labels?.app) {
const appName = kubepod.metadata.labels.app;
const routes = await window.kubernetesListRoutes();
const appRoutes: V1Route[] = (routes as any).items.filter(
(r: V1Route) => r.metadata.labels && r.metadata.labels['app'] === appName,
);
appRoutes.forEach((route: V1Route) => {
openingKubernetesUrls = openingKubernetesUrls.set(
route.metadata.name,
route.spec.tls ? `https://${route.spec.host}` : `http://${route.spec.host}`,
);
});
}
}
}
});

function inProgress(inProgress: boolean, state?: string): void {
Expand Down Expand Up @@ -208,13 +230,50 @@ if (dropdownMenu) {
{/each}
</DropdownMenu>
{/if}
<ListItemButtonIcon
title="Restart Pod"
onClick="{() => restartPod()}"
menu="{dropdownMenu}"
detailed="{detailed}"
icon="{faArrowsRotate}" />
{/if}
{#if pod.kind === 'kubernetes'}
{#if openingKubernetesUrls.size === 0}
<ListItemButtonIcon
title="Open Browser"
menu="{dropdownMenu}"
enabled="{false}"
hidden="{dropdownMenu}"
detailed="{detailed}"
icon="{faExternalLinkSquareAlt}" />
{:else if openingKubernetesUrls.size === 1}
<ListItemButtonIcon
title="Open {[...openingKubernetesUrls][0][0]}"
onClick="{() => window.openExternal([...openingKubernetesUrls][0][1])}"
menu="{dropdownMenu}"
enabled="{pod.status === 'RUNNING'}"
hidden="{dropdownMenu}"
detailed="{detailed}"
icon="{faExternalLinkSquareAlt}" />
{:else if openingKubernetesUrls.size > 1}
<DropdownMenu
title="Open Kubernetes Routes"
icon="{faExternalLinkSquareAlt}"
hidden="{dropdownMenu}"
shownAsMenuActionItem="{true}">
{#each Array.from(openingKubernetesUrls) as [routeName, routeHost]}
<ListItemButtonIcon
title="Open {routeName}"
onClick="{() => window.openExternal(routeHost)}"
menu="{!dropdownMenu}"
enabled="{pod.status === 'RUNNING'}"
hidden="{dropdownMenu}"
detailed="{detailed}"
icon="{faExternalLinkSquareAlt}" />
{/each}
</DropdownMenu>
{/if}
{/if}
<ListItemButtonIcon
title="Restart Pod"
onClick="{() => restartPod()}"
menu="{dropdownMenu}"
detailed="{detailed}"
icon="{faArrowsRotate}" />
<ContributionActions
args="{[pod]}"
contextPrefix="podItem"
Expand Down
2 changes: 2 additions & 0 deletions packages/renderer/src/lib/pod/PodsList.spec.ts
Expand Up @@ -35,6 +35,7 @@ const listPodsMock = vi.fn();
const listContainersMock = vi.fn();
const kubernetesListPodsMock = vi.fn();
const getContributedMenusMock = vi.fn();
const kubernetesGetCurrentNamespaceMock = vi.fn();

const provider: ProviderInfo = {
containerConnections: [
Expand Down Expand Up @@ -265,6 +266,7 @@ beforeAll(() => {
(window as any).listPods = listPodsMock;
(window as any).listContainers = listContainersMock.mockResolvedValue([]);
(window as any).kubernetesListPods = kubernetesListPodsMock;
(window as any).kubernetesGetCurrentNamespace = kubernetesGetCurrentNamespaceMock;
(window as any).onDidUpdateProviderStatus = vi.fn().mockResolvedValue(undefined);
(window.events as unknown) = {
receive: (_channel: string, func: any) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/renderer/src/lib/ui/DropdownMenu.svelte
Expand Up @@ -8,6 +8,7 @@ export let onBeforeToggle = () => {};
export let icon: IconDefinition = faEllipsisVertical;
export let shownAsMenuActionItem = false;
export let hidden = false;
export let title = '';

// Show and hide the menu using clickOutside
let showMenu = false;
Expand Down Expand Up @@ -43,12 +44,13 @@ function onWindowClick(e: any) {
<div class="relative inline-block text-left">
<!-- Button for the dropdown menu -->
<button
aria-label="kebab menu"
aria-label="{title.length > 0 ? title : 'kebab menu'}"
on:click="{e => {
// keep track of the cursor position
clientY = e.clientY;
toggleMenu();
}}"
title="{title}"
bind:this="{outsideWindow}"
class="text-gray-400 {shownAsMenuActionItem
? 'bg-charcoal-800 px-3'
Expand Down
3 changes: 2 additions & 1 deletion packages/renderer/src/lib/ui/ListItemButtonIcon.svelte
Expand Up @@ -91,7 +91,8 @@ $: styleClass = detailed
on:click="{handleClick}"
class="{styleClass} relative"
class:disabled="{inProgress}"
class:hidden="{hidden}">
class:hidden="{hidden}"
disabled="{!enabled}">
<Fa class="h-4 w-4 {iconOffset}" icon="{icon}" />
<div
aria-label="spinner"
Expand Down