Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/backend/src/apis/quadlet-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { PodmanService } from '../services/podman-service';
import type { ProviderService } from '../services/provider-service';
import type { LoggerService } from '../services/logger-service';
import type { SynchronisationInfo } from '/@shared/src/models/synchronisation';
import type { Template } from '/@shared/src/models/template';

interface Dependencies {
quadlet: QuadletService;
Expand Down Expand Up @@ -157,4 +158,8 @@ export class QuadletApiImpl extends QuadletApi {
id: id,
});
}

override async templates(): Promise<Array<Template>> {
return this.dependencies.quadlet.templates();
}
}
31 changes: 31 additions & 0 deletions packages/backend/src/assets/templates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[
{
"id": "nginx-container",
"name": "Nginx Container",
"description": "Simple Nginx Container Quadlet template",
"files": [
{
"language": "ini",
"content": "# nginx.container\n[Container]\nImage=nginx\nPublishPort=8888:80\n\n[Service]\nRestart=always",
"name": "nginx.container"
}
]
},
{
"id": "nginx-pod",
"name": "Nginx Pod",
"description": "Simple Nginx Kube Quadlet Template",
"files": [
{
"language": "ini",
"content": "# play.kube\n[Kube]\nYaml=play.yaml",
"name": "nginx.kube"
},
{
"language": "ini",
"content": "apiVersion: v1\nkind: Pod\nmetadata:\n name: nginx-pod\nspec:\n containers:\n - name: container\n image: nginx\n ports:\n - name: http\n containerPort: 80",
"name": "play.yaml"
}
]
}
]
6 changes: 6 additions & 0 deletions packages/backend/src/services/quadlet-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { TelemetryEvents } from '../utils/telemetry-events';
import { QuadletType } from '/@shared/src/utils/quadlet-type';
import { QuadletKubeParser } from '../utils/parsers/quadlet-kube-parser';
import { isRunError } from '../utils/run-error';
import templates from '../assets/templates.json';
import type { Template } from '/@shared/src/models/template';

export class QuadletService extends QuadletHelper implements Disposable, AsyncInit {
#extensionsEventDisposable: Disposable | undefined;
Expand Down Expand Up @@ -470,4 +472,8 @@ export class QuadletService extends QuadletHelper implements Disposable, AsyncIn
this.#extensionsEventDisposable?.dispose();
this.#extensionsEventDisposable = undefined;
}

public templates(): Array<Template> {
return templates;
}
}
12 changes: 11 additions & 1 deletion packages/frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { Messages } from '/@shared/src/messages';
import type { Unsubscriber } from 'svelte/store';
import QuadletGenerate from '/@/pages/QuadletGenerate.svelte';
import QuadletCompose from '/@/pages/QuadletCompose.svelte';
import QuadletTemplates from '/@/pages/QuadletTemplates.svelte';
import QuadletCreate from '/@/pages/QuadletCreate.svelte';
// import globally the monaco environment
import './lib/monaco-editor/monaco-environment';

Expand Down Expand Up @@ -45,7 +47,15 @@ onDestroy(() => {
<QuadletsList />
</Route>

<!-- create quadlet -->
<Route path="/quadlets/create/*" breadcrumb="Create" let:meta>
<QuadletCreate modelId={meta.query.modelId} />
</Route>

<Route path="/quadlets/templates" breadcrumb="Templates">
<QuadletTemplates />
</Route>

<!-- create quadlet from existing resources -->
<Route path="/quadlets/generate/*" firstmatch let:meta>
<QuadletGenerate
providerId={meta.query.providerId}
Expand Down
24 changes: 17 additions & 7 deletions packages/frontend/src/lib/forms/EditorOverlay.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
<script lang="ts">
import { Button, Tooltip } from '@podman-desktop/ui-svelte';
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';

interface Props {
save(): void;
changed: boolean;
loading: boolean;
actions: Array<{ id: string; label: string; tooltip: string; icon?: IconDefinition }>;
onclick: (actionId: string) => void;
disabled?: boolean;
loading?: boolean;
}

let { save, changed, loading }: Props = $props();
let { actions, disabled, loading, onclick }: Props = $props();
</script>

<div
class="flex flex-row-reverse p-6 bg-transparent fixed bottom-0 right-0 mb-5 pr-10 max-h-20 bg-opacity-90 z-50"
role="group"
aria-label="Edit Buttons">
<Tooltip topLeft tip="Apply the changes">
<Button type="primary" aria-label="Save" on:click={save} disabled={!changed} inProgress={loading}>Save</Button>
</Tooltip>
{#each actions as action (action.id)}
<Tooltip topLeft tip={action.tooltip}>
<Button
type="primary"
aria-label={action.label}
icon={action.icon}
on:click={onclick.bind(undefined, action.id)}
disabled={disabled}
inProgress={loading}>{action.label}</Button>
</Tooltip>
{/each}
</div>
4 changes: 4 additions & 0 deletions packages/frontend/src/lib/monaco-editor/MonacoEditor.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<!--
@component
@deprecated use {@link MonacoEditors}
-->
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api';
Expand Down
57 changes: 57 additions & 0 deletions packages/frontend/src/lib/monaco-editor/MonacoEditors.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api';
import type { HTMLAttributes } from 'svelte/elements';
import { MonacoManager } from '/@/lib/monaco-editor/monaco';

interface Props extends HTMLAttributes<HTMLElement> {
readOnly?: boolean;
noMinimap?: boolean;
models: Array<Monaco.editor.ITextModel>;
modelId?: string;
}

let { readOnly = false, noMinimap, class: className, models: modelsArray, modelId, ...restProps }: Props = $props();

let editorInstance: Monaco.editor.IStandaloneCodeEditor | undefined;
let editorContainer: HTMLElement;
let decorationCollection: Monaco.editor.IEditorDecorationsCollection | undefined = $state();

let models: Map<string, Monaco.editor.ITextModel> = $derived(new Map(modelsArray.map(model => [model.id, model])));

$effect(() => {
// if modelId is undefined or current modelId is already selected
if (!modelId || !editorInstance || editorInstance.getModel()?.id === modelId) return;

const request = models.get(modelId);
if (!request) return;
editorInstance.setModel(request);
});

onMount(async () => {
const monaco = await MonacoManager.getMonaco();

let model: Monaco.editor.ITextModel | undefined;
if (modelId) {
model = models.get(modelId);
}

editorInstance = monaco.editor.create(editorContainer, {
model,
automaticLayout: true,
scrollBeyondLastLine: false,
readOnly: readOnly,
theme: MonacoManager.getThemeName(),
minimap: {
enabled: !noMinimap,
},
});
});

onDestroy(() => {
decorationCollection?.clear();
editorInstance?.dispose();
});
</script>

<div class="h-full w-full {className}" {...restProps} bind:this={editorContainer}></div>
53 changes: 53 additions & 0 deletions packages/frontend/src/lib/pagination/EditableTab.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script lang="ts">
/* eslint-disable sonarjs/no-use-of-empty-return-value */
import { faPen } from '@fortawesome/free-solid-svg-icons/faPen';
import { faFile } from '@fortawesome/free-solid-svg-icons/faFile';
import Fa from 'svelte-fa';
import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash';
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';

interface Props {
selected: boolean;
url: string;
title: string;
onEdit: () => void;
onDelete: () => void;
}

let { url, selected, title, onEdit, onDelete }: Props = $props();

interface BtnProps {
icon: IconDefinition;
title: string;
onclick: () => void;
}
</script>

{#snippet btn({ icon, title, onclick }: BtnProps)}
<button
role="tab"
class="cursor-pointer hover:bg-[var(--pd-link-hover-bg)] w-4 h-4 rounded-full items-center justify-center flex"
title={title}
onclick={onclick}>
<Fa size="xs" icon={icon} />
</button>
{/snippet}

<div
role="tablist"
class="pb-1 border-b-[3px] whitespace-nowrap hover:cursor-pointer focus:outline-[var(--pd-tab-highlight)] px-4 flex gap-x-3 items-center"
class:border-[var(--pd-tab-highlight)]={selected}
class:border-transparent={!selected}
class:hover:border-[var(--pd-tab-hover)]={!selected}>
<Fa size="sm" icon={faFile} />
<a
href={url}
class="text-[var(--pd-tab-text)] no-underline"
class:text-[var(--pd-tab-text-highlight)]={selected}
aria-controls="open-tabs-list-{title.toLowerCase()}-panel"
id="open-tabs-list-{title.toLowerCase()}-link">
{title}
</a>
{@render btn({ icon: faPen, title: 'Rename', onclick: onEdit })}
{@render btn({ icon: faTrash, title: 'Remove', onclick: onDelete })}
</div>
63 changes: 63 additions & 0 deletions packages/frontend/src/lib/templates/TemplateCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script lang="ts">
import Fa from 'svelte-fa';
import { faFileImport } from '@fortawesome/free-solid-svg-icons/faFileImport';
import type { Template } from '/@shared/src/models/template';

interface Props {
template: Template;
onImport: () => void;
}

let { template, onImport }: Props = $props();
</script>

<div role="listitem" aria-label={template.name}>
<div
class="bg-[var(--pd-content-card-bg)] hover:bg-[var(--pd-content-card-hover-bg)] grow p-4 rounded-md flex-nowrap flex flex-col"
role="region"
aria-label={template.name}>
<!-- body -->
<div class="flex flex-col">
<div class="flex flex-row text-base">
<!-- left column -->
<div class="flex flex-col grow">
<span
class="text-[var(--pd-content-card-header-text)]"
role="heading"
aria-level="1"
aria-label={template.name}>{template.name}</span>
<span class="text-sm text-[var(--pd-content-card-text)]" aria-label="description"
>{template.description}</span>
</div>

<div class="flex flex-col">
<!-- Import -->
<button
aria-label="import"
title="Import"
onclick={onImport}
class="justify-center relative rounded-xs text-[var(--pd-button-secondary)] hover:text-[var(--pd-button-text)] text-center cursor-pointer flex flex-row">
<div class="flex flex-row items-center text-[var(--pd-link)]">
<Fa class="mr-2" icon={faFileImport} />
<span> Import </span>
</div>
</button>
</div>
</div>

<!-- files -->
<div class="flex flex-col gap-2 py-2">
<span class="text-[var(--pd-content-card-header-text)]">Files</span>
<!-- List of files -->
<div class="flex flex-row">
<div
class="flex flex-col bg-[var(--pd-label-bg)] text-[var(--pd-label-text)] max-w-full rounded-md p-2 mb-2 w-full h-min text-sm text-nowrap">
{#each template.files as file (file.name)}
<span>{file.name}</span>
{/each}
</div>
</div>
</div>
</div>
</div>
</div>
24 changes: 24 additions & 0 deletions packages/frontend/src/lib/templates/TemplateGrid.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import TemplateCard from '/@/lib/templates/TemplateCard.svelte';
import type { Template } from '/@shared/src/models/template';
import { onMount } from 'svelte';
import { quadletAPI } from '/@/api/client';

interface Props {
onImport: (template: Template) => void;
}

let { onImport }: Props = $props();

let templates: Array<Template> = $state([]);

onMount(async () => {
templates = await quadletAPI.templates();
});
</script>

<div role="list" aria-label="templates" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-4">
{#each templates as template (template.name)}
<TemplateCard template={template} onImport={onImport.bind(undefined, template)} />
{/each}
</div>
Loading
Loading