Improve drawer layout and UX for Docker and Proxmox pages
- Add flex-1 to all drawer cards for consistent space filling - Implement single-drawer-open behavior (accordion-style) - Add text truncation to labels and IP badges to prevent overflow - Replace Map-based state with reactive signals for better performance
This commit is contained in:
parent
27f2038dab
commit
5a328637af
2 changed files with 43 additions and 48 deletions
|
|
@ -12,7 +12,8 @@ import { logger } from '@/utils/logger';
|
||||||
|
|
||||||
type Guest = VM | Container;
|
type Guest = VM | Container;
|
||||||
|
|
||||||
const drawerState = new Map<string, boolean>();
|
// Global state for currently expanded drawer (only one drawer open at a time)
|
||||||
|
const [currentlyExpandedGuestId, setCurrentlyExpandedGuestId] = createSignal<string | null>(null);
|
||||||
// Global editing state - use a signal so all components react
|
// Global editing state - use a signal so all components react
|
||||||
const [currentlyEditingGuestId, setCurrentlyEditingGuestId] = createSignal<string | null>(null);
|
const [currentlyEditingGuestId, setCurrentlyEditingGuestId] = createSignal<string | null>(null);
|
||||||
// Store the editing value globally so it survives re-renders
|
// Store the editing value globally so it survives re-renders
|
||||||
|
|
@ -67,7 +68,7 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
|
|
||||||
const [customUrl, setCustomUrl] = createSignal<string | undefined>(props.customUrl);
|
const [customUrl, setCustomUrl] = createSignal<string | undefined>(props.customUrl);
|
||||||
const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false);
|
const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false);
|
||||||
const [drawerOpen, setDrawerOpen] = createSignal(drawerState.get(initialGuestId) ?? false);
|
const drawerOpen = createMemo(() => currentlyExpandedGuestId() === guestId());
|
||||||
const editingUrlValue = createMemo(() => {
|
const editingUrlValue = createMemo(() => {
|
||||||
editingValuesVersion(); // Subscribe to changes
|
editingValuesVersion(); // Subscribe to changes
|
||||||
return editingValues.get(guestId()) || '';
|
return editingValues.get(guestId()) || '';
|
||||||
|
|
@ -148,23 +149,9 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
);
|
);
|
||||||
const canShowDrawer = createMemo(() => hasDrawerContent() || hasFallbackContent());
|
const canShowDrawer = createMemo(() => hasDrawerContent() || hasFallbackContent());
|
||||||
|
|
||||||
createEffect(on(guestId, (id) => {
|
|
||||||
const stored = drawerState.get(id);
|
|
||||||
if (stored !== undefined) {
|
|
||||||
setDrawerOpen(stored);
|
|
||||||
} else {
|
|
||||||
setDrawerOpen(false);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
drawerState.set(guestId(), drawerOpen());
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!canShowDrawer() && drawerOpen()) {
|
if (!canShowDrawer() && drawerOpen()) {
|
||||||
setDrawerOpen(false);
|
setCurrentlyExpandedGuestId(null);
|
||||||
drawerState.set(guestId(), false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -174,7 +161,8 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
if (target.closest('a, button, input, [data-prevent-toggle]')) {
|
if (target.closest('a, button, input, [data-prevent-toggle]')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDrawerOpen((prev) => !prev);
|
// Toggle: if this guest is currently expanded, close it; otherwise open it (closing any other)
|
||||||
|
setCurrentlyExpandedGuestId(prev => prev === guestId() ? null : guestId());
|
||||||
};
|
};
|
||||||
|
|
||||||
const startEditingUrl = (event: MouseEvent) => {
|
const startEditingUrl = (event: MouseEvent) => {
|
||||||
|
|
@ -674,7 +662,7 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
<Show
|
<Show
|
||||||
when={hasDrawerContent()}
|
when={hasDrawerContent()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="min-w-[220px] rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||||
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">
|
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">
|
||||||
Guest details unavailable
|
Guest details unavailable
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -700,7 +688,7 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<Show when={hasOsInfo() || hasAgentInfo() || ipAddresses().length > 0}>
|
<Show when={hasOsInfo() || hasAgentInfo() || ipAddresses().length > 0}>
|
||||||
<div class="min-w-[220px] rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||||
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Guest Overview</div>
|
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Guest Overview</div>
|
||||||
<div class="mt-1 space-y-1">
|
<div class="mt-1 space-y-1">
|
||||||
<Show when={hasOsInfo()}>
|
<Show when={hasOsInfo()}>
|
||||||
|
|
@ -730,7 +718,10 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<For each={ipAddresses()}>
|
<For each={ipAddresses()}>
|
||||||
{(ip) => (
|
{(ip) => (
|
||||||
<span class="rounded bg-blue-100 px-1.5 py-0.5 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200">
|
<span
|
||||||
|
class="max-w-full truncate rounded bg-blue-100 px-1.5 py-0.5 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200"
|
||||||
|
title={ip}
|
||||||
|
>
|
||||||
{ip}
|
{ip}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -742,7 +733,7 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={memoryExtraLines() && memoryExtraLines()!.length > 0}>
|
<Show when={memoryExtraLines() && memoryExtraLines()!.length > 0}>
|
||||||
<div class="min-w-[220px] rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||||
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Memory</div>
|
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Memory</div>
|
||||||
<div class="mt-1 space-y-1 text-gray-600 dark:text-gray-300">
|
<div class="mt-1 space-y-1 text-gray-600 dark:text-gray-300">
|
||||||
<For each={memoryExtraLines()!}>{(line) => <div>{line}</div>}</For>
|
<For each={memoryExtraLines()!}>{(line) => <div>{line}</div>}</For>
|
||||||
|
|
@ -751,7 +742,7 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={hasFilesystemDetails() && props.guest.disks && props.guest.disks.length > 0}>
|
<Show when={hasFilesystemDetails() && props.guest.disks && props.guest.disks.length > 0}>
|
||||||
<div class="min-w-[220px] rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||||
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Filesystems</div>
|
<div class="text-[11px] font-medium text-gray-700 dark:text-gray-200">Filesystems</div>
|
||||||
<div class="mt-1 text-gray-600 dark:text-gray-300">
|
<div class="mt-1 text-gray-600 dark:text-gray-300">
|
||||||
<DiskList
|
<DiskList
|
||||||
|
|
@ -785,7 +776,10 @@ export function GuestRow(props: GuestRowProps) {
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
<For each={addresses}>
|
<For each={addresses}>
|
||||||
{(ip) => (
|
{(ip) => (
|
||||||
<span class="rounded bg-blue-100 px-1.5 py-0.5 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200">
|
<span
|
||||||
|
class="max-w-full truncate rounded bg-blue-100 px-1.5 py-0.5 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200"
|
||||||
|
title={ip}
|
||||||
|
>
|
||||||
{ip}
|
{ip}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,8 @@ interface DockerUnifiedTableProps {
|
||||||
onCustomUrlUpdate?: (resourceId: string, url: string) => void;
|
onCustomUrlUpdate?: (resourceId: string, url: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowExpandState = new Map<string, boolean>();
|
// Global state for currently expanded drawer (only one drawer open at a time)
|
||||||
|
const [currentlyExpandedRowId, setCurrentlyExpandedRowId] = createSignal<string | null>(null);
|
||||||
|
|
||||||
// Global editing state for Docker resource URLs
|
// Global editing state for Docker resource URLs
|
||||||
const [currentlyEditingDockerResourceId, setCurrentlyEditingDockerResourceId] = createSignal<string | null>(null);
|
const [currentlyEditingDockerResourceId, setCurrentlyEditingDockerResourceId] = createSignal<string | null>(null);
|
||||||
|
|
@ -577,7 +578,7 @@ const DockerContainerRow: Component<{
|
||||||
|
|
||||||
const [customUrl, setCustomUrl] = createSignal<string | undefined>(props.customUrl);
|
const [customUrl, setCustomUrl] = createSignal<string | undefined>(props.customUrl);
|
||||||
const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false);
|
const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false);
|
||||||
const [expanded, setExpanded] = createSignal(rowExpandState.get(rowId) ?? false);
|
const expanded = createMemo(() => currentlyExpandedRowId() === rowId);
|
||||||
const editingUrlValue = createMemo(() => {
|
const editingUrlValue = createMemo(() => {
|
||||||
dockerEditingValuesVersion(); // Subscribe to changes
|
dockerEditingValuesVersion(); // Subscribe to changes
|
||||||
return dockerEditingValues.get(resourceId()) || '';
|
return dockerEditingValues.get(resourceId()) || '';
|
||||||
|
|
@ -684,11 +685,8 @@ const DockerContainerRow: Component<{
|
||||||
if (!hasDrawerContent()) return;
|
if (!hasDrawerContent()) return;
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (target.closest('a, button, input, [data-prevent-toggle]')) return;
|
if (target.closest('a, button, input, [data-prevent-toggle]')) return;
|
||||||
setExpanded((prev) => {
|
// Toggle: if this row is currently expanded, close it; otherwise open it (closing any other)
|
||||||
const next = !prev;
|
setCurrentlyExpandedRowId(prev => prev === rowId ? null : rowId);
|
||||||
rowExpandState.set(rowId, next);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const startEditingUrl = (event: MouseEvent) => {
|
const startEditingUrl = (event: MouseEvent) => {
|
||||||
|
|
@ -1062,7 +1060,7 @@ const DockerContainerRow: Component<{
|
||||||
<tr class="bg-gray-50 dark:bg-gray-900/50">
|
<tr class="bg-gray-50 dark:bg-gray-900/50">
|
||||||
<td colSpan={props.columns} class="px-4 py-3">
|
<td colSpan={props.columns} class="px-4 py-3">
|
||||||
<div class="flex flex-wrap justify-start gap-3">
|
<div class="flex flex-wrap justify-start gap-3">
|
||||||
<div class="min-w-[220px] rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||||
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">
|
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">
|
||||||
Summary
|
Summary
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1191,7 +1189,7 @@ const DockerContainerRow: Component<{
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={container.ports && container.ports.length > 0}>
|
<Show when={container.ports && container.ports.length > 0}>
|
||||||
<div class="min-w-[220px] rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||||
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">
|
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">
|
||||||
Ports
|
Ports
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1211,7 +1209,7 @@ const DockerContainerRow: Component<{
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={container.networks && container.networks.length > 0}>
|
<Show when={container.networks && container.networks.length > 0}>
|
||||||
<div class="min-w-[220px] rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||||
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">
|
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">
|
||||||
Networks
|
Networks
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1238,7 +1236,7 @@ const DockerContainerRow: Component<{
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={hasPodmanMetadata()}>
|
<Show when={hasPodmanMetadata()}>
|
||||||
<div class="min-w-[220px] rounded border border-purple-200 bg-white/70 p-2 shadow-sm dark:border-purple-700/60 dark:bg-purple-950/20">
|
<div class="min-w-[220px] flex-1 rounded border border-purple-200 bg-white/70 p-2 shadow-sm dark:border-purple-700/60 dark:bg-purple-950/20">
|
||||||
<div class="text-[11px] font-medium uppercase tracking-wide text-purple-700 dark:text-purple-200">
|
<div class="text-[11px] font-medium uppercase tracking-wide text-purple-700 dark:text-purple-200">
|
||||||
Podman Metadata
|
Podman Metadata
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1272,7 +1270,7 @@ const DockerContainerRow: Component<{
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={hasBlockIo()}>
|
<Show when={hasBlockIo()}>
|
||||||
<div class="min-w-[220px] rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
<div class="min-w-[220px] flex-1 rounded border border-gray-200 bg-white/70 p-2 shadow-sm dark:border-gray-600/70 dark:bg-gray-900/30">
|
||||||
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">
|
<div class="text-[11px] font-medium uppercase tracking-wide text-gray-700 dark:text-gray-200">
|
||||||
Block I/O
|
Block I/O
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1379,12 +1377,18 @@ const DockerContainerRow: Component<{
|
||||||
Labels
|
Labels
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 flex flex-wrap gap-1 text-[11px] text-gray-600 dark:text-gray-300">
|
<div class="mt-1 flex flex-wrap gap-1 text-[11px] text-gray-600 dark:text-gray-300">
|
||||||
{Object.entries(container.labels!).map(([key, value]) => (
|
{Object.entries(container.labels!).map(([key, value]) => {
|
||||||
<span class="rounded bg-gray-200 px-1.5 py-0.5 text-gray-700 dark:bg-gray-700/60 dark:text-gray-200">
|
const fullLabel = value ? `${key}: ${value}` : key;
|
||||||
{key}
|
return (
|
||||||
<Show when={value}>: {value}</Show>
|
<span
|
||||||
</span>
|
class="max-w-full truncate rounded bg-gray-200 px-1.5 py-0.5 text-gray-700 dark:bg-gray-700/60 dark:text-gray-200"
|
||||||
))}
|
title={fullLabel}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
<Show when={value}>: {value}</Show>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -1409,7 +1413,7 @@ const DockerServiceRow: Component<{
|
||||||
|
|
||||||
const [customUrl, setCustomUrl] = createSignal<string | undefined>(props.customUrl);
|
const [customUrl, setCustomUrl] = createSignal<string | undefined>(props.customUrl);
|
||||||
const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false);
|
const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false);
|
||||||
const [expanded, setExpanded] = createSignal(rowExpandState.get(rowId) ?? false);
|
const expanded = createMemo(() => currentlyExpandedRowId() === rowId);
|
||||||
const editingUrlValue = createMemo(() => {
|
const editingUrlValue = createMemo(() => {
|
||||||
dockerEditingValuesVersion(); // Subscribe to changes
|
dockerEditingValuesVersion(); // Subscribe to changes
|
||||||
return dockerEditingValues.get(resourceId()) || '';
|
return dockerEditingValues.get(resourceId()) || '';
|
||||||
|
|
@ -1446,11 +1450,8 @@ const DockerServiceRow: Component<{
|
||||||
if (!hasTasks()) return;
|
if (!hasTasks()) return;
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (target.closest('a, button, input, [data-prevent-toggle]')) return;
|
if (target.closest('a, button, input, [data-prevent-toggle]')) return;
|
||||||
setExpanded((prev) => {
|
// Toggle: if this row is currently expanded, close it; otherwise open it (closing any other)
|
||||||
const next = !prev;
|
setCurrentlyExpandedRowId(prev => prev === rowId ? null : rowId);
|
||||||
rowExpandState.set(rowId, next);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const startEditingUrl = (event: MouseEvent) => {
|
const startEditingUrl = (event: MouseEvent) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue