Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**********************************************************************
* 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 '@testing-library/jest-dom/vitest';

import { render, screen } from '@testing-library/svelte';
import { beforeEach, expect, test, vi } from 'vitest';
import KubernetesProviderCard from '/@/component/dashboard/KubernetesProviderCard.svelte';
import { RemoteMocks } from '/@/tests/remote-mocks';
import { API_NAVIGATION } from '@kubernetes-dashboard/channels';
import type { NavigationApi } from '@kubernetes-dashboard/channels';
import userEvent from '@testing-library/user-event';

const remoteMocks = new RemoteMocks();

beforeEach(() => {
vi.resetAllMocks();
remoteMocks.reset();

remoteMocks.mock(API_NAVIGATION, {
navigateToProviderNewConnection: vi.fn(),
} as unknown as NavigationApi);
});

test('should render with all values passed', async () => {
render(KubernetesProviderCard, {
provider: {
id: 'k8s-provider',
creationDisplayName: 'Kubernetes Provider',
creationButtonTitle: 'Create Kubernetes Provider',
emptyConnectionMarkdownDescription: 'Create a new Kubernetes Provider',
},
});
screen.getByText('Kubernetes Provider');
screen.getByText('Create Kubernetes Provider');
const btn = screen.getByRole('button', { name: 'Create Kubernetes Provider' });
expect(btn).toBeEnabled();
await userEvent.click(btn);
expect(remoteMocks.get(API_NAVIGATION).navigateToProviderNewConnection).toHaveBeenCalledWith('k8s-provider');
});

test('should render with minimal values passed', async () => {
render(KubernetesProviderCard, {
provider: {
id: 'k8s-provider',
},
});
screen.getByText('Create');
screen.getByText('Create new');
const btn = screen.getByRole('button', { name: 'Create new' });
expect(btn).toBeEnabled();
await userEvent.click(btn);
expect(remoteMocks.get(API_NAVIGATION).navigateToProviderNewConnection).toHaveBeenCalledWith('k8s-provider');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { API_NAVIGATION, type KubernetesProvider } from '@kubernetes-dashboard/channels';
import { Button } from '@podman-desktop/ui-svelte';
import Fa from 'svelte-fa';
import IconImage from '/@/component/icons/IconImage.svelte';
import { getContext } from 'svelte';
import { Remote } from '/@/remote/remote';
import Markdown from '/@/markdown/Markdown.svelte';

interface Props {
provider: KubernetesProvider;
}

let { provider }: Props = $props();

const remote = getContext<Remote>(Remote);
const navigationApi = remote.getProxy(API_NAVIGATION);

async function createNew(provider: KubernetesProvider): Promise<void> {
return navigationApi.navigateToProviderNewConnection(provider.id);
}
</script>

<div class="rounded-xl p-5 text-left bg-[var(--pd-content-card-bg)]">
<div class="flex justify-left text-[var(--pd-details-empty-icon)] py-2 mb-2">
<IconImage image={provider?.images?.icon} class="mx-0 max-h-10" alt={provider.creationDisplayName}></IconImage>
</div>
<h1 class="text-lg font-semibold mb-4">
{provider.creationDisplayName ?? 'Create'}
</h1>

<p class="text-sm text-[var(--pd-content-text)] mb-6">
<Markdown markdown={provider.emptyConnectionMarkdownDescription} />
</p>

<div class="flex justify-center">
<Button
type="primary"
on:click={(): Promise<void> => createNew(provider)}
class="flex items-center"
aria-label={provider.creationButtonTitle ?? 'Create new'}>
<Fa icon={faPlusCircle} size="1.2x" class="mr-1" />
{provider.creationButtonTitle ?? 'Create new'}
</Button>
</div>
</div>
118 changes: 118 additions & 0 deletions packages/webview/src/component/dashboard/NoContextPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**********************************************************************
* 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 '@testing-library/jest-dom/vitest';

import { render, screen } from '@testing-library/svelte';
import { beforeEach, expect, test, vi } from 'vitest';
import NoContextPage from './NoContextPage.svelte';
import { StatesMocks } from '/@/tests/state-mocks';
import { FakeStateObject } from '/@/state/util/fake-state-object.svelte';
import type { KubernetesProvidersInfo } from '@kubernetes-dashboard/channels';
import KubeIcon from '/@/component/icons/KubeIcon.svelte';
import KubernetesProviderCard from '/@/component/dashboard/KubernetesProviderCard.svelte';
import type { Unsubscriber } from 'svelte/store';

vi.mock(import('./KubernetesProviderCard.svelte'));
vi.mock(import('/@/component/icons/KubeIcon.svelte'));

const statesMocks = new StatesMocks();
let kubernetesProvidersMock: FakeStateObject<KubernetesProvidersInfo, void>;

beforeEach(() => {
vi.resetAllMocks();
statesMocks.reset();

kubernetesProvidersMock = new FakeStateObject<KubernetesProvidersInfo, void>();
statesMocks.mock<KubernetesProvidersInfo, void>('stateKubernetesProvidersInfoUI', kubernetesProvidersMock);
});

test('should render the Kubernetes icon', () => {
render(NoContextPage);
expect(KubeIcon).toHaveBeenCalledWith(expect.anything(), { size: '80' });
});

test('should render the main heading', () => {
render(NoContextPage);

const heading = screen.getByRole('heading', { level: 1 });
expect(heading).toHaveTextContent('No Kubernetes cluster');
});

test('should render the description text', () => {
render(NoContextPage);

const description = screen.getByText(/A Kubernetes cluster is a group of nodes/);
expect(description).toBeInTheDocument();
expect(description).toHaveClass('text-[var(--pd-details-empty-sub-header)]', 'text-balance');
});

test('should render providers when data is available', () => {
const mockProviders: KubernetesProvidersInfo = {
providers: [
{
id: 'provider-1',
creationDisplayName: 'Provider 1',
creationButtonTitle: 'Create Provider 1',
emptyConnectionMarkdownDescription: 'Description 1',
},
{
id: 'provider-2',
creationDisplayName: 'Provider 2',
creationButtonTitle: 'Create Provider 2',
emptyConnectionMarkdownDescription: 'Description 2',
},
],
};

kubernetesProvidersMock.setData(mockProviders);
render(NoContextPage);

expect(KubernetesProviderCard).toHaveBeenCalledWith(expect.anything(), { provider: mockProviders.providers[0] });
expect(KubernetesProviderCard).toHaveBeenCalledWith(expect.anything(), { provider: mockProviders.providers[1] });
});

test('should handle providers with minimal data', () => {
const mockProviders = {
providers: [
{
id: 'minimal-provider',
},
],
};

kubernetesProvidersMock.setData(mockProviders);
render(NoContextPage);

expect(KubernetesProviderCard).toHaveBeenCalledWith(expect.anything(), { provider: mockProviders.providers[0] });
});

test('should call subscribe on mount', () => {
render(NoContextPage);

expect(kubernetesProvidersMock.subscribe).toHaveBeenCalledTimes(1);
});

test('should call unsubscribe on unmount', () => {
const unsubscribeMock: Unsubscriber = vi.fn();
vi.mocked(kubernetesProvidersMock.subscribe).mockReturnValue(unsubscribeMock);
const component = render(NoContextPage);

component.unmount();
expect(unsubscribeMock).toHaveBeenCalledTimes(1);
});
26 changes: 24 additions & 2 deletions packages/webview/src/component/dashboard/NoContextPage.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
<script lang="ts">
import KubeIcon from '/@/component/icons/KubeIcon.svelte';
import { getContext, onDestroy, onMount } from 'svelte';
import { States } from '/@/state/states';
import type { Unsubscriber } from 'svelte/store';
import KubernetesProviderCard from '/@/component/dashboard/KubernetesProviderCard.svelte';

const states = getContext<States>(States);
const kubernetesProviders = states.stateKubernetesProvidersInfoUI;

let unsubscribers: Unsubscriber[] = [];
onMount(() => {
unsubscribers.push(kubernetesProviders.subscribe());
});

onDestroy(() => {
unsubscribers.forEach(unsubscriber => unsubscriber());
unsubscribers = [];
});
</script>

<div class="mt-8 flex justify-center overflow-auto">
<div class="max-w-[600px] flex flex-col text-center space-y-3">
<div class="max-w-[800px] flex flex-col text-center content-center space-y-3">
<div class="flex justify-center text-[var(--pd-details-empty-icon)] py-2">
<KubeIcon size="80" />
</div>
<h1 class="text-xl text-[var(--pd-details-empty-header)]">No Kubernetes cluster</h1>
<div class="text-[var(--pd-details-empty-sub-header)] text-pretty">
<div class="text-[var(--pd-details-empty-sub-header)] text-balance">
A Kubernetes cluster is a group of nodes (virtual or physical) that run Kubernetes, a system for automating the
deployment and management of containerized applications.
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 p-2 justify-center">
{#each kubernetesProviders.data?.providers as provider (provider.id)}
<KubernetesProviderCard provider={provider} />
{/each}
</div>
</div>
</div>