mirror of
https://github.com/AlchemillaHQ/Sylve.git
synced 2026-06-14 00:46:34 +03:00
jail: refactor creation from template into separate component
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<script lang="ts">
|
||||
import { createJailFromTemplate } from '$lib/api/jail/jail';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { reload } from '$lib/stores/api.svelte';
|
||||
import { watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
templateId: number;
|
||||
templateLabel: string;
|
||||
sourceCtId?: number;
|
||||
hostname?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
templateId,
|
||||
templateLabel,
|
||||
sourceCtId = 0,
|
||||
hostname
|
||||
}: Props = $props();
|
||||
|
||||
let createMode = $state<'single' | 'multiple'>('single');
|
||||
let singleCTID = $state(sourceCtId || 0);
|
||||
let singleName = $state('');
|
||||
let multipleStartCTID = $state(sourceCtId || 0);
|
||||
let multipleCount = $state(1);
|
||||
let multipleNamePrefix = $state('');
|
||||
let actionLoading = $state(false);
|
||||
|
||||
function normalizeTemplateName(label: string): string {
|
||||
return label.replace(/\s*\((?:CT\s*)?\d+\)\s*$/i, '').trim();
|
||||
}
|
||||
|
||||
let templateName = $derived.by(() => {
|
||||
const cleaned = normalizeTemplateName(templateLabel);
|
||||
return cleaned || `Template ${templateId}`;
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
createMode = 'single';
|
||||
singleCTID = sourceCtId || 0;
|
||||
singleName = '';
|
||||
multipleStartCTID = sourceCtId || 0;
|
||||
multipleCount = 1;
|
||||
multipleNamePrefix = templateName;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => open,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function create() {
|
||||
actionLoading = true;
|
||||
|
||||
const result =
|
||||
createMode === 'single'
|
||||
? await createJailFromTemplate(
|
||||
templateId,
|
||||
{
|
||||
mode: 'single',
|
||||
ctid: Number(singleCTID),
|
||||
name: singleName || undefined
|
||||
},
|
||||
hostname
|
||||
)
|
||||
: await createJailFromTemplate(
|
||||
templateId,
|
||||
{
|
||||
mode: 'multiple',
|
||||
startCtid: Number(multipleStartCTID),
|
||||
count: Number(multipleCount),
|
||||
namePrefix: multipleNamePrefix || undefined
|
||||
},
|
||||
hostname
|
||||
);
|
||||
|
||||
actionLoading = false;
|
||||
|
||||
if (result.error) {
|
||||
toast.error('Failed to create jail from template', { position: 'bottom-center' });
|
||||
return;
|
||||
}
|
||||
|
||||
open = false;
|
||||
reload.leftPanel = true;
|
||||
toast.success('Create jail request queued', { position: 'bottom-center' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-lg">
|
||||
<Dialog.Header class="p-0">
|
||||
<Dialog.Title>Create Jail - Template {templateName}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="grid gap-4 py-2">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={createMode === 'single' ? 'default' : 'outline'}
|
||||
onclick={() => (createMode = 'single')}>Single</Button
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={createMode === 'multiple' ? 'default' : 'outline'}
|
||||
onclick={() => (createMode = 'multiple')}>Multiple</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if createMode === 'single'}
|
||||
<div class="grid gap-2">
|
||||
<Label for={`single-ctid-${templateId}`}>CTID</Label>
|
||||
<Input id={`single-ctid-${templateId}`} type="number" min="1" bind:value={singleCTID} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for={`single-name-${templateId}`}>Name (optional)</Label>
|
||||
<Input id={`single-name-${templateId}`} bind:value={singleName} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
<Label for={`multi-start-${templateId}`}>Starting CTID</Label>
|
||||
<Input
|
||||
id={`multi-start-${templateId}`}
|
||||
type="number"
|
||||
min="1"
|
||||
bind:value={multipleStartCTID}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for={`multi-count-${templateId}`}>Count</Label>
|
||||
<Input id={`multi-count-${templateId}`} type="number" min="1" bind:value={multipleCount} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for={`multi-prefix-${templateId}`}>Name Prefix</Label>
|
||||
<Input id={`multi-prefix-${templateId}`} bind:value={multipleNamePrefix} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button size="sm" disabled={actionLoading} onclick={() => void create()}>Create Jail</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { storage } from '$lib';
|
||||
import { getNodes } from '$lib/api/cluster/cluster';
|
||||
import { getAuditRecords } from '$lib/api/info/audit';
|
||||
import { getSimpleJails, getSimpleJailTemplates } from '$lib/api/jail/jail';
|
||||
import { getActiveLifecycleTasks } from '$lib/api/task/lifecycle';
|
||||
import { getSimpleVMs } from '$lib/api/vm/vm';
|
||||
import SimpleSelect from '$lib/components/custom/SimpleSelect.svelte';
|
||||
@@ -9,6 +10,7 @@
|
||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||
import { reload } from '$lib/stores/api.svelte';
|
||||
import type { ClusterNode } from '$lib/types/cluster/cluster';
|
||||
import type { SimpleJail, SimpleJailTemplate } from '$lib/types/jail/jail';
|
||||
import type { LifecycleTask } from '$lib/types/task/lifecycle';
|
||||
import { updateCache } from '$lib/utils/http';
|
||||
import { convertDbTime } from '$lib/utils/time';
|
||||
@@ -71,14 +73,38 @@
|
||||
);
|
||||
|
||||
const simpleVmList = resource(
|
||||
() => 'simple-vm-list',
|
||||
() => `simple-vm-list-${effectiveHostname || 'default'}`,
|
||||
async (key, prevKey, { signal }) => {
|
||||
const results = await getSimpleVMs();
|
||||
const results = await getSimpleVMs(effectiveHostname || undefined);
|
||||
updateCache(key, results);
|
||||
return results;
|
||||
}
|
||||
);
|
||||
|
||||
const simpleJails = resource(
|
||||
() => `simple-jail-list-${effectiveHostname || 'default'}`,
|
||||
async (key, prevKey, { signal }) => {
|
||||
const results = await getSimpleJails(effectiveHostname || undefined);
|
||||
updateCache(key, results);
|
||||
return results;
|
||||
},
|
||||
{
|
||||
initialValue: [] as SimpleJail[]
|
||||
}
|
||||
);
|
||||
|
||||
const simpleJailTemplates = resource(
|
||||
() => `simple-jail-template-list-${effectiveHostname || 'default'}`,
|
||||
async (key, prevKey, { signal }) => {
|
||||
const results = await getSimpleJailTemplates(effectiveHostname || undefined);
|
||||
updateCache(key, results);
|
||||
return results;
|
||||
},
|
||||
{
|
||||
initialValue: [] as SimpleJailTemplate[]
|
||||
}
|
||||
);
|
||||
|
||||
const activeLifecycleTasks = resource(
|
||||
() => `active-lifecycle-tasks-${effectiveHostname || 'default'}`,
|
||||
async () => {
|
||||
@@ -166,8 +192,8 @@
|
||||
'/api/jail/network/disinheritance': 'Jail - Network Disinherit',
|
||||
'/api/jail/network': 'Jail Network',
|
||||
'/api/jail/description': 'Jail - Update Description',
|
||||
'/api/jail/templates/convert': 'Jail Template - Convert',
|
||||
'/api/jail/templates/create': 'Jail Template - Create Jail',
|
||||
'/api/jail/templates/convert': 'Create Jail Template - From Jail',
|
||||
'/api/jail/templates/create': 'Create Jail - Template',
|
||||
'/api/jail/templates': 'Jail Template',
|
||||
'/api/jail': 'Jail',
|
||||
'/api/utilities/cloud-init/templates': 'Cloud Init Template',
|
||||
@@ -248,6 +274,15 @@
|
||||
|
||||
let activeLifecycleCount = $derived(activeLifecycleTasks.current.length);
|
||||
let lifecycleActive = $derived(activeLifecycleCount > 0);
|
||||
let vmNameById = $derived.by(() => {
|
||||
return new Map((simpleVmList.current || []).map((vm) => [vm.rid, vm.name]));
|
||||
});
|
||||
let jailNameByCtId = $derived.by(() => {
|
||||
return new Map((simpleJails.current || []).map((jail) => [jail.ctId, jail.name]));
|
||||
});
|
||||
let templateNameById = $derived.by(() => {
|
||||
return new Map((simpleJailTemplates.current || []).map((template) => [template.id, template.name]));
|
||||
});
|
||||
|
||||
watch(
|
||||
() => lifecycleActive,
|
||||
@@ -256,14 +291,67 @@
|
||||
}
|
||||
);
|
||||
|
||||
function toTitleCase(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function lifecycleActionLabel(action: string): string {
|
||||
return toTitleCase(action.replace(/[_-]+/g, ' ')) || 'Working';
|
||||
}
|
||||
|
||||
function lifecycleStatusLabel(status: LifecycleTask['status']): string {
|
||||
switch (status) {
|
||||
case 'queued':
|
||||
return 'Queued';
|
||||
case 'running':
|
||||
return 'Running';
|
||||
case 'success':
|
||||
return 'Success';
|
||||
case 'failed':
|
||||
return 'Failed';
|
||||
default:
|
||||
return toTitleCase(status);
|
||||
}
|
||||
}
|
||||
|
||||
function lifecycleGuestLabel(task: LifecycleTask): string {
|
||||
const prefix =
|
||||
task.guestType === 'vm'
|
||||
? 'VM'
|
||||
: task.guestType === 'jail-template'
|
||||
? 'Jail Template'
|
||||
: 'Jail';
|
||||
return `${prefix} ${task.guestId}`;
|
||||
if (task.guestType === 'vm') {
|
||||
const name = vmNameById.get(task.guestId);
|
||||
return name ? `VM ${name} (${task.guestId})` : `VM ${task.guestId}`;
|
||||
}
|
||||
|
||||
if (task.guestType === 'jail-template') {
|
||||
const templateName = templateNameById.get(task.guestId);
|
||||
return templateName
|
||||
? `Template ${templateName} (${task.guestId})`
|
||||
: `Jail Template ${task.guestId}`;
|
||||
}
|
||||
|
||||
const jailName = jailNameByCtId.get(task.guestId);
|
||||
return jailName ? `Jail ${jailName} (${task.guestId})` : `Jail ${task.guestId}`;
|
||||
}
|
||||
|
||||
function lifecycleTaskLabel(task: LifecycleTask): string {
|
||||
if (task.source.includes('jail/templates/create')) {
|
||||
const templateName = templateNameById.get(task.guestId);
|
||||
return templateName
|
||||
? `Create Jail - Template ${templateName}`
|
||||
: `Create Jail - Template ${task.guestId}`;
|
||||
}
|
||||
|
||||
if (task.source.includes('jail/templates/convert')) {
|
||||
const jailName = jailNameByCtId.get(task.guestId);
|
||||
return jailName
|
||||
? `Create Jail Template - ${jailName} (Jail CTID ${task.guestId})`
|
||||
: `Create Jail Template - Jail CTID ${task.guestId}`;
|
||||
}
|
||||
|
||||
return `${lifecycleActionLabel(task.action)} - ${lifecycleGuestLabel(task)}`;
|
||||
}
|
||||
|
||||
export function formatStatus(status: string): string {
|
||||
@@ -294,13 +382,12 @@
|
||||
<span class="inline-flex items-center gap-1 font-medium">
|
||||
<span class="icon-[mdi--loading] h-3.5 w-3.5 animate-spin"></span>
|
||||
{activeLifecycleCount}
|
||||
lifecycle action{activeLifecycleCount === 1 ? '' : 's'} in progress
|
||||
active lifecycle task{activeLifecycleCount === 1 ? '' : 's'}
|
||||
</span>
|
||||
|
||||
{#each activeLifecycleTasks.current as task (task.id)}
|
||||
<span class="bg-background rounded border px-2 py-0.5">
|
||||
{lifecycleGuestLabel(task)}
|
||||
{task.action} ({task.status})
|
||||
{lifecycleTaskLabel(task)} ({lifecycleStatusLabel(task.status)})
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
convertJailToTemplate,
|
||||
createJailFromTemplate,
|
||||
deleteJailTemplate,
|
||||
jailAction
|
||||
} from '$lib/api/jail/jail';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import CreateFromTemplate from '$lib/components/custom/Jail/CreateFromTemplate.svelte';
|
||||
import { actionVm } from '$lib/api/vm/vm';
|
||||
import * as ContextMenu from '$lib/components/ui/context-menu/index.js';
|
||||
import { reload } from '$lib/stores/api.svelte';
|
||||
@@ -77,13 +73,6 @@
|
||||
return segments[segments.length - 1];
|
||||
});
|
||||
let createFromTemplateOpen = $state(false);
|
||||
let createMode = $state<'single' | 'multiple'>('single');
|
||||
let singleCTID = $state(item.sourceCtId || 0);
|
||||
let singleName = $state('');
|
||||
let multipleStartCTID = $state(item.sourceCtId || 0);
|
||||
let multipleCount = $state(1);
|
||||
let multipleNamePrefix = $state(item.label.replace(/\s*\(CT\s*\d+\)$/i, ''));
|
||||
let actionLoading = $state(false);
|
||||
|
||||
const handleActionClick = async (action: 'start' | 'reboot' | 'shutdown' | 'stop') => {
|
||||
if (item.resourceId === undefined || item.resourceType === undefined) {
|
||||
@@ -109,9 +98,7 @@
|
||||
|
||||
const handleConvertToTemplate = async () => {
|
||||
if (!item.resourceId) return;
|
||||
actionLoading = true;
|
||||
const result = await convertJailToTemplate(item.resourceId, item.nodeHostname);
|
||||
actionLoading = false;
|
||||
if (result.error) {
|
||||
toast.error('Failed to convert jail to template', { position: 'bottom-center' });
|
||||
return;
|
||||
@@ -123,9 +110,7 @@
|
||||
const handleDeleteTemplate = async () => {
|
||||
if (!item.resourceId) return;
|
||||
if (!confirm(`Delete template "${item.label}"?`)) return;
|
||||
actionLoading = true;
|
||||
const result = await deleteJailTemplate(item.resourceId, item.nodeHostname);
|
||||
actionLoading = false;
|
||||
if (result.error) {
|
||||
toast.error('Failed to delete template', { position: 'bottom-center' });
|
||||
return;
|
||||
@@ -134,39 +119,6 @@
|
||||
toast.success('Template deleted', { position: 'bottom-center' });
|
||||
};
|
||||
|
||||
const handleCreateFromTemplate = async () => {
|
||||
if (!item.resourceId) return;
|
||||
actionLoading = true;
|
||||
const result =
|
||||
createMode === 'single'
|
||||
? await createJailFromTemplate(
|
||||
item.resourceId,
|
||||
{
|
||||
mode: 'single',
|
||||
ctid: Number(singleCTID),
|
||||
name: singleName || undefined
|
||||
},
|
||||
item.nodeHostname
|
||||
)
|
||||
: await createJailFromTemplate(
|
||||
item.resourceId,
|
||||
{
|
||||
mode: 'multiple',
|
||||
startCtid: Number(multipleStartCTID),
|
||||
count: Number(multipleCount),
|
||||
namePrefix: multipleNamePrefix || undefined
|
||||
},
|
||||
item.nodeHostname
|
||||
);
|
||||
actionLoading = false;
|
||||
if (result.error) {
|
||||
toast.error('Failed to create jail from template', { position: 'bottom-center' });
|
||||
return;
|
||||
}
|
||||
createFromTemplateOpen = false;
|
||||
reload.leftPanel = true;
|
||||
toast.success('Template restore job queued', { position: 'bottom-center' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<li class="w-full">
|
||||
@@ -313,62 +265,12 @@
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if item.resourceType === 'jail-template'}
|
||||
<Dialog.Root bind:open={createFromTemplateOpen}>
|
||||
<Dialog.Content class="max-w-lg">
|
||||
<Dialog.Header class="p-0">
|
||||
<Dialog.Title>Create Jail From Template</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="grid gap-4 py-2">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={createMode === 'single' ? 'default' : 'outline'}
|
||||
onclick={() => (createMode = 'single')}>Single</Button
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={createMode === 'multiple' ? 'default' : 'outline'}
|
||||
onclick={() => (createMode = 'multiple')}>Multiple</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if createMode === 'single'}
|
||||
<div class="grid gap-2">
|
||||
<Label for={`single-ctid-${item.id}`}>CTID</Label>
|
||||
<Input id={`single-ctid-${item.id}`} type="number" min="1" bind:value={singleCTID} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for={`single-name-${item.id}`}>Name (optional)</Label>
|
||||
<Input id={`single-name-${item.id}`} bind:value={singleName} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-2">
|
||||
<Label for={`multi-start-${item.id}`}>Starting CTID</Label>
|
||||
<Input
|
||||
id={`multi-start-${item.id}`}
|
||||
type="number"
|
||||
min="1"
|
||||
bind:value={multipleStartCTID}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for={`multi-count-${item.id}`}>Count</Label>
|
||||
<Input id={`multi-count-${item.id}`} type="number" min="1" bind:value={multipleCount} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for={`multi-prefix-${item.id}`}>Name Prefix</Label>
|
||||
<Input id={`multi-prefix-${item.id}`} bind:value={multipleNamePrefix} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={actionLoading}
|
||||
onclick={() => void handleCreateFromTemplate()}>Create Jail</Button
|
||||
>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
{#if item.resourceType === 'jail-template' && item.resourceId}
|
||||
<CreateFromTemplate
|
||||
bind:open={createFromTemplateOpen}
|
||||
templateId={item.resourceId}
|
||||
templateLabel={item.label}
|
||||
sourceCtId={item.sourceCtId}
|
||||
hostname={item.nodeHostname}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user