Pulse/frontend-modern/src/components/shared/ColumnPicker.tsx
rcourtman a15dc6b899 feat: Toggleable table columns with rich tooltips for Proxmox dashboard
Replace drawer-based info display with inline columns that can be toggled:
- Add IP, Uptime, Node, Backup, OS, Tags columns (user-toggleable)
- Add ColumnPicker dropdown to show/hide columns with localStorage persistence
- Columns auto-show based on screen width using priority system
- Remove GuestDrawer - all info now visible inline or via tooltips

Rich hover tooltips:
- Disk bar: Shows all mount points with usage %, color-coded by severity
- Memory bar: Shows used/free/balloon/swap breakdown
- IP column: Shows network icon + count, hover for interfaces, MACs, IPs, traffic

Also:
- Create useColumnVisibility hook for responsive column management
- Create ColumnPicker component for column toggle UI
- Update drawer layouts in Hosts/Docker tabs for consistency
2025-12-05 12:12:56 +00:00

127 lines
4.9 KiB
TypeScript

import { Component, Show, For, createSignal, onCleanup, createEffect } from 'solid-js';
import type { ColumnDef } from '@/hooks/useColumnVisibility';
interface ColumnPickerProps {
/** Columns that can be toggled */
columns: ColumnDef[];
/** Check if a column is currently hidden */
isHidden: (id: string) => boolean;
/** Toggle a column's visibility */
onToggle: (id: string) => void;
/** Reset all columns to visible */
onReset?: () => void;
}
export const ColumnPicker: Component<ColumnPickerProps> = (props) => {
const [isOpen, setIsOpen] = createSignal(false);
let containerRef: HTMLDivElement | undefined;
// Close on click outside
const handleClickOutside = (e: MouseEvent) => {
if (containerRef && !containerRef.contains(e.target as Node)) {
setIsOpen(false);
}
};
createEffect(() => {
if (isOpen()) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}
});
onCleanup(() => {
document.removeEventListener('mousedown', handleClickOutside);
});
// Count how many are hidden
const hiddenCount = () => props.columns.filter(c => props.isHidden(c.id)).length;
return (
<div ref={containerRef} class="relative">
<button
type="button"
onClick={() => setIsOpen(!isOpen())}
class={`inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-lg transition-all
${isOpen()
? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
title="Choose which columns to display"
>
{/* Columns icon */}
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 4v16M15 4v16M4 9h16M4 15h16" />
</svg>
<span>Columns</span>
<Show when={hiddenCount() > 0}>
<span class="ml-0.5 px-1.5 py-0.5 text-[10px] font-semibold rounded-full bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-200">
{hiddenCount()} hidden
</span>
</Show>
</button>
<Show when={isOpen()}>
<div
class="absolute right-0 mt-1 w-56 rounded-lg border border-gray-200 bg-white shadow-xl z-50
dark:border-gray-700 dark:bg-gray-800"
>
<div class="px-3 py-2 border-b border-gray-100 dark:border-gray-700">
<div class="flex items-center justify-between">
<span class="text-xs font-medium text-gray-700 dark:text-gray-200">Show Columns</span>
<Show when={props.onReset && hiddenCount() > 0}>
<button
type="button"
onClick={() => props.onReset?.()}
class="text-[10px] text-blue-600 dark:text-blue-400 hover:underline"
>
Show all
</button>
</Show>
</div>
</div>
<div class="max-h-64 overflow-y-auto py-1">
<For each={props.columns}>
{(col) => {
const isChecked = () => !props.isHidden(col.id);
return (
<label
class="flex items-center gap-2.5 px-3 py-2 cursor-pointer
hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<input
type="checkbox"
checked={isChecked()}
onChange={() => props.onToggle(col.id)}
class="w-3.5 h-3.5 rounded border-gray-300 text-blue-600
focus:ring-blue-500 focus:ring-offset-0
dark:border-gray-600 dark:bg-gray-700 dark:checked:bg-blue-600"
/>
<span class={`text-sm ${isChecked() ? 'text-gray-700 dark:text-gray-200' : 'text-gray-400 dark:text-gray-500'}`}>
{col.label}
</span>
<Show when={col.priority !== 'essential'}>
<span class="ml-auto text-[10px] text-gray-400 dark:text-gray-500">
{col.priority === 'secondary' && 'md+'}
{col.priority === 'supplementary' && 'lg+'}
{col.priority === 'detailed' && 'xl+'}
</span>
</Show>
</label>
);
}}
</For>
</div>
<Show when={props.columns.length === 0}>
<div class="px-3 py-4 text-xs text-gray-500 dark:text-gray-400 text-center">
No columns available to toggle at this screen size
</div>
</Show>
</div>
</Show>
</div>
);
};