Skip to content

Commit

Permalink
feat: allow to create a volume
Browse files Browse the repository at this point in the history
fixes #925
Signed-off-by: Florent Benoit <fbenoit@redhat.com>
  • Loading branch information
benoitf committed Sep 4, 2023
1 parent 7c3da48 commit 896a27a
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 2 deletions.
4 changes: 4 additions & 0 deletions packages/renderer/src/App.svelte
Expand Up @@ -41,6 +41,7 @@ import TroubleshootingPage from './lib/troubleshooting/TroubleshootingPage.svelt
import IconsStyle from './lib/style/IconsStyle.svelte';
import CustomPick from './lib/dialogs/CustomPick.svelte';
import ContextKey from './lib/context/ContextKey.svelte';
import CreateVolume from './lib/volume/CreateVolume.svelte';
router.mode.hash();
Expand Down Expand Up @@ -154,6 +155,9 @@ window.events?.receive('display-troubleshooting', () => {
<Route path="/volumes" breadcrumb="Volumes" navigationHint="root">
<VolumesList />
</Route>
<Route path="/volumes/create" breadcrumb="Create a Volume">
<CreateVolume />
</Route>
<Route path="/volumes/:name/:engineId/*" breadcrumb="Volume Details" let:meta navigationHint="details">
<VolumeDetails volumeName="{decodeURI(meta.params.name)}" engineId="{decodeURI(meta.params.engineId)}" />
</Route>
Expand Down
184 changes: 184 additions & 0 deletions packages/renderer/src/lib/volume/CreateVolume.spec.ts
@@ -0,0 +1,184 @@
/**********************************************************************
* 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, vi, beforeAll } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import { providerInfos } from '../../stores/providers';
import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
import userEvent from '@testing-library/user-event';
import CreateVolume from './CreateVolume.svelte';

const createVolumeMock = vi.fn();

// fake the window.events object
beforeAll(() => {
(window as any).createVolume = createVolumeMock;
});

const createButtonTitle = 'Create';

test('Expect no create button with no providers', async () => {
providerInfos.set([]);

render(CreateVolume, {});

// no button
const createButton = screen.queryByRole('button', { name: createButtonTitle });
expect(createButton).not.toBeInTheDocument();

// expect empty screen
const emptyScreen = screen.getByText('No Container Engine');
expect(emptyScreen).toBeInTheDocument();

// expect that we never call
expect(createVolumeMock).not.toBeCalled();
});

test('Expect Create button is working', async () => {
providerInfos.set([
{
name: 'podman',
status: 'started',
internalId: 'podman-internal-id',
containerConnections: [
{
name: 'podman-machine-default',
status: 'started',
},
],
} as unknown as ProviderInfo,
]);

render(CreateVolume, {});

const createButton = screen.getByRole('button', { name: createButtonTitle });
expect(createButton).toBeInTheDocument();
expect(createButton).toBeEnabled();

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

// expect that we called createVolume API
expect(createVolumeMock).toHaveBeenCalledWith(expect.anything(), { Name: '' });
});

test('Expect Create with a custom name', async () => {
providerInfos.set([
{
name: 'podman',
status: 'started',
internalId: 'podman-internal-id',
containerConnections: [
{
name: 'podman-machine-default',
status: 'started',
},
],
} as unknown as ProviderInfo,
]);

render(CreateVolume, {});

const createButton = screen.getByRole('button', { name: createButtonTitle });
expect(createButton).toBeInTheDocument();
expect(createButton).toBeEnabled();

// expect no combo box as only one provider
const providerSelect = screen.queryByRole('combobox', { name: 'Provider Choice' });
expect(providerSelect).not.toBeInTheDocument();

// grab input field
const nameInput = screen.getByRole('textbox', { name: 'Volume Name' });
expect(nameInput).toBeInTheDocument();
expect(nameInput).toBeEnabled();

const customVolumeName = 'my-custom-volume';

// enter the text 'my-custom-volume
await userEvent.type(nameInput, customVolumeName);

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

// expect that we called createVolume API
expect(createVolumeMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'podman-machine-default' }), {
Name: customVolumeName,
});
});

test('Expect Create with a custom name and multiple providers', async () => {
providerInfos.set([
{
name: 'podman',
status: 'started',
internalId: 'podman-internal-id',
containerConnections: [
{
name: 'podman-machine-default',
status: 'started',
},
],
} as unknown as ProviderInfo,
{
name: 'docker',
status: 'started',
internalId: 'docker-internal-id',
containerConnections: [
{
name: 'docker',
status: 'started',
},
],
} as unknown as ProviderInfo,
]);

render(CreateVolume, {});

const createButton = screen.getByRole('button', { name: createButtonTitle });
expect(createButton).toBeInTheDocument();
expect(createButton).toBeEnabled();

// change the provider's choice
const providerSelect = screen.getByRole('combobox', { name: 'Provider Choice' });
expect(providerSelect).toBeInTheDocument();
expect(providerSelect).toBeEnabled();

// change to the second provider
await userEvent.selectOptions(providerSelect, 'docker');

// grab input field
const nameInput = screen.getByRole('textbox', { name: 'Volume Name' });
expect(nameInput).toBeInTheDocument();
expect(nameInput).toBeEnabled();

const customVolumeName = 'my-custom-volume';

// enter the text 'my-custom-volume
await userEvent.type(nameInput, customVolumeName);

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

// expect that we called createVolume API with the docker provider as we changed the toggle
expect(createVolumeMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'docker' }), {
Name: customVolumeName,
});
});
117 changes: 117 additions & 0 deletions packages/renderer/src/lib/volume/CreateVolume.svelte
@@ -0,0 +1,117 @@
<script lang="ts">
/* eslint-disable import/no-duplicates */
// https://github.com/import-js/eslint-plugin-import/issues/1479
import { get } from 'svelte/store';
import { onDestroy, onMount } from 'svelte';
/* eslint-enable import/no-duplicates */
import type { ProviderContainerConnectionInfo, ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
import { providerInfos } from '/@/stores/providers';
import FormPage from '/@/lib/ui/FormPage.svelte';
import NoContainerEngineEmptyScreen from '/@/lib/image/NoContainerEngineEmptyScreen.svelte';
import Button from '/@/lib/ui/Button.svelte';
import VolumeIcon from '/@/lib/images/VolumeIcon.svelte';
import { router } from 'tinro';
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons';
let providers: ProviderInfo[] = [];
let providerConnections: ProviderContainerConnectionInfo[] = [];
let selectedProvider: ProviderContainerConnectionInfo | undefined = undefined;
let selectedProviderConnection: ProviderContainerConnectionInfo | undefined = undefined;
onMount(async () => {
providers = get(providerInfos);
providerConnections = providers
.map(provider => provider.containerConnections)
.flat()
.filter(providerContainerConnection => providerContainerConnection.status === 'started');
const selectedProviderConnection = providerConnections.length > 0 ? providerConnections[0] : undefined;
selectedProvider = !selectedProvider && selectedProviderConnection ? selectedProviderConnection : selectedProvider;
});
let createVolumeInProgress = false;
onDestroy(() => {});
async function createVolume(providerConnectionInfo: ProviderContainerConnectionInfo) {
createVolumeInProgress = true;
try {
await window.createVolume(providerConnectionInfo, { Name: volumeName });
} finally {
createVolumeInProgress = false;
createVolumeFinished = true;
}
}
function end() {
// redirect to the volumes page
router.goto('/volumes');
}
let createVolumeFinished = false;
export let volumeName = '';
</script>

<FormPage title="Create a volume">
<svelte:fragment slot="icon">
<VolumeIcon />
</svelte:fragment>
<div slot="content" class="p-5 min-w-full h-fit">
{#if providerConnections.length === 0}
<NoContainerEngineEmptyScreen />
{:else}
<div class="bg-charcoal-900 pt-5 space-y-6 px-8 sm:pb-6 xl:pb-8 rounded-lg">
<div>
<label for="containerBuildContextDirectory" class="block mb-2 text-sm font-bold text-gray-400"
>Volume name:</label>
<input
aria-label="Volume Name"
disabled="{createVolumeFinished}"
bind:value="{volumeName}"
class="w-full p-2 outline-none text-sm bg-charcoal-600 rounded-sm text-gray-700 placeholder-gray-700"
required />
</div>
<div class:hidden="{providerConnections.length < 2}">
{#if providerConnections.length > 1}
<label for="providerChoice" class="py-3 block mb-2 text-sm font-bold text-gray-400 dark:text-gray-400"
>Container Engine
<select
class="w-full p-2 outline-none text-sm bg-charcoal-600 rounded-sm text-gray-700 placeholder-gray-700"
aria-label="Provider Choice"
disabled="{createVolumeFinished}"
bind:value="{selectedProvider}">
{#each providerConnections as providerConnection}
<option value="{providerConnection}">{providerConnection.name}</option>
{/each}
</select>
</label>
{/if}
</div>
{#if providerConnections.length === 1 && selectedProviderConnection}
<input type="hidden" aria-label="Provider Choice" readonly bind:value="{selectedProvider}" />
{/if}

<div class="w-full flex flex-row space-x-4">
{#if !createVolumeFinished && selectedProvider}
{@const connection = selectedProvider}
<Button
on:click="{() => createVolume(connection)}"
disabled="{createVolumeInProgress}"
class="w-full"
inProgress="{createVolumeInProgress}"
icon="{faPlusCircle}">
Create
</Button>
{/if}

{#if createVolumeFinished}
<Button on:click="{() => end()}" class="w-full">Done</Button>
{/if}
</div>
</div>
{/if}
</div>
</FormPage>
49 changes: 48 additions & 1 deletion packages/renderer/src/lib/volume/VolumesList.spec.ts
Expand Up @@ -19,12 +19,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import '@testing-library/jest-dom/vitest';
import { beforeAll, test, expect, vi, beforeEach } from 'vitest';
import { beforeAll, test, expect, vi, beforeEach, describe } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import VolumesList from './VolumesList.svelte';
import { get } from 'svelte/store';
import { providerInfos } from '/@/stores/providers';
import { volumeListInfos, volumesEventStore } from '/@/stores/volumes';
import type { ProviderInfo } from '../../../../main/src/plugin/api/provider-info';
import userEvent from '@testing-library/user-event';

const listVolumesMock = vi.fn();
const getProviderInfosMock = vi.fn();
Expand Down Expand Up @@ -210,3 +212,48 @@ test('Expect volumes being displayed once extensions are started (with size data

expect(volumeName.compareDocumentPosition(volumeSize)).toBe(4);
});

describe('Create volume', () => {
beforeEach(() => {
vi.resetAllMocks();
vi.clearAllMocks();
});

const createVolumeButtonTitle = 'Create a volume';
test('no create volume button if no providers', async () => {
providerInfos.set([]);
await waitRender({});

// now check if we have a create volume button, it should not be there
const createVolumeButton = screen.queryByRole('button', { name: createVolumeButtonTitle });
expect(createVolumeButton).not.toBeInTheDocument();
});

test('create volume button is there if there is one provider', async () => {
providerInfos.set([
{
name: 'podman',
status: 'started',
internalId: 'podman-internal-id',
containerConnections: [
{
name: 'podman-machine-default',
status: 'started',
},
],
} as unknown as ProviderInfo,
]);

await waitRender({});

// now check if we have a create volume button, it should not be there
const createVolumeButton = screen.getByRole('button', { name: createVolumeButtonTitle });
expect(createVolumeButton).toBeInTheDocument();

// click on the button
await userEvent.click(createVolumeButton);

// check we are redirected to the right page
expect(window.location.pathname).toBe('/volumes/create');
});
});

0 comments on commit 896a27a

Please sign in to comment.