Skip to content

Commit

Permalink
feat: provider cards (#5013)
Browse files Browse the repository at this point in the history
* 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 <git@tdeboer.ca>
  • Loading branch information
deboer-tim committed Dec 13, 2023
1 parent ce9073f commit 2bf9ab2
Show file tree
Hide file tree
Showing 13 changed files with 447 additions and 160 deletions.
156 changes: 156 additions & 0 deletions 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');
});
34 changes: 34 additions & 0 deletions packages/renderer/src/lib/dashboard/ProviderCard.svelte
@@ -0,0 +1,34 @@
<script lang="ts">
import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
import ProviderStatus from '../ui/ProviderStatus.svelte';
import ProviderLinks from './ProviderLinks.svelte';
import ProviderLogo from './ProviderLogo.svelte';
export let provider: ProviderInfo;
</script>

<div class="bg-charcoal-800 mb-5 rounded-md p-3 flex-nowrap" role="region" aria-label="{provider.name} Provider">
<div class="flex flex-row">
<div class="flex flex-row">
<ProviderLogo provider="{provider}" />
<div class="flex flex-col text-gray-400 ml-3 whitespace-nowrap" aria-label="context-name">
<div class="flex flex-row items-center">
{provider.name}
{#if provider.version}
<div class="text-gray-800 text-sm pl-1" aria-label="Provider Version">
v{provider.version}
</div>
{/if}
</div>
<div class="flex flex-row pt-1" aria-label="Actual State">
<ProviderStatus status="{provider.status}" />
</div>
</div>
</div>
<div class="flex flex-col items-center text-center flex-grow">
<slot name="content" />
</div>
</div>

<ProviderLinks provider="{provider}" />
</div>
36 changes: 13 additions & 23 deletions packages/renderer/src/lib/dashboard/ProviderConfigured.svelte
Expand Up @@ -2,15 +2,14 @@
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';
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;
Expand Down Expand Up @@ -47,17 +46,9 @@ onMount(() => {
});
</script>

<div class="p-2 flex flex-col bg-charcoal-800 rounded-lg" role="region" aria-label="{provider.name} Provider">
<ProviderLogo provider="{provider}" />
<div class="flex flex-col items-center text-center">
<p class="text-xl text-gray-400" aria-label="Actual State">
{provider.name}
{#if provider.version}
v{provider.version}
{/if}
is stopped
</p>
<p class="text-base text-gray-700" aria-label="Suggested Actions">
<ProviderCard provider="{provider}">
<svelte:fragment slot="content">
<p class="text-base text-gray-700">
To start working with containers, {provider.name}
{#if provider.version}
v{provider.version}
Expand Down Expand Up @@ -88,13 +79,12 @@ onMount(() => {
{#if runError}
<ErrorMessage class="flex flex-col mt-2 my-2 text-sm" error="{runError}" />
{/if}
</div>
{#if provider.updateInfo?.version && provider.version !== provider.updateInfo?.version}
<div class="mt-5 mb-1 w-full flex justify-around">
<ProviderUpdateButton onPreflightChecks="{checks => (preflightChecks = checks)}" provider="{provider}" />
</div>
{/if}
<PreflightChecks preflightChecks="{preflightChecks}" />
<div class="mt-5 mb-1 w-full flex justify-around"></div>
<ProviderLinks provider="{provider}" />
</div>

{#if provider.updateInfo?.version && provider.version !== provider.updateInfo?.version}
<div class="mt-5 mb-1 w-full flex justify-around">
<ProviderUpdateButton onPreflightChecks="{checks => (preflightChecks = checks)}" provider="{provider}" />
</div>
{/if}
<PreflightChecks preflightChecks="{preflightChecks}" />
</svelte:fragment>
</ProviderCard>
37 changes: 12 additions & 25 deletions packages/renderer/src/lib/dashboard/ProviderConfiguring.svelte
@@ -1,8 +1,6 @@
<script lang="ts">
import type { CheckStatus, ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
import PreflightChecks from './PreflightChecks.svelte';
import ProviderLinks from './ProviderLinks.svelte';
import ProviderLogo from './ProviderLogo.svelte';
import ProviderUpdateButton from './ProviderUpdateButton.svelte';
import { onDestroy, onMount } from 'svelte';
import { Terminal } from 'xterm';
Expand All @@ -13,6 +11,7 @@ import { getPanelDetailColor } from '../color/color';
import { type InitializationContext, InitializationSteps, InitializeAndStartMode } from './ProviderInitUtils';
import Steps from '../ui/Steps.svelte';
import Spinner from '../ui/Spinner.svelte';
import ProviderCard from './ProviderCard.svelte';
export let provider: ProviderInfo;
export let initializationContext: InitializationContext;
Expand Down Expand Up @@ -86,18 +85,9 @@ onDestroy(() => {
});
</script>

<div class="p-2 flex flex-col bg-charcoal-800 rounded-lg" role="region" aria-label="{provider.name} Provider">
<ProviderLogo provider="{provider}" />
<div class="flex flex-col items-center text-center">
<p class="text-xl text-gray-400" aria-label="Actual State">
{provider.name}
{#if provider.version}
v{provider.version}
{/if}
is initializing
</p>

<div class="mt-5">
<ProviderCard provider="{provider}">
<svelte:fragment slot="content">
<div>
{#if initializationContext.mode === InitializeAndStartMode}
<Steps steps="{InitializationSteps}" />
{/if}
Expand All @@ -118,15 +108,12 @@ onDestroy(() => {
class:min-w-full="{noErrors === false}"
bind:this="{logsXtermDiv}">
</div>
</div>

{#if provider.updateInfo}
<div class="mt-5 mb-1 w-full flex justify-around">
<ProviderUpdateButton onPreflightChecks="{checks => (preflightChecks = checks)}" provider="{provider}" />
</div>
{/if}
<PreflightChecks preflightChecks="{preflightChecks}" />

<div class="mt-5 mb-1 w-full flex justify-around"></div>
<ProviderLinks provider="{provider}" />
</div>
{#if provider.updateInfo}
<div class="mt-5 mb-1 w-full flex justify-around">
<ProviderUpdateButton onPreflightChecks="{checks => (preflightChecks = checks)}" provider="{provider}" />
</div>
{/if}
<PreflightChecks preflightChecks="{preflightChecks}" />
</svelte:fragment>
</ProviderCard>
14 changes: 8 additions & 6 deletions packages/renderer/src/lib/dashboard/ProviderInstalled.spec.ts
Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down

0 comments on commit 2bf9ab2

Please sign in to comment.