From 94144fedf7df87e86f26ce1c98ef6530b3bc030e Mon Sep 17 00:00:00 2001 From: Hayzam Sherif Date: Tue, 1 Apr 2025 21:56:48 +0400 Subject: [PATCH] ui: zfs: fix alignments, add warning icon, docs: add demo video --- README.md | 2 + web/src/app.css | 104 ++-- web/src/lib/utils/disk.ts | 202 ++++---- web/src/lib/utils/dnd.ts | 190 +++---- .../routes/[node]/storage/zfs/+page.svelte | 483 ++++++++++-------- 5 files changed, 520 insertions(+), 461 deletions(-) diff --git a/README.md b/README.md index 731b805a..9fe72add 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ > [!WARNING] > This project is still in development so expect breaking changes! +https://github.com/user-attachments/assets/bf727ef4-4316-4084-a61f-cb8ec978e43d + Sylve aims to be a lightweight, open-source virtualization platform for FreeBSD, leveraging Bhyve for VMs and Jails for containerization, with deep ZFS integration. It seeks to provide a streamlined, Proxmox-like experience tailored for FreeBSD environments. It's backend is written in Go and the frontend is written in Svelte (with Kit). # Requirements diff --git a/web/src/app.css b/web/src/app.css index 859e67e8..650f0306 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -3,58 +3,62 @@ @tailwind utilities; @layer base { - :root { - --background: 0 0% 100%; - --foreground: 20 14.3% 4.1%; - --card: 0 0% 100%; - --card-foreground: 20 14.3% 4.1%; - --popover: 0 0% 100%; - --popover-foreground: 20 14.3% 4.1%; - --primary: 24 9.8% 10%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 60 4.8% 95.9%; - --secondary-foreground: 24 9.8% 10%; - --muted: 60 4.8% 95.9%; - --muted-foreground: 25 5.3% 44.7%; - --accent: 60 4.8% 95.9%; - --accent-foreground: 24 9.8% 10%; - --destructive: 0 72.22% 50.59%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 20 5.9% 90%; - --input: 20 5.9% 90%; - --ring: 20 14.3% 4.1%; - --radius: 0.3rem; - } + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + --primary: 24 9.8% 10%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 72.22% 50.59%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 20 14.3% 4.1%; + --radius: 0.3rem; + } - .dark { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 60 9.1% 97.8%; - --primary-foreground: 24 9.8% 10%; - --secondary: 12 6.5% 15.1%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 15.1%; - --ring: 24 5.7% 82.9%; - } + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 60 9.1% 97.8%; + --primary-foreground: 24 9.8% 10%; + --secondary: 12 6.5% 15.1%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 15.1%; + --input: 12 6.5% 15.1%; + --ring: 24 5.7% 82.9%; + } } @layer base { - * { - @apply border-border; - } + * { + @apply border-border; + } - body { - @apply bg-background text-foreground; - } -} \ No newline at end of file + body { + @apply bg-background text-foreground; + } + + .droppable { + @apply border-2 border-neutral-300 dark:border-neutral-800; + } +} diff --git a/web/src/lib/utils/disk.ts b/web/src/lib/utils/disk.ts index b31f9ac4..c458ae2b 100644 --- a/web/src/lib/utils/disk.ts +++ b/web/src/lib/utils/disk.ts @@ -12,122 +12,124 @@ import type { Disk, DiskInfo, SmartAttribute, SmartCtl, SmartNVME } from '$lib/t import type { Zpool } from '$lib/types/zfs/pool'; export async function simplifyDisks(disks: DiskInfo): Promise { - const transformed: Disk[] = []; - for (const disk of disks) { - if (disk.size > 0) { - const o = { - Device: `/dev/${disk.device}`, - Type: disk.type, - Usage: disk.usage, - Size: disk.size, - GPT: disk.gpt, - Model: disk.model, - Serial: disk.serial, - Wearout: - typeof disk.wearOut === 'number' && !isNaN(disk.wearOut) ? `-` : `${disk.wearOut} %`, - 'S.M.A.R.T.': 'Passed', - Partitions: disk.partitions, - SmartData: disk.smartData ?? null - }; + const transformed: Disk[] = []; + for (const disk of disks) { + if (disk.size > 0) { + const o = { + Device: `/dev/${disk.device}`, + Type: disk.type, + Usage: disk.usage, + Size: disk.size, + GPT: disk.gpt, + Model: disk.model, + Serial: disk.serial, + Wearout: + typeof disk.wearOut === 'number' && !isNaN(disk.wearOut) ? `-` : `${disk.wearOut} %`, + 'S.M.A.R.T.': 'Passed', + Partitions: disk.partitions, + SmartData: disk.smartData ?? null + }; - o.Partitions.sort((a, b) => a.size - b.size); - transformed.push(o); - } - } + o.Partitions.sort((a, b) => a.size - b.size); + transformed.push(o); + } + } - transformed.sort((a, b) => { - if (a.Usage === 'Partitions' && b.Usage !== 'Partitions') { - return -1; - } - if (a.Usage !== 'Partitions' && b.Usage === 'Partitions') { - return 1; - } - return 0; - }); + transformed.sort((a, b) => { + if (a.Usage === 'Partitions' && b.Usage !== 'Partitions') { + return -1; + } + if (a.Usage !== 'Partitions' && b.Usage === 'Partitions') { + return 1; + } + return 0; + }); - return transformed; + return transformed; } export function parseSMART(disk: Disk): SmartAttribute | SmartAttribute[] { - if (disk.Type === 'NVMe') { - return { - 'Available Spare': (disk.SmartData as SmartNVME).availableSpare, - 'Available Spare Threshold': (disk.SmartData as SmartNVME).availableSpareThreshold, - 'Controller Busy Time': (disk.SmartData as SmartNVME).controllerBusyTime, - 'Critical Warning': (disk.SmartData as SmartNVME).criticalWarning, - 'Critical Warning State': { - 'Available Spare': (disk.SmartData as SmartNVME).criticalWarningState.availableSpare, - 'Device Reliability': (disk.SmartData as SmartNVME).criticalWarningState.deviceReliability, - 'Read Only': (disk.SmartData as SmartNVME).criticalWarningState.readOnly, - Temperature: (disk.SmartData as SmartNVME).criticalWarningState.temperature, - 'Volatile Memory Backup': (disk.SmartData as SmartNVME).criticalWarningState - .volatileMemoryBackup - }, - 'Data Units Read': (disk.SmartData as SmartNVME).dataUnitsRead, - 'Data Units Written': (disk.SmartData as SmartNVME).dataUnitsWritten, - 'Error Info Log Entries': (disk.SmartData as SmartNVME).errorInfoLogEntries, - 'Host Read Commands': (disk.SmartData as SmartNVME).hostReadCommands, - 'Host Write Commands': (disk.SmartData as SmartNVME).hostWriteCommands, - 'Media Errors': (disk.SmartData as SmartNVME).mediaErrors, - 'Percentage Used': (disk.SmartData as SmartNVME).percentageUsed, - 'Power Cycles': (disk.SmartData as SmartNVME).powerCycles, - 'Power On Hours': (disk.SmartData as SmartNVME).powerOnHours, - Temperature: (disk.SmartData as SmartNVME).temperature, - 'Temperature 1 Transition Count': (disk.SmartData as SmartNVME).temperature1TransitionCnt, - 'Temperature 2 Transition Count': (disk.SmartData as SmartNVME).temperature2TransitionCnt, - 'Total Time For Temperature 1': (disk.SmartData as SmartNVME).totalTimeForTemperature1, - 'Total Time For Temperature 2': (disk.SmartData as SmartNVME).totalTimeForTemperature2, - 'Unsafe Shutdowns': (disk.SmartData as SmartNVME).unsafeShutdowns, - 'Warning Composite Temp Time': (disk.SmartData as SmartNVME).warningCompositeTempTime - }; - } else if (disk.Type === 'HDD' || disk.Type === 'SSD') { - const data = disk.SmartData as SmartCtl; - const attributes: SmartAttribute[] = []; + if (disk.Type === 'NVMe') { + return { + 'Available Spare': (disk.SmartData as SmartNVME).availableSpare, + 'Available Spare Threshold': (disk.SmartData as SmartNVME).availableSpareThreshold, + 'Controller Busy Time': (disk.SmartData as SmartNVME).controllerBusyTime, + 'Critical Warning': (disk.SmartData as SmartNVME).criticalWarning, + 'Critical Warning State': { + 'Available Spare': (disk.SmartData as SmartNVME).criticalWarningState.availableSpare, + 'Device Reliability': (disk.SmartData as SmartNVME).criticalWarningState.deviceReliability, + 'Read Only': (disk.SmartData as SmartNVME).criticalWarningState.readOnly, + Temperature: (disk.SmartData as SmartNVME).criticalWarningState.temperature, + 'Volatile Memory Backup': (disk.SmartData as SmartNVME).criticalWarningState + .volatileMemoryBackup + }, + 'Data Units Read': (disk.SmartData as SmartNVME).dataUnitsRead, + 'Data Units Written': (disk.SmartData as SmartNVME).dataUnitsWritten, + 'Error Info Log Entries': (disk.SmartData as SmartNVME).errorInfoLogEntries, + 'Host Read Commands': (disk.SmartData as SmartNVME).hostReadCommands, + 'Host Write Commands': (disk.SmartData as SmartNVME).hostWriteCommands, + 'Media Errors': (disk.SmartData as SmartNVME).mediaErrors, + 'Percentage Used': (disk.SmartData as SmartNVME).percentageUsed, + 'Power Cycles': (disk.SmartData as SmartNVME).powerCycles, + 'Power On Hours': (disk.SmartData as SmartNVME).powerOnHours, + Temperature: (disk.SmartData as SmartNVME).temperature, + 'Temperature 1 Transition Count': (disk.SmartData as SmartNVME).temperature1TransitionCnt, + 'Temperature 2 Transition Count': (disk.SmartData as SmartNVME).temperature2TransitionCnt, + 'Total Time For Temperature 1': (disk.SmartData as SmartNVME).totalTimeForTemperature1, + 'Total Time For Temperature 2': (disk.SmartData as SmartNVME).totalTimeForTemperature2, + 'Unsafe Shutdowns': (disk.SmartData as SmartNVME).unsafeShutdowns, + 'Warning Composite Temp Time': (disk.SmartData as SmartNVME).warningCompositeTempTime + }; + } else if (disk.Type === 'HDD' || disk.Type === 'SSD') { + const data = disk.SmartData as SmartCtl; + const attributes: SmartAttribute[] = []; - if (data?.ata_smart_attributes?.table?.length) { - for (const element of data.ata_smart_attributes.table) { - attributes.push({ - ID: element.id, - Name: element.name, - Value: element.value, - Worst: element.worst, - Threshold: element.thresh, - Flags: element.flags.string, - Failing: element.when_failed || '-' - }); - } - } + if (data?.ata_smart_attributes?.table?.length) { + for (const element of data.ata_smart_attributes.table) { + attributes.push({ + ID: element.id, + Name: element.name, + Value: element.value, + Worst: element.worst, + Threshold: element.thresh, + Flags: element.flags.string, + Failing: element.when_failed || '-' + }); + } + } - if (attributes.length > 0) { - return attributes; - } - } + if (attributes.length > 0) { + return attributes; + } + } - return {}; + return {}; } export function diskSpaceAvailable(disk: Disk, required: number): boolean { - if (disk.Usage === 'Partitions') { - const total = disk.Size; - const used = disk.Partitions.reduce((acc, cur) => acc + cur.size, 0); - return total - used >= required; - } + if (disk.Usage === 'Partitions') { + const total = disk.Size; + const used = disk.Partitions.reduce((acc, cur) => acc + cur.size, 0); + return total - used >= required; + } - return disk.Size >= required; + return disk.Size >= required; } export function getGPTLabel(disk: Disk, pools: Zpool[]): string { - if (disk.GPT) { - return 'Yes'; - } + if (disk) { + if (disk.GPT) { + return 'Yes'; + } - if (disk.GPT === false) { - if (pools.length > 0) { - if (pools.some((pool) => pool.vdevs.some((vdev) => vdev.name === disk.Device))) { - return '-'; - } - } - } + if (disk.GPT === false) { + if (pools.length > 0) { + if (pools.some((pool) => pool.vdevs.some((vdev) => vdev.name === disk.Device))) { + return '-'; + } + } + } + } - return 'No'; + return 'No'; } diff --git a/web/src/lib/utils/dnd.ts b/web/src/lib/utils/dnd.ts index 2d7c0c6c..78d2f59b 100644 --- a/web/src/lib/utils/dnd.ts +++ b/web/src/lib/utils/dnd.ts @@ -9,122 +9,122 @@ */ export function draggable(node: HTMLElement, data: string) { - let state = data; + let state = data; - node.draggable = true; - node.style.cursor = 'grab'; + node.draggable = true; + node.style.cursor = 'grab'; - function handle_dragstart(e: DragEvent) { - if (!e.dataTransfer) return; - //const dataToTransfer = typeof state === 'string' ? state : state.toString(); - e.dataTransfer.setData('application/disk', state); - e.dataTransfer.setData('text/plain', state); - // Add this to make the drag image transparent if desired - // setTimeout(() => { - // node.style.opacity = '0.4'; - // }, 0); - } + function handle_dragstart(e: DragEvent) { + if (!e.dataTransfer) return; + //const dataToTransfer = typeof state === 'string' ? state : state.toString(); + e.dataTransfer.setData('application/disk', state); + e.dataTransfer.setData('text/plain', state); + // Add this to make the drag image transparent if desired + // setTimeout(() => { + // node.style.opacity = '0.4'; + // }, 0); + } - function handle_dragend(e: DragEvent) { - // Reset any visual changes - // node.style.opacity = '1'; - } + function handle_dragend(e: DragEvent) { + // Reset any visual changes + // node.style.opacity = '1'; + } - node.addEventListener('dragstart', handle_dragstart); - node.addEventListener('dragend', handle_dragend); + node.addEventListener('dragstart', handle_dragstart); + node.addEventListener('dragend', handle_dragend); - return { - update(data: string) { - state = data; - }, + return { + update(data: string) { + state = data; + }, - destroy() { - node.removeEventListener('dragstart', handle_dragstart); - node.removeEventListener('dragend', handle_dragend); - } - }; + destroy() { + node.removeEventListener('dragstart', handle_dragstart); + node.removeEventListener('dragend', handle_dragend); + } + }; } export function dropzone( - node: HTMLElement, - options: { - dropEffect?: 'move' | 'none' | 'copy' | 'link'; - dragover_class?: string; - on_dropzone?: (data: string, e: DragEvent) => void; - } + node: HTMLElement, + options: { + dropEffect?: 'move' | 'none' | 'copy' | 'link'; + dragover_class?: string; + on_dropzone?: (data: string, e: DragEvent) => void; + } ) { - let state = { - dropEffect: 'move' as 'move' | 'none' | 'copy' | 'link', - dragover_class: 'droppable', - ...options - }; + let state = { + dropEffect: 'move' as 'move' | 'none' | 'copy' | 'link', + dragover_class: 'droppable', + ...options + }; - let dragCounter = 0; + let dragCounter = 0; - function handle_dragenter(e: DragEvent) { - e.preventDefault(); - dragCounter++; + function handle_dragenter(e: DragEvent) { + e.preventDefault(); + dragCounter++; - if (dragCounter === 1) { - node.classList.add(state.dragover_class); - } - } + if (dragCounter === 1) { + node.classList.add(state.dragover_class); + } + } - function handle_dragleave(e: DragEvent) { - dragCounter--; + function handle_dragleave(e: DragEvent) { + dragCounter--; - if (dragCounter === 0) { - // Only remove class when fully leaving - node.classList.remove(state.dragover_class); - console.log('Removing dropzone class from', node); - } - } + if (dragCounter === 0) { + // Only remove class when fully leaving + node.classList.remove(state.dragover_class); + console.log('Removing dropzone class from', node); + } + } - function handle_dragover(e: DragEvent) { - e.preventDefault(); - if (!e.dataTransfer) return; - e.dataTransfer.dropEffect = state.dropEffect; - } + function handle_dragover(e: DragEvent) { + e.preventDefault(); + if (!e.dataTransfer) return; + e.dataTransfer.dropEffect = state.dropEffect; + } - function handle_drop(e: DragEvent) { - e.preventDefault(); - e.stopPropagation(); + function handle_drop(e: DragEvent) { + e.preventDefault(); + e.stopPropagation(); - dragCounter = 0; - node.classList.remove(state.dragover_class); + dragCounter = 0; + node.classList.remove(state.dragover_class); - if (!e.dataTransfer) return; - const data = e.dataTransfer.getData('text/plain'); - console.log('Dropped data:', data); + if (!e.dataTransfer) return; + const data = e.dataTransfer.getData('text/plain'); + console.log('Dropped data:', data); - if (typeof state.on_dropzone === 'function') { - state.on_dropzone(data, e); - } - } + if (typeof state.on_dropzone === 'function') { + state.on_dropzone(data, e); + } + } - node.addEventListener('dragenter', handle_dragenter); - node.addEventListener('dragleave', handle_dragleave); - node.addEventListener('dragover', handle_dragover); - node.addEventListener('drop', handle_drop); + node.addEventListener('dragenter', handle_dragenter); + node.addEventListener('dragleave', handle_dragleave); + node.addEventListener('dragover', handle_dragover); + node.addEventListener('drop', handle_drop); - return { - update(options: { - dropEffect?: 'move' | 'none' | 'copy' | 'link'; - dragover_class?: string; - on_dropzone?: (data: string, e: DragEvent) => void; - }) { - state = { - dropEffect: 'move', - dragover_class: 'droppable', - ...options - }; - }, + return { + update(options: { + dropEffect?: 'move' | 'none' | 'copy' | 'link'; + dragover_class?: string; + on_dropzone?: (data: string, e: DragEvent) => void; + }) { + state = { + dropEffect: 'move', + dragover_class: 'droppable', + ...options + }; + }, - destroy() { - node.removeEventListener('dragenter', handle_dragenter); - node.removeEventListener('dragleave', handle_dragleave); - node.removeEventListener('dragover', handle_dragover); - node.removeEventListener('drop', handle_drop); - } - }; + destroy() { + node.removeEventListener('dragenter', handle_dragenter); + node.removeEventListener('dragleave', handle_dragleave); + node.removeEventListener('dragover', handle_dragover); + node.removeEventListener('drop', handle_drop); + } + }; } diff --git a/web/src/routes/[node]/storage/zfs/+page.svelte b/web/src/routes/[node]/storage/zfs/+page.svelte index 598ad098..e51fd33a 100644 --- a/web/src/routes/[node]/storage/zfs/+page.svelte +++ b/web/src/routes/[node]/storage/zfs/+page.svelte @@ -10,6 +10,7 @@ import { ScrollArea } from '$lib/components/ui/scroll-area/index.js'; import * as Select from '$lib/components/ui/select/index.js'; import * as Tabs from '$lib/components/ui/tabs/index.js'; + import * as Tooltip from '$lib/components/ui/tooltip'; import type { Disk } from '$lib/types/disk/disk'; import type { Zpool } from '$lib/types/zfs/pool'; import { simplifyDisks } from '$lib/utils/disk'; @@ -68,6 +69,8 @@ let pools = $results[1].data as Zpool[]; let advancedChecked: boolean = $state(false); + $inspect('advancedChecked', advancedChecked); + let useableDisks = $derived.by(() => { const unusedDisks: UnusedDisk[] = []; for (const disk of disks) { @@ -76,7 +79,7 @@ name: disk.Device, size: disk.Size, gpt: disk.GPT, - type: disk.Type + type: disk.Type === 'Unknown' ? 'HDD' : disk.Type }); } @@ -254,9 +257,9 @@ close()}> -
+
Create ZFS Pool @@ -269,13 +272,13 @@
- - Devices - Options + + Devices + Options - - - + + +
@@ -285,33 +288,60 @@
- - -
- -
- {#each Array(vdevCount) as _, i} - {#if diskContainers} - {#if diskContainers[i]} -
-
- handleDiskDrop(i, event) - }} - > - {#if diskContainers[i].length === 0} -
- Drop disks here + +
+ +
+ +
+ {#each Array(vdevCount) as _, i} + {#if diskContainers} + {#if diskContainers[i]} +
+ {#if diskContainers[i].length > 0} +
+ + + +

+ Lorem Ipsum is simply dummy text of the printing and + typesetting industry. Lorem Ipsum has been the industry's +

+
+
- {:else} -
- {#each diskContainers[i] as disk (disk.id)} -
- - {#if disk.type === 'SSD'} - - {:else if disk.type === 'NVMe'} - - {:else if disk.type === 'HDD'} - - {:else if disk.type === 'Partition'} - - {/if} -
- {disk.name.split('/').pop()} + {#if disk.type === 'SSD'} + + {:else if disk.type === 'NVMe'} + + {:else if disk.type === 'HDD'} + + {:else if disk.type === 'Partition'} + + {/if} +
+ {disk.name.split('/').pop()} +
+
- -
- {/each} -
- {/if} + {/each} +
+ {/if} +
+

+ VDEV {i + 1} +

-

- VDEV {i + 1} -

-
+ {/if} {/if} - {/if} - {/each} -
- + {/each} +
+ +
- -
-
- -
- -
- {#each availableDisks.filter((disk) => disk.type === 'HDD') as disk (disk.id)} -
-
- +
+ +
+
+ +
+ +
+ {#each availableDisks.filter((disk) => disk.type === 'HDD') as disk (disk.id)} +
+
+ +
+
+ {disk.name.split('/').pop()} +
+
+ {Math.round(disk.size / (1024 * 1024 * 1024))} GB +
-
- {disk.name.split('/').pop()} -
-
- {Math.round(disk.size / (1024 * 1024 * 1024))} GB -
-
- {/each} + {/each} - {#if availableDisks.filter((disk) => disk.type === 'HDD').length === 0} -
- No available disks -
- {/if} -
- + {#if availableDisks.filter((disk) => disk.type === 'HDD').length === 0} +
+ No available disks +
+ {/if} +
+ +
-
-
- -
- -
- {#each availableDisks.filter((disk) => disk.type === 'SSD') as disk (disk.id)} -
-
- +
+ +
+ +
+ {#each availableDisks.filter((disk) => disk.type === 'SSD') as disk (disk.id)} +
+
+ +
+
+ {disk.name.split('/').pop()} +
+
+ {Math.round(disk.size / (1024 * 1024 * 1024))} GB +
-
- {disk.name.split('/').pop()} -
-
- {Math.round(disk.size / (1024 * 1024 * 1024))} GB -
-
- {/each} + {/each} - {#if availableDisks.filter((disk) => disk.type === 'SSD').length === 0} -
- No available disks -
- {/if} -
- + {#if availableDisks.filter((disk) => disk.type === 'SSD').length === 0} +
+ No available disks +
+ {/if} +
+ +
-
-
- -
- -
- {#each availableDisks.filter((disk) => disk.type === 'NVMe') as disk (disk.id)} -
-
- +
+ +
+ +
+ {#each availableDisks.filter((disk) => disk.type === 'NVMe') as disk (disk.id)} +
+
+ +
+
+ {disk.name.split('/').pop()} +
+
+ {Math.round(disk.size / (1024 * 1024 * 1024))} GB +
-
- {disk.name.split('/').pop()} -
-
- {Math.round(disk.size / (1024 * 1024 * 1024))} GB -
-
- {/each} + {/each} - {#if availableDisks.filter((disk) => disk.type === 'NVMe').length === 0} -
- No available disks -
- {/if} -
- + {#if availableDisks.filter((disk) => disk.type === 'NVMe').length === 0} +
+ No available disks +
+ {/if} +
+ +
-
-
- -
- -
- {#each availableDisks.filter((disk) => disk.type === 'Partition') as disk (disk.id)} -
-
- +
+ +
+ +
+ {#each availableDisks.filter((disk) => disk.type === 'Partition') as disk (disk.id)} +
+
+ +
+
+ {disk.name.split('/').pop()} +
+
+ {Math.round(disk.size / (1024 * 1024 * 1024))} GB +
-
- {disk.name.split('/').pop()} -
-
- {Math.round(disk.size / (1024 * 1024 * 1024))} GB -
-
- {/each} + {/each} - {#if availableDisks.filter((disk) => disk.type === 'Partition').length === 0} -
- No available partitions -
- {/if} -
- + {#if availableDisks.filter((disk) => disk.type === 'Partition').length === 0} +
+ No available partitions +
+ {/if} +
+ +
@@ -510,17 +554,17 @@ - - - + + +
-
- - +
+ + - + {#each raid as fruit} - +
-
- - + +
+ + @@ -547,12 +592,13 @@ {/each} - +
-
- - + +
+ + @@ -565,19 +611,24 @@ {/each} - +
-
+ +
- +
{#if advancedChecked} @@ -605,14 +656,14 @@ onclick={() => removePair(index)} class="hover:bg-muted rounded px-2 py-1 text-white" > - + {/if}
{/each}
-
@@ -622,7 +673,7 @@ - +