feat(backups): add column visibility toggle with Comment column

- Add useColumnVisibility hook to Backups table
- Add ColumnPicker component for toggling columns
- Add Comment column for PBS backups (hidden by default)
- Owner, Size, Storage, Verified columns now toggleable
- Column visibility persisted to localStorage

PBS backups with comments now show the comment when the column
is enabled via the Columns picker.
This commit is contained in:
rcourtman
2025-12-26 10:30:45 +00:00
parent c0b5149360
commit 5ed2a70d2e
3 changed files with 62 additions and 8 deletions
@@ -11,9 +11,11 @@ import { Card } from '@/components/shared/Card';
import { EmptyState } from '@/components/shared/EmptyState';
import { SectionHeader } from '@/components/shared/SectionHeader';
import { showTooltip, hideTooltip } from '@/components/shared/Tooltip';
import { ColumnPicker } from '@/components/shared/ColumnPicker';
import type { BackupType, GuestType, UnifiedBackup } from '@/types/backups';
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useColumnVisibility, type ColumnDef } from '@/hooks/useColumnVisibility';
import { logger } from '@/utils/logger';
type BackupSortKey = keyof Pick<
@@ -162,6 +164,31 @@ const UnifiedBackups: Component = () => {
false, // Default to absolute time
);
// Column visibility definitions for the backup table
const BACKUP_COLUMNS: ColumnDef[] = [
{ id: 'vmid', label: 'VMID/ID', priority: 'essential' },
{ id: 'type', label: 'Type', priority: 'essential' },
{ id: 'name', label: 'Name', priority: 'essential' },
{ id: 'node', label: 'Node', priority: 'essential' },
{ id: 'owner', label: 'Owner', priority: 'secondary', toggleable: true },
{ id: 'backupTime', label: 'Time', priority: 'essential' },
{ id: 'size', label: 'Size', priority: 'secondary', toggleable: true },
{ id: 'backupType', label: 'Backup Type', priority: 'essential' },
{ id: 'storage', label: 'Location', priority: 'secondary', toggleable: true },
{ id: 'verified', label: 'Verified', priority: 'secondary', toggleable: true },
{ id: 'comment', label: 'Comment', priority: 'supplementary', toggleable: true },
{ id: 'details', label: 'Details', priority: 'essential' },
];
// Column visibility with persistence - hide less useful columns by default
const columnVisibility = useColumnVisibility(
STORAGE_KEYS.BACKUPS_HIDDEN_COLUMNS,
BACKUP_COLUMNS,
['comment'] // Comment hidden by default (often empty)
);
const visibleColumns = columnVisibility.visibleColumns;
const isColumnVisible = (id: string) => visibleColumns().some(col => col.id === id);
// Helper functions
const getDaySuffix = (day: number) => {
if (day >= 11 && day <= 13) return 'th';
@@ -456,6 +483,7 @@ const UnifiedBackups: Component = () => {
protected: backup.protected || false,
encrypted: isEncrypted,
owner: backup.owner,
comment: backup.comment,
});
});
@@ -2003,6 +2031,16 @@ const UnifiedBackups: Component = () => {
setUseRelativeTime={setUseRelativeTime}
/>
{/* Column Visibility Toggle */}
<div class="flex justify-end mb-2 -mt-2">
<ColumnPicker
columns={columnVisibility.availableToggles()}
isHidden={columnVisibility.isHiddenByUser}
onToggle={columnVisibility.toggle}
onReset={columnVisibility.resetToDefaults}
/>
</div>
{/* Table */}
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
@@ -2107,7 +2145,7 @@ const UnifiedBackups: Component = () => {
>
Node {sortKey() === 'node' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}>
<Show when={isColumnVisible('owner') && (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote')}>
<th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('owner')}
@@ -2121,7 +2159,7 @@ const UnifiedBackups: Component = () => {
>
Time {sortKey() === 'backupTime' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<Show when={backupTypeFilter() !== 'snapshot'}>
<Show when={isColumnVisible('size') && backupTypeFilter() !== 'snapshot'}>
<th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('size')}
@@ -2136,7 +2174,7 @@ const UnifiedBackups: Component = () => {
Backup{' '}
{sortKey() === 'backupType' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
<Show when={backupTypeFilter() !== 'snapshot'}>
<Show when={isColumnVisible('storage') && backupTypeFilter() !== 'snapshot'}>
<th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('storage')}
@@ -2145,7 +2183,7 @@ const UnifiedBackups: Component = () => {
{sortKey() === 'storage' && (sortDirection() === 'asc' ? '▲' : '▼')}
</th>
</Show>
<Show when={backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'}>
<Show when={isColumnVisible('verified') && (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote')}>
<th
class="px-2 py-1.5 text-center text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={() => handleSort('verified')}
@@ -2154,6 +2192,11 @@ const UnifiedBackups: Component = () => {
{sortKey() === 'verified' && (sortDirection() === 'asc' ? ' ▲' : ' ▼')}
</th>
</Show>
<Show when={isColumnVisible('comment') && (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote')}>
<th class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider">
Comment
</th>
</Show>
<th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider"
>
@@ -2202,7 +2245,7 @@ const UnifiedBackups: Component = () => {
<td class="p-0.5 px-1.5 text-sm align-middle">{item.node}</td>
<Show
when={
backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'
isColumnVisible('owner') && (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote')
}
>
<td class="p-0.5 px-1.5 text-xs align-middle text-gray-500 dark:text-gray-400">
@@ -2214,7 +2257,7 @@ const UnifiedBackups: Component = () => {
>
{formatTime(item.backupTime * 1000)}
</td>
<Show when={backupTypeFilter() !== 'snapshot'}>
<Show when={isColumnVisible('size') && backupTypeFilter() !== 'snapshot'}>
<td
class={`p-0.5 px-1.5 align-middle ${getSizeColor(item.size)}`}
>
@@ -2286,7 +2329,7 @@ const UnifiedBackups: Component = () => {
</Show>
</div>
</td>
<Show when={backupTypeFilter() !== 'snapshot'}>
<Show when={isColumnVisible('storage') && backupTypeFilter() !== 'snapshot'}>
<td class="p-0.5 px-1.5 text-sm align-middle">
{item.storage ||
(item.datastore &&
@@ -2298,7 +2341,7 @@ const UnifiedBackups: Component = () => {
</Show>
<Show
when={
backupTypeFilter() === 'all' || backupTypeFilter() === 'remote'
isColumnVisible('verified') && (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote')
}
>
<td class="p-0.5 px-1.5 text-center align-middle">
@@ -2346,6 +2389,15 @@ const UnifiedBackups: Component = () => {
)}
</td>
</Show>
<Show
when={
isColumnVisible('comment') && (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote')
}
>
<td class="p-0.5 px-1.5 text-xs align-middle text-gray-500 dark:text-gray-400 max-w-[150px] truncate" title={item.comment || ''}>
{item.comment || '-'}
</td>
</Show>
<td
class="p-0.5 px-1.5 align-middle"
onMouseEnter={(e) => {
+1
View File
@@ -20,4 +20,5 @@ export interface UnifiedBackup {
protected: boolean;
encrypted?: boolean;
owner?: string;
comment?: string; // PBS backup comment (only available from direct PBS)
}
@@ -114,6 +114,7 @@ export const STORAGE_KEYS = {
DASHBOARD_HIDDEN_COLUMNS: 'dashboardHiddenColumns',
HOSTS_HIDDEN_COLUMNS: 'hostsHiddenColumns',
DOCKER_HIDDEN_COLUMNS: 'dockerHiddenColumns',
BACKUPS_HIDDEN_COLUMNS: 'backupsHiddenColumns',
// Resources search
RESOURCES_SEARCH_HISTORY: 'resourcesSearchHistory',