diff --git a/web/src/lib/components/custom/Jail/CreateFromTemplate.svelte b/web/src/lib/components/custom/Jail/CreateFromTemplate.svelte
new file mode 100644
index 00000000..0eae3f94
--- /dev/null
+++ b/web/src/lib/components/custom/Jail/CreateFromTemplate.svelte
@@ -0,0 +1,152 @@
+
+
+
+
+
+ Create Jail - Template {templateName}
+
+
+
+
+
+
+
diff --git a/web/src/lib/components/skeleton/BottomPanel.svelte b/web/src/lib/components/skeleton/BottomPanel.svelte
index 6a0a857f..5b407306 100644
--- a/web/src/lib/components/skeleton/BottomPanel.svelte
+++ b/web/src/lib/components/skeleton/BottomPanel.svelte
@@ -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 @@
{activeLifecycleCount}
- lifecycle action{activeLifecycleCount === 1 ? '' : 's'} in progress
+ active lifecycle task{activeLifecycleCount === 1 ? '' : 's'}
{#each activeLifecycleTasks.current as task (task.id)}
- {lifecycleGuestLabel(task)}
- {task.action} ({task.status})
+ {lifecycleTaskLabel(task)} ({lifecycleStatusLabel(task.status)})
{/each}
diff --git a/web/src/lib/components/skeleton/TreeViewCluster.svelte b/web/src/lib/components/skeleton/TreeViewCluster.svelte
index 7c74c342..9e5353f8 100644
--- a/web/src/lib/components/skeleton/TreeViewCluster.svelte
+++ b/web/src/lib/components/skeleton/TreeViewCluster.svelte
@@ -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' });
- };
@@ -313,62 +265,12 @@
{/if}
-{#if item.resourceType === 'jail-template'}
-
-
-
- Create Jail From Template
-
-
-
-
-
-
-
+{#if item.resourceType === 'jail-template' && item.resourceId}
+
{/if}