Files
nadir-webui/src/routes/dashboard/[machineId]/storage/fstab/+page.svelte
T
2026-06-25 23:11:32 +02:00

147 lines
5.1 KiB
Svelte

<script lang="ts">
import MoreHorizontalIcon from '@lucide/svelte/icons/more-horizontal';
import { page as pageState } from '$app/state';
import DataTable from '$lib/components/dashboard/data-table.svelte';
import PageMeta from '$lib/components/seo/page-meta.svelte';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Table from '$lib/components/ui/table';
import { m } from '$lib/paraglide/messages';
import { listFstab, removeMount } from '$lib/remotes/storage.remote';
import { getWhoami } from '$lib/remotes/system.remote';
import { extractErrorMessage, hasPermission } from '$lib/utils';
import { toast } from 'svelte-sonner';
type FstabEntry = {
device: string;
dump: number;
fstype: string;
mountpoint: string;
options: string;
pass: number;
};
const machineId = $derived(pageState.params.machineId!);
const fstab = $derived(listFstab(machineId));
const whoami = $derived(getWhoami(machineId));
const canWrite = $derived(hasPermission('storage', 'write', whoami.current?.permissions));
let search = $state('');
const filtered = $derived.by(() => {
const all = (fstab.current ?? []) as FstabEntry[];
const q = search.trim().toLowerCase();
return all
.filter(
(e) =>
!q ||
e.device.toLowerCase().includes(q) ||
e.mountpoint.toLowerCase().includes(q) ||
e.fstype.toLowerCase().includes(q)
)
.sort((a, b) => a.mountpoint.localeCompare(b.mountpoint));
});
let deleteOpen = $state(false);
let deleting = $state<FstabEntry | null>(null);
async function doDelete() {
if (!deleting) return;
try {
await removeMount({ machineId, mountpoint: deleting.mountpoint });
toast.success(m.storage_mount_removed());
deleteOpen = false;
deleting = null;
} catch (e) {
console.error(e);
toast.error(extractErrorMessage(e) ?? m.errors_generic());
}
}
</script>
<PageMeta title={m.seo_title_storage_fstab()} description={m.seo_desc_storage_fstab()} />
<DataTable
title={m.nav_storage_fstab()}
description={m.storage_fstab_description()}
searchPlaceholder={m.storage_mounts_search_placeholder()}
emptyMessage={m.storage_no_fstab()}
items={filtered}
loading={fstab.loading}
pageSizeKey="storage.fstab.pageSize"
defaultPageSize={10}
pageSizePresets={[10, 20, 50, 100]}
bind:search
onrefresh={() => fstab.refresh()}
i18n={{
display: m.users_filter_display(),
filterTitle: m.users_filter(),
next: () => m.pagination_next(),
pageOf: (p) => m.pagination_page_of(p),
previous: () => m.pagination_previous(),
rowsPerPage: m.users_rows_per_page()
}}
>
{#snippet columns()}
<Table.Head>{m.storage_col_mountpoint()}</Table.Head>
<Table.Head>{m.storage_col_device()}</Table.Head>
<Table.Head>{m.storage_col_fstype()}</Table.Head>
<Table.Head>{m.storage_col_options()}</Table.Head>
<Table.Head class="text-right">{m.storage_col_dump()}</Table.Head>
<Table.Head class="text-right">{m.storage_col_pass()}</Table.Head>
<Table.Head class="w-12 text-right">{m.users_actions()}</Table.Head>
{/snippet}
{#snippet row(e: FstabEntry)}
<Table.Cell class="max-w-[18rem] font-medium font-mono text-xs">
<span class="block truncate" title={e.mountpoint}>{e.mountpoint}</span>
</Table.Cell>
<Table.Cell class="max-w-[16rem] text-muted-foreground font-mono text-xs">
<span class="block truncate" title={e.device}>{e.device}</span>
</Table.Cell>
<Table.Cell><Badge variant="outline">{e.fstype}</Badge></Table.Cell>
<Table.Cell class="max-w-[20rem] text-muted-foreground font-mono text-xs">
<span class="block truncate" title={e.options}>{e.options}</span>
</Table.Cell>
<Table.Cell class="text-muted-foreground text-right tabular-nums">{e.dump}</Table.Cell>
<Table.Cell class="text-muted-foreground text-right tabular-nums">{e.pass}</Table.Cell>
<Table.Cell class="text-right">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="size-8">
<MoreHorizontalIcon class="size-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item
variant="destructive"
disabled={!canWrite}
onclick={() => {
deleting = e;
deleteOpen = true;
}}>{m.storage_mount_remove()}</DropdownMenu.Item
>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Table.Cell>
{/snippet}
</DataTable>
<AlertDialog.Root bind:open={deleteOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title
>{m.storage_mount_remove_title({
mountpoint: deleting?.mountpoint ?? ''
})}</AlertDialog.Title
>
<AlertDialog.Description>{m.storage_mount_remove_description()}</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>{m.cancel()}</AlertDialog.Cancel>
<AlertDialog.Action onclick={doDelete}>{m.storage_mount_remove()}</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>