Skip to content

Commit

Permalink
Kubecontext live information (frontend) (podman-desktop#5917)
Browse files Browse the repository at this point in the history
* feat: display contexts information
Signed-off-by: Philippe Martin <[email protected]>

* chore: add overflow-hidden
Signed-off-by: Philippe Martin <[email protected]>

* test: frontend unit tests
Signed-off-by: Philippe Martin <[email protected]>
  • Loading branch information
feloy authored Feb 21, 2024
1 parent f0d0bca commit 9331a7d
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 23 deletions.
6 changes: 5 additions & 1 deletion packages/main/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,13 @@ import { OpenDevToolsInit } from './open-devtools-init.js';
import { NavigationManager } from '/@/plugin/navigation/navigation-manager.js';
import { WebviewRegistry } from './webview/webview-registry.js';
import type { IDisposable } from './types/disposable.js';

import { KubernetesUtils } from './kubernetes-util.js';
import { downloadGuideList } from './learning-center/learning-center.js';
import type { ColorInfo } from './api/color-info.js';
import { ColorRegistry } from './color-registry.js';
import { DialogRegistry } from './dialog-registry.js';
import type { Deferred } from './util/deferred.js';
import type { ContextState } from './kubernetes-context-state.js';

type LogType = 'log' | 'warn' | 'trace' | 'debug' | 'error';

Expand Down Expand Up @@ -2082,6 +2082,10 @@ export class PluginSystem {
return kubernetesClient.setContext(contextName);
});

this.ipcHandle('kubernetes-client:getContextsState', async (): Promise<Map<string, ContextState>> => {
return kubernetesClient.getContextsState();
});

this.ipcHandle('feedback:send', async (_listener, feedbackProperties: unknown): Promise<void> => {
return telemetry.sendFeedback(feedbackProperties);
});
Expand Down
5 changes: 5 additions & 0 deletions packages/main/src/plugin/kubernetes-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import type { KubernetesInformerManager } from './kubernetes-informer-registry.j
import type { KubernetesInformerResourcesType } from './api/kubernetes-informer-info.js';
import type { IncomingMessage } from 'node:http';
import { ContextsManager } from './kubernetes-context-state.js';
import type { ContextState } from './kubernetes-context-state.js';

interface KubernetesObjectWithKind extends KubernetesObject {
kind: string;
Expand Down Expand Up @@ -1312,4 +1313,8 @@ export class KubernetesClient {
await this.startInformer(informerInfo.resourcesType, id);
}
}

public getContextsState(): Map<string, ContextState> {
return this.contextsState.getContextsState();
}
}
4 changes: 4 additions & 0 deletions packages/preload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import type { KubernetesGeneratorInfo } from '../../main/src/plugin/api/Kubernet
import type { NotificationCard, NotificationCardOptions } from '../../main/src/plugin/api/notification';
import type { ApiSenderType } from '../../main/src/plugin/api';
import type { IDisposable } from '../../main/src/plugin/types/disposable';
import type { ContextState } from '../../main/src/plugin/kubernetes-context-state.js';

export type DialogResultCallback = (openDialogReturnValue: Electron.OpenDialogReturnValue) => void;
export type OpenSaveDialogResultCallback = (result: string | string[] | undefined) => void;
Expand Down Expand Up @@ -1662,6 +1663,9 @@ export function initExposure(): void {
contextBridge.exposeInMainWorld('kubernetesSetContext', async (contextName: string): Promise<void> => {
return ipcInvoke('kubernetes-client:setContext', contextName);
});
contextBridge.exposeInMainWorld('kubernetesGetContextsState', async (): Promise<Map<string, ContextState>> => {
return ipcInvoke('kubernetes-client:getContextsState');
});

contextBridge.exposeInMainWorld('kubernetesGetClusters', async (): Promise<Cluster[]> => {
return ipcInvoke('kubernetes-client:getClusters');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ import { fireEvent, render, screen, within } from '@testing-library/svelte';
import PreferencesKubernetesContextsRendering from './PreferencesKubernetesContextsRendering.svelte';
import { kubernetesContexts } from '/@/stores/kubernetes-contexts';
import type { KubeContext } from '../../../../main/src/plugin/kubernetes-context';
import type { ContextState } from '../../../../main/src/plugin/kubernetes-context-state';
import * as kubernetesContextsState from '/@/stores/kubernetes-contexts-state';
import { readable } from 'svelte/store';

vi.mock('/@/stores/kubernetes-contexts-state', async () => {
return {
kubernetesContextsState: vi.fn(),
};
});

// Create a fake KubeContextUI
const mockContext1: KubeContext = {
Expand Down Expand Up @@ -58,9 +67,11 @@ const mockContext3: KubeContext = {

beforeEach(() => {
kubernetesContexts.set([mockContext1, mockContext2, mockContext3]);
(window as any).kubernetesGetContextsState = vi.fn().mockResolvedValue(new Map<string, ContextState>());
});

test('test that name, cluster and the server is displayed when rendering', async () => {
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextState>>(new Map());
(window as any).kubernetesGetCurrentContextName = vi.fn().mockResolvedValue('my-current-context');
render(PreferencesKubernetesContextsRendering, {});
expect(await screen.findByText('context-name')).toBeInTheDocument();
Expand All @@ -70,17 +81,20 @@ test('test that name, cluster and the server is displayed when rendering', async
});

test('Test that namespace is displayed when available in the context', async () => {
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextState>>(new Map());
render(PreferencesKubernetesContextsRendering, {});
expect(await screen.findByText('namespace-name3')).toBeInTheDocument();
});

test('If nothing is returned for contexts, expect that the page shows a message', async () => {
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextState>>(new Map());
kubernetesContexts.set([]);
render(PreferencesKubernetesContextsRendering, {});
expect(await screen.findByText('No Kubernetes contexts found')).toBeInTheDocument();
});

test('Test that context-name2 is the current context', async () => {
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextState>>(new Map());
(window as any).kubernetesGetCurrentContextName = vi.fn().mockResolvedValue('context-name2');
render(PreferencesKubernetesContextsRendering, {});

Expand All @@ -97,6 +111,7 @@ test('Test that context-name2 is the current context', async () => {
});

test('when deleting the current context, a popup should ask confirmation', async () => {
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextState>>(new Map());
const showMessageBoxMock = vi.fn();
(window as any).showMessageBox = showMessageBoxMock;
showMessageBoxMock.mockResolvedValue({ result: 1 });
Expand All @@ -115,6 +130,7 @@ test('when deleting the current context, a popup should ask confirmation', async
});

test('when deleting the non current context, no popup should ask confirmation', async () => {
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextState>>(new Map());
const showMessageBoxMock = vi.fn();
(window as any).showMessageBox = showMessageBoxMock;
showMessageBoxMock.mockResolvedValue({ result: 1 });
Expand All @@ -131,3 +147,41 @@ test('when deleting the non current context, no popup should ask confirmation',
await fireEvent.click(deleteBtn);
expect(showMessageBoxMock).not.toHaveBeenCalled();
});

test('state and resources counts are displayed in contexts', () => {
const state: Map<string, ContextState> = new Map();
state.set('context-name', {
reachable: true,
podsCount: 1,
deploymentsCount: 2,
});
state.set('context-name2', {
reachable: false,
podsCount: 0,
deploymentsCount: 0,
});
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextState>>(state);
render(PreferencesKubernetesContextsRendering, {});
const context1 = screen.getAllByRole('row')[0];
const context2 = screen.getAllByRole('row')[1];
expect(within(context1).queryByText('REACHABLE')).toBeInTheDocument();
expect(within(context1).queryByText('PODS')).toBeInTheDocument();
expect(within(context1).queryByText('DEPLOYMENTS')).toBeInTheDocument();

const checkCount = (el: HTMLElement, label: string, count: number) => {
const countEl = within(el).getByLabelText(label);
expect(countEl).toBeInTheDocument();
expect(within(countEl).queryByText(count));
};
checkCount(context1, 'context-pods-count', 1);
checkCount(context1, 'context-deployments-count', 2);

expect(within(context2).queryByText('UNREACHABLE')).toBeInTheDocument();
expect(within(context2).queryByText('PODS')).not.toBeInTheDocument();
expect(within(context2).queryByText('DEPLOYMENTS')).not.toBeInTheDocument();

const podsCountContext2 = within(context2).queryByLabelText('context-pods-count');
expect(podsCountContext2).not.toBeInTheDocument();
const deploymentsCountContext2 = within(context2).queryByLabelText('context-deployments-count');
expect(deploymentsCountContext2).not.toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ListItemButtonIcon from '../ui/ListItemButtonIcon.svelte';
import ErrorMessage from '../ui/ErrorMessage.svelte';
import { kubernetesContexts } from '../../stores/kubernetes-contexts';
import { clearKubeUIContextErrors, setKubeUIContextError } from '../kube/KubeContextUI';
import { kubernetesContextsState } from '/@/stores/kubernetes-contexts-state';
$: currentContextName = $kubernetesContexts.find(c => c.currentContext)?.name;
Expand Down Expand Up @@ -95,32 +96,70 @@ async function handleDeleteContext(contextName: string) {
{/if}
</div>
<div class="grow flex-column divide-gray-900 text-gray-400">
<div class="text-xs bg-charcoal-800 p-2 rounded-lg mt-1 grid grid-cols-6">
<span class="my-auto font-bold col-span-1 text-right">CLUSTER</span>
<span class="my-auto col-span-5 text-left pl-0.5 ml-3" aria-label="context-cluster">{context.cluster}</span>
</div>
<div class="flex flex-row">
{#if $kubernetesContextsState.get(context.name)}
<div class="flex-none w-36">
{#if $kubernetesContextsState.get(context.name)?.reachable}
<div class="flex flex-row pt-2">
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<div class="ml-1 font-bold text-[9px] text-green-500" aria-label="context-reachable">REACHABLE</div>
</div>
<div class="flex flex-row gap-4 mt-4">
<div class="text-center">
<div class="font-bold text-[9px] text-gray-800">PODS</div>
<div class="text-[16px] text-white" aria-label="context-pods-count">
{$kubernetesContextsState.get(context.name)?.podsCount}
</div>
</div>
<div class="text-center">
<div class="font-bold text-[9px] text-gray-800">DEPLOYMENTS</div>
<div class="text-[16px] text-white" aria-label="context-deployments-count">
{$kubernetesContextsState.get(context.name)?.deploymentsCount}
</div>
</div>
</div>
{:else}
<div class="flex flex-row pt-2">
<div class="w-3 h-3 rounded-full bg-gray-900"></div>
<div class="ml-1 font-bold text-[9px] text-gray-900">UNREACHABLE</div>
</div>
{/if}
</div>
{/if}
<div class="grow">
<div class="text-xs bg-charcoal-800 p-2 rounded-lg mt-1 grid grid-cols-6">
<span class="my-auto font-bold col-span-1 text-right overflow-hidden text-ellipsis">CLUSTER</span>
<span
class="my-auto col-span-5 text-left pl-0.5 ml-3 overflow-hidden text-ellipsis"
aria-label="context-cluster">{context.cluster}</span>
</div>

{#if context.clusterInfo !== undefined}
<div class="text-xs bg-charcoal-800 p-2 rounded-lg mt-1 grid grid-cols-6">
<span class="my-auto font-bold col-span-1 text-right">SERVER</span>
<span class="my-auto col-span-5 text-left ml-3">
{context.clusterInfo.server}
</span>
</div>
{/if}
{#if context.clusterInfo !== undefined}
<div class="text-xs bg-charcoal-800 p-2 rounded-lg mt-1 grid grid-cols-6">
<span class="my-auto font-bold col-span-1 text-right overflow-hidden text-ellipsis">SERVER</span>
<span class="my-auto col-span-5 text-left ml-3 overflow-hidden text-ellipsis">
{context.clusterInfo.server}
</span>
</div>
{/if}

<div class="text-xs bg-charcoal-800 p-2 rounded-lg mt-1 grid grid-cols-6">
<span class="my-auto font-bold col-span-1 text-right">USER</span>
<span class="my-auto col-span-5 text-left pl-0.5 ml-3" aria-label="context-user">{context.user}</span>
</div>
<div class="text-xs bg-charcoal-800 p-2 rounded-lg mt-1 grid grid-cols-6">
<span class="my-auto font-bold col-span-1 text-right overflow-hidden text-ellipsis">USER</span>
<span
class="my-auto col-span-5 text-left pl-0.5 ml-3 overflow-hidden text-ellipsis"
aria-label="context-user">{context.user}</span>
</div>

{#if context.namespace}
<div class="text-xs bg-charcoal-800 p-2 rounded-lg mt-1 grid grid-cols-6">
<span class="my-auto font-bold col-span-1 text-right">NAMESPACE</span>
<span class="my-auto col-span-5 text-left pl-0.5 ml-3" aria-label="context-namespace"
>{context.namespace}</span>
{#if context.namespace}
<div class="text-xs bg-charcoal-800 p-2 rounded-lg mt-1 grid grid-cols-6">
<span class="my-auto font-bold col-span-1 text-right overflow-hidden text-ellipsis">NAMESPACE</span>
<span
class="my-auto col-span-5 text-left pl-0.5 ml-3 overflow-hidden text-ellipsis"
aria-label="context-namespace">{context.namespace}</span>
</div>
{/if}
</div>
{/if}
</div>
</div>
</div>
{/each}
Expand Down
27 changes: 27 additions & 0 deletions packages/renderer/src/stores/kubernetes-contexts-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**********************************************************************
* Copyright (C) 2024 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 { readable } from 'svelte/store';
import type { ContextState } from '../../../main/src/plugin/kubernetes-context-state';

export const kubernetesContextsState = readable(new Map<string, ContextState>(), set => {
window.kubernetesGetContextsState().then(value => set(value));
window.events?.receive('kubernetes-contexts-state-update', (value: unknown) => {
set(value as Map<string, ContextState>);
});
});

0 comments on commit 9331a7d

Please sign in to comment.