Skip to content

Commit

Permalink
feat: display files added/removed/modified
Browse files Browse the repository at this point in the history
Signed-off-by: Philippe Martin <phmartin@redhat.com>
  • Loading branch information
feloy committed Oct 2, 2024
1 parent 7072189 commit c59d134
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 59 deletions.
24 changes: 10 additions & 14 deletions packages/renderer/src/lib/image/ImageDetailsFilesLayers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@

import '@testing-library/jest-dom/vitest';

import type { ImageFile } from '@podman-desktop/api';
import { render, screen, within } from '@testing-library/svelte';
import { expect, test } from 'vitest';

import type { FilesystemTree } from './filesystem-tree';
import type { ImageFilesystemLayerUI } from './imageDetailsFiles';
import { signedHumanSize } from './ImageDetailsFilesLayers';
import ImageDetailsFilesLayers from './ImageDetailsFilesLayers.svelte';
Expand All @@ -38,28 +36,26 @@ test('render', async () => {
id: 'layer1',
createdBy: 'creator',
sizeInArchive: 1000,
sizeInContainer: 900,
stackTree: {
size: 2000,
} as unknown as FilesystemTree<ImageFile>,
addedCount: 5,
addedSize: 1000,
} as unknown as ImageFilesystemLayerUI,
{
id: 'layer2',
createdBy: 'creator',
addedCount: 1,
addedSize: 10,
sizeInArchive: 0,
sizeInContainer: -300,
stackTree: {
size: 1700,
} as unknown as FilesystemTree<ImageFile>,
modifiedCount: 1,
modifiedSize: -5,
removedCount: 2,
removedSize: -7,
} as unknown as ImageFilesystemLayerUI,
];
render(ImageDetailsFilesLayers, { layers });
const rows = screen.getAllByRole('row');
expect(rows.length).toBe(2);
within(rows[0]).getByText('1 kB • layer1');
within(rows[0]).getByText('contribute to FS: +900 B');
within(rows[0]).getByText('total FS: 2 kB');
within(rows[0]).getByText('files: 5 added (+1 kB)');
within(rows[1]).getByText('0 B • layer2');
within(rows[1]).getByText('contribute to FS: -300 B');
within(rows[1]).getByText('total FS: 1.7 kB');
within(rows[1]).getByText('files: 1 added (+10 B) • 1 modified (-5 B) • 2 removed (-7 B)');
});
22 changes: 19 additions & 3 deletions packages/renderer/src/lib/image/ImageDetailsFilesLayers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,27 @@ function onLayerSelected(layer: ImageFilesystemLayerUI) {
currentLayerId = layer.id;
dispatch('selected', layer);
}
function getSizesText(layer: ImageFilesystemLayerUI): string {
let parts: string[] = [];
if (layer.addedCount) {
parts.push(`${layer.addedCount} added (${signedHumanSize(layer.addedSize)})`);
}
if (layer.modifiedCount) {
parts.push(`${layer.modifiedCount} modified (${signedHumanSize(layer.modifiedSize)})`);
}
if (layer.removedCount) {
parts.push(`${layer.removedCount} removed (${signedHumanSize(layer.removedSize)})`);
}
if (!parts.length) {
return '';
}
return `files: ${parts.join('')}`;
}
</script>

{#each layers as layer}
{@const sizesText = getSizesText(layer)}
<button
on:click={() => onLayerSelected(layer)}
role="row"
Expand All @@ -29,9 +47,7 @@ function onLayerSelected(layer: ImageFilesystemLayerUI) {
<div class="text-sm opacity-70">{new ImageUtils().getHumanSize(layer.sizeInArchive)} &bull; {layer.id}</div>
<ImageDetailsFilesExpandableCommand command={layer.createdBy} />
<div class="text-sm opacity-70">
<span>contribute to FS: {signedHumanSize(layer.sizeInContainer)}</span>
<span> | </span>
<span>total FS: {new ImageUtils().getHumanSize(layer.stackTree.size)}</span>
{sizesText}
</div>
</div>
</button>
Expand Down
75 changes: 55 additions & 20 deletions packages/renderer/src/lib/image/filesystem-tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ interface Typ {

test('add paths to filetree', () => {
const tree = new FilesystemTree<Typ>('tree1')
.addPath('A', { path: 'A-path' }, 5)
.addPath('a/', { path: 'a/-path' }, 0)
.addPath('a/b/c/d.txt', { path: 'a/b/c/d.txt-path' }, 3)
.addPath('a/b/c/e.txt', { path: 'a/b/c/e.txt-path' }, 4);
.addPath('A', { path: 'A-path' }, 5, false)
.addPath('a/', { path: 'a/-path' }, 0, false)
.addPath('a/b/c/d.txt', { path: 'a/b/c/d.txt-path' }, 3, false)
.addPath('a/b/c/e.txt', { path: 'a/b/c/e.txt-path' }, 4, false);

const copy = tree.copy();

Expand Down Expand Up @@ -71,21 +71,21 @@ test('add paths to filetree', () => {
});

test('currentSize with existing file', () => {
const tree = new FilesystemTree<Typ>('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5);
const tree = new FilesystemTree<Typ>('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5, false);
const current = tree.currentSize('A/B/C.txt');
expect(current).toBe(5);
});

test('currentSize with non existing file', () => {
const tree = new FilesystemTree<Typ>('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5);
const tree = new FilesystemTree<Typ>('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5, false);
const current = tree.currentSize('A/B/C.log');
expect(current).toBe(undefined);
});

test('add an existing file', () => {
const tree = new FilesystemTree<Typ>('tree1')
.addPath('A/B/C.txt', { path: 'A/B/C.txt ' }, 5)
.addPath('A/B/C.txt', { path: 'A/B/C.txt ' }, 4);
.addPath('A/B/C.txt', { path: 'A/B/C.txt ' }, 5, false)
.addPath('A/B/C.txt', { path: 'A/B/C.txt ' }, 4, false);
expect(tree.size).toBe(4);
expect(tree.root.children).toHaveLength(1);
expect(tree.root.children.get('A')!.children).toHaveLength(1);
Expand All @@ -95,31 +95,53 @@ test('add an existing file', () => {

test('add an existing directory containing files', () => {
const tree = new FilesystemTree<Typ>('tree1')
.addPath('A/B/C.txt', { path: 'A/B/C.txt ' }, 5)
.addPath('A/B/D.txt', { path: 'A/B/D.txt ' }, 4)
.addPath('A/B', { path: 'A/B ' }, 0)
.addPath('A/B/E.txt', { path: 'A/B/E.txt ' }, 1);
.addPath('A/B/C.txt', { path: 'A/B/C.txt ' }, 5, false)
.addPath('A/B/D.txt', { path: 'A/B/D.txt ' }, 4, false)
.addPath('A/B', { path: 'A/B ' }, 0, false)
.addPath('A/B/E.txt', { path: 'A/B/E.txt ' }, 1, false);
expect(tree.size).toBe(10);
expect(tree.addedCount).toBe(3);
expect(tree.modifiedCount).toBe(1); // A/B
expect(tree.removedCount).toBe(0);
expect(tree.addedSize).toBe(10);
expect(tree.modifiedSize).toBe(0);
expect(tree.removedSize).toBe(0);
});

test('remove a non existing file', () => {
const tree = new FilesystemTree<Typ>('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5).hidePath('A/B/D.txt');
const tree = new FilesystemTree<Typ>('tree1')
.addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5, false)
.hidePath('A/B/D.txt');
expect(tree.size).toBe(5);
expect(tree.root.children).toHaveLength(1);
expect(tree.root.children.get('A')!.children).toHaveLength(1);
expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(1);
expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0);
expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.hidden).toBeFalsy();
expect(tree.addedCount).toBe(1);
expect(tree.modifiedCount).toBe(0);
expect(tree.removedCount).toBe(0);
expect(tree.addedSize).toBe(5);
expect(tree.modifiedSize).toBe(0);
expect(tree.removedSize).toBe(0);
});

test('remove an existing file', () => {
const tree = new FilesystemTree<Typ>('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5).hidePath('A/B/C.txt');
const tree = new FilesystemTree<Typ>('tree1')
.addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5, false)
.hidePath('A/B/C.txt');
expect(tree.size).toBe(0);
expect(tree.root.children).toHaveLength(1);
expect(tree.root.children.get('A')!.children).toHaveLength(1);
expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(1);
expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0);
expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.hidden).toBeTruthy();
expect(tree.addedCount).toBe(1);
expect(tree.modifiedCount).toBe(0);
expect(tree.removedCount).toBe(1);
expect(tree.addedSize).toBe(5);
expect(tree.modifiedSize).toBe(0);
expect(tree.removedSize).toBe(-5);
});

test('add a whiteout', () => {
Expand All @@ -130,12 +152,18 @@ test('add a whiteout', () => {
expect(tree.root.children.get('A')!.children.get('B')!.children).toHaveLength(1);
expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.children).toHaveLength(0);
expect(tree.root.children.get('A')!.children.get('B')!.children.get('C.txt')!.hidden).toBeTruthy();
expect(tree.addedCount).toBe(0);
expect(tree.modifiedCount).toBe(0);
expect(tree.removedCount).toBe(0);
expect(tree.addedSize).toBe(0);
expect(tree.modifiedSize).toBe(0);
expect(tree.removedSize).toBe(0);
});

test('hide content of non-existing directory', () => {
const tree = new FilesystemTree<Typ>('tree1')
.addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 1)
.addPath('A/B/D.txt', { path: 'A/B/D.txt' }, 2)
.addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 1, false)
.addPath('A/B/D.txt', { path: 'A/B/D.txt' }, 2, false)
.hideDirectoryContent('A/E');
expect(tree.size).toBe(3);
expect(tree.root.children).toHaveLength(1);
Expand All @@ -150,8 +178,8 @@ test('hide content of non-existing directory', () => {

test('hide directory content', () => {
const tree = new FilesystemTree<Typ>('tree1')
.addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 1)
.addPath('A/B/D.txt', { path: 'A/B/D.txt' }, 2)
.addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 1, false)
.addPath('A/B/D.txt', { path: 'A/B/D.txt' }, 2, false)
.hideDirectoryContent('A');
expect(tree.size).toBe(0);
expect(tree.root.children).toHaveLength(1);
Expand All @@ -164,10 +192,17 @@ test('hide directory content', () => {
expect(tree.root.children.get('A')!.children.get('B')!.children.get('D.txt')!.hidden).toBeTruthy();
});

test('isDirectory', () => {
const tree = new FilesystemTree<Typ>('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5);
test('isDirectory with children', () => {
const tree = new FilesystemTree<Typ>('tree1').addPath('A/B/C.txt', { path: 'A/B/C.txt' }, 5, false);
expect(tree.isDirectory('/A/B')).toBeTruthy();
expect(tree.isDirectory('/A/C')).toBeFalsy();
expect(tree.isDirectory('/A/B/C.txt')).toBeFalsy();
expect(tree.isDirectory('/A/B/D.txt')).toBeFalsy();
});

test('isDirectory without children', () => {
const tree1 = new FilesystemTree<Typ>('tree1').addPath('A/B', { path: 'A/B' }, 0, true);
expect(tree1.isDirectory('/A/B')).toBeTruthy();
const tree2 = new FilesystemTree<Typ>('tree1').addPath('A/B', { path: 'A/B' }, 0, false);
expect(tree2.isDirectory('/A/B')).toBeFalsy();
});
48 changes: 39 additions & 9 deletions packages/renderer/src/lib/image/filesystem-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,27 @@
export class FilesystemNode<T> {
name: string;
data?: T;
isDirectory: boolean;
children: Map<string, FilesystemNode<T>>;
size: number;
hidden: boolean;

constructor(name: string) {
constructor(name: string, isDirectory: boolean) {
this.name = name;
this.children = new Map<string, FilesystemNode<T>>();
this.size = 0;
this.hidden = false;
this.isDirectory = isDirectory;
}

addChild(name: string): FilesystemNode<T> {
const child = new FilesystemNode<T>(name);
addChild(name: string, isDirectory: boolean): FilesystemNode<T> {
const child = new FilesystemNode<T>(name, isDirectory);
this.children.set(name, child);
return child;
}

copy(): FilesystemNode<T> {
const result = new FilesystemNode<T>(this.name);
const result = new FilesystemNode<T>(this.name, this.isDirectory);
result.data = this.data;
result.size = this.size;
result.hidden = this.hidden;
Expand All @@ -53,16 +55,42 @@ export class FilesystemTree<T> {
root: FilesystemNode<T>;
size: number;

// The number of added/modified/removed files
addedCount: number;
modifiedCount: number;
removedCount: number;

addedSize: number;
modifiedSize: number;
removedSize: number;

constructor(name: string) {
this.name = name;
this.root = new FilesystemNode<T>('/');
this.root = new FilesystemNode<T>('/', true);
this.size = 0;

this.addedCount = 0;
this.modifiedCount = 0;
this.removedCount = 0;

this.addedSize = 0;
this.modifiedSize = 0;
this.removedSize = 0;
}

addPath(path: string, entry: T, size: number): FilesystemTree<T> {
addPath(path: string, entry: T, size: number, isDirectory: boolean): FilesystemTree<T> {
const currentSize = this.currentSize(path);
// If we add an already existing directory, we replace its size with its current one
// so we do not overwrite it (being the size of all its descendants)
if (currentSize && !this.isDirectory(path)) {
this.modifiedCount++;
this.modifiedSize += size - currentSize;
} else if (!this.isDirectory(path)) {
this.addedCount++;
this.addedSize += size;
} else if (this.isDirectory(path)) {
this.modifiedCount++;
}
if (currentSize && this.isDirectory(path)) {
size = currentSize;
}
Expand All @@ -78,7 +106,7 @@ export class FilesystemTree<T> {
if (next) {
node = next;
} else {
node = node.addChild(part);
node = node.addChild(part, isDirectory);
}
node.size += size - (currentSize ?? 0);
}
Expand Down Expand Up @@ -127,6 +155,8 @@ export class FilesystemTree<T> {
// the path is not found, return now
return this;
}
this.removedCount++;
this.removedSize -= currentSize;
this.size -= currentSize;
const parts = path.split('/');
let node = this.root;
Expand Down Expand Up @@ -161,7 +191,7 @@ export class FilesystemTree<T> {
if (next) {
node = next;
} else {
node = node.addChild(part);
node = node.addChild(part, false);
}
node.size -= currentSize ?? 0;
}
Expand Down Expand Up @@ -203,7 +233,7 @@ export class FilesystemTree<T> {
return false;
}
}
return node.children.size > 0;
return node.children.size > 0 || node.isDirectory;
}

copy(): FilesystemTree<T> {
Expand Down
Loading

0 comments on commit c59d134

Please sign in to comment.