mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 03:04:03 +00:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user