mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-13 07:41:27 +00:00
refactor(backups): simplify filter bar layout
- Made search bar full-width and prominent - Removed redundant Status Filter (can use search) - Removed redundant Type Filter (overlaps with Source) - Removed Time Format toggle (rarely used) - Integrated ColumnPicker inline with filter groups - Layout now matches Dashboard/Overview pattern Reduces filter controls from ~10 groups to 5 for cleaner UX.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { Component, Show, For, createSignal, onMount, createEffect, onCleanup } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
import { SearchTipsPopover } from '@/components/shared/SearchTipsPopover';
|
||||
import { ColumnPicker } from '@/components/shared/ColumnPicker';
|
||||
import type { ColumnDef } from '@/hooks/useColumnVisibility';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import { createSearchHistoryManager } from '@/utils/searchHistory';
|
||||
|
||||
@@ -12,34 +14,27 @@ interface BackupsFilterProps {
|
||||
groupBy: () => 'date' | 'guest';
|
||||
setGroupBy: (value: 'date' | 'guest') => void;
|
||||
searchInputRef?: (el: HTMLInputElement) => void;
|
||||
typeFilter?: () => 'all' | 'VM' | 'LXC' | 'Host';
|
||||
setTypeFilter?: (value: 'all' | 'VM' | 'LXC' | 'Host') => void;
|
||||
hasHostBackups?: () => boolean;
|
||||
sortKey: () => string;
|
||||
setSortKey: (value: string) => void;
|
||||
sortDirection: () => 'asc' | 'desc';
|
||||
setSortDirection: (value: 'asc' | 'desc') => void;
|
||||
sortOptions?: { value: string; label: string }[];
|
||||
onReset?: () => void;
|
||||
statusFilter?: () => 'all' | 'verified' | 'unverified';
|
||||
setStatusFilter?: (value: 'all' | 'verified' | 'unverified') => void;
|
||||
// Time format toggle
|
||||
useRelativeTime?: () => boolean;
|
||||
setUseRelativeTime?: (value: boolean) => void;
|
||||
// Column visibility (optional)
|
||||
columnVisibility?: {
|
||||
availableToggles: () => ColumnDef[];
|
||||
isHiddenByUser: (id: string) => boolean;
|
||||
toggle: (id: string) => void;
|
||||
resetToDefaults: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
|
||||
const sortOptions = props.sortOptions ?? [
|
||||
{ value: 'backupTime', label: 'Time' },
|
||||
{ value: 'name', label: 'Guest Name' },
|
||||
{ value: 'node', label: 'Node' },
|
||||
{ value: 'name', label: 'Name' },
|
||||
{ value: 'vmid', label: 'VMID' },
|
||||
{ value: 'backupType', label: 'Backup Type' },
|
||||
{ value: 'size', label: 'Size' },
|
||||
{ value: 'storage', label: 'Storage' },
|
||||
{ value: 'verified', label: 'Verified' },
|
||||
{ value: 'type', label: 'Guest Type' },
|
||||
{ value: 'owner', label: 'Owner' },
|
||||
];
|
||||
const historyManager = createSearchHistoryManager(STORAGE_KEYS.BACKUPS_SEARCH_HISTORY);
|
||||
const [searchHistory, setSearchHistory] = createSignal<string[]>([]);
|
||||
@@ -106,204 +101,202 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
|
||||
suppressBlurCommit = false;
|
||||
});
|
||||
};
|
||||
|
||||
const hasActiveFilters = () =>
|
||||
props.search().trim() !== '' ||
|
||||
props.viewMode() !== 'all' ||
|
||||
props.groupBy() !== 'date' ||
|
||||
props.sortKey() !== 'backupTime' ||
|
||||
props.sortDirection() !== 'desc';
|
||||
|
||||
return (
|
||||
<Card class="backups-filter mb-3" padding="sm">
|
||||
<div class="flex flex-col lg:flex-row gap-3">
|
||||
{/* Search Bar */}
|
||||
<div class="flex gap-2 flex-1 items-center">
|
||||
<div class="relative flex-1">
|
||||
<input
|
||||
ref={(el) => {
|
||||
searchInputEl = el;
|
||||
props.searchInputRef?.(el);
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Search backups or node:nodename"
|
||||
value={props.search()}
|
||||
onInput={(e) => props.setSearch(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
commitSearchToHistory(e.currentTarget.value);
|
||||
closeHistory();
|
||||
} else if (e.key === 'ArrowDown' && searchHistory().length > 0) {
|
||||
e.preventDefault();
|
||||
setIsHistoryOpen(true);
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (suppressBlurCommit) {
|
||||
return;
|
||||
}
|
||||
const next = e.relatedTarget as HTMLElement | null;
|
||||
const interactingWithHistory = next
|
||||
? historyMenuRef?.contains(next) || historyToggleRef?.contains(next)
|
||||
: false;
|
||||
const interactingWithTips =
|
||||
next?.getAttribute('aria-controls') === 'backups-search-help';
|
||||
if (!interactingWithHistory && !interactingWithTips) {
|
||||
commitSearchToHistory(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
class="w-full pl-9 pr-16 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all"
|
||||
title="Search backups by name or filter by node"
|
||||
<div class="flex flex-col gap-3">
|
||||
{/* Row 1: Search Bar - Full width */}
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
ref={(el) => {
|
||||
searchInputEl = el;
|
||||
props.searchInputRef?.(el);
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Search backups... (e.g., media, vm-104, node:pve1)"
|
||||
value={props.search()}
|
||||
onInput={(e) => props.setSearch(e.currentTarget.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
commitSearchToHistory(e.currentTarget.value);
|
||||
closeHistory();
|
||||
} else if (e.key === 'ArrowDown' && searchHistory().length > 0) {
|
||||
e.preventDefault();
|
||||
setIsHistoryOpen(true);
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (suppressBlurCommit) {
|
||||
return;
|
||||
}
|
||||
const next = e.relatedTarget as HTMLElement | null;
|
||||
const interactingWithHistory = next
|
||||
? historyMenuRef?.contains(next) || historyToggleRef?.contains(next)
|
||||
: false;
|
||||
const interactingWithTips =
|
||||
next?.getAttribute('aria-controls') === 'backups-search-help';
|
||||
if (!interactingWithHistory && !interactingWithTips) {
|
||||
commitSearchToHistory(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
class="w-full pl-9 pr-20 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all"
|
||||
title="Search backups by name, VMID, or filter by node"
|
||||
/>
|
||||
<svg
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
<svg
|
||||
class="absolute left-3 top-2 h-4 w-4 text-gray-400 dark:text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
</svg>
|
||||
<Show when={props.search()}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-14 top-1/2 -translate-y-1/2 transform p-1 rounded-full
|
||||
bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-300
|
||||
hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/50 dark:hover:text-red-400
|
||||
transition-all duration-150 active:scale-90"
|
||||
onClick={() => props.setSearch('')}
|
||||
onMouseDown={markSuppressCommit}
|
||||
aria-label="Clear search"
|
||||
title="Clear search"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Show when={props.search()}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-9 top-1/2 -translate-y-1/2 transform text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
onClick={() => props.setSearch('')}
|
||||
onMouseDown={markSuppressCommit}
|
||||
aria-label="Clear search"
|
||||
title="Clear search"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Show>
|
||||
<div class="absolute inset-y-0 right-2 flex items-center gap-1">
|
||||
<button
|
||||
ref={(el) => (historyToggleRef = el)}
|
||||
type="button"
|
||||
class="flex h-6 w-6 items-center justify-center rounded-lg border border-transparent text-gray-400 transition-colors hover:border-gray-200 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:ring-offset-1 focus:ring-offset-white dark:text-gray-500 dark:hover:border-gray-700 dark:hover:text-gray-200 dark:focus:ring-blue-400/40 dark:focus:ring-offset-gray-900"
|
||||
onClick={() =>
|
||||
setIsHistoryOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (!next) {
|
||||
queueMicrotask(() => historyToggleRef?.blur());
|
||||
}
|
||||
return next;
|
||||
})
|
||||
}
|
||||
onMouseDown={markSuppressCommit}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isHistoryOpen()}
|
||||
title={
|
||||
searchHistory().length > 0
|
||||
? 'Show recent searches'
|
||||
: 'No recent searches yet'
|
||||
}
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l2.5 1.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Show search history</span>
|
||||
</button>
|
||||
<SearchTipsPopover
|
||||
popoverId="backups-search-help"
|
||||
intro="Quick examples"
|
||||
tips={[
|
||||
{ code: 'media', description: 'Backups with "media" in the name' },
|
||||
{ code: 'node:pve1', description: 'Show backups from a specific node' },
|
||||
{ code: 'vm-104', description: 'Locate backups for VM 104' },
|
||||
]}
|
||||
footerHighlight="node:pve1 vm-104"
|
||||
footerText="Mix terms to focus on the exact backups you need."
|
||||
triggerVariant="icon"
|
||||
buttonLabel="Search tips"
|
||||
openOnHover
|
||||
/>
|
||||
</div>
|
||||
<Show when={isHistoryOpen()}>
|
||||
<div
|
||||
ref={(el) => (historyMenuRef = el)}
|
||||
class="absolute left-0 right-0 top-full z-50 mt-2 w-full overflow-hidden rounded-lg border border-gray-200 bg-white text-sm shadow-xl dark:border-gray-700 dark:bg-gray-800"
|
||||
role="listbox"
|
||||
>
|
||||
<Show
|
||||
when={searchHistory().length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Your recent backup searches will show here.
|
||||
</div>
|
||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Show>
|
||||
<div class="absolute inset-y-0 right-2 flex items-center gap-1">
|
||||
<button
|
||||
ref={(el) => (historyToggleRef = el)}
|
||||
type="button"
|
||||
class={`flex h-7 w-7 items-center justify-center rounded-md transition-colors
|
||||
${isHistoryOpen()
|
||||
? 'bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300'
|
||||
}`}
|
||||
onClick={() =>
|
||||
setIsHistoryOpen((prev) => {
|
||||
const next = !prev;
|
||||
if (!next) {
|
||||
queueMicrotask(() => historyToggleRef?.blur());
|
||||
}
|
||||
>
|
||||
<div class="max-h-52 overflow-y-auto py-1">
|
||||
<For each={searchHistory()}>
|
||||
{(entry) => (
|
||||
<div class="flex items-center justify-between px-2 py-1.5 hover:bg-blue-50 dark:hover:bg-blue-900/20">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 truncate pr-2 text-left text-sm text-gray-700 transition-colors hover:text-blue-600 focus:outline-none dark:text-gray-200 dark:hover:text-blue-300"
|
||||
onClick={() => {
|
||||
props.setSearch(entry);
|
||||
commitSearchToHistory(entry);
|
||||
setIsHistoryOpen(false);
|
||||
focusSearchInput();
|
||||
}}
|
||||
onMouseDown={markSuppressCommit}
|
||||
>
|
||||
{entry}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:ring-offset-1 focus:ring-offset-white dark:text-gray-500 dark:hover:bg-gray-700/70 dark:hover:text-gray-200 dark:focus:ring-blue-400/40 dark:focus:ring-offset-gray-900"
|
||||
title="Remove from history"
|
||||
onClick={() => deleteHistoryEntry(entry)}
|
||||
onMouseDown={markSuppressCommit}
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Remove from history</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-center gap-2 border-t border-gray-200 px-3 py-2 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 focus:outline-none dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700/80 dark:hover:text-gray-200"
|
||||
onClick={clearHistory}
|
||||
onMouseDown={markSuppressCommit}
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3m-9 0h12"
|
||||
/>
|
||||
</svg>
|
||||
Clear history
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
return next;
|
||||
})
|
||||
}
|
||||
onMouseDown={markSuppressCommit}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isHistoryOpen()}
|
||||
title={
|
||||
searchHistory().length > 0
|
||||
? 'Show recent searches'
|
||||
: 'No recent searches yet'
|
||||
}
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<span class="sr-only">Show search history</span>
|
||||
</button>
|
||||
<SearchTipsPopover
|
||||
popoverId="backups-search-help"
|
||||
intro="Quick examples"
|
||||
tips={[
|
||||
{ code: 'media', description: 'Backups with "media" in the name' },
|
||||
{ code: 'node:pve1', description: 'Show backups from a specific node' },
|
||||
{ code: 'vm-104', description: 'Locate backups for VM 104' },
|
||||
]}
|
||||
footerHighlight="node:pve1 vm-104"
|
||||
footerText="Mix terms to focus on the exact backups you need."
|
||||
triggerVariant="icon"
|
||||
buttonLabel="Search tips"
|
||||
openOnHover
|
||||
/>
|
||||
</div>
|
||||
<Show when={isHistoryOpen()}>
|
||||
<div
|
||||
ref={(el) => (historyMenuRef = el)}
|
||||
class="absolute left-0 right-0 top-full z-50 mt-2 w-full overflow-hidden rounded-lg border border-gray-200 bg-white text-sm shadow-xl dark:border-gray-700 dark:bg-gray-800"
|
||||
role="listbox"
|
||||
>
|
||||
<Show
|
||||
when={searchHistory().length > 0}
|
||||
fallback={
|
||||
<div class="px-3 py-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Your recent backup searches will show here.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="max-h-52 overflow-y-auto py-1">
|
||||
<For each={searchHistory()}>
|
||||
{(entry) => (
|
||||
<div class="flex items-center justify-between px-2 py-1.5 hover:bg-blue-50 dark:hover:bg-blue-900/20">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 truncate pr-2 text-left text-sm text-gray-700 transition-colors hover:text-blue-600 focus:outline-none dark:text-gray-200 dark:hover:text-blue-300"
|
||||
onClick={() => {
|
||||
props.setSearch(entry);
|
||||
commitSearchToHistory(entry);
|
||||
setIsHistoryOpen(false);
|
||||
focusSearchInput();
|
||||
}}
|
||||
onMouseDown={markSuppressCommit}
|
||||
>
|
||||
{entry}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-1 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 focus:outline-none dark:text-gray-500 dark:hover:bg-gray-700/70 dark:hover:text-gray-200"
|
||||
title="Remove from history"
|
||||
onClick={() => deleteHistoryEntry(entry)}
|
||||
onMouseDown={markSuppressCommit}
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-center gap-2 border-t border-gray-200 px-3 py-2 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700/80 dark:hover:text-gray-200"
|
||||
onClick={clearHistory}
|
||||
onMouseDown={markSuppressCommit}
|
||||
>
|
||||
Clear history
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
{/* Row 2: Filters - Compact horizontal layout */}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{/* Source Filter */}
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
@@ -351,60 +344,6 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
|
||||
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
|
||||
{/* Type Filter - Only show when there are Host backups */}
|
||||
<Show
|
||||
when={
|
||||
props.hasHostBackups &&
|
||||
props.hasHostBackups() &&
|
||||
props.typeFilter &&
|
||||
props.setTypeFilter
|
||||
}
|
||||
>
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setTypeFilter!('all')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${props.typeFilter!() === 'all'
|
||||
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
All Types
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setTypeFilter!(props.typeFilter!() === 'VM' ? 'all' : 'VM')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${props.typeFilter!() === 'VM'
|
||||
? 'bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
VM
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setTypeFilter!(props.typeFilter!() === 'LXC' ? 'all' : 'LXC')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${props.typeFilter!() === 'LXC'
|
||||
? 'bg-white dark:bg-gray-800 text-green-600 dark:text-green-400 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
LXC
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setTypeFilter!(props.typeFilter!() === 'Host' ? 'all' : 'Host')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${props.typeFilter!() === 'Host'
|
||||
? 'bg-white dark:bg-gray-800 text-orange-600 dark:text-orange-400 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
PMG
|
||||
</button>
|
||||
</div>
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
</Show>
|
||||
|
||||
{/* Group By Filter */}
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
<button
|
||||
@@ -432,14 +371,11 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
|
||||
{/* Sort controls */}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Sort
|
||||
</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<select
|
||||
value={props.sortKey()}
|
||||
onChange={(e) => props.setSortKey(e.currentTarget.value)}
|
||||
class="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400"
|
||||
class="px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500"
|
||||
>
|
||||
{sortOptions.map((option) => (
|
||||
<option value={option.value}>{option.label}</option>
|
||||
@@ -469,102 +405,50 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setStatusFilter && props.setStatusFilter('all')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${props.statusFilter && props.statusFilter() === 'all'
|
||||
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setStatusFilter && props.setStatusFilter('verified')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${props.statusFilter && props.statusFilter() === 'verified'
|
||||
? 'bg-white dark:bg-gray-800 text-green-600 dark:text-green-400 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
Verified
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setStatusFilter && props.setStatusFilter('unverified')}
|
||||
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${props.statusFilter && props.statusFilter() === 'unverified'
|
||||
? 'bg-white dark:bg-gray-800 text-amber-600 dark:text-amber-400 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
Unverified
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
|
||||
{/* Time Format Toggle */}
|
||||
<Show when={props.useRelativeTime && props.setUseRelativeTime}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.setUseRelativeTime!(!props.useRelativeTime!())}
|
||||
title={props.useRelativeTime!() ? 'Switch to absolute time' : 'Switch to relative time'}
|
||||
class={`inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-lg transition-all ${props.useRelativeTime!()
|
||||
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border border-blue-200 dark:border-blue-800'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">{props.useRelativeTime!() ? 'Relative' : 'Absolute'}</span>
|
||||
</button>
|
||||
{/* Column Picker - Only if provided */}
|
||||
<Show when={props.columnVisibility}>
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
<ColumnPicker
|
||||
columns={props.columnVisibility!.availableToggles()}
|
||||
isHidden={props.columnVisibility!.isHiddenByUser}
|
||||
onToggle={props.columnVisibility!.toggle}
|
||||
onReset={props.columnVisibility!.resetToDefaults}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
{/* Reset Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
if (props.onReset) {
|
||||
props.onReset();
|
||||
} else {
|
||||
props.setSearch('');
|
||||
props.setViewMode('all');
|
||||
props.setGroupBy('date');
|
||||
if (props.setStatusFilter) props.setStatusFilter('all');
|
||||
}
|
||||
}}
|
||||
title="Reset all filters"
|
||||
class={`flex items-center justify-center px-2.5 py-1 text-xs font-medium rounded-lg transition-colors ${props.search().trim() !== '' ||
|
||||
props.viewMode() !== 'all' ||
|
||||
props.groupBy() !== 'date' ||
|
||||
props.sortKey() !== 'backupTime' ||
|
||||
props.sortDirection() !== 'desc' ||
|
||||
(props.typeFilter && props.typeFilter() !== 'all') ||
|
||||
(props.statusFilter && props.statusFilter() !== 'all')
|
||||
? 'text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/50 hover:bg-blue-200 dark:hover:bg-blue-900/70'
|
||||
: 'text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
{/* Reset Button - Only show when filters are active */}
|
||||
<Show when={hasActiveFilters()}>
|
||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (props.onReset) {
|
||||
props.onReset();
|
||||
} else {
|
||||
props.setSearch('');
|
||||
props.setViewMode('all');
|
||||
props.setGroupBy('date');
|
||||
}
|
||||
}}
|
||||
title="Reset all filters"
|
||||
class="flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-lg transition-colors
|
||||
text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/50 hover:bg-blue-200 dark:hover:bg-blue-900/70"
|
||||
>
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
||||
<path d="M21 3v5h-5" />
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
||||
<path d="M8 16H3v5" />
|
||||
</svg>
|
||||
<span class="ml-1 hidden sm:inline">Reset</span>
|
||||
</button>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
||||
<path d="M21 3v5h-5" />
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
||||
<path d="M8 16H3v5" />
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -11,7 +11,6 @@ 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';
|
||||
@@ -2016,31 +2015,15 @@ const UnifiedBackups: Component = () => {
|
||||
groupBy={groupByMode}
|
||||
setGroupBy={setGroupByMode}
|
||||
searchInputRef={(el) => (searchInputRef = el)}
|
||||
typeFilter={typeFilter}
|
||||
setTypeFilter={setTypeFilter}
|
||||
statusFilter={statusFilter}
|
||||
setStatusFilter={setStatusFilter}
|
||||
hasHostBackups={hasHostBackups}
|
||||
sortOptions={sortKeyOptions}
|
||||
sortKey={sortKey}
|
||||
setSortKey={(value) => setSortKey(value as BackupSortKey)}
|
||||
sortDirection={sortDirection}
|
||||
setSortDirection={setSortDirection}
|
||||
onReset={resetFilters}
|
||||
useRelativeTime={useRelativeTime}
|
||||
setUseRelativeTime={setUseRelativeTime}
|
||||
columnVisibility={columnVisibility}
|
||||
/>
|
||||
|
||||
{/* 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;">
|
||||
|
||||
Reference in New Issue
Block a user