feat: consistent filter UX across all tabs

- Deselectable radio toggles on all filter tabs
- Blue reset button when filters are active
- Clean search placeholders with help tooltips
- Working tooltips with proper styling on Dashboard tab
- Better placeholder text: "Search or filter guests..."
This commit is contained in:
rcourtman
2025-10-01 16:33:18 +00:00
parent a236244730
commit 0e97303431
3 changed files with 64 additions and 47 deletions
@@ -114,7 +114,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
</button>
<button
type="button"
onClick={() => props.setViewMode('snapshot')}
onClick={() => props.setViewMode(props.viewMode() === 'snapshot' ? 'all' : 'snapshot')}
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
props.viewMode() === 'snapshot'
? 'bg-white dark:bg-gray-800 text-yellow-600 dark:text-yellow-400 shadow-sm'
@@ -125,7 +125,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
</button>
<button
type="button"
onClick={() => props.setViewMode('pve')}
onClick={() => props.setViewMode(props.viewMode() === 'pve' ? 'all' : 'pve')}
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
props.viewMode() === 'pve'
? 'bg-white dark:bg-gray-800 text-orange-600 dark:text-orange-400 shadow-sm'
@@ -136,7 +136,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
</button>
<button
type="button"
onClick={() => props.setViewMode('pbs')}
onClick={() => props.setViewMode(props.viewMode() === 'pbs' ? 'all' : 'pbs')}
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
props.viewMode() === 'pbs'
? 'bg-white dark:bg-gray-800 text-purple-600 dark:text-purple-400 shadow-sm'
@@ -172,7 +172,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
</button>
<button
type="button"
onClick={() => props.setTypeFilter!('VM')}
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'
@@ -183,7 +183,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
</button>
<button
type="button"
onClick={() => props.setTypeFilter!('LXC')}
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'
@@ -194,7 +194,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
</button>
<button
type="button"
onClick={() => props.setTypeFilter!('Host')}
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'
@@ -222,7 +222,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
</button>
<button
type="button"
onClick={() => props.setGroupBy('guest')}
onClick={() => props.setGroupBy(props.groupBy() === 'guest' ? 'date' : 'guest')}
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
props.groupBy() === 'guest'
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
@@ -287,9 +287,16 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
}
}}
title="Reset all filters"
class="flex items-center justify-center px-2.5 py-1 text-xs font-medium text-gray-600 dark:text-gray-400
bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600
rounded-lg transition-colors"
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')
? '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"
@@ -306,21 +313,6 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
</svg>
<span class="ml-1 hidden sm:inline">Reset</span>
</button>
{/* Active Indicator */}
<Show
when={
props.search().trim() !== '' ||
props.viewMode() !== 'all' ||
props.groupBy() !== 'date' ||
props.sortKey() !== 'backupTime' ||
props.sortDirection() !== 'desc'
}
>
<span class="text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-2 py-0.5 rounded-full font-medium">
Active
</span>
</Show>
</div>
</div>
</Card>
@@ -1,5 +1,6 @@
import { Component, Show } from 'solid-js';
import { Card } from '@/components/shared/Card';
import { showTooltip, hideTooltip } from '@/components/shared/Tooltip';
interface DashboardFilterProps {
search: () => string;
@@ -21,18 +22,17 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
<Card class="dashboard-filter mb-3" padding="sm">
<div class="flex flex-col lg:flex-row gap-3">
{/* Search Bar */}
<div class="flex gap-2 flex-1">
<div class="flex gap-2 flex-1 items-center">
<div class="relative flex-1">
<input
ref={props.searchInputRef}
type="text"
placeholder="Search by name, cpu>80, memory<20, tags:prod, node:pve1"
placeholder="Search or filter guests..."
value={props.search()}
onInput={(e) => props.setSearch(e.currentTarget.value)}
class={`w-full pl-9 pr-9 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
class={`w-full pl-9 pr-9 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 guests or use filters like cpu>80"
/>
<svg
class="absolute left-3 top-2 h-4 w-4 text-gray-400 dark:text-gray-500"
@@ -66,6 +66,40 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
</button>
</Show>
</div>
{/* Help Icon with Tooltip */}
<button
type="button"
class="flex-shrink-0 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
onMouseEnter={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const tooltipContent = `
<div class="space-y-2 p-1">
<div class="font-semibold mb-2">Search Examples:</div>
<div class="space-y-1">
<div><span class="font-mono bg-gray-700 px-1 rounded">media</span> - Find guests with "media" in name</div>
<div><span class="font-mono bg-gray-700 px-1 rounded">cpu>80</span> - Guests using over 80% CPU</div>
<div><span class="font-mono bg-gray-700 px-1 rounded">memory<20</span> - Guests using under 20% memory</div>
<div><span class="font-mono bg-gray-700 px-1 rounded">tags:prod</span> - Filter by tag</div>
<div><span class="font-mono bg-gray-700 px-1 rounded">node:pve1</span> - Filter by node</div>
</div>
</div>
`;
showTooltip(tooltipContent, rect.left, rect.top);
}}
onMouseLeave={() => hideTooltip()}
onClick={(e) => e.preventDefault()}
aria-label="Search help"
>
<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="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
</div>
{/* Filters */}
@@ -107,7 +107,7 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
</button>
<button
type="button"
onClick={() => props.setGroupBy!('storage')}
onClick={() => props.setGroupBy!(props.groupBy!() === 'storage' ? 'node' : 'storage')}
class={`px-2.5 py-1 text-xs font-medium rounded-md transition-all ${
props.groupBy!() === 'storage'
? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm'
@@ -168,9 +168,14 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
if (props.setGroupBy) props.setGroupBy('node');
}}
title="Reset all filters"
class="flex items-center justify-center px-2.5 py-1 text-xs font-medium text-gray-600 dark:text-gray-400
bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600
rounded-lg transition-colors"
class={`flex items-center justify-center px-2.5 py-1 text-xs font-medium rounded-lg transition-colors ${
props.search().trim() !== '' ||
props.sortKey() !== 'name' ||
props.sortDirection() !== 'asc' ||
(props.groupBy && props.groupBy!() !== 'node')
? '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"
@@ -187,20 +192,6 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
</svg>
<span class="ml-1 hidden sm:inline">Reset</span>
</button>
{/* Active Indicator */}
<Show
when={
props.search().trim() !== '' ||
props.sortKey() !== 'name' ||
props.sortDirection() !== 'asc' ||
(props.groupBy && props.groupBy!() !== 'node')
}
>
<span class="text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-2 py-0.5 rounded-full font-medium">
Active
</span>
</Show>
</div>
</div>
</Card>