Skip to content

Commit

Permalink
wip dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
cdrage committed Feb 26, 2024
1 parent 0a01fdc commit fb31161
Show file tree
Hide file tree
Showing 18 changed files with 642 additions and 12 deletions.
15 changes: 14 additions & 1 deletion packages/backend/src/api-impl.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import * as podmanDesktopApi from '@podman-desktop/api';
import type { ImageInfo } from '@podman-desktop/api';
import type { BootcApi } from '@shared/src/BootcAPI';
import type { BootcBuildOptions } from '@shared/src/models/build';
import type { BootcBuildOptions, BootcImageInfo } from '@shared/src/models/build';
import { buildDiskImage } from './build-disk-image';
import { History } from './history';

export class BootcApiImpl implements BootcApi {

constructor(
private readonly extensionContext: podmanDesktopApi.ExtensionContext,
) {}

async ping(): Promise<string> {
return podmanDesktopApi.window.showInformationMessage('pong');
}
Expand Down Expand Up @@ -40,4 +46,11 @@ export class BootcApiImpl implements BootcApi {
}
return images;
}

async listBuiltImages(): Promise<BootcImageInfo[]> {
const history = new History(this.extensionContext.storagePath);
await history.loadFile();
return history.infos;
}

}
2 changes: 1 addition & 1 deletion packages/backend/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export async function activate(extensionContext: ExtensionContext): Promise<void

// Register the 'api' for the webview to communicate to the backend
const rpcExtension = new RpcExtension(panel.webview);
const bootcApi = new BootcApiImpl();
const bootcApi = new BootcApiImpl(extensionContext);
rpcExtension.registerInstance<BootcApiImpl>(BootcApiImpl, bootcApi);
}

Expand Down
11 changes: 3 additions & 8 deletions packages/backend/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@
import { existsSync } from 'node:fs';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import * as path from 'node:path';
import type { BootcImageInfo } from '@shared/src/models/build';

const filename = 'history.json';

interface ImageInfo {
image: string;
type: string;
location: string;
}

export class History {
infos: ImageInfo[] = [];
infos: BootcImageInfo[] = [];

constructor(private readonly storagePath: string) {}

Expand Down Expand Up @@ -64,7 +59,7 @@ export class History {
this.infos = this.infos.filter(info => info.image !== image || info.type !== type);

// add new item to the front
this.infos = [{ image, type, location } as ImageInfo, ...this.infos];
this.infos = [{ image, type, location } as BootcImageInfo, ...this.infos];

// cull the full list at the last 100
if (this.infos.length > 100) {
Expand Down
6 changes: 5 additions & 1 deletion packages/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '@fortawesome/fontawesome-free/css/all.min.css';
import { router } from 'tinro';
import Route from './lib/Route.svelte';
import Build from './Build.svelte';
import List from './List.svelte';
import { onMount } from 'svelte';
import { getRouterState } from './api/client';
Expand All @@ -22,7 +23,10 @@ onMount(() => {
<Route path="/*" breadcrumb="Home" isAppMounted="{isMounted}" let:meta>
<main class="flex flex-col w-screen h-screen overflow-hidden bg-charcoal-700">
<div class="flex flex-row w-full h-full overflow-hidden">
<Route path="/" breadcrumb="Build">
<Route path="/" breadcrumb="Dashboard">
<List />
</Route>
<Route path="/build" breadcrumb="Build">
<Build />
</Route>
</div>
Expand Down
76 changes: 76 additions & 0 deletions packages/frontend/src/List.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script lang="ts">
import { onMount } from 'svelte';
import { router } from 'tinro';
import type { BootcImageInfo } from '@shared/src/models/build';
import NavPage from './lib/NavPage.svelte';
import NoBootcImagesEmptyScreen from './lib/NoBootcImagesEmptyScreen.svelte';
import Button from './lib/Button.svelte';
import { faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import Table from './lib/Table.svelte';
import { Column, Row } from './lib/table';
import BootcColumnActions from './lib/BootcColumnActions.svelte';
import BootcColumnName from './lib/BootcColumnImageName.svelte';
import { bootcClient } from './api/client';
// Columns
/*
import VolumeColumnStatus from './VolumeColumnStatus.svelte';
import VolumeColumnName from './VolumeColumnName.svelte';
import VolumeColumnEnvironment from './VolumeColumnEnvironment.svelte';
import VolumeColumnActions from './VolumeColumnActions.svelte';
*/
export let searchTerm = '';
let builds: BootcImageInfo[] = [];
onMount(async () => {
// Load data to volumes from "history"
builds = await bootcClient.listBuiltImages();
});
function gotoBuild(): void {
router.goto('/build');
}
let selectedItemsNumber: number;
let table: Table;
let nameColumn = new Column<BootcImageInfo>('Image', {
width: '3fr',
renderer: BootcColumnName,
comparator: (a, b) => a.image.localeCompare(b.image),
});
const columns: Column<BootcImageInfo, BootcImageInfo | string>[] = [
nameColumn,
new Column<BootcImageInfo>('Actions', { align: 'right', renderer: BootcColumnActions, overflow: true }),
];
const row = new Row<BootcImageInfo>({});
</script>

<NavPage bind:searchTerm="{searchTerm}" title="volumes">

<svelte:fragment slot="additional-actions">
<Button on:click="{() => gotoBuild()}" icon="{faPlusCircle}" title="Build Bootc Image">Build Bootc Image</Button>
</svelte:fragment>

<div class="flex min-w-full h-full" slot="content">
<Table
kind="volume"
bind:this="{table}"
bind:selectedItemsNumber="{selectedItemsNumber}"
data="{builds}"
columns="{columns}"
row="{row}"
defaultSortColumn="Name"
on:update="{() => (builds = builds)}">
</Table>

{#if builds.length === 0}
<NoBootcImagesEmptyScreen />
{/if}
</div>
</NavPage>
10 changes: 10 additions & 0 deletions packages/frontend/src/lib/BootcActions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { BootcImageInfo } from '@shared/src/models/build';
export let bootcImage: BootcImageInfo;
onMount(() => {
console.log('bootcImage', bootcImage);
});
</script>

<!-- ADD ACTIONS HERE EVENTUALLY -->
8 changes: 8 additions & 0 deletions packages/frontend/src/lib/BootcColumnActions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import type { BootcImageInfo } from '@shared/src/models/build';
import BootcActions from './BootcActions.svelte';
export let object: BootcImageInfo;
</script>

<BootcActions bootcImage="{object}" on:update />
10 changes: 10 additions & 0 deletions packages/frontend/src/lib/BootcColumnImageName.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import { router } from 'tinro';
import type { BootcImageInfo } from '@shared/src/models/build';
export let object: BootcImageInfo;
</script>

<button class="flex text-sm text-gray-300">
{object.image}
</button>
10 changes: 10 additions & 0 deletions packages/frontend/src/lib/BootcEmptyScreen.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import EmptyScreen from './EmptyScreen.svelte';
import BootcIcon from './BootcIcon.svelte';
</script>

<EmptyScreen
icon="{BootcIcon}"
title="No past bootc build available"
message="Start your first build by clicking the 'Build' button"
/>
46 changes: 46 additions & 0 deletions packages/frontend/src/lib/BootcIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts">
export let size = '40';
export let solid = false;
</script>

<!-- TODO: CHANGE THIS TO AN ACTUAL BOOTC ICON -->
<svg
width="{size}"
height="{size}"
class="{$$props.class}"
style="{$$props.style}"
viewBox="0.926 0.926 4.498 4.498"
version="1.1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
{#if solid}
<defs>
<mask id="volumemask">
<rect x="0" y="0" width="100" height="100" fill="white"></rect>
<path
style="stroke-linejoin:round;stroke-miterlimit:10"
d="M 8,-0.5 C 5.308199,-0.44627808 2.7353389,-0.26074086 0.3828125,0.73242187 -0.08998295,0.99973104 -0.5,1.3654569 -0.5,1.9121094 v 5.1757812 c 0,0.5466525 0.41001705,0.9123784 0.8828125,1.1796875 C 2.7589104,9.3313527 5.7271019,9.491748 8,9.5 10.691802,9.4462782 13.264661,9.2607406 15.617188,8.2675781 16.246686,7.9041374 16.5,7.8219799 16.5,7.0878906 V 1.9121094 C 16.5,1.3654569 16.089983,0.99973104 15.617188,0.73242187 13.24109,-0.33135254 10.272898,-0.49174799 8,-0.5 Z m 0,1 c 2.649222,0.0569525 5.021785,0.005065 7.5,1.4121094 0,-0.018671 -0.03806,0.1200895 -0.375,0.3105469 C 12.902157,3.1721288 10.113737,3.3183277 8,3.3261719 5.4907046,3.2640566 3.1296592,3.1307059 0.875,2.2226563 0.53806505,2.0321989 0.5,1.8934389 0.5,1.9121094 2.9262917,0.39519991 5.8400742,0.50801972 8,0.5 Z M 0.5,3.1328125 C 2.8423807,4.1650465 5.7688795,4.318076 8,4.3261719 10.645243,4.2636654 13.190761,4.1064332 15.5,3.1328125 V 7.0878906 C 13.289959,8.4696007 10.159926,8.4919803 8,8.5 5.3507782,8.4430475 2.6797453,8.3254743 0.5,7.0878906 Z"
></path>
</mask>
</defs>
<g transform="translate(-0.50925903,-15.109665)">
<g transform="matrix(0.26458333,0,0,0.26458333,1.5675951,17.09404)">
<path
style="fill:currentColor;fill-rule:nonzero;stroke-width:1;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none"
mask="url(#volumemask)"
d="m 16,1.913 z m 0,0 C 16,2.969 12.418,3.826 8,3.826 3.582,3.826 0,2.969 0,1.913 v 0 C 0,0.856 3.582,0 8,0 c 4.418,0 8,0.856 8,1.913 z m 0,0 V 7.087 C 16,8.144 12.418,9 8,9 3.582,9 0,8.144 0,7.087 V 1.913"
></path>
</g>
</g>
{:else}
<g transform="translate(-0.50925903,-15.109665)">
<g transform="matrix(0.26458333,0,0,0.26458333,1.5675951,17.09404)">
<path
style="fill:currentColor;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:10"
d="M 8,-0.5 C 5.308199,-0.44627808 2.7353389,-0.26074086 0.3828125,0.73242187 -0.08998295,0.99973104 -0.5,1.3654569 -0.5,1.9121094 v 5.1757812 c 0,0.5466525 0.41001705,0.9123784 0.8828125,1.1796875 C 2.7589104,9.3313527 5.7271019,9.491748 8,9.5 10.691802,9.4462782 13.264661,9.2607406 15.617188,8.2675781 16.246686,7.9041374 16.5,7.8219799 16.5,7.0878906 V 1.9121094 C 16.5,1.3654569 16.089983,0.99973104 15.617188,0.73242187 13.24109,-0.33135254 10.272898,-0.49174799 8,-0.5 Z m 0,1 c 2.649222,0.0569525 5.021785,0.005065 7.5,1.4121094 0,-0.018671 -0.03806,0.1200895 -0.375,0.3105469 C 12.902157,3.1721288 10.113737,3.3183277 8,3.3261719 5.4907046,3.2640566 3.1296592,3.1307059 0.875,2.2226563 0.53806505,2.0321989 0.5,1.8934389 0.5,1.9121094 2.9262917,0.39519991 5.8400742,0.50801972 8,0.5 Z M 0.5,3.1328125 C 2.8423807,4.1650465 5.7688795,4.318076 8,4.3261719 10.645243,4.2636654 13.190761,4.1064332 15.5,3.1328125 V 7.0878906 C 13.289959,8.4696007 10.159926,8.4919803 8,8.5 5.3507782,8.4430475 2.6797453,8.3254743 0.5,7.0878906 Z"
></path>
</g>
</g>
{/if}
</svg>
49 changes: 49 additions & 0 deletions packages/frontend/src/lib/Checkbox.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts">
import Fa from 'svelte-fa';
import { createEventDispatcher } from 'svelte';
import { faCheckSquare, faMinusSquare, faSquare } from '@fortawesome/free-solid-svg-icons';
import { faSquare as faOutlineSquare } from '@fortawesome/free-regular-svg-icons';
export let checked = false;
export let disabled = false;
export let indeterminate = false;
export let disabledTooltip = '';
export let title = '';
export let id: string | undefined = undefined;
export let name: string | undefined = undefined;
export let required = false;
const dispatch = createEventDispatcher<{ click: boolean }>();
function onClick(checked: boolean) {
dispatch('click', checked);
}
</script>

<label class="{$$props.class || ''}">
<input
aria-label="{title}"
type="checkbox"
id="{id}"
name="{name}"
bind:checked="{checked}"
disabled="{disabled}"
required="{required}"
class="sr-only"
on:click="{event => onClick(event.currentTarget.checked)}" />
<div
class="grid place-content-center"
title="{disabled ? disabledTooltip : title}"
class:cursor-pointer="{!disabled}"
class:cursor-not-allowed="{disabled}">
{#if disabled}
<Fa size="1.1x" icon="{faSquare}" class="text-charcoal-300" />
{:else if indeterminate}
<Fa size="1.1x" icon="{faMinusSquare}" class="text-dustypurple-500" />
{:else if checked}
<Fa size="1.1x" icon="{faCheckSquare}" class="text-purple-500" />
{:else}
<Fa size="1.1x" icon="{faOutlineSquare}" class="text-gray-400" />
{/if}
</div>
</label>
48 changes: 48 additions & 0 deletions packages/frontend/src/lib/NavPage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script lang="ts">
import SearchInput from './SearchInput.svelte';
export let title: string;
export let searchTerm = '';
export let searchEnabled = true;
</script>

<div class="flex flex-col w-full h-full shadow-pageheader">
<div class="flex flex-col w-full h-full pt-4" role="region" aria-label="{title}">
<div class="flex pb-2" role="region" aria-label="header">
<div class="px-5">
<h1 class="text-xl first-letter:uppercase">{title}</h1>
</div>
<div class="flex flex-1 justify-end">
<div class="px-5" role="group" aria-label="additionalActions">
{#if $$slots['additional-actions']}
<div class="space-x-2 flex flex-nowrap"><slot name="additional-actions" /></div>
{:else}&nbsp;{/if}
</div>
</div>
</div>
{#if searchEnabled}
<div class="flex flex-row pb-4" role="region" aria-label="search">
<div class="pl-5 lg:w-[35rem] w-[22rem]">
<SearchInput bind:searchTerm="{searchTerm}" title="{title}" />
</div>
<div class="flex flex-1 px-5" role="group" aria-label="bottomAdditionalActions">
{#if $$slots['bottom-additional-actions']}
<div class="space-x-2 flex flex-row justify-start items-center w-full">
<slot name="bottom-additional-actions" />
</div>
{:else}&nbsp;{/if}
</div>
</div>
{/if}

{#if $$slots.tabs}
<div class="flex flex-row px-2 mb-2 border-b border-charcoal-400">
<slot name="tabs" />
</div>
{/if}

<div class="flex w-full h-full overflow-auto" role="region" aria-label="content">
<slot name="content" />
</div>
</div>
</div>
6 changes: 6 additions & 0 deletions packages/frontend/src/lib/NoBootcImagesEmptyScreen.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script lang="ts">
import EmptyScreen from './EmptyScreen.svelte';
import { faLayerGroup } from '@fortawesome/free-solid-svg-icons';
</script>

<EmptyScreen title="No bootc images available" message="Build a bootc image container first" icon="{faLayerGroup}" />
32 changes: 32 additions & 0 deletions packages/frontend/src/lib/SearchInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import Input from './Input.svelte';
export let title: string;
export let searchTerm: string = '';
</script>

<Input
class="{$$props.class || ''}"
id="search-{title}"
name="search-{title}"
placeholder="Search {title}..."
bind:value="{searchTerm}"
aria-label="search {title}"
clearable
on:input>
<svelte:fragment slot="left">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 mx-1 text-gray-700"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
role="img">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</svelte:fragment>
</Input>
Loading

0 comments on commit fb31161

Please sign in to comment.