mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 11:46:28 +00:00
refactor: use search field for node/PBS filtering instead of duplicate logic
- Replace separate selectedNode/selectedPBSInstance state with search-based filtering - Add node: prefix to search when node/PBS cards clicked - Lock search field (greyed out) when auto-populated from node selection - Reuse existing search filter logic instead of duplicating filtering code - Clear node filter updates search field instead of separate state - Much cleaner implementation leveraging existing search capabilities
This commit is contained in:
@@ -44,7 +44,28 @@ const UnifiedBackups: Component = () => {
|
||||
const [selectedDateRange, setSelectedDateRange] = createSignal<{ start: number; end: number } | null>(null);
|
||||
const [chartTimeRange, setChartTimeRange] = createSignal(30);
|
||||
const [tooltip, setTooltip] = createSignal<{ text: string; x: number; y: number } | null>(null);
|
||||
const [selectedPBSInstance, setSelectedPBSInstance] = createSignal<string | null>(null);
|
||||
const [isSearchLocked, setIsSearchLocked] = createSignal(false);
|
||||
|
||||
// Extract PBS instance from search term
|
||||
const selectedPBSInstance = createMemo(() => {
|
||||
const search = searchTerm();
|
||||
const match = search.match(/node:(\S+)/);
|
||||
if (match && state.pbs?.some(pbs => pbs.name === match[1])) {
|
||||
return match[1];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Auto-set backup type filter when PBS instance is selected
|
||||
createEffect(() => {
|
||||
const pbsInstance = selectedPBSInstance();
|
||||
if (pbsInstance) {
|
||||
setBackupTypeFilter('remote');
|
||||
} else if (!isSearchLocked()) {
|
||||
setBackupTypeFilter('all');
|
||||
}
|
||||
});
|
||||
|
||||
const [showFilters, setShowFilters] = createLocalStorageBooleanSignal(
|
||||
STORAGE_KEYS.BACKUPS_SHOW_FILTERS,
|
||||
false // Default to collapsed
|
||||
@@ -54,18 +75,6 @@ const UnifiedBackups: Component = () => {
|
||||
false // Default to absolute time
|
||||
);
|
||||
|
||||
// Auto-set backup type filter to 'remote' when a PBS instance is selected
|
||||
createEffect(() => {
|
||||
const pbsInstance = selectedPBSInstance();
|
||||
if (pbsInstance) {
|
||||
// When a PBS instance is selected, only show remote backups
|
||||
setBackupTypeFilter('remote');
|
||||
} else {
|
||||
// When deselected, reset to show all
|
||||
setBackupTypeFilter('all');
|
||||
}
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
const getDaySuffix = (day: number) => {
|
||||
if (day >= 11 && day <= 13) return 'th';
|
||||
@@ -489,6 +498,7 @@ const UnifiedBackups: Component = () => {
|
||||
// Reset filters
|
||||
const resetFilters = () => {
|
||||
setSearchTerm('');
|
||||
setIsSearchLocked(false);
|
||||
setTypeFilter('all');
|
||||
setBackupTypeFilter('all');
|
||||
setSortKey('backupTime');
|
||||
@@ -690,7 +700,10 @@ const UnifiedBackups: Component = () => {
|
||||
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Showing only backups from PBS server: <strong>{selectedPBSInstance()}</strong></span>
|
||||
<button
|
||||
onClick={() => setSelectedPBSInstance(null)}
|
||||
onClick={() => {
|
||||
setSearchTerm('');
|
||||
setIsSearchLocked(false);
|
||||
}}
|
||||
class="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
Show all backups
|
||||
@@ -703,10 +716,26 @@ const UnifiedBackups: Component = () => {
|
||||
<div
|
||||
class="flex-1 min-w-[250px] cursor-pointer transition-transform hover:scale-[1.02]"
|
||||
onClick={() => {
|
||||
// Toggle selection - click again to deselect
|
||||
setSelectedPBSInstance(
|
||||
selectedPBSInstance() === instance.name ? null : instance.name
|
||||
);
|
||||
const currentSearch = searchTerm();
|
||||
const nodeFilter = `node:${instance.name}`;
|
||||
|
||||
// Check if this PBS filter is already in the search
|
||||
if (currentSearch.includes(nodeFilter)) {
|
||||
// Remove the PBS filter
|
||||
setSearchTerm(currentSearch.replace(nodeFilter, '').trim().replace(/,\s*,/g, ',').replace(/^,|,$/g, ''));
|
||||
setIsSearchLocked(false);
|
||||
} else {
|
||||
// Clear any existing node: filters and add the new one
|
||||
const cleanedSearch = currentSearch.replace(/node:\S+/g, '').trim().replace(/,\s*,/g, ',').replace(/^,|,$/g, '');
|
||||
const newSearch = cleanedSearch ? `${cleanedSearch}, ${nodeFilter}` : nodeFilter;
|
||||
setSearchTerm(newSearch);
|
||||
setIsSearchLocked(true);
|
||||
|
||||
// Expand filters if collapsed
|
||||
if (!showFilters()) {
|
||||
setShowFilters(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PBSCard
|
||||
@@ -1152,10 +1181,16 @@ const UnifiedBackups: Component = () => {
|
||||
type="text"
|
||||
placeholder="Search VMID, Name, Node, Storage (use ',' for OR)"
|
||||
value={searchTerm()}
|
||||
onInput={(e) => setSearchTerm(e.currentTarget.value)}
|
||||
class="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
onInput={(e) => {
|
||||
if (!isSearchLocked()) {
|
||||
setSearchTerm(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
disabled={isSearchLocked()}
|
||||
class={`w-full pl-9 pr-3 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"
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all
|
||||
${isSearchLocked() ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
/>
|
||||
<svg class="absolute left-3 top-2.5 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" />
|
||||
|
||||
@@ -24,7 +24,17 @@ type StatusMode = 'all' | 'running' | 'stopped';
|
||||
export function Dashboard(props: DashboardProps) {
|
||||
const { connected, activeAlerts, initialDataReceived } = useWebSocket();
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
|
||||
const [isSearchLocked, setIsSearchLocked] = createSignal(false);
|
||||
|
||||
// Extract selected node from search term
|
||||
const selectedNode = createMemo(() => {
|
||||
const searchStr = search();
|
||||
const match = searchStr.match(/node:(\S+)/);
|
||||
if (match && props.nodes.some(node => node.name === match[1])) {
|
||||
return match[1];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Initialize from localStorage with proper type checking
|
||||
const storedViewMode = localStorage.getItem('dashboardViewMode');
|
||||
@@ -131,6 +141,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
if (search().trim() || sortKey() !== 'vmid' || sortDirection() !== 'asc') {
|
||||
// Clear search and reset filters
|
||||
setSearch('');
|
||||
setIsSearchLocked(false);
|
||||
setSortKey('vmid');
|
||||
setSortDirection('asc');
|
||||
|
||||
@@ -175,11 +186,6 @@ export function Dashboard(props: DashboardProps) {
|
||||
const filteredGuests = createMemo(() => {
|
||||
let guests = allGuests();
|
||||
|
||||
// Filter by selected node
|
||||
if (selectedNode()) {
|
||||
guests = guests.filter(g => g.node === selectedNode());
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (viewMode() === 'vm') {
|
||||
guests = guests.filter(g => g.type === 'qemu');
|
||||
@@ -329,7 +335,10 @@ export function Dashboard(props: DashboardProps) {
|
||||
<div class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>Showing guests for node: <strong>{selectedNode()}</strong></span>
|
||||
<button
|
||||
onClick={() => setSelectedNode(null)}
|
||||
onClick={() => {
|
||||
setSearch('');
|
||||
setIsSearchLocked(false);
|
||||
}}
|
||||
class="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
Clear filter
|
||||
@@ -346,16 +355,32 @@ export function Dashboard(props: DashboardProps) {
|
||||
<div
|
||||
class="flex-1 min-w-[250px] cursor-pointer transition-transform hover:scale-[1.02]"
|
||||
onClick={() => {
|
||||
// Toggle selection - click again to deselect
|
||||
setSelectedNode(
|
||||
selectedNode() === node.name ? null : node.name
|
||||
);
|
||||
const currentSearch = search();
|
||||
const nodeFilter = `node:${node.name}`;
|
||||
|
||||
// Check if this node filter is already in the search
|
||||
if (currentSearch.includes(nodeFilter)) {
|
||||
// Remove the node filter
|
||||
setSearch(currentSearch.replace(nodeFilter, '').trim().replace(/,\s*,/g, ',').replace(/^,|,$/g, ''));
|
||||
setIsSearchLocked(false);
|
||||
} else {
|
||||
// Clear any existing node: filters and add the new one
|
||||
const cleanedSearch = currentSearch.replace(/node:\w+/g, '').trim().replace(/,\s*,/g, ',').replace(/^,|,$/g, '');
|
||||
const newSearch = cleanedSearch ? `${cleanedSearch}, ${nodeFilter}` : nodeFilter;
|
||||
setSearch(newSearch);
|
||||
setIsSearchLocked(true);
|
||||
|
||||
// Expand filters if collapsed
|
||||
if (!showFilters()) {
|
||||
setShowFilters(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ComponentErrorBoundary name="NodeCard">
|
||||
<NodeCard
|
||||
node={node}
|
||||
isSelected={selectedNode() === node.name}
|
||||
isSelected={search().includes(`node:${node.name}`)}
|
||||
/>
|
||||
</ComponentErrorBoundary>
|
||||
</div>
|
||||
@@ -442,10 +467,16 @@ export function Dashboard(props: DashboardProps) {
|
||||
type="text"
|
||||
placeholder="Search: name, jellyfin,plex, or cpu>80"
|
||||
value={search()}
|
||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||
class="w-full pl-9 pr-9 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
onInput={(e) => {
|
||||
if (!isSearchLocked()) {
|
||||
setSearch(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
disabled={isSearchLocked()}
|
||||
class={`w-full pl-9 pr-9 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"
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all
|
||||
${isSearchLocked() ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
title="Search guests or use filters like cpu>80"
|
||||
/>
|
||||
<svg class="absolute left-3 top-2.5 h-4 w-4 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -491,6 +522,7 @@ export function Dashboard(props: DashboardProps) {
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearch('');
|
||||
setIsSearchLocked(false);
|
||||
setSortKey('vmid');
|
||||
setSortDirection('asc');
|
||||
setViewMode('all');
|
||||
|
||||
Reference in New Issue
Block a user