From 2bf9ab2e8946df523278b58ecc0aff6544f6ebc3 Mon Sep 17 00:00:00 2001 From: Tim deBoer Date: Wed, 13 Dec 2023 12:42:47 -0500 Subject: [PATCH] feat: provider cards (#5013) * feat: provider cards Creates a ProviderCard component and makes each of the Provider* components use it in order to get some common layout and code between the different states. With the card I tried to make it more like the example in #4395 and our other cards (e.g. Resources, Kubernetes contexts, or CLI), creating two columns with the first having the common bits: logo at the top-left corner with the name and version, with status below it, and the right containing everything else that is specific to the state. The existing status dots + text components don't quite work for providers so I created a new one. Need to review this, overall componentization, and create tests for ProviderCard before raising for official review. Signed-off-by: Tim deBoer --- .../src/lib/dashboard/ProviderCard.spec.ts | 156 ++++++++++++++++++ .../src/lib/dashboard/ProviderCard.svelte | 34 ++++ .../lib/dashboard/ProviderConfigured.svelte | 36 ++-- .../lib/dashboard/ProviderConfiguring.svelte | 37 ++--- .../lib/dashboard/ProviderInstalled.spec.ts | 14 +- .../lib/dashboard/ProviderInstalled.svelte | 33 ++-- .../src/lib/dashboard/ProviderLinks.svelte | 2 +- .../lib/dashboard/ProviderNotInstalled.svelte | 50 +++--- .../src/lib/dashboard/ProviderReady.svelte | 36 ++-- .../src/lib/dashboard/ProviderStarting.svelte | 21 +-- .../src/lib/dashboard/ProviderStopped.svelte | 18 +- .../src/lib/ui/ProviderStatus.spec.ts | 80 +++++++++ .../renderer/src/lib/ui/ProviderStatus.svelte | 90 ++++++++++ 13 files changed, 447 insertions(+), 160 deletions(-) create mode 100644 packages/renderer/src/lib/dashboard/ProviderCard.spec.ts create mode 100644 packages/renderer/src/lib/dashboard/ProviderCard.svelte create mode 100644 packages/renderer/src/lib/ui/ProviderStatus.spec.ts create mode 100644 packages/renderer/src/lib/ui/ProviderStatus.svelte diff --git a/packages/renderer/src/lib/dashboard/ProviderCard.spec.ts b/packages/renderer/src/lib/dashboard/ProviderCard.spec.ts new file mode 100644 index 000000000000..98334f7f803a --- /dev/null +++ b/packages/renderer/src/lib/dashboard/ProviderCard.spec.ts @@ -0,0 +1,156 @@ +/********************************************************************** + * 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 { expect, test } from 'vitest'; +import ProviderCard from './ProviderCard.svelte'; +import { screen, render } from '@testing-library/svelte'; +import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info'; +import type { ProviderImages } from '@podman-desktop/api'; + +test('Expect provider region', async () => { + const provider: ProviderInfo = { + internalId: 'internal-id', + id: 'my-provider', + extensionId: '', + name: 'Podman', + containerConnections: [], + kubernetesConnections: [], + status: 'not-installed', + containerProviderConnectionCreation: false, + containerProviderConnectionInitialization: false, + kubernetesProviderConnectionCreation: false, + kubernetesProviderConnectionInitialization: false, + links: [], + detectionChecks: [], + warnings: [], + images: {} as ProviderImages, + installationSupport: false, + }; + render(ProviderCard, { provider }); + + const region = screen.getByRole('region', { name: provider.name + ' Provider' }); + expect(region).toBeInTheDocument(); +}); + +test('Expect provider name', async () => { + const provider: ProviderInfo = { + internalId: 'internal-id', + id: 'my-provider', + extensionId: '', + name: 'Podman', + containerConnections: [], + kubernetesConnections: [], + status: 'not-installed', + containerProviderConnectionCreation: false, + containerProviderConnectionInitialization: false, + kubernetesProviderConnectionCreation: false, + kubernetesProviderConnectionInitialization: false, + links: [], + detectionChecks: [], + warnings: [], + images: {} as ProviderImages, + installationSupport: false, + }; + render(ProviderCard, { provider }); + + const name = screen.getByLabelText('context-name'); + expect(name).toBeInTheDocument(); + expect(name.textContent).toContain(provider.name); +}); + +test('Expect no provider version', async () => { + const provider: ProviderInfo = { + internalId: 'internal-id', + id: 'my-provider', + extensionId: '', + name: 'Podman', + containerConnections: [], + kubernetesConnections: [], + status: 'not-installed', + containerProviderConnectionCreation: false, + containerProviderConnectionInitialization: false, + kubernetesProviderConnectionCreation: false, + kubernetesProviderConnectionInitialization: false, + links: [], + detectionChecks: [], + warnings: [], + images: {} as ProviderImages, + installationSupport: false, + // no version + }; + render(ProviderCard, { provider }); + + const version = screen.queryByLabelText('Provider Version'); + expect(version).not.toBeInTheDocument(); +}); + +test('Expect provider version', async () => { + const provider: ProviderInfo = { + internalId: 'internal-id', + id: 'my-provider', + extensionId: '', + name: 'Podman', + containerConnections: [], + kubernetesConnections: [], + status: 'not-installed', + containerProviderConnectionCreation: false, + containerProviderConnectionInitialization: false, + kubernetesProviderConnectionCreation: false, + kubernetesProviderConnectionInitialization: false, + links: [], + detectionChecks: [], + warnings: [], + images: {} as ProviderImages, + installationSupport: false, + version: '1.2.3', + }; + render(ProviderCard, { provider }); + + const version = screen.getByLabelText('Provider Version'); + expect(version).toBeInTheDocument(); + expect(version.textContent).toBe('v' + provider.version); +}); + +test('Expect provider state', async () => { + const provider: ProviderInfo = { + internalId: 'internal-id', + id: 'my-provider', + extensionId: '', + name: 'Podman', + containerConnections: [], + kubernetesConnections: [], + status: 'not-installed', + containerProviderConnectionCreation: false, + containerProviderConnectionInitialization: false, + kubernetesProviderConnectionCreation: false, + kubernetesProviderConnectionInitialization: false, + links: [], + detectionChecks: [], + warnings: [], + images: {} as ProviderImages, + installationSupport: false, + }; + render(ProviderCard, { provider }); + + const state = screen.getByLabelText('Actual State'); + expect(state).toBeInTheDocument(); + expect(state.textContent).toContain('NOT-INSTALLED'); +}); diff --git a/packages/renderer/src/lib/dashboard/ProviderCard.svelte b/packages/renderer/src/lib/dashboard/ProviderCard.svelte new file mode 100644 index 000000000000..b4708685393e --- /dev/null +++ b/packages/renderer/src/lib/dashboard/ProviderCard.svelte @@ -0,0 +1,34 @@ + + +
+
+
+ +
+
+ {provider.name} + {#if provider.version} +
+ v{provider.version} +
+ {/if} +
+
+ +
+
+
+
+ +
+
+ + +
diff --git a/packages/renderer/src/lib/dashboard/ProviderConfigured.svelte b/packages/renderer/src/lib/dashboard/ProviderConfigured.svelte index 5903215887f5..67c4ed8ecffd 100644 --- a/packages/renderer/src/lib/dashboard/ProviderConfigured.svelte +++ b/packages/renderer/src/lib/dashboard/ProviderConfigured.svelte @@ -2,8 +2,6 @@ import type { CheckStatus, ProviderInfo } from '../../../../main/src/plugin/api/provider-info'; import ErrorMessage from '../ui/ErrorMessage.svelte'; import PreflightChecks from './PreflightChecks.svelte'; -import ProviderLinks from './ProviderLinks.svelte'; -import ProviderLogo from './ProviderLogo.svelte'; import ProviderUpdateButton from './ProviderUpdateButton.svelte'; import Steps from '../ui/Steps.svelte'; @@ -11,6 +9,7 @@ import { onMount } from 'svelte'; import { InitializeAndStartMode, InitializationSteps, type InitializationContext } from './ProviderInitUtils'; import Spinner from '../ui/Spinner.svelte'; import Button from '../ui/Button.svelte'; +import ProviderCard from './ProviderCard.svelte'; export let provider: ProviderInfo; export let initializationContext: InitializationContext; @@ -47,17 +46,9 @@ onMount(() => { }); -
- -
-

- {provider.name} - {#if provider.version} - v{provider.version} - {/if} - is stopped -

-

+ + +

To start working with containers, {provider.name} {#if provider.version} v{provider.version} @@ -88,13 +79,12 @@ onMount(() => { {#if runError} {/if} -

- {#if provider.updateInfo?.version && provider.version !== provider.updateInfo?.version} -
- -
- {/if} - -
- -
+ + {#if provider.updateInfo?.version && provider.version !== provider.updateInfo?.version} +
+ +
+ {/if} + + + diff --git a/packages/renderer/src/lib/dashboard/ProviderConfiguring.svelte b/packages/renderer/src/lib/dashboard/ProviderConfiguring.svelte index 597c4742aa5f..d711b89cdb0e 100644 --- a/packages/renderer/src/lib/dashboard/ProviderConfiguring.svelte +++ b/packages/renderer/src/lib/dashboard/ProviderConfiguring.svelte @@ -1,8 +1,6 @@ -
- -
-

- {provider.name} - {#if provider.version} - v{provider.version} - {/if} - is initializing -

- -
+ + +
{#if initializationContext.mode === InitializeAndStartMode} {/if} @@ -118,15 +108,12 @@ onDestroy(() => { class:min-w-full="{noErrors === false}" bind:this="{logsXtermDiv}">
-
- {#if provider.updateInfo} -
- -
- {/if} - - -
- -
+ {#if provider.updateInfo} +
+ +
+ {/if} + + + diff --git a/packages/renderer/src/lib/dashboard/ProviderInstalled.spec.ts b/packages/renderer/src/lib/dashboard/ProviderInstalled.spec.ts index 3fc344d39478..bf2fd3eff428 100644 --- a/packages/renderer/src/lib/dashboard/ProviderInstalled.spec.ts +++ b/packages/renderer/src/lib/dashboard/ProviderInstalled.spec.ts @@ -69,11 +69,12 @@ test('Expect installed provider shows button', async () => { const initializationContext: InitializationContext = { mode: InitializeAndStartMode }; render(ProviderInstalled, { provider: provider, initializationContext: initializationContext }); - const providerText = screen.getByText( - content => content.includes('MyProvider') && content.includes('is installed but not ready'), - ); + const providerText = screen.getByText(content => content === 'MyProvider'); expect(providerText).toBeInTheDocument(); + const installedText = screen.getByText(content => content.toLowerCase().includes('installed but not ready')); + expect(installedText).toBeInTheDocument(); + const button = screen.getByRole('button', { name: 'Initialize and start' }); expect(button).toBeInTheDocument(); @@ -115,11 +116,12 @@ test('Expect to see the initialize context error if provider installation fails' const initializationContext: InitializationContext = { mode: InitializeAndStartMode }; render(ProviderInstalled, { provider: provider, initializationContext: initializationContext }); - const providerText = screen.getByText( - content => content.includes('MyProvider') && content.includes('is installed but not ready'), - ); + const providerText = screen.getByText(content => content === 'MyProvider'); expect(providerText).toBeInTheDocument(); + const installedText = screen.getByText(content => content.toLowerCase().includes('installed but not ready')); + expect(installedText).toBeInTheDocument(); + const button = screen.getByRole('button', { name: 'Initialize and start' }); expect(button).toBeInTheDocument(); diff --git a/packages/renderer/src/lib/dashboard/ProviderInstalled.svelte b/packages/renderer/src/lib/dashboard/ProviderInstalled.svelte index 9746d5edbc19..8f2f4f4c713d 100644 --- a/packages/renderer/src/lib/dashboard/ProviderInstalled.svelte +++ b/packages/renderer/src/lib/dashboard/ProviderInstalled.svelte @@ -1,8 +1,6 @@ -
- -
-

- {provider.name} - {#if provider.version} - v{provider.version} - {/if} - is installed but not ready -

+ +

To start working with containers, {provider.name} needs to be initialized.

@@ -216,14 +207,12 @@ function onInstallationClick() { class:min-w-full="{noErrors === false}" bind:this="{logsXtermDiv}">
-
- {#if provider.updateInfo?.version && provider.version !== provider.updateInfo?.version} -
- -
- {/if} - - - -
+ {#if provider.updateInfo?.version && provider.version !== provider.updateInfo?.version} +
+ +
+ {/if} + + + diff --git a/packages/renderer/src/lib/dashboard/ProviderLinks.svelte b/packages/renderer/src/lib/dashboard/ProviderLinks.svelte index 37b53d57bef9..c5b055205c0a 100644 --- a/packages/renderer/src/lib/dashboard/ProviderLinks.svelte +++ b/packages/renderer/src/lib/dashboard/ProviderLinks.svelte @@ -6,7 +6,7 @@ export let provider: ProviderInfo; {#if provider.links.length > 0} -
+
{#each provider.links as link} {#if link.group === undefined} diff --git a/packages/renderer/src/lib/dashboard/ProviderNotInstalled.svelte b/packages/renderer/src/lib/dashboard/ProviderNotInstalled.svelte index 7aebd12a9be4..3453f9c75aa1 100644 --- a/packages/renderer/src/lib/dashboard/ProviderNotInstalled.svelte +++ b/packages/renderer/src/lib/dashboard/ProviderNotInstalled.svelte @@ -5,8 +5,7 @@ import type { CheckStatus, ProviderInfo } from '../../../../main/src/plugin/api/ import PreflightChecks from './PreflightChecks.svelte'; import ProviderDetectionChecksButton from './ProviderDetectionChecksButton.svelte'; import ProviderInstallationButton from './ProviderInstallationButton.svelte'; -import ProviderLinks from './ProviderLinks.svelte'; -import ProviderLogo from './ProviderLogo.svelte'; +import ProviderCard from './ProviderCard.svelte'; export let provider: ProviderInfo; @@ -14,32 +13,27 @@ let detectionChecks: ProviderDetectionCheck[] = []; let preflightChecks: CheckStatus[] = []; -
- -
-

- Podman Desktop was not able to find an installation of {provider.name}. -

+ +

- To start working with containers, {provider.name} needs to be detected/installed. + Could not find an installation. To start working with containers, {provider.name} needs to be detected/installed.

-
-
- - -
- {#if detectionChecks.length > 0} -
- {#each detectionChecks as detectionCheck} -
-

{detectionCheck.status ? '✅' : '❌'} {detectionCheck.name}

- {#if detectionCheck.details} - Details:

{detectionCheck.details}

- {/if} -
- {/each} +
+ +
- {/if} - - -
+ {#if detectionChecks.length > 0} +
+ {#each detectionChecks as detectionCheck} +
+

{detectionCheck.status ? '✅' : '❌'} {detectionCheck.name}

+ {#if detectionCheck.details} + Details:

{detectionCheck.details}

+ {/if} +
+ {/each} +
+ {/if} + + + diff --git a/packages/renderer/src/lib/dashboard/ProviderReady.svelte b/packages/renderer/src/lib/dashboard/ProviderReady.svelte index dc9ed3e27060..6d31b3b65867 100644 --- a/packages/renderer/src/lib/dashboard/ProviderReady.svelte +++ b/packages/renderer/src/lib/dashboard/ProviderReady.svelte @@ -2,26 +2,16 @@ import type { CheckStatus, ProviderInfo } from '../../../../main/src/plugin/api/provider-info'; import PreflightChecks from './PreflightChecks.svelte'; import ProviderWarnings from './ProviderWarnings.svelte'; -import ProviderLinks from './ProviderLinks.svelte'; -import ProviderLogo from './ProviderLogo.svelte'; import ProviderUpdateButton from './ProviderUpdateButton.svelte'; +import ProviderCard from './ProviderCard.svelte'; export let provider: ProviderInfo; let preflightChecks: CheckStatus[] = []; -
- -
-

- {provider.name} is running -

- {#if provider.version} -

- version {provider.version} -

- {/if} + + {#if provider.containerConnections.length > 0}

@@ -29,14 +19,14 @@ let preflightChecks: CheckStatus[] = [];

{/if} -
- {#if provider.updateInfo?.version && provider.version !== provider.updateInfo?.version} -
- -
- {/if} - - - -
+ {#if provider.updateInfo?.version && provider.version !== provider.updateInfo?.version} +
+ +
+ {/if} + + + + + diff --git a/packages/renderer/src/lib/dashboard/ProviderStarting.svelte b/packages/renderer/src/lib/dashboard/ProviderStarting.svelte index 12bd0e9aab7a..4c86e97cf7ce 100644 --- a/packages/renderer/src/lib/dashboard/ProviderStarting.svelte +++ b/packages/renderer/src/lib/dashboard/ProviderStarting.svelte @@ -1,22 +1,12 @@ -
- -
-

- {provider.name} is starting... -

- {#if provider.version} -

- version {provider.version} -

- {/if} + + {#if provider.containerConnections.length > 0}

@@ -24,6 +14,5 @@ export let provider: ProviderInfo;

{/if} -
- -
+ + diff --git a/packages/renderer/src/lib/dashboard/ProviderStopped.svelte b/packages/renderer/src/lib/dashboard/ProviderStopped.svelte index dd9f3c912a88..2f89d7ed07a2 100644 --- a/packages/renderer/src/lib/dashboard/ProviderStopped.svelte +++ b/packages/renderer/src/lib/dashboard/ProviderStopped.svelte @@ -1,22 +1,8 @@ -
- -
-

- {provider.name} is stopped -

- {#if provider.version} -

- version {provider.version} -

- {/if} -
- -
+ diff --git a/packages/renderer/src/lib/ui/ProviderStatus.spec.ts b/packages/renderer/src/lib/ui/ProviderStatus.spec.ts new file mode 100644 index 000000000000..39abfd677da5 --- /dev/null +++ b/packages/renderer/src/lib/ui/ProviderStatus.spec.ts @@ -0,0 +1,80 @@ +/********************************************************************** + * 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 + ***********************************************************************/ + +import '@testing-library/jest-dom/vitest'; +import { test, expect } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import ExtensionStatus from './ExtensionStatus.svelte'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-empty-function */ + +test('Expect green text and icon when connection is running', async () => { + render(ExtensionStatus, { status: 'started' }); + const icon = screen.getByLabelText('connection-status-icon'); + const label = screen.getByLabelText('connection-status-label'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass('bg-green-500'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('text-green-500'); + expect(label).toHaveTextContent('ENABLED'); +}); + +test('Expect green text and icon when connection is starting', async () => { + render(ExtensionStatus, { status: 'starting' }); + const icon = screen.getByLabelText('connection-status-icon'); + const label = screen.getByLabelText('connection-status-label'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass('bg-green-500'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('text-green-500'); + expect(label).toHaveTextContent('ENABLING'); +}); + +test('Expect green text and icon when connection is stopped', async () => { + render(ExtensionStatus, { status: 'stopped' }); + const icon = screen.getByLabelText('connection-status-icon'); + const label = screen.getByLabelText('connection-status-label'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass('bg-gray-900'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('text-gray-900'); + expect(label).toHaveTextContent('DISABLED'); +}); + +test('Expect green text and icon when connection is stopping', async () => { + render(ExtensionStatus, { status: 'stopping' }); + const icon = screen.getByLabelText('connection-status-icon'); + const label = screen.getByLabelText('connection-status-label'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass('bg-red-500'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('text-red-500'); + expect(label).toHaveTextContent('DISABLING'); +}); + +test('Expect green text and icon when connection is unknown', async () => { + render(ExtensionStatus, { status: 'unknown' }); + const icon = screen.getByLabelText('connection-status-icon'); + const label = screen.getByLabelText('connection-status-label'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveClass('bg-gray-900'); + expect(label).toBeInTheDocument(); + expect(label).toHaveClass('text-gray-900'); + expect(label).toHaveTextContent('UNKNOWN'); +}); diff --git a/packages/renderer/src/lib/ui/ProviderStatus.svelte b/packages/renderer/src/lib/ui/ProviderStatus.svelte new file mode 100644 index 000000000000..118987dd0ad6 --- /dev/null +++ b/packages/renderer/src/lib/ui/ProviderStatus.svelte @@ -0,0 +1,90 @@ + + +
+{statusStyle.label}