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: improved empty screens #3988

Merged
merged 4 commits into from Sep 20, 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
47 changes: 47 additions & 0 deletions packages/renderer/src/lib/ContainerList.spec.ts
Expand Up @@ -334,3 +334,50 @@ test('Try to delete a pod without deleting container', async () => {
// and the standalone container has not been deleted
expect(deleteContainerMock).not.toHaveBeenCalled();
});

test('Expect filter empty screen', async () => {
getProviderInfosMock.mockResolvedValue([
{
name: 'podman',
status: 'started',
internalId: 'podman-internal-id',
containerConnections: [
{
name: 'podman-machine-default',
status: 'started',
},
],
},
]);

const singleContainer = {
Id: 'sha256:1234567890123',
Image: 'sha256:123',
Names: ['foo'],
Status: 'Running',
engineId: 'podman',
engineName: 'podman',
};

// one single container
const mockedContainers = [singleContainer];

listContainersMock.mockResolvedValue(mockedContainers);

window.dispatchEvent(new CustomEvent('extensions-already-started'));
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));
window.dispatchEvent(new CustomEvent('tray:update-provider'));

// wait store are populated
while (get(containersInfos).length === 0) {
await new Promise(resolve => setTimeout(resolve, 500));
}

while (get(providerInfos).length === 0) {
await new Promise(resolve => setTimeout(resolve, 500));
}
await waitRender({ searchTerm: 'No match' });

const filterButton = screen.getByRole('button', { name: 'Clear filter' });
expect(filterButton).toBeInTheDocument();
});
8 changes: 7 additions & 1 deletion packages/renderer/src/lib/ContainerList.svelte
Expand Up @@ -11,7 +11,9 @@ import { router } from 'tinro';
import { ContainerGroupInfoTypeUI, type ContainerGroupInfoUI, type ContainerInfoUI } from './container/ContainerInfoUI';
import ContainerActions from './container/ContainerActions.svelte';
import PodActions from './pod/PodActions.svelte';
import ContainerIcon from './images/ContainerIcon.svelte';
import ContainerEmptyScreen from './container/ContainerEmptyScreen.svelte';
import FilteredEmptyScreen from './ui/FilteredEmptyScreen.svelte';
import Modal from './dialogs/Modal.svelte';
import { ContainerUtils } from './container/container-utils';
import { providerInfos } from '../stores/providers';
Expand Down Expand Up @@ -641,7 +643,11 @@ function errorCallback(container: ContainerInfoUI, errorMessage: string): void {
{#if providerConnections.length === 0}
<NoContainerEngineEmptyScreen />
{:else if $filtered.length === 0}
<ContainerEmptyScreen />
{#if searchTerm}
<FilteredEmptyScreen icon="{ContainerIcon}" kind="containers" bind:searchTerm="{searchTerm}" />
{:else}
<ContainerEmptyScreen />
{/if}
{/if}
</div>
</NavPage>
Expand Down
9 changes: 7 additions & 2 deletions packages/renderer/src/lib/ImagesList.svelte
Expand Up @@ -2,6 +2,7 @@
import { filtered, searchPattern, imagesInfos } from '../stores/images';
import { onDestroy, onMount } from 'svelte';
import ImageEmptyScreen from './image/ImageEmptyScreen.svelte';
import FilteredEmptyScreen from './ui/FilteredEmptyScreen.svelte';

import { router } from 'tinro';
import type { ImageInfoUI } from './image/ImageInfoUI';
Expand All @@ -27,7 +28,7 @@ import Checkbox from './ui/Checkbox.svelte';
import Button from './ui/Button.svelte';
import { faArrowCircleDown, faCube, faTrash } from '@fortawesome/free-solid-svg-icons';

let searchTerm = '';
export let searchTerm = '';
$: searchPattern.set(searchTerm);

let images: ImageInfoUI[] = [];
Expand Down Expand Up @@ -328,7 +329,11 @@ function computeInterval(): number {
{#if providerConnections.length === 0}
<NoContainerEngineEmptyScreen />
{:else if $filtered.length === 0}
<ImageEmptyScreen />
{#if searchTerm}
<FilteredEmptyScreen icon="{ImageIcon}" kind="images" bind:searchTerm="{searchTerm}" />
{:else}
<ImageEmptyScreen />
{/if}
{/if}

{#if pushImageModal && pushImageModalImageInfo}
Expand Down
45 changes: 45 additions & 0 deletions packages/renderer/src/lib/ImagestList.spec.ts
Expand Up @@ -135,3 +135,48 @@ test('Expect images being ordered by newest first', async () => {
expect(fedoraRecent.compareDocumentPosition(veryOld)).toBe(4);
expect(fedoraOld.compareDocumentPosition(veryOld)).toBe(4);
});

test('Expect filter empty screen', async () => {
getProviderInfosMock.mockResolvedValue([
{
name: 'podman',
status: 'started',
internalId: 'podman-internal-id',
containerConnections: [
{
name: 'podman-machine-default',
status: 'started',
},
],
},
]);

listImagesMock.mockResolvedValue([
{
Id: 'sha256:1234567890123',
RepoTags: ['fedora:old'],
Created: 1644009612,
Size: 123,
Status: 'Running',
engineId: 'podman',
engineName: 'podman',
},
]);

window.dispatchEvent(new CustomEvent('extensions-already-started'));
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));
window.dispatchEvent(new CustomEvent('image-build'));

// wait store are populated
while (get(imagesInfos).length === 0) {
await new Promise(resolve => setTimeout(resolve, 500));
}
while (get(providerInfos).length === 0) {
await new Promise(resolve => setTimeout(resolve, 500));
}

await waitRender({ searchTerm: 'No match' });

const filterButton = screen.getByRole('button', { name: 'Clear filter' });
expect(filterButton).toBeInTheDocument();
});
20 changes: 20 additions & 0 deletions packages/renderer/src/lib/pod/PodsList.spec.ts
Expand Up @@ -231,3 +231,23 @@ test('Expect 2 kubernetes pods being displayed', async () => {
const pod2Details = screen.getByRole('cell', { name: 'kubepod2 e8129c57 0 container k8s context2 tooltip' });
expect(pod2Details).toBeInTheDocument();
});

test('Expect filter empty screen', async () => {
getProvidersInfoMock.mockResolvedValue([provider]);
listPodsMock.mockResolvedValue([pod1]);
kubernetesListPodsMock.mockResolvedValue([]);
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));
window.dispatchEvent(new CustomEvent('extensions-already-started'));

while (get(providerInfos).length !== 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}

while (get(podsInfos).length !== 1) {
await new Promise(resolve => setTimeout(resolve, 500));
}

render(PodsList, { searchTerm: 'No match' });
const filterButton = screen.getByRole('button', { name: 'Clear filter' });
expect(filterButton).toBeInTheDocument();
});
9 changes: 7 additions & 2 deletions packages/renderer/src/lib/pod/PodsList.svelte
Expand Up @@ -11,6 +11,7 @@ import { PodUtils } from './pod-utils';
import type { PodInfo } from '../../../../main/src/plugin/api/pod-info';
import NoContainerEngineEmptyScreen from '../image/NoContainerEngineEmptyScreen.svelte';
import PodEmptyScreen from './PodEmptyScreen.svelte';
import FilteredEmptyScreen from '../ui/FilteredEmptyScreen.svelte';
import StatusIcon from '../images/StatusIcon.svelte';
import PodIcon from '../images/PodIcon.svelte';
import PodActions from './PodActions.svelte';
Expand All @@ -24,7 +25,7 @@ import Checkbox from '../ui/Checkbox.svelte';
import Button from '../ui/Button.svelte';
import { faTrash } from '@fortawesome/free-solid-svg-icons';

let searchTerm = '';
export let searchTerm = '';
$: searchPattern.set(searchTerm);

let pods: PodInfoUI[] = [];
Expand Down Expand Up @@ -306,7 +307,11 @@ function errorCallback(pod: PodInfoUI, errorMessage: string): void {
{#if $filtered.length === 0 && providerConnections.length === 0}
<NoContainerEngineEmptyScreen />
{:else if $filtered.length === 0}
<PodEmptyScreen />
{#if searchTerm}
<FilteredEmptyScreen icon="{PodIcon}" kind="pods" bind:searchTerm="{searchTerm}" />
{:else}
<PodEmptyScreen />
{/if}
{/if}
</div>
</NavPage>
6 changes: 5 additions & 1 deletion packages/renderer/src/lib/ui/EmptyScreen.svelte
Expand Up @@ -6,6 +6,7 @@ import { onMount } from 'svelte';
export let icon: any;
export let title = 'No title';
export let message = 'Message';
export let detail = '';
export let commandline = '';
export let hidden = false;

Expand Down Expand Up @@ -46,7 +47,10 @@ let copyTextDivElement: HTMLDivElement;
</div>
<h1 class="text-xl">{title}</h1>
<span class="text-gray-700">{message}</span>
{#if commandline.length > 0}
{#if detail}
<span class="text-gray-700">{detail}</span>
{/if}
{#if commandline}
<div class="flex flex-row bg-charcoal-900 items-center justify-between rounded-sm p-3 mt-4">
<div class="font-mono text-gray-400" bind:this="{copyTextDivElement}" data-testid="copyTextDivElement">
{commandline}
Expand Down
23 changes: 23 additions & 0 deletions packages/renderer/src/lib/ui/FilteredEmptyScreen.svelte
@@ -0,0 +1,23 @@
<script lang="ts">
import EmptyScreen from '../ui/EmptyScreen.svelte';
import Button from '../ui/Button.svelte';

export let icon: any;
export let kind: string;
export let searchTerm: string;

$: filter = searchTerm && searchTerm.length > 20 ? 'filter' : `'${searchTerm}'`;
</script>

<EmptyScreen
icon="{icon}"
title="No {kind} matching {filter} found"
message="Not what you expected? Double-check your spelling."
detail="Just want to view all of your {kind}?">
<Button
on:click="{() => {
searchTerm = '';
}}">
Clear filter
</Button>
</EmptyScreen>
59 changes: 59 additions & 0 deletions packages/renderer/src/lib/volume/VolumesList.spec.ts
Expand Up @@ -257,3 +257,62 @@ describe('Create volume', () => {
expect(window.location.pathname).toBe('/volumes/create');
});
});

test('Expect filter empty screen', async () => {
getProviderInfosMock.mockResolvedValue([
{
name: 'podman',
status: 'started',
internalId: 'podman-internal-id',
containerConnections: [
{
name: 'podman-machine-default',
status: 'started',
},
],
},
]);

listVolumesMock.mockResolvedValue([
{
Volumes: [
{
Driver: 'local',
Labels: {},
Mountpoint: '/var/lib/containers/storage/volumes/fedora/_data',
Name: '0052074a2ade930338c00aea982a90e4243e6cf58ba920eb411c388630b8c967',
Options: {},
Scope: 'local',
engineName: 'Podman',
engineId: 'podman.Podman Machine',
UsageData: { RefCount: 1, Size: -1 },
containersUsage: [],
},
],
},
]);

window.dispatchEvent(new CustomEvent('extensions-already-started'));
window.dispatchEvent(new CustomEvent('provider-lifecycle-change'));

// ask to fetch the volumes
const volumesEventStoreInfo = volumesEventStore.setup();

await volumesEventStoreInfo.fetch();

// first call is with listing without details
expect(listVolumesMock).toHaveBeenNthCalledWith(1, false);

// wait store are populated
while (get(volumeListInfos).length === 0) {
await new Promise(resolve => setTimeout(resolve, 500));
}
while (get(providerInfos).length === 0) {
await new Promise(resolve => setTimeout(resolve, 500));
}

await waitRender({ searchTerm: 'No match' });

const filterButton = screen.getByRole('button', { name: 'Clear filter' });
expect(filterButton).toBeInTheDocument();
});
9 changes: 7 additions & 2 deletions packages/renderer/src/lib/volume/VolumesList.svelte
Expand Up @@ -10,6 +10,7 @@ import NavPage from '../ui/NavPage.svelte';
import { VolumeUtils } from './volume-utils';
import NoContainerEngineEmptyScreen from '../image/NoContainerEngineEmptyScreen.svelte';
import VolumeEmptyScreen from './VolumeEmptyScreen.svelte';
import FilteredEmptyScreen from '../ui/FilteredEmptyScreen.svelte';
import VolumeActions from './VolumeActions.svelte';
import VolumeIcon from '../images/VolumeIcon.svelte';
import StatusIcon from '../images/StatusIcon.svelte';
Expand All @@ -20,7 +21,7 @@ import Checkbox from '../ui/Checkbox.svelte';
import Button from '../ui/Button.svelte';
import { faPieChart, faPlusCircle, faTrash } from '@fortawesome/free-solid-svg-icons';

let searchTerm = '';
export let searchTerm = '';
$: searchPattern.set(searchTerm);

let volumes: VolumeInfoUI[] = [];
Expand Down Expand Up @@ -288,7 +289,11 @@ function gotoCreateVolume(): void {
{#if providerConnections.length === 0}
<NoContainerEngineEmptyScreen />
{:else if $filtered.map(volumeInfo => volumeInfo.Volumes).flat().length === 0}
<VolumeEmptyScreen />
{#if searchTerm}
<FilteredEmptyScreen icon="{VolumeIcon}" kind="volumes" bind:searchTerm="{searchTerm}" />
{:else}
<VolumeEmptyScreen />
{/if}
{/if}
</div>
</NavPage>