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 { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import { getBackupInfo } from '@/utils/format';
|
||||
|
||||
type GuestMetadataRecord = Record<string, GuestMetadata>;
|
||||
type IdleCallbackHandle = number;
|
||||
|
|
@ -196,6 +197,7 @@ interface DashboardProps {
|
|||
|
||||
type ViewMode = 'all' | 'vm' | 'lxc';
|
||||
type StatusMode = 'all' | 'running' | 'degraded' | 'stopped';
|
||||
type BackupMode = 'all' | 'needs-backup';
|
||||
type GroupingMode = 'grouped' | 'flat';
|
||||
|
||||
export function Dashboard(props: DashboardProps) {
|
||||
|
|
@ -236,6 +238,11 @@ export function Dashboard(props: DashboardProps) {
|
|||
: '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
|
||||
const [groupingMode, setGroupingMode] = usePersistentSignal<GroupingMode>(
|
||||
'dashboardGroupingMode',
|
||||
|
|
@ -426,7 +433,8 @@ export function Dashboard(props: DashboardProps) {
|
|||
sortDirection() !== 'asc' ||
|
||||
selectedNode() !== null ||
|
||||
viewMode() !== 'all' ||
|
||||
statusMode() !== 'all';
|
||||
statusMode() !== 'all' ||
|
||||
backupMode() !== 'all';
|
||||
|
||||
if (hasActiveFilters) {
|
||||
// Clear ALL filters including search text, tag filters, node selection, and view modes
|
||||
|
|
@ -437,6 +445,7 @@ export function Dashboard(props: DashboardProps) {
|
|||
setSelectedNode(null);
|
||||
setViewMode('all');
|
||||
setStatusMode('all');
|
||||
setBackupMode('all');
|
||||
|
||||
// Blur the search input if it's focused
|
||||
if (searchInputRef && document.activeElement === searchInputRef) {
|
||||
|
|
@ -502,6 +511,17 @@ export function Dashboard(props: DashboardProps) {
|
|||
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
|
||||
const searchTerm = search().trim();
|
||||
if (searchTerm) {
|
||||
|
|
@ -800,6 +820,8 @@ export function Dashboard(props: DashboardProps) {
|
|||
setViewMode={setViewMode}
|
||||
statusMode={statusMode}
|
||||
setStatusMode={setStatusMode}
|
||||
backupMode={backupMode}
|
||||
setBackupMode={setBackupMode}
|
||||
groupingMode={groupingMode}
|
||||
setGroupingMode={setGroupingMode}
|
||||
setSortKey={setSortKey}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ interface DashboardFilterProps {
|
|||
setViewMode: (value: 'all' | 'vm' | 'lxc') => void;
|
||||
statusMode: () => 'all' | 'running' | 'degraded' | 'stopped';
|
||||
setStatusMode: (value: 'all' | 'running' | 'degraded' | 'stopped') => void;
|
||||
backupMode: () => 'all' | 'needs-backup';
|
||||
setBackupMode: (value: 'all' | 'needs-backup') => void;
|
||||
groupingMode: () => 'grouped' | 'flat';
|
||||
setGroupingMode: (value: 'grouped' | 'flat') => 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>
|
||||
|
||||
{/* 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 */}
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
<button
|
||||
|
|
@ -410,12 +431,14 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
|||
props.setSortDirection('asc');
|
||||
props.setViewMode('all');
|
||||
props.setStatusMode('all');
|
||||
props.setBackupMode('all');
|
||||
props.setGroupingMode('grouped');
|
||||
}}
|
||||
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() ||
|
||||
props.viewMode() !== 'all' ||
|
||||
props.statusMode() !== 'all' ||
|
||||
props.backupMode() !== 'all' ||
|
||||
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-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