ui: zfs: improvements to disks/partition selection

This commit is contained in:
hayzamjs
2025-04-02 20:02:45 +04:00
parent bda622a375
commit 556c445d72
6 changed files with 322 additions and 274 deletions
+7 -3
View File
@@ -47,7 +47,7 @@ func DestroyDisk(device string) error {
return nil
}
func CreatePartition(device string, size uint64) error {
func CreatePartition(device string, size uint64, ptype string) error {
err := CheckDevice(device)
if err != nil {
@@ -59,7 +59,11 @@ func CreatePartition(device string, size uint64) error {
return fmt.Errorf("size must be at least 1MB")
}
output, err := utils.RunCommand("gpart", "add", "-t", "freebsd-zfs", "-s", fmt.Sprintf("%dMB", mbytes), device)
if ptype == "" {
ptype = "freebsd-zfs"
}
output, err := utils.RunCommand("gpart", "add", "-t", ptype, "-s", fmt.Sprintf("%dMB", mbytes), device)
if err != nil {
return fmt.Errorf("error creating partition on disk %s: %v, output: %s", device, err, output)
}
@@ -91,7 +95,7 @@ func CreatePartitions(device string, sizes []uint64) error {
}
for _, size := range sizes {
err = CreatePartition(device, size)
err = CreatePartition(device, size, "")
if err != nil {
return err
+4 -4
View File
@@ -37,7 +37,7 @@
"@eslint/js": "^9.18.0",
"@iconify/svelte": "^4.2.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.19.2",
"@sveltejs/kit": "^2.20.3",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
@@ -1856,9 +1856,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.19.2",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.19.2.tgz",
"integrity": "sha512-OkW7MMGkjXtdfqdHWlyPozh/Ct1X3pthXAKTSqHm+mwmvmTBASmPE6FhwlvUgsqlCceRYL+5QUGiIJfOy0xIjQ==",
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.3.tgz",
"integrity": "sha512-z1SQ8qra/kGY3DzarG7xc6XsbKm8UY3SnI82XLI3PqMYWbYj/LpjPWuAz9WA5EyLjFNLD7sOAOEW8Gt4yjr5Vg==",
"dev": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -19,7 +19,7 @@
"@eslint/js": "^9.18.0",
"@iconify/svelte": "^4.2.0",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.19.2",
"@sveltejs/kit": "^2.20.3",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
+3
View File
@@ -0,0 +1,3 @@
export function createEmptyArrayOfArrays(length: number): Array<Array<any>> {
return Array.from({ length }, () => []);
}
+98 -92
View File
@@ -9,116 +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 }) {
console.log('options:', options);
export function dropzone(
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;
// Track if we're currently dragging over this zone
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) {
// Only add class when first entering
node.classList.add(state.dragover_class);
console.log('Adding dropzone class to', node);
}
}
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(); // Stop event from bubbling up
dragCounter = 0;
node.classList.remove(state.dragover_class);
dragCounter = 0; // Reset counter
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);
}
};
}
+209 -174
View File
@@ -18,6 +18,7 @@
import { flip } from 'svelte/animate';
import { slide } from 'svelte/transition';
import { createEmptyArrayOfArrays } from '$lib/utils/arr';
import { draggable, dropzone } from '$lib/utils/dnd';
import { untrack } from 'svelte';
@@ -33,6 +34,13 @@
type: string;
}
interface DiskContainer {
id: string;
name: string;
size: number;
type: string;
}
let { data }: { data: Data } = $props();
const results = useQueries([
@@ -75,20 +83,23 @@
if (disk.Usage === 'Partitions') {
for (const partition of disk.Partitions) {
for (const pool of pools) {
let skip = false;
for (const vdev of pool.vdevs) {
if (
vdev.name !== `/dev/${partition.name}` &&
vdev.name !== partition.name &&
partition.usage === 'ZFS'
) {
unusedDisks.push({
name: `/dev/${partition.name}`,
size: partition.size,
gpt: disk.GPT,
type: 'Partition'
});
if (vdev.name.includes(partition.name)) {
skip = true;
continue;
}
}
if (partition.usage === 'ZFS' && !skip) {
unusedDisks.push({
name: `/dev/${partition.name}`,
size: partition.size,
gpt: disk.GPT,
type: 'Partition'
});
}
}
}
}
@@ -99,12 +110,17 @@
let open: boolean = $state(false);
let name: string = $state('');
let vdevCount: number = $state(21);
let vdevCount: number = $state(1);
let createEnabled: boolean = $state(false);
$effect(() => {
vdevCount = Math.max(1, Math.min(128, vdevCount));
});
function mapAvailable(): DiskContainer[] {
return useableDisks.map((disk) => ({
id: disk.name,
name: disk.name,
size: disk.size,
type: disk.type
}));
}
function close() {
open = false;
@@ -112,130 +128,85 @@
vdevCount = 1;
}
// Convert useable disks to a format with IDs for drag and drop
let availableDisks = $state(
useableDisks.map((disk) => ({
id: disk.name,
name: disk.name,
size: disk.size,
type: disk.type
}))
);
let availableDisks: DiskContainer[] = $state(mapAvailable());
let diskContainers: DiskContainer[][] | null = $state(null);
// Add dummy disks for testing if needed
if (availableDisks.length === 0) {
availableDisks = [
// HDD disks
{ id: 'disk1', name: '/dev/sda', size: 500 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk2', name: '/dev/sdb', size: 1000 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk3', name: '/dev/sdc', size: 2000 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk4', name: '/dev/sdd', size: 4000 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk5', name: '/dev/sde', size: 8000 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk6', name: '/dev/sdf', size: 10000 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk7', name: '/dev/sdg', size: 12000 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk8', name: '/dev/sdh', size: 14000 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk9', name: '/dev/sdi', size: 16000 * 1024 * 1024 * 1024, type: 'HDD' },
{ id: 'disk10', name: '/dev/sdj', size: 18000 * 1024 * 1024 * 1024, type: 'HDD' },
// SSD disks
{ id: 'ssd1', name: '/dev/ssd0n1', size: 250 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd2', name: '/dev/ssd1n1', size: 500 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd3', name: '/dev/ssd2n1', size: 1000 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd4', name: '/dev/ssd3n1', size: 2000 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd5', name: '/dev/ssd4n1', size: 4000 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd6', name: '/dev/ssd5n1', size: 8000 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd7', name: '/dev/ssd6n1', size: 10000 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd8', name: '/dev/ssd7n1', size: 12000 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd9', name: '/dev/ssd8n1', size: 14000 * 1024 * 1024 * 1024, type: 'SSD' },
{ id: 'ssd10', name: '/dev/ssd9n1', size: 16000 * 1024 * 1024 * 1024, type: 'SSD' },
$effect(() => {
vdevCount = Math.max(1, Math.min(128, vdevCount));
untrack(() => {
console.log(useableDisks);
if (diskContainers === null) {
diskContainers = createEmptyArrayOfArrays(vdevCount);
} else {
// Create a new array with the new length first
const newContainers = Array(vdevCount).fill([]);
{ id: 'nvm1', name: '/dev/nvme0n1', size: 250 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm2', name: '/dev/nvme1n1', size: 500 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm3', name: '/dev/nvme2n1', size: 1000 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm4', name: '/dev/nvme3n1', size: 2000 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm5', name: '/dev/nvme4n1', size: 4000 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm6', name: '/dev/nvme5n1', size: 8000 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm7', name: '/dev/nvme6n1', size: 10000 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm8', name: '/dev/nvme7n1', size: 12000 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm9', name: '/dev/nvme8n1', size: 14000 * 1024 * 1024 * 1024, type: 'NVMe' },
{ id: 'nvm10', name: '/dev/nvme9n1', size: 16000 * 1024 * 1024 * 1024, type: 'NVMe' }
];
}
// Then copy over existing containers up to the minimum of old and new length
const copyLength = Math.min(diskContainers.length, vdevCount);
for (let i = 0; i < copyLength; i++) {
newContainers[i] = [...diskContainers[i]]; // Create a new array reference
}
// Create containers for the disks
let diskContainers: { id: string; name: string; size: number; type: string }[][] = $state(
Array(vdevCount)
.fill(null)
.map((_, i) => [])
);
diskContainers = newContainers;
}
});
});
function handleDiskDrop(containerId: number, event: DragEvent) {
// event.preventDefault();
console.log('Dropped disk in container', containerId);
event.preventDefault();
const diskId = event.dataTransfer ? event.dataTransfer.getData('application/disk') : null;
const diskId = event.dataTransfer?.getData('application/disk');
if (!diskId) return;
// Check if the disk is already in a container
let foundInContainer = -1;
let diskToMove = null;
let diskToMove: any = null;
// Look in containers first
for (let i = 0; i < diskContainers.length; i++) {
const diskIndex = diskContainers[i].findIndex((d) => d.id === diskId);
if (diskIndex !== -1) {
diskToMove = diskContainers[i][diskIndex];
foundInContainer = i;
break;
}
}
// Look in available disks if not found in containers
if (foundInContainer === -1) {
const diskIndex = availableDisks.findIndex((d) => d.id === diskId);
if (diskIndex !== -1) {
diskToMove = availableDisks[diskIndex];
availableDisks = availableDisks.filter((d) => d.id !== diskId);
}
} else if (foundInContainer !== containerId) {
// Remove from original container if it's a different one
diskContainers[foundInContainer] = diskContainers[foundInContainer].filter(
(d) => d.id !== diskId
if (diskContainers) {
// Try to find and remove the disk from a container
const sourceContainerIndex = diskContainers.findIndex((container) =>
container.some((disk) => disk.id === diskId)
);
}
// Add to target container if we found the disk
if (diskToMove && foundInContainer !== containerId) {
diskContainers[containerId] = [...diskContainers[containerId], diskToMove];
}
if (sourceContainerIndex !== -1) {
const container = diskContainers[sourceContainerIndex];
diskToMove = container.find((disk) => disk.id === diskId) || null;
// Enable create button if any disks are assigned
createEnabled = diskContainers.some((container) => container.length > 0);
}
// Remove it only if moving to a different container
if (sourceContainerIndex !== containerId) {
diskContainers[sourceContainerIndex] = container.filter((disk) => disk.id !== diskId);
}
} else {
// Try to find and remove from available disks
const diskIndex = availableDisks.findIndex((disk) => disk.id === diskId);
if (diskIndex !== -1) {
diskToMove = availableDisks[diskIndex];
availableDisks.splice(diskIndex, 1); // Remove from availableDisks
}
}
// Function to handle removing a disk from a container
function returnDiskToPool(containerId: number, diskId: string) {
const container = diskContainers[containerId];
const diskIndex = container.findIndex((d) => d.id === diskId);
// Add to target container if found and it's not already there
if (diskToMove && sourceContainerIndex !== containerId) {
diskContainers[containerId].push(diskToMove);
console.log(diskContainers);
}
if (diskIndex !== -1) {
const disk = container[diskIndex];
diskContainers[containerId] = container.filter((d) => d.id !== diskId);
availableDisks = [...availableDisks, disk];
// Update create button state
// Enable create button if any container has disks
createEnabled = diskContainers.some((container) => container.length > 0);
}
}
// $effect(() => {
// const oldContainers = [...diskContainers];
// untrack(() => {
// diskContainers = Array(vdevCount)
// .fill(null)
// .map((_, i) => {
// return oldContainers[i] || [];
// });
// });
// });
// Function to handle removing a disk from a container
function returnDiskToPool(containerId: number, diskId: string) {
if (diskContainers) {
const container = diskContainers[containerId];
const diskIndex = container.findIndex((d) => d.id === diskId);
if (diskIndex !== -1) {
const disk = container[diskIndex];
diskContainers[containerId] = container.filter((d) => d.id !== diskId);
availableDisks = [...availableDisks, disk];
createEnabled = diskContainers.some((container) => container.length > 0);
}
}
}
const raid = [
{ value: 'mirror', label: 'Mirror' },
@@ -299,8 +270,8 @@
</div>
<Tabs.Root value="tab-1" class="w-full overflow-hidden">
<Tabs.List class="grid w-full grid-cols-2">
<Tabs.Trigger value="tab-1">Tab 1</Tabs.Trigger>
<Tabs.Trigger value="tab-2">Tab 2</Tabs.Trigger>
<Tabs.Trigger value="tab-1">Devices</Tabs.Trigger>
<Tabs.Trigger value="tab-2">Options</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab-1">
<Card.Root class="pb-6">
@@ -316,57 +287,85 @@
</Card.Content>
<Card.Content class="flex flex-col gap-6 !pb-0">
<Label for="vdev_count" class="">VDEV</Label>
<!-- Disk Containers Section -->
<div
class="border-primary-foreground bg-primary-foreground w-full overflow-hidden border-y p-4"
class="border-primary-foreground bg-primary-foreground w-full overflow-hidden rounded-lg border-y p-4"
>
<ScrollArea class="w-full whitespace-nowrap rounded-md " orientation="horizontal">
<ScrollArea class="w-full whitespace-nowrap rounded-md" orientation="horizontal">
<div class="flex justify-center gap-7 pr-4">
{#each Array(vdevCount) as _, i}
<div class="flex flex-col">
<div
class="h-28 w-48 flex-shrink-0 overflow-auto rounded-lg border border-neutral-300 bg-neutral-200 p-2 dark:border-neutral-800 dark:bg-neutral-950"
use:dropzone={{
on_dropzone: (_: unknown, event: DragEvent) => handleDiskDrop(i, event)
}}
>
{#if diskContainers[i].length === 0}
<div class="flex h-full items-center justify-center text-neutral-500">
Drop disks here
</div>
{:else}
<div class="flex flex-wrap items-center justify-center gap-2">
{#each diskContainers[i] as disk (disk.id)}
<div animate:flip={{ duration: 300 }} class="relative">
{#if disk.type === 'NVMe'}
<Icon icon="bi:nvme" class="h-11 w-11 rotate-90 text-blue-500" />
{:else}
<Icon
icon={disk.type === 'SSD'
? 'icon-park-outline:ssd'
: 'mdi:harddisk'}
class="h-12 w-12 {disk.type === 'SSD'
? 'text-blue-500'
: 'text-green-500'}"
/>
{/if}
<div class="max-w-[48px] truncate text-center text-xs">
{disk.name.split('/').pop()}
</div>
<button
class="absolute -right-1 -top-1 rounded-full bg-red-500 p-0.5 text-white hover:bg-red-600"
onclick={() => returnDiskToPool(i, disk.id)}
>
<Icon icon="mdi:close" class="h-3 w-3" />
</button>
{#if diskContainers}
{#if diskContainers[i]}
<div class="flex flex-col">
<div
class="h-28 w-48 flex-shrink-0 overflow-auto rounded-lg border border-neutral-300 bg-neutral-200 p-2 dark:border-neutral-800 dark:bg-neutral-950"
use:dropzone={{
on_dropzone: (_: unknown, event: DragEvent) =>
handleDiskDrop(i, event)
}}
>
{#if diskContainers[i].length === 0}
<div class="flex h-full items-center justify-center text-neutral-500">
Drop disks here
</div>
{/each}
{:else}
<div class="flex flex-wrap items-center justify-center gap-2">
{#each diskContainers[i] as disk (disk.id)}
<div animate:flip={{ duration: 300 }} class="relative">
<!-- {#if disk.type === 'NVMe'}
<Icon
icon="bi:nvme"
class="h-11 w-11 rotate-90 text-blue-500"
/>
{:else}
<Icon
icon={disk.type === 'SSD'
? 'icon-park-outline:ssd'
: 'mdi:harddisk'}
class="h-12 w-12 {disk.type === 'SSD'
? 'text-blue-500'
: 'text-green-500'}"
/>
{/if} -->
{#if disk.type === 'SSD'}
<Icon
icon="icon-park-outline:ssd"
class="h-11 w-11 text-blue-500"
/>
{:else if disk.type === 'NVMe'}
<Icon
icon="bi:nvme"
class="h-11 w-11 rotate-90 text-blue-500"
/>
{:else if disk.type === 'HDD'}
<Icon icon="mdi:harddisk" class="h-11 w-11 text-green-500" />
{:else if disk.type === 'Partition'}
<Icon
icon="ant-design:partition-outlined"
class="h-11 w-11 rotate-90 text-blue-500"
/>
{/if}
<div class="max-w-[48px] truncate text-center text-xs">
{disk.name.split('/').pop()}
</div>
<button
class="absolute -right-1 -top-1 rounded-full bg-red-500 p-0.5 text-white hover:bg-red-600"
onclick={() => returnDiskToPool(i, disk.id)}
>
<Icon icon="mdi:close" class="h-3 w-3" />
</button>
</div>
{/each}
</div>
{/if}
</div>
{/if}
</div>
<p class="mt-2 text-center text-xs text-neutral-100">VDEV {i + 1}</p>
</div>
<p
class="mt-2 text-center text-xs text-neutral-800 dark:text-neutral-300"
>
VDEV {i + 1}
</p>
</div>
{/if}
{/if}
{/each}
</div>
</ScrollArea>
@@ -374,7 +373,7 @@
<Label for="vdev_count" class="">Disks</Label>
<div
class="border-primary-foreground bg-primary-foreground grid grid-cols-3 gap-6 overflow-hidden border-y p-4"
class="border-primary-foreground bg-primary-foreground grid grid-cols-4 gap-6 overflow-hidden border-y p-4"
>
<div class="">
<label class="">HDD</label>
@@ -383,7 +382,7 @@
class="mt-1 w-full whitespace-nowrap rounded-md"
orientation="horizontal"
>
<div class="flex justify-center gap-4 pr-4">
<div class="flex min-h-[80px] justify-center gap-4 pr-4">
{#each availableDisks.filter((disk) => disk.type === 'HDD') as disk (disk.id)}
<div class="text-center" animate:flip={{ duration: 300 }}>
<div class="cursor-move" use:draggable={disk.id}>
@@ -398,7 +397,7 @@
</div>
{/each}
{#if availableDisks.length === 0}
{#if availableDisks.filter((disk) => disk.type === 'HDD').length === 0}
<div class="flex h-16 w-full items-center justify-center text-neutral-400">
No available disks
</div>
@@ -415,7 +414,7 @@
class="mt-1 w-full whitespace-nowrap rounded-md "
orientation="horizontal"
>
<div class="flex justify-center gap-4 pr-4">
<div class="flex min-h-[80px] justify-center gap-4 pr-4">
{#each availableDisks.filter((disk) => disk.type === 'SSD') as disk (disk.id)}
<div class="text-center" animate:flip={{ duration: 300 }}>
<div class="cursor-move" use:draggable={disk.id}>
@@ -447,7 +446,7 @@
class="mt-1 w-full whitespace-nowrap rounded-md "
orientation="horizontal"
>
<div class="flex justify-center gap-4 pr-4">
<div class="flex min-h-[80px] justify-center gap-4 pr-4">
{#each availableDisks.filter((disk) => disk.type === 'NVMe') as disk (disk.id)}
<div class="text-center" animate:flip={{ duration: 300 }}>
<div class="cursor-move" use:draggable={disk.id}>
@@ -471,10 +470,46 @@
</ScrollArea>
</div>
</div>
<div class="">
<label class="">Partition</label>
<div class="mt-1 rounded-lg bg-neutral-200 p-4 dark:bg-neutral-950">
<ScrollArea
class="mt-1 w-full whitespace-nowrap rounded-md "
orientation="horizontal"
>
<div class="flex min-h-[80px] justify-center gap-4 pr-4">
{#each availableDisks.filter((disk) => disk.type === 'Partition') as disk (disk.id)}
<div class="text-center" animate:flip={{ duration: 300 }}>
<div class="cursor-move" use:draggable={disk.id}>
<Icon
icon="ant-design:partition-outlined"
class="h-11 w-11 rotate-90 text-blue-500"
/>
</div>
<div class="max-w-[64px] truncate text-xs">
{disk.name.split('/').pop()}
</div>
<div class="text-xs text-neutral-400">
{Math.round(disk.size / (1024 * 1024 * 1024))} GB
</div>
</div>
{/each}
{#if availableDisks.filter((disk) => disk.type === 'Partition').length === 0}
<div class="flex h-16 w-full items-center justify-center text-neutral-400">
No available partitions
</div>
{/if}
</div>
</ScrollArea>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
</Tabs.Content>
<Tabs.Content value="tab-2">
<Card.Root>
<Card.Content>