147 lines
5.1 KiB
Svelte
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>
|