Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: experimental health frontend (#10349)
Browse files Browse the repository at this point in the history
* feat: display health in frontend
Signed-off-by: Philippe Martin <phmartin@redhat.com>

* refactor: get experimentalStates config value
Signed-off-by: Philippe Martin <phmartin@redhat.com>

* test: unit tests
Signed-off-by: Philippe Martin <phmartin@redhat.com>

* fix: use vi.mocked
Signed-off-by: Philippe Martin <phmartin@redhat.com>

* fix: do not wast window to any
Signed-off-by: Philippe Martin <phmartin@redhat.com>

* refactor: use EventStore
Signed-off-by: Philippe Martin <phmartin@redhat.com>

* fix: compute outside of template
Signed-off-by: Philippe Martin <phmartin@redhat.com>
feloy authored Dec 16, 2024
1 parent da13d98 commit 7da6e4a
Showing 10 changed files with 418 additions and 51 deletions.
23 changes: 23 additions & 0 deletions packages/api/src/kubernetes-contexts-healths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**********************************************************************
* 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
***********************************************************************/

export interface ContextHealth {
contextName: string;
checking: boolean;
reachable: boolean;
}
5 changes: 5 additions & 0 deletions packages/main/src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -87,6 +87,7 @@ import type { ImageInfo } from '/@api/image-info.js';
import type { ImageInspectInfo } from '/@api/image-inspect-info.js';
import type { ImageSearchOptions, ImageSearchResult, ImageTagsListOptions } from '/@api/image-registry.js';
import type { KubeContext } from '/@api/kubernetes-context.js';
import type { ContextHealth } from '/@api/kubernetes-contexts-healths.js';
import type { ContextGeneralState, ResourceName } from '/@api/kubernetes-contexts-states.js';
import type { ForwardConfig, ForwardOptions } from '/@api/kubernetes-port-forward-model.js';
import type { ManifestCreateOptions, ManifestInspectInfo, ManifestPushOptions } from '/@api/manifest-info.js';
@@ -2584,6 +2585,10 @@ export class PluginSystem {
},
);

this.ipcHandle('kubernetes:getContextsHealths', async (_listener): Promise<ContextHealth[]> => {
return kubernetesClient.getContextsHealths();
});

const kubernetesExecCallbackMap = new Map<
number,
{ onStdIn: (data: string) => void; onResize: (columns: number, rows: number) => void }
Original file line number Diff line number Diff line change
@@ -18,8 +18,11 @@

import { expect, test, vi } from 'vitest';

import type { ApiSenderType } from '../api.js';
import type { ContextHealthState } from './context-health-checker.js';
import type { ContextsManagerExperimental } from './contexts-manager-experimental.js';
import { ContextsStatesDispatcher } from './contexts-states-dispatcher.js';
import type { KubeConfigSingleContext } from './kubeconfig-single-context.js';

test('ContextsStatesDispatcher should call updateHealthStates when onContextHealthStateChange event is fired', () => {
const onContextHealthStateChangeMock = vi.fn();
@@ -31,14 +34,18 @@ test('ContextsStatesDispatcher should call updateHealthStates when onContextHeal
getHealthCheckersStates: vi.fn(),
getPermissions: vi.fn(),
} as unknown as ContextsManagerExperimental;
const dispatcher = new ContextsStatesDispatcher(manager);
const apiSender: ApiSenderType = {
send: vi.fn(),
} as unknown as ApiSenderType;
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates');
const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions');
dispatcher.init();
expect(updateHealthStatesSpy).not.toHaveBeenCalled();
expect(updatePermissionsSpy).not.toHaveBeenCalled();

onContextHealthStateChangeMock.mockImplementation(f => f());
vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map<string, ContextHealthState>());
dispatcher.init();
expect(updateHealthStatesSpy).toHaveBeenCalled();
expect(updatePermissionsSpy).not.toHaveBeenCalled();
@@ -54,7 +61,8 @@ test('ContextsStatesDispatcher should call updatePermissions when onContextPermi
getHealthCheckersStates: vi.fn(),
getPermissions: vi.fn(),
} as unknown as ContextsManagerExperimental;
const dispatcher = new ContextsStatesDispatcher(manager);
const apiSender: ApiSenderType = {} as unknown as ApiSenderType;
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates');
const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions');
dispatcher.init();
@@ -76,9 +84,13 @@ test('ContextsStatesDispatcher should call updateHealthStates and updatePermissi
getHealthCheckersStates: vi.fn(),
getPermissions: vi.fn(),
} as unknown as ContextsManagerExperimental;
const dispatcher = new ContextsStatesDispatcher(manager);
const apiSender: ApiSenderType = {
send: vi.fn(),
} as unknown as ApiSenderType;
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates');
const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions');
vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map<string, ContextHealthState>());
dispatcher.init();
expect(updateHealthStatesSpy).not.toHaveBeenCalled();
expect(updatePermissionsSpy).not.toHaveBeenCalled();
@@ -88,3 +100,53 @@ test('ContextsStatesDispatcher should call updateHealthStates and updatePermissi
expect(updateHealthStatesSpy).toHaveBeenCalled();
expect(updatePermissionsSpy).toHaveBeenCalled();
});

test('getContextsHealths should return the values of the map returned by manager.getHealthCheckersStates without kubeConfig', () => {
const manager: ContextsManagerExperimental = {
onContextHealthStateChange: vi.fn(),
onContextPermissionResult: vi.fn(),
onContextDelete: vi.fn(),
getHealthCheckersStates: vi.fn(),
getPermissions: vi.fn(),
} as unknown as ContextsManagerExperimental;
const sendMock = vi.fn();
const apiSender: ApiSenderType = {
send: sendMock,
} as unknown as ApiSenderType;
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
const context1State = {
contextName: 'context1',
checking: true,
reachable: false,
};
const context2State = {
contextName: 'context2',
checking: false,
reachable: true,
};
const value = new Map<string, ContextHealthState>([
['context1', { ...context1State, kubeConfig: {} as unknown as KubeConfigSingleContext }],
['context2', { ...context2State, kubeConfig: {} as unknown as KubeConfigSingleContext }],
]);
vi.mocked(manager.getHealthCheckersStates).mockReturnValue(value);
const result = dispatcher.getContextsHealths();
expect(result).toEqual([context1State, context2State]);
});

test('updateHealthStates should call apiSender.send with kubernetes-contexts-healths', () => {
const manager: ContextsManagerExperimental = {
onContextHealthStateChange: vi.fn(),
onContextPermissionResult: vi.fn(),
onContextDelete: vi.fn(),
getHealthCheckersStates: vi.fn(),
getPermissions: vi.fn(),
} as unknown as ContextsManagerExperimental;
const sendMock = vi.fn();
const apiSender: ApiSenderType = {
send: sendMock,
} as unknown as ApiSenderType;
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
vi.spyOn(dispatcher, 'getContextsHealths').mockReturnValue([]);
dispatcher.updateHealthStates();
expect(sendMock).toHaveBeenCalledWith('kubernetes-contexts-healths');
});
22 changes: 20 additions & 2 deletions packages/main/src/plugin/kubernetes/contexts-states-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -16,13 +16,19 @@
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import type { ContextHealth } from '/@api/kubernetes-contexts-healths.js';

import type { ApiSenderType } from '../api.js';
import type { ContextHealthState } from './context-health-checker.js';
import type { ContextPermissionResult } from './context-permissions-checker.js';
import type { DispatcherEvent } from './contexts-dispatcher.js';
import type { ContextsManagerExperimental } from './contexts-manager-experimental.js';

export class ContextsStatesDispatcher {
constructor(private manager: ContextsManagerExperimental) {}
constructor(
private manager: ContextsManagerExperimental,
private apiSender: ApiSenderType,
) {}

init(): void {
this.manager.onContextHealthStateChange((_state: ContextHealthState) => this.updateHealthStates());
@@ -34,7 +40,19 @@ export class ContextsStatesDispatcher {
}

updateHealthStates(): void {
console.log('current health check states', this.manager.getHealthCheckersStates());
this.apiSender.send('kubernetes-contexts-healths');
}

getContextsHealths(): ContextHealth[] {
const value: ContextHealth[] = [];
for (const [contextName, health] of this.manager.getHealthCheckersStates()) {
value.push({
contextName,
checking: health.checking,
reachable: health.reachable,
});
}
return value;
}

updatePermissions(): void {
10 changes: 9 additions & 1 deletion packages/main/src/plugin/kubernetes/kubernetes-client.ts
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ import { parseAllDocuments } from 'yaml';
import type { KubernetesPortForwardService } from '/@/plugin/kubernetes/kubernetes-port-forward-service.js';
import { KubernetesPortForwardServiceProvider } from '/@/plugin/kubernetes/kubernetes-port-forward-service.js';
import type { KubeContext } from '/@api/kubernetes-context.js';
import type { ContextHealth } from '/@api/kubernetes-contexts-healths.js';
import type { ContextGeneralState, ResourceName } from '/@api/kubernetes-contexts-states.js';
import type { ForwardConfig, ForwardOptions } from '/@api/kubernetes-port-forward-model.js';
import type { V1Route } from '/@api/openshift-types.js';
@@ -270,7 +271,7 @@ export class KubernetesClient {
if (statesExperimental) {
const manager = new ContextsManagerExperimental();
this.contextsState = manager;
this.contextsStatesDispatcher = new ContextsStatesDispatcher(manager);
this.contextsStatesDispatcher = new ContextsStatesDispatcher(manager, this.apiSender);
this.contextsStatesDispatcher.init();
}

@@ -1844,4 +1845,11 @@ export class KubernetesClient {
public async deletePortForward(config: ForwardConfig): Promise<void> {
return this.ensurePortForwardService().deleteForward(config);
}

public getContextsHealths(): ContextHealth[] {
if (!this.contextsStatesDispatcher) {
throw new Error('contextsStatesDispatcher is undefined. This should not happen in Kubernetes experimental');
}
return this.contextsStatesDispatcher?.getContextsHealths();
}
}
5 changes: 5 additions & 0 deletions packages/preload/src/index.ts
Original file line number Diff line number Diff line change
@@ -67,6 +67,7 @@ import type { ImageInfo } from '/@api/image-info';
import type { ImageInspectInfo } from '/@api/image-inspect-info';
import type { ImageSearchOptions, ImageSearchResult, ImageTagsListOptions } from '/@api/image-registry';
import type { KubeContext } from '/@api/kubernetes-context';
import type { ContextHealth } from '/@api/kubernetes-contexts-healths';
import type { ContextGeneralState, ResourceName } from '/@api/kubernetes-contexts-states';
import type { ForwardConfig, ForwardOptions } from '/@api/kubernetes-port-forward-model';
import type { ManifestCreateOptions, ManifestInspectInfo, ManifestPushOptions } from '/@api/manifest-info';
@@ -1871,6 +1872,10 @@ export function initExposure(): void {
},
);

contextBridge.exposeInMainWorld('kubernetesGetContextsHealths', async (): Promise<ContextHealth[]> => {
return ipcInvoke('kubernetes:getContextsHealths');
});

contextBridge.exposeInMainWorld('kubernetesGetClusters', async (): Promise<Cluster[]> => {
return ipcInvoke('kubernetes-client:getClusters');
});
Original file line number Diff line number Diff line change
@@ -20,8 +20,9 @@ import '@testing-library/jest-dom/vitest';

import { fireEvent, render, screen, within } from '@testing-library/svelte';
import { readable } from 'svelte/store';
import { beforeEach, expect, test, vi } from 'vitest';
import { beforeEach, describe, expect, test, vi } from 'vitest';

import { kubernetesContextsHealths } from '/@/stores/kubernetes-context-health';
import { kubernetesContexts } from '/@/stores/kubernetes-contexts';
import * as kubernetesContextsState from '/@/stores/kubernetes-contexts-state';
import type { KubeContext } from '/@api/kubernetes-context';
@@ -157,45 +158,95 @@ test('when deleting the non current context, no popup should ask confirmation',
expect(showMessageBoxMock).not.toHaveBeenCalled();
});

test('state and resources counts are displayed in contexts', () => {
const state: Map<string, ContextGeneralState> = new Map();
state.set('context-name', {
reachable: true,
resources: {
pods: 1,
deployments: 2,
describe.each([
{
name: 'experimental states',
implemented: {
health: true,
resourcesCount: false,
},
});
state.set('context-name2', {
reachable: false,
resources: {
pods: 0,
deployments: 0,
initMocks: () => {
Object.defineProperty(global, 'window', {
value: {
getConfigurationValue: vi.fn(),
},
});
vi.mocked(window.getConfigurationValue<boolean>).mockResolvedValue(true);
kubernetesContextsHealths.set([
{
contextName: 'context-name',
reachable: true,
checking: false,
},
{
contextName: 'context-name2',
reachable: false,
checking: false,
},
]);
},
},
{
name: 'non-experimental states',
implemented: {
health: true,
resourcesCount: true,
},
initMocks: () => {
const state: Map<string, ContextGeneralState> = new Map();
state.set('context-name', {
reachable: true,
resources: {
pods: 1,
deployments: 2,
},
});
state.set('context-name2', {
reachable: false,
resources: {
pods: 0,
deployments: 0,
},
});
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextGeneralState>>(state);
vi.mocked(kubernetesContextsState).kubernetesContextsCheckingStateDelayed = readable<Map<string, boolean>>(
new Map(),
);
},
},
])('$name', ({ implemented, initMocks }) => {
test('state and resources counts are displayed in contexts', async () => {
initMocks();
render(PreferencesKubernetesContextsRendering, {});
const context1 = screen.getAllByRole('row')[0];
const context2 = screen.getAllByRole('row')[1];
if (implemented.health) {
await vi.waitFor(() => {
expect(within(context1).queryByText('REACHABLE')).toBeInTheDocument();
});
}
expect(within(context1).queryByText('PODS')).toBeInTheDocument();
expect(within(context1).queryByText('DEPLOYMENTS')).toBeInTheDocument();

if (implemented.resourcesCount) {
const checkCount = (el: HTMLElement, label: string, count: number) => {
const countEl = within(el).getByLabelText(label);
expect(countEl).toBeInTheDocument();
expect(within(countEl).queryByText(count)).toBeTruthy();
};
checkCount(context1, 'Context Pods Count', 1);
checkCount(context1, 'Context Deployments Count', 2);
}

if (implemented.health) {
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();
});
vi.mocked(kubernetesContextsState).kubernetesContextsState = readable<Map<string, ContextGeneralState>>(state);
vi.mocked(kubernetesContextsState).kubernetesContextsCheckingStateDelayed = readable<Map<string, boolean>>(new Map());
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)).toBeTruthy();
};
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
@@ -4,17 +4,35 @@ import { Button, EmptyScreen, ErrorMessage, Spinner } from '@podman-desktop/ui-s
import { onMount } from 'svelte';
import { router } from 'tinro';
import { kubernetesContextsHealths } from '/@/stores/kubernetes-context-health';
import { kubernetesContextsCheckingStateDelayed, kubernetesContextsState } from '/@/stores/kubernetes-contexts-state';
import type { KubeContext } from '/@api/kubernetes-context';
import { kubernetesContexts } from '../../stores/kubernetes-contexts';
import { clearKubeUIContextErrors, setKubeUIContextError } from '../kube/KubeContextUI';
import EngineIcon from '../ui/EngineIcon.svelte';
import ListItemButtonIcon from '../ui/ListItemButtonIcon.svelte';
import SettingsPage from './SettingsPage.svelte';
$: currentContextName = $kubernetesContexts.find(c => c.currentContext)?.name;
interface KubeContextWithStates extends KubeContext {
isReachable: boolean;
isKnown: boolean;
isBeingChecked: boolean;
}
const currentContextName = $derived($kubernetesContexts.find(c => c.currentContext)?.name);
let kubeconfigFilePath: string = '';
let kubeconfigFilePath: string = $state('');
let experimentalStates: boolean = $state(false);
const kubernetesContextsWithStates: KubeContextWithStates[] = $derived(
$kubernetesContexts.map(kubeContext => ({
...kubeContext,
isReachable: isContextReachable(kubeContext.name, experimentalStates),
isKnown: isContextKnown(kubeContext.name, experimentalStates),
isBeingChecked: isContextBeingChecked(kubeContext.name, experimentalStates),
})),
);
onMount(async () => {
try {
@@ -27,6 +45,12 @@ onMount(async () => {
} catch (error) {
kubeconfigFilePath = 'Default is usually ~/.kube/config';
}
try {
experimentalStates = (await window.getConfigurationValue<boolean>('kubernetes.statesExperimental')) ?? false;
} catch {
// keep default value
}
});
async function handleSetContext(contextName: string) {
@@ -61,6 +85,31 @@ async function handleDeleteContext(contextName: string) {
}
}
}
function isContextReachable(contextName: string, experimental: boolean): boolean {
if (experimental) {
return $kubernetesContextsHealths.some(
contextHealth => contextHealth.contextName === contextName && contextHealth.reachable,
);
}
return $kubernetesContextsState.get(contextName)?.reachable ?? false;
}
function isContextKnown(contextName: string, experimental: boolean): boolean {
if (experimental) {
return $kubernetesContextsHealths.some(contextHealth => contextHealth.contextName === contextName);
}
return !!$kubernetesContextsState.get(contextName);
}
function isContextBeingChecked(contextName: string, experimental: boolean): boolean {
if (experimental) {
return $kubernetesContextsHealths.some(
contextHealth => contextHealth.contextName === contextName && contextHealth.checking,
);
}
return !!$kubernetesContextsCheckingStateDelayed?.get(contextName);
}
</script>

<SettingsPage title="Kubernetes Contexts">
@@ -80,7 +129,7 @@ async function handleDeleteContext(contextName: string) {
Go to Resources
</Button>
</EmptyScreen>
{#each $kubernetesContexts as context}
{#each kubernetesContextsWithStates as context}
<!-- If current context, use lighter background -->
<div
role="row"
@@ -125,7 +174,7 @@ async function handleDeleteContext(contextName: string) {
<div class="grow flex-column divide-gray-900 text-[var(--pd-invert-content-card-text)]">
<div class="flex flex-row">
<div class="flex-none w-36">
{#if $kubernetesContextsState.get(context.name)?.reachable}
{#if context.isReachable}
<div class="flex flex-row pt-2">
<div class="w-3 h-3 rounded-full bg-[var(--pd-status-connected)]"></div>
<div
@@ -154,13 +203,13 @@ async function handleDeleteContext(contextName: string) {
<div class="flex flex-row pt-2">
<div class="w-3 h-3 rounded-full bg-[var(--pd-status-disconnected)]"></div>
<div class="ml-1 text-xs text-[var(--pd-status-disconnected)]" aria-label="Context Unreachable">
{#if $kubernetesContextsState.get(context.name)}
{#if context.isKnown}
UNREACHABLE
{:else}
UNKNOWN
{/if}
</div>
{#if $kubernetesContextsCheckingStateDelayed?.get(context.name)}
{#if context.isBeingChecked}
<div class="ml-1"><Spinner size="12px"></Spinner></div>
{/if}
</div>
94 changes: 94 additions & 0 deletions packages/renderer/src/stores/kubernetes-context-health.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**********************************************************************
* 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 { get } from 'svelte/store';
import { expect, test, vi } from 'vitest';

import { kubernetesContextsHealths, kubernetesContextsHealthsStore } from './kubernetes-context-health';

const callbacks = new Map<string, any>();
const eventEmitter = {
receive: (message: string, callback: any) => {
callbacks.set(message, callback);
},
};

Object.defineProperty(global, 'window', {
value: {
kubernetesGetContextsHealths: vi.fn(),
addEventListener: eventEmitter.receive,
events: {
receive: eventEmitter.receive,
},
},
writable: true,
});

test('kubernetesContextsHealths', async () => {
const initialValues = [
{
contextName: 'context1',
checking: true,
reachable: false,
},
{
contextName: 'context2',
checking: false,
reachable: true,
},
];

const nextValues = [
{
contextName: 'context1',
checking: false,
reachable: true,
},
{
contextName: 'context2',
checking: false,
reachable: true,
},
];

vi.mocked(window.kubernetesGetContextsHealths).mockResolvedValue(initialValues);

const kubernetesContextsHealthsInfo = kubernetesContextsHealthsStore.setup();
await kubernetesContextsHealthsInfo.fetch();
let currentValue = get(kubernetesContextsHealths);
expect(currentValue).toEqual(initialValues);

// send 'extensions-already-started' event
const callbackExtensionsStarted = callbacks.get('extensions-already-started');
expect(callbackExtensionsStarted).toBeDefined();
await callbackExtensionsStarted();

// send an event indicating the data is updated
const event = 'kubernetes-contexts-healths';
const callback = callbacks.get(event);
expect(callback).toBeDefined();
await callback();

// data has been updated in the backend
vi.mocked(window.kubernetesGetContextsHealths).mockResolvedValue(nextValues);

// check received data is updated
await kubernetesContextsHealthsInfo.fetch();
currentValue = get(kubernetesContextsHealths);
expect(currentValue).toEqual(nextValues);
});
52 changes: 52 additions & 0 deletions packages/renderer/src/stores/kubernetes-context-health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**********************************************************************
* 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 { type Writable, writable } from 'svelte/store';

import type { ContextHealth } from '/@api/kubernetes-contexts-healths';

import { EventStore } from './event-store';

const windowEvents = ['kubernetes-contexts-healths', 'extension-stopped', 'extensions-started'];
const windowListeners = ['extensions-already-started'];

let readyToUpdate = false;
export async function checkForUpdate(eventName: string): Promise<boolean> {
if ('extensions-already-started' === eventName) {
readyToUpdate = true;
}
// do not fetch until extensions are all started
return readyToUpdate;
}

export const kubernetesContextsHealths: Writable<ContextHealth[]> = writable([]);

// use helper here as window methods are initialized after the store in tests
const listContextsHealths = (): Promise<ContextHealth[]> => {
return window.kubernetesGetContextsHealths();
};

export const kubernetesContextsHealthsStore = new EventStore<ContextHealth[]>(
'contexts healths',
kubernetesContextsHealths,
checkForUpdate,
windowEvents,
windowListeners,
listContextsHealths,
);
kubernetesContextsHealthsStore.setup();

0 comments on commit 7da6e4a

Please sign in to comment.