major updates to device and links management in admin
This commit is contained in:
@@ -1,11 +1,511 @@
|
||||
<script lang="ts">
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import { Button, buttonVariants } from "$lib/components/ui/button";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import * as Table from "$lib/components/ui/table";
|
||||
import { goto } from "$app/navigation";
|
||||
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
|
||||
import { mainNavTree } from "$lib/core/constants";
|
||||
import DeviceForm from "$lib/domains/device/device-form.svelte";
|
||||
import { deviceVM } from "$lib/domains/device/device.vm.svelte";
|
||||
import { breadcrumbs } from "$lib/global.stores";
|
||||
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||
import Pencil from "@lucide/svelte/icons/pencil";
|
||||
import Monitor from "@lucide/svelte/icons/monitor";
|
||||
import Plus from "@lucide/svelte/icons/plus";
|
||||
import RefreshCw from "@lucide/svelte/icons/refresh-cw";
|
||||
import Trash2 from "@lucide/svelte/icons/trash-2";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
breadcrumbs.set([mainNavTree[0]]);
|
||||
|
||||
let title = $state("");
|
||||
let version = $state("");
|
||||
let host = $state("");
|
||||
let containerId = $state("");
|
||||
let wsPort = $state("");
|
||||
let isActive = $state(false);
|
||||
let editId = $state<number | null>(null);
|
||||
let editTitle = $state("");
|
||||
let editVersion = $state("");
|
||||
let editHost = $state("");
|
||||
let editContainerId = $state("");
|
||||
let editWsPort = $state("");
|
||||
let editIsActive = $state(false);
|
||||
let editInUse = $state(false);
|
||||
let editStatus = $state("offline" as
|
||||
| "online"
|
||||
| "offline"
|
||||
| "busy"
|
||||
| "error");
|
||||
let showEditDialog = $state(false);
|
||||
|
||||
onMount(async () => {
|
||||
await deviceVM.fetchDevices();
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
title = "";
|
||||
version = "";
|
||||
host = "";
|
||||
containerId = "";
|
||||
wsPort = "";
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
function openEditDialog(device: (typeof deviceVM.devices)[number]) {
|
||||
editId = device.id;
|
||||
editTitle = device.title;
|
||||
editVersion = device.version;
|
||||
editHost = device.host;
|
||||
editContainerId = device.containerId;
|
||||
editWsPort = device.wsPort;
|
||||
editIsActive = device.isActive;
|
||||
editInUse = device.inUse;
|
||||
editStatus = device.status as typeof editStatus;
|
||||
showEditDialog = true;
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editId = null;
|
||||
editTitle = "";
|
||||
editVersion = "";
|
||||
editHost = "";
|
||||
editContainerId = "";
|
||||
editWsPort = "";
|
||||
editIsActive = false;
|
||||
editInUse = false;
|
||||
editStatus = "offline";
|
||||
}
|
||||
|
||||
async function handleCreate(e: Event) {
|
||||
e.preventDefault();
|
||||
const success = await deviceVM.createDevice({
|
||||
title,
|
||||
version,
|
||||
host,
|
||||
containerId,
|
||||
wsPort,
|
||||
isActive,
|
||||
});
|
||||
if (success) resetForm();
|
||||
}
|
||||
|
||||
async function handleUpdate(e: Event) {
|
||||
e.preventDefault();
|
||||
if (!editId) return;
|
||||
|
||||
const success = await deviceVM.updateDevice(editId, {
|
||||
title: editTitle,
|
||||
version: editVersion,
|
||||
host: editHost,
|
||||
containerId: editContainerId,
|
||||
wsPort: editWsPort,
|
||||
isActive: editIsActive,
|
||||
inUse: editInUse,
|
||||
status: editStatus,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
showEditDialog = false;
|
||||
resetEditForm();
|
||||
}
|
||||
}
|
||||
|
||||
function statusColor(status: string): string {
|
||||
switch (status) {
|
||||
case "online":
|
||||
return "bg-green-500";
|
||||
case "offline":
|
||||
return "bg-zinc-400";
|
||||
case "busy":
|
||||
return "bg-amber-500";
|
||||
case "error":
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-zinc-400";
|
||||
}
|
||||
}
|
||||
|
||||
function openDeviceDetails(id: number) {
|
||||
void goto(`/dashboard/${id}`);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (showEditDialog) return;
|
||||
resetEditForm();
|
||||
});
|
||||
</script>
|
||||
|
||||
<MaxWidthWrapper cls="space-y-4">
|
||||
<span>dunno</span>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div
|
||||
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={Monitor} cls="h-5 w-5 text-primary" />
|
||||
<Card.Title>Devices</Card.Title>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{deviceVM.devices.length} total
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => void deviceVM.fetchDevices()}
|
||||
disabled={deviceVM.loading}
|
||||
>
|
||||
<Icon
|
||||
icon={RefreshCw}
|
||||
cls={`h-4 w-4 mr-2 ${deviceVM.loading ? "animate-spin" : ""}`}
|
||||
/>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => (deviceVM.showCreateDialog = true)}
|
||||
>
|
||||
<Icon icon={Plus} cls="h-4 w-4 mr-2" />
|
||||
Add Device
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
{#if !deviceVM.loading && deviceVM.devices.length === 0}
|
||||
<div class="py-10 text-center text-sm text-muted-foreground">
|
||||
No devices registered yet.
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Mobile cards -->
|
||||
<div class="space-y-3 md:hidden">
|
||||
{#each deviceVM.devices as device (device.id)}
|
||||
<div class="rounded-lg border bg-background p-3">
|
||||
<div
|
||||
class="flex items-start justify-between gap-3"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="group min-w-0 text-left"
|
||||
onclick={() => openDeviceDetails(device.id)}
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p
|
||||
class="truncate text-[15px] font-semibold text-primary underline decoration-primary/40 underline-offset-4"
|
||||
>
|
||||
{device.title}
|
||||
</p>
|
||||
<Icon
|
||||
icon={ChevronRight}
|
||||
cls="h-3.5 w-3.5 shrink-0 text-primary/70 transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="text-muted-foreground truncate text-xs"
|
||||
>
|
||||
{device.host}
|
||||
</p>
|
||||
</button>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 text-xs capitalize"
|
||||
>
|
||||
<span
|
||||
class={`h-2 w-2 rounded-full ${statusColor(device.status)}`}
|
||||
></span>
|
||||
{device.status}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-3 grid grid-cols-2 gap-x-3 gap-y-2 text-xs"
|
||||
>
|
||||
<div>
|
||||
<p class="text-muted-foreground">Version</p>
|
||||
<p>{device.version}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">Active</p>
|
||||
<p>{device.isActive ? "Yes" : "No"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">In Use</p>
|
||||
<p>{device.inUse ? "Yes" : "No"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">
|
||||
Container
|
||||
</p>
|
||||
<p class="truncate font-mono">{device.containerId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground">
|
||||
WS Port
|
||||
</p>
|
||||
<p>{device.wsPort}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => openEditDialog(device)}
|
||||
>
|
||||
<Icon icon={Pencil} cls="h-4 w-4" />
|
||||
</Button>
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger
|
||||
class={buttonVariants({
|
||||
variant: "destructive",
|
||||
size: "sm",
|
||||
})}
|
||||
disabled={deviceVM.deletingId ===
|
||||
device.id}
|
||||
>
|
||||
<Icon icon={Trash2} cls="h-4 w-4" />
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>
|
||||
Delete device?
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This will permanently remove
|
||||
"{device.title}" and unlink it
|
||||
from any associated links.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>
|
||||
Cancel
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
onclick={() =>
|
||||
deviceVM.deleteDevice(
|
||||
device.id,
|
||||
)}
|
||||
>
|
||||
Delete
|
||||
</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Desktop table -->
|
||||
<div class="hidden md:block">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Title</Table.Head>
|
||||
<Table.Head>Host</Table.Head>
|
||||
<Table.Head>Version</Table.Head>
|
||||
<Table.Head>Status</Table.Head>
|
||||
<Table.Head>Active</Table.Head>
|
||||
<Table.Head>In Use</Table.Head>
|
||||
<Table.Head>Container</Table.Head>
|
||||
<Table.Head>Actions</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each deviceVM.devices as device (device.id)}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">
|
||||
<button
|
||||
type="button"
|
||||
class="group inline-flex items-center gap-1.5 text-left"
|
||||
onclick={() =>
|
||||
openDeviceDetails(device.id)}
|
||||
>
|
||||
<span
|
||||
class="text-[15px] font-semibold text-primary underline decoration-primary/40 underline-offset-4"
|
||||
>
|
||||
{device.title}
|
||||
</span>
|
||||
<Icon
|
||||
icon={ChevronRight}
|
||||
cls="h-3.5 w-3.5 shrink-0 text-primary/70 transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
</button>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span class="font-mono text-xs">
|
||||
{device.host}
|
||||
</span>
|
||||
{#if device.wsPort}
|
||||
<span
|
||||
class="text-muted-foreground text-xs"
|
||||
>
|
||||
:{device.wsPort}
|
||||
</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{device.version}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 text-xs capitalize"
|
||||
>
|
||||
<span
|
||||
class={`h-2 w-2 rounded-full ${statusColor(device.status)}`}
|
||||
></span>
|
||||
{device.status}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if device.isActive}
|
||||
<Badge variant="default"
|
||||
>Active</Badge
|
||||
>
|
||||
{:else}
|
||||
<Badge variant="secondary"
|
||||
>Inactive</Badge
|
||||
>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{#if device.inUse}
|
||||
<Badge variant="destructive"
|
||||
>In Use</Badge
|
||||
>
|
||||
{:else}
|
||||
<Badge variant="secondary"
|
||||
>Free</Badge
|
||||
>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<span
|
||||
class="inline-block max-w-[120px] truncate font-mono text-xs"
|
||||
>
|
||||
{device.containerId}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() =>
|
||||
openEditDialog(device)}
|
||||
>
|
||||
<Icon
|
||||
icon={Pencil}
|
||||
cls="h-4 w-4"
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger
|
||||
class={buttonVariants({
|
||||
variant: "destructive",
|
||||
size: "sm",
|
||||
})}
|
||||
disabled={deviceVM.deletingId ===
|
||||
device.id}
|
||||
>
|
||||
<Icon
|
||||
icon={Trash2}
|
||||
cls="h-4 w-4"
|
||||
/>
|
||||
Delete
|
||||
</AlertDialog.Trigger>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>
|
||||
Delete device?
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This will permanently
|
||||
remove "{device.title}"
|
||||
and unlink it from any
|
||||
associated links.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>
|
||||
Cancel
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
onclick={() =>
|
||||
deviceVM.deleteDevice(
|
||||
device.id,
|
||||
)}
|
||||
>
|
||||
Delete
|
||||
</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</MaxWidthWrapper>
|
||||
|
||||
<!-- Create Device Dialog -->
|
||||
<Dialog.Root bind:open={deviceVM.showCreateDialog}>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Add Device</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Register a new Docker-Android instance.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<DeviceForm
|
||||
fieldPrefix="create-device"
|
||||
bind:title
|
||||
bind:version
|
||||
bind:host
|
||||
bind:containerId
|
||||
bind:wsPort
|
||||
bind:isActive
|
||||
submitLabel="Create"
|
||||
submitting={deviceVM.creating}
|
||||
onsubmit={handleCreate}
|
||||
oncancel={() => (deviceVM.showCreateDialog = false)}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Dialog.Root bind:open={showEditDialog}>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Edit Device</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Update connection details or manually override device state.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<DeviceForm
|
||||
fieldPrefix="edit-device"
|
||||
bind:title={editTitle}
|
||||
bind:version={editVersion}
|
||||
bind:host={editHost}
|
||||
bind:containerId={editContainerId}
|
||||
bind:wsPort={editWsPort}
|
||||
bind:isActive={editIsActive}
|
||||
bind:inUse={editInUse}
|
||||
bind:status={editStatus}
|
||||
submitLabel="Save Changes"
|
||||
submitting={deviceVM.updating}
|
||||
showAdvanced={true}
|
||||
onsubmit={handleUpdate}
|
||||
oncancel={() => {
|
||||
showEditDialog = false;
|
||||
resetEditForm();
|
||||
}}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
Reference in New Issue
Block a user