Skip to content

Commit

Permalink
chore: always show image push button (podman-desktop#5857)
Browse files Browse the repository at this point in the history
### What does this PR do?

* Allows the image push button to always appear
* Instead, we will show an unauthenticated warning in the modal with the
  button disabled, if the image is unauthenticated / unable to push to a
  registry.

### Screenshot / video of UI

<!-- If this PR is changing UI, please include
screenshots or screencasts showing the difference -->

### What issues does this PR fix or reference?

<!-- Include any related issues from Podman Desktop
repository (or from another issue tracker). -->

Closes podman-desktop#5506

### How to test this PR?

<!-- Please explain steps to reproduce -->

1. Go to push on any image, the push button should be there.
2. If unauthenticated, the warning should appear.

Signed-off-by: Charlie Drage <[email protected]>
  • Loading branch information
cdrage authored Feb 12, 2024
1 parent 859f4f2 commit f8ba14b
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 29 deletions.
20 changes: 20 additions & 0 deletions packages/renderer/src/lib/image/ImageActions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,23 @@ test('Expect no dropdown when several contributions and dropdownMenu mode on', a
expect(button.lastChild?.textContent).toBe('dummy-contrib');
});
});

test('Expect Push image to be there', async () => {
// Mock the showMessageBox to return 0 (yes)
showMessageBoxMock.mockResolvedValue({ response: 0 });
getContributedMenusMock.mockImplementation(() => Promise.resolve([]));

const image: ImageInfoUI = {
name: 'dummy',
status: 'UNUSED',
} as ImageInfoUI;

render(ImageActions, {
onPushImage: vi.fn(),
onRenameImage: vi.fn(),
image,
});

const button = screen.getByTitle('Push Image');
expect(button).toBeDefined();
});
17 changes: 6 additions & 11 deletions packages/renderer/src/lib/image/ImageActions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export let dropdownMenu = false;
export let detailed = false;
export let groupContributions = false;
let isAuthenticatedForThisImage = false;
const imageUtils = new ImageUtils();
let contributions: Menu[] = [];
Expand Down Expand Up @@ -59,8 +58,6 @@ async function runImage(imageInfo: ImageInfoUI) {
router.goto('/images/run/basic');
}
$: window.hasAuthconfigForImage(image.name).then(result => (isAuthenticatedForThisImage = result));
async function deleteImage(): Promise<void> {
image.status = 'DELETING';
dispatch('update', image);
Expand Down Expand Up @@ -109,14 +106,12 @@ function onError(error: string): void {
onBeforeToggle="{() => {
globalContext?.setValue('selectedImageId', image.id);
}}">
{#if isAuthenticatedForThisImage}
<ListItemButtonIcon
title="Push Image"
onClick="{() => pushImage(image)}"
menu="{dropdownMenu}"
detailed="{detailed}"
icon="{faArrowUp}" />
{/if}
<ListItemButtonIcon
title="Push Image"
onClick="{() => pushImage(image)}"
menu="{dropdownMenu}"
detailed="{detailed}"
icon="{faArrowUp}" />

<ListItemButtonIcon
title="Edit Image"
Expand Down
176 changes: 176 additions & 0 deletions packages/renderer/src/lib/image/PushImageModal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**********************************************************************
* Copyright (C) 2024 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, vi, beforeAll, type Mock } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import PushImageModal from './PushImageModal.svelte';
import type { ImageInfoUI } from './ImageInfoUI';
import type { ImageInspectInfo } from '../../../../main/src/plugin/api/image-inspect-info';
import { fireEvent } from '@testing-library/dom';

vi.mock('xterm', () => {
return {
Terminal: vi.fn().mockReturnValue({ loadAddon: vi.fn(), open: vi.fn(), write: vi.fn(), clear: vi.fn() }),
};
});

const getConfigurationValueMock = vi.fn();
const hasAuthMock = vi.fn();
const pushImageMock = vi.fn();

beforeAll(() => {
(window.events as unknown) = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
receive: (_channel: string, func: any) => {
func();
},
};
(window as any).ResizeObserver = vi.fn().mockReturnValue({ observe: vi.fn(), unobserve: vi.fn() });
(window as any).getImageInspect = vi.fn().mockImplementation(() => Promise.resolve({}));
(window as any).logsContainer = vi.fn().mockResolvedValue(undefined);
(window as any).refreshTerminal = vi.fn();
(window as any).getConfigurationValue = getConfigurationValueMock;
(window as any).hasAuthconfigForImage = hasAuthMock;
(window as any).showMessageBox = vi.fn();
(window as any).pushImage = pushImageMock;
});

// fake ImageInfoUI
const fakedImage: ImageInfoUI = {
id: 'id',
shortId: 'shortId',
name: 'name',
engineId: 'engineId',
engineName: 'engineName',
tag: 'tag',
createdAt: 0,
age: 'age',
size: 0,
humanSize: 'humanSize',
base64RepoTag: 'base64RepoTag',
selected: false,
status: 'UNUSED',
icon: {},
badges: [],
};
const fakedImageInspect: ImageInspectInfo = {
Architecture: '',
Author: '',
Comment: '',
Config: {
ArgsEscaped: false,
AttachStderr: false,
AttachStdin: false,
AttachStdout: false,
Cmd: [],
Domainname: '',
Entrypoint: [],
Env: [],
ExposedPorts: {},
Hostname: '',
Image: '',
Labels: {},
OnBuild: [],
OpenStdin: false,
StdinOnce: false,
Tty: false,
User: '',
Volumes: {},
WorkingDir: '',
},
Container: '',
ContainerConfig: {
ArgsEscaped: false,
AttachStderr: false,
AttachStdin: false,
AttachStdout: false,
Cmd: [],
Domainname: '',
Env: [],
ExposedPorts: {},
Hostname: '',
Image: '',
Labels: {},
OpenStdin: false,
StdinOnce: false,
Tty: false,
User: '',
Volumes: {},
WorkingDir: '',
},
Created: '',
DockerVersion: '',
GraphDriver: { Data: { DeviceId: '', DeviceName: '', DeviceSize: '' }, Name: '' },
Id: '',
Os: '',
Parent: '',
RepoDigests: [],
RepoTags: [],
RootFS: {
Type: '',
},
Size: 0,
VirtualSize: 0,
engineId: 'engineid',
engineName: 'engineName',
};

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

test('Expect "Push Image" button to be disabled if window.hasAuthconfigForImage returns false', async () => {
hasAuthMock.mockImplementation(() => {
return new Promise(() => false);
});
(window.getImageInspect as Mock).mockResolvedValue(fakedImageInspect);

await waitRender({
imageInfoToPush: fakedImage,
closeCallback: vi.fn(),
});

// Get the push button
const pushButton = screen.getByRole('button', { name: 'Push image' });
expect(pushButton).toBeInTheDocument();
expect(pushButton).toBeDisabled();
});

test('Expect "Push Image" button to actually be clickable if window.hasAuthconfigForImage is true', async () => {
hasAuthMock.mockImplementation(() => {
return new Promise(() => true);
});
(window.getImageInspect as Mock).mockResolvedValue(fakedImageInspect);

await waitRender({
imageInfoToPush: fakedImage,
closeCallback: vi.fn(),
});

// Get the push button
const pushButton = screen.getByRole('button', { name: 'Push image' });
expect(pushButton).toBeInTheDocument();

// Actually able to click it
fireEvent.click(pushButton);
});
63 changes: 45 additions & 18 deletions packages/renderer/src/lib/image/PushImageModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { TerminalSettings } from '../../../../main/src/plugin/terminal-settings'
import Modal from '../dialogs/Modal.svelte';
import type { ImageInfoUI } from './ImageInfoUI';
import Button from '../ui/Button.svelte';
import { faCircleArrowUp } from '@fortawesome/free-solid-svg-icons';
import Link from '../ui/Link.svelte';
import { faCheckCircle, faTriangleExclamation, faCircleArrowUp } from '@fortawesome/free-solid-svg-icons';
import Fa from 'svelte-fa';
export let closeCallback: () => void;
export let imageInfoToPush: ImageInfoUI;
Expand Down Expand Up @@ -102,6 +104,9 @@ function callback(name: string, data: string) {
}
}
let pushLogsXtermDiv: HTMLDivElement;
let isAuthenticatedForThisImage = false;
$: window.hasAuthconfigForImage(imageInfoToPush.name).then(result => (isAuthenticatedForThisImage = result));
</script>

<Modal
Expand All @@ -110,38 +115,60 @@ let pushLogsXtermDiv: HTMLDivElement;
}}">
<div class="modal flex flex-col place-self-center bg-charcoal-800 shadow-xl shadow-black">
<div class="flex items-center justify-between px-6 py-5 space-x-2">
<h1 class="grow text-lg font-bold capitalize">Push Image</h1>
<h1 class="grow text-lg font-bold">Push image</h1>

<button class="hover:text-gray-300 py-1" on:click="{() => closeCallback()}">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>

<div class="flex flex-col px-10 py-4 text-sm leading-5 space-y-5">
<div>
<label for="modalImageTag" class="block mb-2 text-sm font-medium text-gray-400">Image Tag</label>
<div class="flex flex-col px-6 py-4 pt-0 text-sm leading-5 space-y-5">
<div class="pb-4">
<label for="modalImageTag" class="block mb-2 text-sm font-medium text-gray-400">Image tag</label>
{#if isAuthenticatedForThisImage}
<Fa class="absolute mt-3 ml-1.5 text-green-300" size="16" icon="{faCheckCircle}" />
{:else}
<Fa class="absolute mt-3 ml-1.5 text-amber-500" size="16" icon="{faTriangleExclamation}" />
{/if}

<select
class="border text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 block w-full p-2.5 bg-charcoal-600 border-gray-900 placeholder-gray-700 text-white"
class="text-sm rounded-lg block w-full p-2.5 bg-charcoal-600 pl-6 border-r-8 border-transparent outline-1 outline {isAuthenticatedForThisImage
? 'outline-gray-900'
: 'outline-amber-500'} placeholder-gray-700 text-white"
name="imageChoice"
bind:value="{selectedImageTag}">
{#each imageTags as imageTag}
<option value="{imageTag}">{imageTag}</option>
{/each}
</select>
<!-- If the image is UNAUTHENTICATED, show a warning that the image is unable to be pushed
and to click to go to the registries page -->
{#if !isAuthenticatedForThisImage}
<p class="text-amber-500 pt-1">
No registry with push permissions found. <Link internalRef="/preferences/registries"
>Add a registry now.</Link>
</p>{/if}
</div>

{#if !pushFinished}
<Button
icon="{faCircleArrowUp}"
on:click="{() => {
pushImage(selectedImageTag);
}}"
bind:inProgress="{pushInProgress}">
Push image
</Button>
{:else}
<Button on:click="{() => pushImageFinished()}">Done</Button>
{/if}
<div class="flex justify-end space-x-2">
{#if !pushInProgress && !pushFinished}
<Button class="w-auto" type="secondary" on:click="{() => closeCallback()}">Cancel</Button>
{/if}
{#if !pushFinished}
<Button
class="w-auto"
icon="{faCircleArrowUp}"
disabled="{!isAuthenticatedForThisImage}"
on:click="{() => {
pushImage(selectedImageTag);
}}"
bind:inProgress="{pushInProgress}">
Push image
</Button>
{:else}
<Button on:click="{() => pushImageFinished()}" class="w-auto">Done</Button>
{/if}
</div>

<div bind:this="{pushLogsXtermDiv}"></div>
</div>
Expand Down

0 comments on commit f8ba14b

Please sign in to comment.