Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow to embed existing component to onboarding (#3755) #3763

Merged
merged 4 commits into from Sep 6, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions extensions/podman/package.json
Expand Up @@ -185,6 +185,25 @@
{
"id": "podmanInstalled",
"title": "Podman successfully installed",
"when": "onboardingContext:podmanIsNotInstalled == false"
},
{
"id": "preCreatePodmanMachine",
"title": "We could not find any Podman machine. Let's create one!",
"when": "onboardingContext:podmanMachineExists == false && onboardingContext:isLinux == false"
},
{
"id": "createPodmanMachine",
"title": "Create a Podman machine",
"when": "onboardingContext:podmanMachineExists == false && onboardingContext:isLinux == false",
"completionEvents": [
"onboardingContext:podmanMachineExists == true"
],
"component": "createContainerProviderConnection"
},
{
"id": "podmanSuccessfullySetup",
"title": "Podman successfully setup",
"when": "onboardingContext:podmanIsNotInstalled == false",
"state": "succeeded"
}
Expand Down
4 changes: 3 additions & 1 deletion extensions/podman/src/extension.spec.ts
Expand Up @@ -127,7 +127,9 @@ vi.mock('@podman-desktop/api', async () => {
window: {
showInformationMessage: vi.fn(),
},

context: {
setValue: vi.fn(),
},
process: {
exec: vi.fn(),
},
Expand Down
2 changes: 2 additions & 0 deletions extensions/podman/src/extension.ts
Expand Up @@ -95,6 +95,7 @@ async function updateMachines(provider: extensionApi.Provider): Promise<void> {

// parse output
const machines = JSON.parse(machineListOutput) as MachineJSON[];
extensionApi.context.setValue('podmanMachineExists', machines.length > 0, 'onboarding');

// update status of existing machines
for (const machine of machines) {
Expand Down Expand Up @@ -1168,6 +1169,7 @@ export async function createMachine(
}
}
await extensionApi.process.exec(getPodmanCli(), parameters, { logger, env, token });
extensionApi.context.setValue('podmanMachineExists', true, 'onboarding');
}

function setupDisguisedPodmanSocketWatcher(
Expand Down
4 changes: 4 additions & 0 deletions packages/main/src/plugin/api/onboarding.ts
Expand Up @@ -29,6 +29,9 @@ export interface OnboardingStepItem {

export type OnboardingStatus = 'completed' | 'failed' | 'skipped';
export type OnboardingState = 'completed' | 'failed';
export type OnboardingEmbeddedComponentType =
| 'createContainerProviderConnection'
| 'createKubernetesProviderConnection';

export interface OnboardingStep {
id: string;
Expand All @@ -39,6 +42,7 @@ export interface OnboardingStep {
enableCompletionEvents?: string[];
completionEvents?: string[];
content?: OnboardingStepItem[][];
component?: OnboardingEmbeddedComponentType;
when?: string;
status?: OnboardingStatus;
showNext?: boolean;
Expand Down
58 changes: 58 additions & 0 deletions packages/renderer/src/lib/onboarding/Onboarding.spec.ts
Expand Up @@ -86,3 +86,61 @@ test('Expect not to have the "Try again" button disabled if the step represent a
const infoMessage = screen.getByLabelText('next-info-message');
expect(infoMessage).toBeInTheDocument();
});

test('Expect to have the "step body" div if the step does not include a component', async () => {
(window as any).resetOnboarding = vi.fn();
(window as any).updateStepState = vi.fn();

onboardingList.set([
{
extension: 'id',
title: 'onboarding',
steps: [
{
id: 'step',
title: 'step',
state: 'failed',
completionEvents: [],
},
],
},
]);
context.set(new ContextUI());
await waitRender({
extensionIds: ['id'],
});
const bodyDiv = screen.getByLabelText('step body');
expect(bodyDiv).toBeInTheDocument();
const onboardingComponent = screen.queryByLabelText('onboarding component');
expect(onboardingComponent).not.toBeInTheDocument();
});

test('Expect to have the embedded component if the step includes a component', async () => {
(window as any).resetOnboarding = vi.fn();
(window as any).updateStepState = vi.fn();

onboardingList.set([
{
extension: 'id',
title: 'onboarding',
steps: [
{
id: 'step',
title: 'step',
state: 'failed',
component: 'createContainerProviderConnection',
completionEvents: [],
},
],
},
]);
context.set(new ContextUI());
await waitRender({
extensionIds: ['id'],
});

const onboardingComponent = screen.getByLabelText('onboarding component');
expect(onboardingComponent).toBeInTheDocument();
const bodyDiv = screen.queryByLabelText('step body');
expect(bodyDiv).not.toBeInTheDocument();
});
107 changes: 57 additions & 50 deletions packages/renderer/src/lib/onboarding/Onboarding.svelte
Expand Up @@ -21,6 +21,7 @@ import {
import { lastPage } from '/@/stores/breadcrumb';
import Button from '../ui/Button.svelte';
import Link from '../ui/Link.svelte';
import OnboardingComponent from './OnboardingComponent.svelte';

interface ActiveOnboardingStep {
onboarding: OnboardingInfo;
Expand Down Expand Up @@ -282,7 +283,7 @@ async function cleanContext() {

{#if activeStep}
<div class="flex flex-col bg-[#36373a] h-full">
<div class="flex flex-row justify-between mt-5 mx-5 mb-20">
<div class="flex flex-row justify-between mt-5 mx-5 mb-5">
<div class="flex flew-row">
{#if activeStep.onboarding.media}
<img
Expand All @@ -304,63 +305,69 @@ async function cleanContext() {
</div>
</div>
</div>
<div class="w-[450px] flex flex-col mx-auto">
{#if activeStep.step.media}
<div class="mx-auto">
<img
class="w-24 h-24 object-contain"
alt="{activeStep.step.media.altText}"
src="{activeStep.step.media.path}" />
</div>
{:else if activeStep.onboarding.media}
<div class="mx-auto">
<img
class="w-24 h-24 object-contain"
alt="{activeStep.onboarding.media.altText}"
src="{activeStep.onboarding.media.path}" />
</div>
{/if}
<div class="flex flex-row mx-auto">
{#if executing}
<div class="mt-1 mr-6">
<i class="pf-c-button__progress text-purple-400">
<span class="pf-c-spinner pf-m-md" role="progressbar">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</i>
{#if activeStep.step.component}
<div class="min-w-[700px] mx-auto overflow-y-auto" aria-label="onboarding component">
<OnboardingComponent component="{activeStep.step.component}" extensionId="{activeStep.onboarding.extension}" />
</div>
{:else}
<div class="w-[450px] flex flex-col mt-16 mx-auto" aria-label="step body">
{#if activeStep.step.media}
<div class="mx-auto">
<img
class="w-24 h-24 object-contain"
alt="{activeStep.step.media.altText}"
src="{activeStep.step.media.path}" />
</div>
{:else if activeStep.onboarding.media}
<div class="mx-auto">
<img
class="w-24 h-24 object-contain"
alt="{activeStep.onboarding.media.altText}"
src="{activeStep.onboarding.media.path}" />
</div>
{/if}
<div class="flex flex-row mx-auto">
{#if executing}
<div class="mt-1 mr-6">
<i class="pf-c-button__progress text-purple-400">
<span class="pf-c-spinner pf-m-md" role="progressbar">
<span class="pf-c-spinner__clipper"></span>
lstocchi marked this conversation as resolved.
Show resolved Hide resolved
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</i>
</div>
{/if}
<div class="text-lg text-white">{activeStep.step.title}</div>
</div>
{#if activeStep.step.description}
<div class="text-sm text-white mx-auto">{activeStep.step.description}</div>
{/if}
<div class="text-lg text-white">{activeStep.step.title}</div>
</div>
{#if activeStep.step.description}
<div class="text-sm text-white mx-auto">{activeStep.step.description}</div>

{#if activeStep.step.state === 'failed'}
<div class="mx-auto mt-4">
<Button on:click="{() => restartSetup()}">Try again</Button>
</div>
{/if}
</div>

{#if activeStep.step.state === 'failed'}
<div class="mx-auto mt-4">
<Button on:click="{() => restartSetup()}">Try again</Button>
<div class="flex flex-col mx-auto">
{#if activeStep.step.content}
{#each activeStep.step.content as row}
<div class="flex flex-row mx-auto">
{#each row as item}
<OnboardingItem
extension="{activeStep.onboarding.extension}"
item="{item}"
getContext="{() => globalContext}"
inProgressCommandExecution="{inProgressCommandExecution}" />
{/each}
</div>
{/each}
{/if}
</div>
{/if}

<div class="flex flex-col mx-auto">
{#if activeStep.step.content}
{#each activeStep.step.content as row}
<div class="flex flex-row mx-auto">
{#each row as item}
<OnboardingItem
extension="{activeStep.onboarding.extension}"
item="{item}"
getContext="{() => globalContext}"
inProgressCommandExecution="{inProgressCommandExecution}" />
{/each}
</div>
{/each}
{/if}
</div>

{#if !activeStep.step.completionEvents || activeStep.step.completionEvents.length === 0}
<div class="grow"></div>
{#if activeStep.step.state !== 'failed'}
Expand Down
114 changes: 114 additions & 0 deletions packages/renderer/src/lib/onboarding/OnboardingComponent.spec.ts
@@ -0,0 +1,114 @@
/**********************************************************************
* 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, beforeAll, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import OnboardingComponent from './OnboardingComponent.svelte';
import { configurationProperties } from '/@/stores/configurationProperties';
import { providerInfos } from '/@/stores/providers';
import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info';

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

async function waitRender(customProperties: any): Promise<void> {
const result = render(OnboardingComponent, { ...customProperties });
// wait that result.component.$$.ctx[2] is set
while (result.component.$$.ctx[2] === undefined) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}

beforeAll(() => {
(window as any).getConfigurationValue = vi.fn();
(window as any).updateConfigurationValue = vi.fn();
(window as any).getOsMemory = vi.fn();
(window as any).getOsCpu = vi.fn();
(window as any).getOsFreeDiskSize = vi.fn();
(window as any).getCancellableTokenSource = vi.fn();
(window as any).auditConnectionParameters = vi.fn();
(window as any).telemetryTrack = vi.fn();

Object.defineProperty(window, 'matchMedia', {
value: () => {
return {
matches: false,
addListener: () => {},
removeListener: () => {},
};
},
});
});

test('Expect to find PreferencesConnectionCreationRendering component if step includes "create" component', async () => {
configurationProperties.set([]);
providerInfos.set([providerInfo]);
await waitRender({
component: 'createContainerProviderConnection',
extensionId: 'id',
});

const title = screen.getAllByRole('heading', { name: 'title' });
expect(title[0].textContent).equal('Create a Podman machine ');
});

test('Expect to find "not supported" message if step includes a component not supported by the provider', async () => {
const customProviderInfo = providerInfo;
customProviderInfo.containerProviderConnectionCreation = false;
configurationProperties.set([]);
providerInfos.set([customProviderInfo]);
await waitRender({
component: 'createContainerProviderConnection',
extensionId: 'id',
});

const div = screen.getByLabelText('not supported warning');
expect(div).toBeInTheDocument();
expect(div.textContent).toContain(
'This extension does not provide a component of type "createContainerProviderConnection"',
);
});