Skip to content

Commit

Permalink
fix: report error if container engine action fails in details page (#…
Browse files Browse the repository at this point in the history
…3796)

Signed-off-by: lstocchi <lstocchi@redhat.com>
  • Loading branch information
lstocchi committed Nov 3, 2023
1 parent 47b2dd1 commit c0545c6
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 9 deletions.
Expand Up @@ -23,9 +23,8 @@
import '@testing-library/jest-dom/vitest';
import { test, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import PreferencesResourcesRendering from './PreferencesResourcesRendering.svelte';
import { providerInfos } from '../../stores/providers';
import type { ProviderContainerConnectionInfo, ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
import userEvent from '@testing-library/user-event';
import { router } from 'tinro';
import PreferencesContainerConnectionRendering from './PreferencesContainerConnectionRendering.svelte';
Expand Down Expand Up @@ -213,3 +212,79 @@ test('Expect that removing the connection is going back to the previous page', a
const afterRoute = window.location;
expect(afterRoute.href).toBe('http://localhost:3000/last');
});

test('Expect to see error message if action fails', async () => {
const socketPath = '/my/common-socket-path';
const podmanMachineName = 'podman machine';

const deleteMock = vi.fn();
(window as any).deleteProviderConnectionLifecycle = deleteMock;

const providerInfo: ProviderInfo = {
id: 'podman',
name: 'podman',
images: {
icon: 'img',
},
status: 'started',
warnings: [],
containerProviderConnectionCreation: true,
detectionChecks: [],
containerConnections: [
{
name: podmanMachineName,
status: 'stopped',
endpoint: {
socketPath,
},
type: 'podman',
lifecycleMethods: ['delete'],
},
],
installationSupport: false,
internalId: '0',
kubernetesConnections: [],
kubernetesProviderConnectionCreation: true,
links: [],
containerProviderConnectionInitialization: false,
containerProviderConnectionCreationDisplayName: 'Podman machine',
kubernetesProviderConnectionInitialization: false,
extensionId: '',
};

providerInfos.set([providerInfo]);

// encode name with base64 of the second machine
const name = Buffer.from(podmanMachineName).toString('base64');

const connection = Buffer.from(socketPath).toString('base64');

// simulate that the delete action fails
deleteMock.mockRejectedValue('failed to delete machine');

render(PreferencesContainerConnectionRendering, {
name,
connection,
providerInternalId: '0',
});

// expect to have the machine title being displayed
const title = screen.getByRole('heading', { name: 'podman machine', level: 1 });
expect(title).toBeInTheDocument();

let deleteFailedButton = screen.queryByRole('button', { name: 'delete failed' });

// expect that the delete failed button is not in the page
expect(deleteFailedButton).not.toBeInTheDocument();

// ok now we delete the connection
const deleteButton = screen.getByRole('button', { name: 'Delete' });

// click on it
await userEvent.click(deleteButton);

deleteFailedButton = screen.getByRole('button', { name: 'delete failed' });

// expect to see the delete failed button
expect(deleteFailedButton).toBeInTheDocument();
});
Expand Up @@ -21,6 +21,7 @@ import PreferencesConnectionDetailsLogs from './PreferencesConnectionDetailsLogs
import Tab from '../ui/Tab.svelte';
import DetailsPage from '../ui/DetailsPage.svelte';
import CustomIcon from '../images/CustomIcon.svelte';
import ConnectionErrorInfoButton from '../ui/ConnectionErrorInfoButton.svelte';
export let properties: IConfigurationPropertyRecordedSchema[] = [];
export let providerInternalId: string | undefined = undefined;
Expand Down Expand Up @@ -136,6 +137,7 @@ function setNoLogs() {
<svelte:fragment slot="subtitle">
<div class="flex flex-row">
<ConnectionStatus status="{connectionInfo.status}" />
<ConnectionErrorInfoButton status="{connectionStatus}" />
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
Expand Down
Expand Up @@ -31,7 +31,6 @@ import PreferencesKubernetesConnectionRendering from './PreferencesKubernetesCon
import { lastPage } from '/@/stores/breadcrumb';

test('Expect that removing the connection is going back to the previous page', async () => {
const socketPath = '/my/common-socket-path';
const kindCluster1 = 'kind cluster 1';
const kindCluster2 = 'kind cluster 2';
const kindCluster3 = 'kind cluster 3';
Expand Down Expand Up @@ -89,7 +88,7 @@ test('Expect that removing the connection is going back to the previous page', a
// 3 connections with the same socket path
providerInfos.set([providerInfo]);

// encode name with base64 of the second cluster
// encode apiUrl of the second cluster
const apiUrlBase64 = Buffer.from('http://localhost:8181').toString('base64');

// defines a fake lastPage so we can check where we will be redirected
Expand Down Expand Up @@ -134,3 +133,75 @@ test('Expect that removing the connection is going back to the previous page', a
const afterRoute = window.location;
expect(afterRoute.href).toBe('http://localhost:3000/last');
});

test('Expect to see error message if action fails', async () => {
const apiURL = 'http://localhost:8081';
const kindCluster = 'kind cluster';

const deleteMock = vi.fn();
(window as any).deleteProviderConnectionLifecycle = deleteMock;

const providerInfo: ProviderInfo = {
id: 'kind',
name: 'kind',
images: {
icon: 'img',
},
status: 'started',
warnings: [],
containerProviderConnectionCreation: true,
detectionChecks: [],
containerConnections: [],
installationSupport: false,
internalId: '0',
kubernetesConnections: [
{
name: kindCluster,
status: 'stopped',
endpoint: {
apiURL,
},
lifecycleMethods: ['delete'],
},
],
kubernetesProviderConnectionCreation: true,
links: [],
containerProviderConnectionInitialization: false,
containerProviderConnectionCreationDisplayName: 'Podman machine',
kubernetesProviderConnectionInitialization: false,
extensionId: '',
};

providerInfos.set([providerInfo]);

// encode apiUrl of the second cluster
const apiUrlBase64 = Buffer.from(apiURL).toString('base64');

// simulate that the delete action fails
deleteMock.mockRejectedValue('failed to delete machine');

render(PreferencesKubernetesConnectionRendering, {
apiUrlBase64,
providerInternalId: '0',
});

// expect to have the machine title being displayed
const title = screen.getByRole('heading', { name: 'kind cluster', level: 1 });
expect(title).toBeInTheDocument();

let deleteFailedButton = screen.queryByRole('button', { name: 'delete failed' });

// expect that the delete failed button is not in the page
expect(deleteFailedButton).not.toBeInTheDocument();

// ok now we delete the connection
const deleteButton = screen.getByRole('button', { name: 'Delete' });

// click on it
await userEvent.click(deleteButton);

deleteFailedButton = screen.getByRole('button', { name: 'delete failed' });

// expect to see the delete failed button
expect(deleteFailedButton).toBeInTheDocument();
});
Expand Up @@ -21,6 +21,7 @@ import PreferencesKubernetesConnectionDetailsSummary from './PreferencesKubernet
import PreferencesConnectionDetailsLogs from './PreferencesConnectionDetailsLogs.svelte';
import DetailsPage from '../ui/DetailsPage.svelte';
import CustomIcon from '../images/CustomIcon.svelte';
import ConnectionErrorInfoButton from '../ui/ConnectionErrorInfoButton.svelte';
export let properties: IConfigurationPropertyRecordedSchema[] = [];
export let providerInternalId: string | undefined = undefined;
Expand Down Expand Up @@ -132,6 +133,7 @@ function setNoLogs() {
<svelte:fragment slot="subtitle">
<div class="flex flex-row">
<ConnectionStatus status="{connectionInfo.status}" />
<ConnectionErrorInfoButton status="{connectionStatus}" />
</div>
</svelte:fragment>
<svelte:fragment slot="actions">
Expand Down
Expand Up @@ -41,6 +41,7 @@ import { ContextKeyExpr } from '../context/contextKey';
import { normalizeOnboardingWhenClause } from '../onboarding/onboarding-utils';
import Donut from '/@/lib/donut/Donut.svelte';
import { PeerProperties } from './PeerProperties';
import ConnectionErrorInfoButton from '../ui/ConnectionErrorInfoButton.svelte';
export let properties: IConfigurationPropertyRecordedSchema[] = [];
let providers: ProviderInfo[] = [];
$: containerConnectionStatus = new Map<string, IConnectionStatus>();
Expand Down Expand Up @@ -469,11 +470,7 @@ function hasAnyConfiguration(provider: ProviderInfo) {
<ConnectionStatus status="{container.status}" />
{#if containerConnectionStatus.has(getProviderConnectionName(provider, container))}
{@const status = containerConnectionStatus.get(getProviderConnectionName(provider, container))}
{#if status?.error}
<button
class="ml-3 text-[9px] text-red-500 underline"
on:click="{() => window.events?.send('toggle-task-manager', '')}">{status.action} failed</button>
{/if}
<ConnectionErrorInfoButton status="{status}" />
{/if}
</div>

Expand Down
77 changes: 77 additions & 0 deletions packages/renderer/src/lib/ui/ConnectionErrorInfoButton.spec.ts
@@ -0,0 +1,77 @@
/**********************************************************************
* Copyright (C) 2023 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
***********************************************************************/

/* eslint-disable @typescript-eslint/no-explicit-any */

import '@testing-library/jest-dom/vitest';
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import ConnectionErrorInfoButton from './ConnectionErrorInfoButton.svelte';

test('Expect nothing if status is undefined', async () => {
render(ConnectionErrorInfoButton, {
status: undefined,
});

// check element does not exists
const button = screen.queryByRole('button', { name: 'action failed' });
expect(button).not.toBeInTheDocument();
});

test('Expect error button if status action and error are defined', async () => {
render(ConnectionErrorInfoButton, {
status: {
action: 'action',
error: 'error',
inProgress: false,
status: 'failed',
},
});

// check element does exist
const button = screen.getByRole('button', { name: 'action failed' });
expect(button).toBeInTheDocument();
});

test('Expect nothing if status action is not defined', async () => {
render(ConnectionErrorInfoButton, {
status: {
error: 'error',
inProgress: false,
status: 'failed',
},
});

// check element does not exists
const button = screen.queryByRole('button', { name: 'action failed' });
expect(button).not.toBeInTheDocument();
});

test('Expect nothing if status error is not defined', async () => {
render(ConnectionErrorInfoButton, {
status: {
action: 'action',
inProgress: false,
status: 'failed',
},
});

// check element does not exists
const button = screen.queryByRole('button', { name: 'action failed' });
expect(button).not.toBeInTheDocument();
});
12 changes: 12 additions & 0 deletions packages/renderer/src/lib/ui/ConnectionErrorInfoButton.svelte
@@ -0,0 +1,12 @@
<script lang="ts">
import type { IConnectionStatus } from '../preferences/Util';
export let status: IConnectionStatus | undefined;
</script>

{#if status?.action && status?.error}
<button
aria-label="{status.action} failed"
class="ml-3 text-[9px] text-red-500 underline"
on:click="{() => window.events?.send('toggle-task-manager', '')}">{status.action} failed</button>
{/if}

0 comments on commit c0545c6

Please sign in to comment.