Skip to content

Commit

Permalink
feat: allow to prune only untagged images (and not only unused)
Browse files Browse the repository at this point in the history
fixes podman-desktop/podman-desktop#7043

Signed-off-by: Florent Benoit <[email protected]>
  • Loading branch information
benoitf committed Dec 19, 2024
1 parent 6a8f437 commit 7448e9d
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 9 deletions.
165 changes: 165 additions & 0 deletions packages/renderer/src/lib/engine/Prune.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**********************************************************************
* Copyright (C) 2023-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 { fireEvent, render, screen } from '@testing-library/svelte';
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';

import Prune from './Prune.svelte';

beforeAll(() => {
Object.defineProperty(global, 'window', {
value: {
showMessageBox: vi.fn(),
pruneContainers: vi.fn(),
pruneImages: vi.fn(),
},
writable: true,
});
});

beforeEach(() => {
vi.resetAllMocks();
});

describe('containers', () => {
test('pruned containers', async () => {
render(Prune, {
type: 'containers',
engines: [
{
id: 'podman',
name: 'Podman',
},
],
});

// mock the window.showMessageBox method to return "all"
const response = 1;
vi.mocked(window.showMessageBox).mockResolvedValue({
response,
});

// search for the button
const button = await screen.findByRole('button', { name: 'Prune' });
expect(button).toBeInTheDocument();
await fireEvent.click(button);

// check if the showMessageBox method was called with all the right parameters
expect(window.showMessageBox).toHaveBeenCalledWith({
buttons: ['Cancel', 'Yes'],
title: 'Prune',
message: 'This action will prune all unused containers from the Podman engine.',
});

expect(window.pruneContainers).toHaveBeenCalledWith('podman');
});
});

describe('images', () => {
const CANCEL_BUTTON = 'Cancel';
const ALL_UNUSED_IMAGES = 'All unused images';
const ALL_UNTAGGED_IMAGES = 'All untagged images';

const IMAGE_BUTTONS = [CANCEL_BUTTON, ALL_UNUSED_IMAGES, ALL_UNTAGGED_IMAGES];

const imageRender = () => {
render(Prune, {
type: 'images',
engines: [
{
id: 'podman',
name: 'Podman',
},
],
});
};

test('prune all untagged images', async () => {
imageRender();

// mock the window.showMessageBox method to return "all untaged images"
const response = IMAGE_BUTTONS.indexOf(ALL_UNTAGGED_IMAGES);
vi.mocked(window.showMessageBox).mockResolvedValue({
response,
});

// search for the button
const button = await screen.findByRole('button', { name: 'Prune' });
expect(button).toBeInTheDocument();
await fireEvent.click(button);

// check if the showMessageBox method was called with all the right parameters
expect(window.showMessageBox).toHaveBeenCalledWith({
buttons: IMAGE_BUTTONS,
title: 'Prune',
message: 'This action will prune images from the Podman engine.',
});

expect(window.pruneImages).toHaveBeenCalledWith('podman', false);
});

test('prune all unused images', async () => {
imageRender();

// mock the window.showMessageBox method to return "all unused images"
const response = IMAGE_BUTTONS.indexOf(ALL_UNUSED_IMAGES);
vi.mocked(window.showMessageBox).mockResolvedValue({
response,
});

// search for the button
const button = await screen.findByRole('button', { name: 'Prune' });
expect(button).toBeInTheDocument();
await fireEvent.click(button);

// check if the showMessageBox method was called with all the right parameters
expect(window.showMessageBox).toHaveBeenCalledWith({
buttons: IMAGE_BUTTONS,
title: 'Prune',
message: 'This action will prune images from the Podman engine.',
});

expect(window.pruneImages).toHaveBeenCalledWith('podman', true);
});

test('prune nothing (click cancel)', async () => {
imageRender();

// mock the window.showMessageBox method to return "Cancel"
const response = IMAGE_BUTTONS.indexOf(CANCEL_BUTTON);
vi.mocked(window.showMessageBox).mockResolvedValue({
response,
});

// search for the button
const button = await screen.findByRole('button', { name: 'Prune' });
expect(button).toBeInTheDocument();
await fireEvent.click(button);

// check if the showMessageBox method was called with all the right parameters
expect(window.showMessageBox).toHaveBeenCalledWith({
buttons: IMAGE_BUTTONS,
title: 'Prune',
message: 'This action will prune images from the Podman engine.',
});

expect(window.pruneImages).not.toBeCalled();
});
});
34 changes: 27 additions & 7 deletions packages/renderer/src/lib/engine/Prune.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,52 @@ import { Button } from '@podman-desktop/ui-svelte';
import type { EngineInfoUI } from './EngineInfoUI';
// Imported type for prune (containers, images, pods, volumes)
export let type: string;
export let type: 'containers' | 'images' | 'pods' | 'volumes';
// List of engines that the prune will work on
export let engines: EngineInfoUI[];
const LABEL_IMAGE_UNUSED = 'All unused images';
const LABEL_IMAGE_UNTAGGED = 'All untagged images';
async function openPruneDialog(): Promise<void> {
let message = 'This action will prune all unused ' + type;
let message = 'This action will prune';
if (type === 'images') {
message += ' images';
} else {
message += ` all unused ${type}`;
}
if (engines.length > 1) {
message += ' from all container engines.';
} else {
message += ' from the ' + engines[0].name + ' engine.';
}
const buttons: string[] = [];
const cancel = 'Cancel';
buttons.push(cancel);
if (type === 'images') {
buttons.push(LABEL_IMAGE_UNUSED);
buttons.push(LABEL_IMAGE_UNTAGGED);
} else {
buttons.push('Yes');
}
const result = await window.showMessageBox({
title: 'Prune',
message: message,
buttons: ['Yes', 'Cancel'],
buttons,
});
if (result && result.response === 0) {
await prune(type);
const selectedItemLabel = buttons[result.response ?? 0];
if (selectedItemLabel !== cancel) {
await prune(type, selectedItemLabel);
}
}
// Function to prune the selected type: containers, pods, images and volumes
async function prune(type: string) {
async function prune(type: string, selectedItemLabel: string): Promise<void> {
switch (type) {
case 'containers':
for (let engine of engines) {
Expand Down Expand Up @@ -62,7 +82,7 @@ async function prune(type: string) {
case 'images':
for (let engine of engines) {
try {
await window.pruneImages(engine.id);
await window.pruneImages(engine.id, selectedItemLabel === LABEL_IMAGE_UNUSED);
} catch (error) {
console.error(error);
}
Expand Down
4 changes: 2 additions & 2 deletions tests/playwright/src/model/pages/images-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class ImagesPage extends MainPage {
exact: true,
});
this.pruneConfirmationButton = this.page.getByRole('button', {
name: 'Yes',
name: 'All unused images',
exact: true,
});
this.loadImagesFromTarButton = this.additionalActions.getByLabel('Load Images', { exact: true });
Expand Down Expand Up @@ -128,7 +128,7 @@ export class ImagesPage extends MainPage {
async pruneImages(): Promise<ImagesPage> {
return test.step('Prune images', async () => {
await this.pruneImagesButton.click();
await handleConfirmationDialog(this.page, 'Prune');
await handleConfirmationDialog(this.page, 'Prune', true, 'All unused images');
return this;
});
}
Expand Down

0 comments on commit 7448e9d

Please sign in to comment.