feat: Add 'Needs Backup' filter to Dashboard
Add a new filter button that shows only guests with stale, critical, or missing backups. This makes it easy to identify which VMs and containers need attention for backup scheduling. - Adds backupMode state with 'all' and 'needs-backup' options - Filters out templates (they don't need backups) - Uses existing getBackupInfo() thresholds (>24h stale, >72h critical) - Integrates with Reset button and Escape key handling - Persists filter state in localStorage Related to #762
This commit is contained in:
parent
6c76823754
commit
b70a4eba9d
2 changed files with 46 additions and 1 deletions
|
|
@ -20,6 +20,7 @@ import { getNodeDisplayName } from '@/utils/nodes';
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||||
|
import { getBackupInfo } from '@/utils/format';
|
||||||
|
|
||||||
type GuestMetadataRecord = Record<string, GuestMetadata>;
|
type GuestMetadataRecord = Record<string, GuestMetadata>;
|
||||||
type IdleCallbackHandle = number;
|
type IdleCallbackHandle = number;
|
||||||
|
|
@ -196,6 +197,7 @@ interface DashboardProps {
|
||||||
|
|
||||||
type ViewMode = 'all' | 'vm' | 'lxc';
|
type ViewMode = 'all' | 'vm' | 'lxc';
|
||||||
type StatusMode = 'all' | 'running' | 'degraded' | 'stopped';
|
type StatusMode = 'all' | 'running' | 'degraded' | 'stopped';
|
||||||
|
type BackupMode = 'all' | 'needs-backup';
|
||||||
type GroupingMode = 'grouped' | 'flat';
|
type GroupingMode = 'grouped' | 'flat';
|
||||||
|
|
||||||
export function Dashboard(props: DashboardProps) {
|
export function Dashboard(props: DashboardProps) {
|
||||||
|
|
@ -236,6 +238,11 @@ export function Dashboard(props: DashboardProps) {
|
||||||
: 'all',
|
: 'all',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Backup filter mode - filter by backup status
|
||||||
|
const [backupMode, setBackupMode] = usePersistentSignal<BackupMode>('dashboardBackupMode', 'all', {
|
||||||
|
deserialize: (raw) => (raw === 'all' || raw === 'needs-backup' ? raw : 'all'),
|
||||||
|
});
|
||||||
|
|
||||||
// Grouping mode - grouped by node or flat list
|
// Grouping mode - grouped by node or flat list
|
||||||
const [groupingMode, setGroupingMode] = usePersistentSignal<GroupingMode>(
|
const [groupingMode, setGroupingMode] = usePersistentSignal<GroupingMode>(
|
||||||
'dashboardGroupingMode',
|
'dashboardGroupingMode',
|
||||||
|
|
@ -426,7 +433,8 @@ export function Dashboard(props: DashboardProps) {
|
||||||
sortDirection() !== 'asc' ||
|
sortDirection() !== 'asc' ||
|
||||||
selectedNode() !== null ||
|
selectedNode() !== null ||
|
||||||
viewMode() !== 'all' ||
|
viewMode() !== 'all' ||
|
||||||
statusMode() !== 'all';
|
statusMode() !== 'all' ||
|
||||||
|
backupMode() !== 'all';
|
||||||
|
|
||||||
if (hasActiveFilters) {
|
if (hasActiveFilters) {
|
||||||
// Clear ALL filters including search text, tag filters, node selection, and view modes
|
// Clear ALL filters including search text, tag filters, node selection, and view modes
|
||||||
|
|
@ -437,6 +445,7 @@ export function Dashboard(props: DashboardProps) {
|
||||||
setSelectedNode(null);
|
setSelectedNode(null);
|
||||||
setViewMode('all');
|
setViewMode('all');
|
||||||
setStatusMode('all');
|
setStatusMode('all');
|
||||||
|
setBackupMode('all');
|
||||||
|
|
||||||
// Blur the search input if it's focused
|
// Blur the search input if it's focused
|
||||||
if (searchInputRef && document.activeElement === searchInputRef) {
|
if (searchInputRef && document.activeElement === searchInputRef) {
|
||||||
|
|
@ -502,6 +511,17 @@ export function Dashboard(props: DashboardProps) {
|
||||||
guests = guests.filter((g) => g.status !== 'running');
|
guests = guests.filter((g) => g.status !== 'running');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter by backup status
|
||||||
|
if (backupMode() === 'needs-backup') {
|
||||||
|
guests = guests.filter((g) => {
|
||||||
|
// Skip templates - they don't need backups
|
||||||
|
if (g.template) return false;
|
||||||
|
const backupInfo = getBackupInfo(g.lastBackup);
|
||||||
|
// Show guests that need backup: stale, critical, or never backed up
|
||||||
|
return backupInfo.status === 'stale' || backupInfo.status === 'critical' || backupInfo.status === 'never';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Apply search/filter
|
// Apply search/filter
|
||||||
const searchTerm = search().trim();
|
const searchTerm = search().trim();
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
|
|
@ -800,6 +820,8 @@ export function Dashboard(props: DashboardProps) {
|
||||||
setViewMode={setViewMode}
|
setViewMode={setViewMode}
|
||||||
statusMode={statusMode}
|
statusMode={statusMode}
|
||||||
setStatusMode={setStatusMode}
|
setStatusMode={setStatusMode}
|
||||||
|
backupMode={backupMode}
|
||||||
|
setBackupMode={setBackupMode}
|
||||||
groupingMode={groupingMode}
|
groupingMode={groupingMode}
|
||||||
setGroupingMode={setGroupingMode}
|
setGroupingMode={setGroupingMode}
|
||||||
setSortKey={setSortKey}
|
setSortKey={setSortKey}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ interface DashboardFilterProps {
|
||||||
setViewMode: (value: 'all' | 'vm' | 'lxc') => void;
|
setViewMode: (value: 'all' | 'vm' | 'lxc') => void;
|
||||||
statusMode: () => 'all' | 'running' | 'degraded' | 'stopped';
|
statusMode: () => 'all' | 'running' | 'degraded' | 'stopped';
|
||||||
setStatusMode: (value: 'all' | 'running' | 'degraded' | 'stopped') => void;
|
setStatusMode: (value: 'all' | 'running' | 'degraded' | 'stopped') => void;
|
||||||
|
backupMode: () => 'all' | 'needs-backup';
|
||||||
|
setBackupMode: (value: 'all' | 'needs-backup') => void;
|
||||||
groupingMode: () => 'grouped' | 'flat';
|
groupingMode: () => 'grouped' | 'flat';
|
||||||
setGroupingMode: (value: 'grouped' | 'flat') => void;
|
setGroupingMode: (value: 'grouped' | 'flat') => void;
|
||||||
setSortKey: (value: string) => void;
|
setSortKey: (value: string) => void;
|
||||||
|
|
@ -369,6 +371,25 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
||||||
|
|
||||||
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||||
|
|
||||||
|
{/* Backup Filter */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => props.setBackupMode(props.backupMode() === 'needs-backup' ? 'all' : 'needs-backup')}
|
||||||
|
class={`inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-lg transition-all ${props.backupMode() === 'needs-backup'
|
||||||
|
? 'bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300 shadow-sm'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
|
||||||
|
}`}
|
||||||
|
title="Show guests with stale or missing backups"
|
||||||
|
>
|
||||||
|
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||||
|
<path d="M12 8v4M12 16h.01" />
|
||||||
|
</svg>
|
||||||
|
Needs Backup
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="h-5 w-px bg-gray-200 dark:bg-gray-600 hidden sm:block"></div>
|
||||||
|
|
||||||
{/* Grouping Mode Toggle */}
|
{/* Grouping Mode Toggle */}
|
||||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||||
<button
|
<button
|
||||||
|
|
@ -410,12 +431,14 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
||||||
props.setSortDirection('asc');
|
props.setSortDirection('asc');
|
||||||
props.setViewMode('all');
|
props.setViewMode('all');
|
||||||
props.setStatusMode('all');
|
props.setStatusMode('all');
|
||||||
|
props.setBackupMode('all');
|
||||||
props.setGroupingMode('grouped');
|
props.setGroupingMode('grouped');
|
||||||
}}
|
}}
|
||||||
title="Reset all filters"
|
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() ||
|
class={`flex items-center justify-center px-2.5 py-1 text-xs font-medium rounded-lg transition-colors ${props.search() ||
|
||||||
props.viewMode() !== 'all' ||
|
props.viewMode() !== 'all' ||
|
||||||
props.statusMode() !== 'all' ||
|
props.statusMode() !== 'all' ||
|
||||||
|
props.backupMode() !== 'all' ||
|
||||||
props.groupingMode() !== 'grouped'
|
props.groupingMode() !== 'grouped'
|
||||||
? '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-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'
|
: 'text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue