mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 02:12:59 +00:00
refactor(frontend): extract useFleetOverview from FleetView (F5-8) (#922)
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useExperimental } from '@/hooks/useExperimental';
|
||||
import {
|
||||
RefreshCw, Search, Camera,
|
||||
@@ -9,17 +8,14 @@ import { FleetMasthead } from './fleet/FleetMasthead';
|
||||
import { ReconnectingOverlay } from './FleetView/ReconnectingOverlay';
|
||||
import { NodeUpdatesSheet } from './FleetView/NodeUpdatesSheet';
|
||||
import { LocalUpdateConfirmDialog } from './FleetView/LocalUpdateConfirmDialog';
|
||||
import { isCritical, getNodeCpu, getNodeMem, getNodeDisk } from './FleetView/nodeUtils';
|
||||
import { OverviewTab } from './FleetView/OverviewTab';
|
||||
import type { FleetNode, ViewMode } from './FleetView/types';
|
||||
import { useFleetPreferences } from './FleetView/hooks/useFleetPreferences';
|
||||
import { useFleetLabels, labelPaletteKey } from './FleetView/hooks/useFleetLabels';
|
||||
import { useFleetUpdateStatus } from './FleetView/hooks/useFleetUpdateStatus';
|
||||
import { useFleetPolling } from './FleetView/hooks/useFleetPolling';
|
||||
import { useFleetOverview } from './FleetView/hooks/useFleetOverview';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger, TabsHighlight, TabsHighlightItem } from '@/components/ui/tabs';
|
||||
import { springs } from '@/lib/motion';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { useLicense } from '@/context/LicenseContext';
|
||||
import { AdmiralGate } from './AdmiralGate';
|
||||
import FleetSnapshots from './FleetSnapshots';
|
||||
@@ -33,170 +29,35 @@ interface FleetViewProps {
|
||||
}
|
||||
|
||||
export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
const [nodes, setNodes] = useState<FleetNode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
const { isPaid, license } = useLicense();
|
||||
const isAdmiral = isPaid && license?.variant === 'admiral';
|
||||
const experimental = useExperimental();
|
||||
|
||||
const { prefs, updatePrefs } = useFleetPreferences();
|
||||
const { fleetPalette, fleetStackLabelMap, labelFilters, setLabelFilters } = useFleetLabels({ isPaid, nodes });
|
||||
const updateStatus = useFleetUpdateStatus({ isPaid });
|
||||
|
||||
const fetchOverview = useCallback(async (showRefresh = false) => {
|
||||
if (showRefresh) setRefreshing(true);
|
||||
try {
|
||||
const res = await apiFetch('/fleet/overview', { localOnly: true });
|
||||
if (res.ok) {
|
||||
setNodes(await res.json());
|
||||
setLastSyncAt(Date.now());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch fleet overview:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
const overview = useFleetOverview({ isPaid, prefs, updatePrefs, updateStatuses: updateStatus.updateStatuses });
|
||||
|
||||
useFleetPolling({
|
||||
isPaid,
|
||||
fetchOverview,
|
||||
fetchOverview: overview.fetchOverview,
|
||||
fetchUpdateStatus: updateStatus.fetchUpdateStatus,
|
||||
updateStatuses: updateStatus.updateStatuses,
|
||||
});
|
||||
|
||||
// --- Computed values ---
|
||||
|
||||
const onlineNodes = useMemo(() => nodes.filter(n => n.status === 'online'), [nodes]);
|
||||
const onlineCount = onlineNodes.length;
|
||||
const totalContainers = nodes.reduce((sum, n) => sum + (n.stats?.active ?? 0), 0);
|
||||
const totalContainersAll = nodes.reduce((sum, n) => sum + (n.stats?.total ?? 0), 0);
|
||||
const criticalCount = onlineNodes.filter(isCritical).length;
|
||||
|
||||
const avgCpuNum = onlineNodes.length > 0
|
||||
? onlineNodes.reduce((sum, n) => sum + getNodeCpu(n), 0) / onlineNodes.length
|
||||
: 0;
|
||||
const worstCpuNode = onlineNodes.length > 0
|
||||
? onlineNodes.reduce((worst, n) => getNodeCpu(n) > getNodeCpu(worst) ? n : worst, onlineNodes[0])
|
||||
: null;
|
||||
const worstCpu = worstCpuNode
|
||||
? { name: worstCpuNode.name, percent: getNodeCpu(worstCpuNode) }
|
||||
: null;
|
||||
|
||||
const totalMemUsed = onlineNodes.reduce((sum, n) => sum + (n.systemStats?.memory.used ?? 0), 0);
|
||||
const totalMemTotal = onlineNodes.reduce((sum, n) => sum + (n.systemStats?.memory.total ?? 0), 0);
|
||||
|
||||
const updateStatusMap = useMemo(
|
||||
() => new Map(updateStatus.updateStatuses.map(s => [s.nodeId, s])),
|
||||
[updateStatus.updateStatuses]
|
||||
);
|
||||
|
||||
// --- Filtering & Sorting (Skipper+) ---
|
||||
|
||||
const processedNodes = useMemo(() => {
|
||||
let filtered = [...nodes];
|
||||
|
||||
// Search (paid only, but harmless if applied - free users won't see the search bar)
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(n =>
|
||||
n.name.toLowerCase().includes(q) ||
|
||||
n.stacks?.some(s => s.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
|
||||
if (isPaid) {
|
||||
// Status filter
|
||||
if (prefs.filterStatus === 'online') filtered = filtered.filter(n => n.status === 'online');
|
||||
if (prefs.filterStatus === 'offline') filtered = filtered.filter(n => n.status !== 'online');
|
||||
|
||||
// Type filter
|
||||
if (prefs.filterType === 'local') filtered = filtered.filter(n => n.type === 'local');
|
||||
if (prefs.filterType === 'remote') filtered = filtered.filter(n => n.type === 'remote');
|
||||
|
||||
// Critical filter
|
||||
if (prefs.filterCritical) filtered = filtered.filter(isCritical);
|
||||
|
||||
// Label filter: match by (name, color) palette key so equivalent
|
||||
// labels across nodes behave as one filter.
|
||||
if (labelFilters.size > 0) {
|
||||
filtered = filtered.filter(n => {
|
||||
const nodeStackLabels = fleetStackLabelMap[n.id] ?? {};
|
||||
return n.stacks?.some(s => {
|
||||
const sLabels = nodeStackLabels[s] ?? [];
|
||||
return sLabels.some(l => labelFilters.has(labelPaletteKey(l.name, l.color)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Sort
|
||||
filtered.sort((a, b) => {
|
||||
let cmp = 0;
|
||||
switch (prefs.sortBy) {
|
||||
case 'name':
|
||||
cmp = a.name.localeCompare(b.name);
|
||||
break;
|
||||
case 'cpu':
|
||||
cmp = getNodeCpu(b) - getNodeCpu(a);
|
||||
break;
|
||||
case 'memory':
|
||||
cmp = getNodeMem(b) - getNodeMem(a);
|
||||
break;
|
||||
case 'containers':
|
||||
cmp = (b.stats?.active ?? 0) - (a.stats?.active ?? 0);
|
||||
break;
|
||||
case 'status':
|
||||
cmp = (a.status === 'online' ? 0 : 1) - (b.status === 'online' ? 0 : 1);
|
||||
break;
|
||||
}
|
||||
return prefs.sortDir === 'desc' ? -cmp : cmp;
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [nodes, searchQuery, isPaid, prefs, labelFilters, fleetStackLabelMap]);
|
||||
|
||||
const localNode = useMemo(() => processedNodes.find(n => n.type === 'local') ?? null, [processedNodes]);
|
||||
const remoteNodes = useMemo(() => processedNodes.filter(n => n.type !== 'local'), [processedNodes]);
|
||||
const topologyNodes = useMemo(() => processedNodes.map(n => ({
|
||||
id: n.id,
|
||||
name: n.name,
|
||||
type: n.type,
|
||||
status: n.status,
|
||||
cpuPercent: getNodeCpu(n),
|
||||
memPercent: getNodeMem(n),
|
||||
diskPercent: getNodeDisk(n),
|
||||
stackCount: n.stacks?.length ?? 0,
|
||||
runningCount: n.stats?.active ?? 0,
|
||||
critical: n.status === 'online' && isCritical(n),
|
||||
})), [processedNodes]);
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
updatePrefs({ filterStatus: 'all', filterType: 'all', filterCritical: false });
|
||||
setLabelFilters(new Set());
|
||||
}, [updatePrefs, setLabelFilters]);
|
||||
const allNodes = useMemo(
|
||||
() => (localNode ? [localNode, ...remoteNodes] : remoteNodes),
|
||||
[localNode, remoteNodes]
|
||||
);
|
||||
const { mastheadStats, lastSyncAt, loading, refreshing } = overview;
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto p-6">
|
||||
<FleetMasthead
|
||||
nodeCount={nodes.length}
|
||||
onlineCount={onlineCount}
|
||||
criticalCount={criticalCount}
|
||||
totalCpuPercent={avgCpuNum}
|
||||
worstCpu={worstCpu}
|
||||
totalMemUsed={totalMemUsed}
|
||||
totalMemTotal={totalMemTotal}
|
||||
activeContainers={totalContainers}
|
||||
totalContainers={totalContainersAll}
|
||||
nodeCount={mastheadStats.nodeCount}
|
||||
onlineCount={mastheadStats.onlineCount}
|
||||
criticalCount={mastheadStats.criticalCount}
|
||||
totalCpuPercent={mastheadStats.avgCpuNum}
|
||||
worstCpu={mastheadStats.worstCpu}
|
||||
totalMemUsed={mastheadStats.totalMemUsed}
|
||||
totalMemTotal={mastheadStats.totalMemTotal}
|
||||
activeContainers={mastheadStats.totalContainers}
|
||||
totalContainers={mastheadStats.totalContainersAll}
|
||||
lastSyncAt={lastSyncAt}
|
||||
loading={loading}
|
||||
/>
|
||||
@@ -267,7 +128,7 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => fetchOverview(true)}
|
||||
onClick={() => overview.fetchOverview(true)}
|
||||
disabled={refreshing}
|
||||
className="gap-2"
|
||||
>
|
||||
@@ -280,23 +141,23 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
<TabsContent value="overview">
|
||||
<OverviewTab
|
||||
loading={loading}
|
||||
nodes={nodes}
|
||||
processedNodes={processedNodes}
|
||||
allNodes={allNodes}
|
||||
topologyNodes={topologyNodes}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
searchQuery={searchQuery}
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
nodes={overview.nodes}
|
||||
processedNodes={overview.processedNodes}
|
||||
allNodes={overview.allNodes}
|
||||
topologyNodes={overview.topologyNodes}
|
||||
viewMode={overview.viewMode}
|
||||
onViewModeChange={overview.setViewMode}
|
||||
searchQuery={overview.searchQuery}
|
||||
onSearchQueryChange={overview.setSearchQuery}
|
||||
prefs={prefs}
|
||||
onPrefsChange={updatePrefs}
|
||||
fleetPalette={fleetPalette}
|
||||
labelFilters={labelFilters}
|
||||
onLabelFiltersChange={setLabelFilters}
|
||||
onClearFilters={clearFilters}
|
||||
fleetPalette={overview.fleetPalette}
|
||||
labelFilters={overview.labelFilters}
|
||||
onLabelFiltersChange={overview.setLabelFilters}
|
||||
onClearFilters={overview.clearFilters}
|
||||
isPaid={isPaid}
|
||||
fleetStackLabelMap={fleetStackLabelMap}
|
||||
updateStatusMap={updateStatusMap}
|
||||
fleetStackLabelMap={overview.fleetStackLabelMap}
|
||||
updateStatusMap={overview.updateStatusMap}
|
||||
onNavigateToNode={onNavigateToNode}
|
||||
onUpdate={isPaid ? updateStatus.triggerNodeUpdate : undefined}
|
||||
updatingNodeId={updateStatus.updatingNodeId}
|
||||
@@ -357,10 +218,10 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
)}
|
||||
</Tabs>
|
||||
|
||||
{/* Reconnecting overlay shown when local node is updating */}
|
||||
{updateStatus.reconnecting && <ReconnectingOverlay preUpdateStartedAt={updateStatus.preUpdateStartedAt} />}
|
||||
{updateStatus.reconnecting && (
|
||||
<ReconnectingOverlay preUpdateStartedAt={updateStatus.preUpdateStartedAt} />
|
||||
)}
|
||||
|
||||
{/* Node Updates sheet */}
|
||||
<NodeUpdatesSheet
|
||||
open={updateStatus.showUpdateModal}
|
||||
onOpenChange={updateStatus.setShowUpdateModal}
|
||||
@@ -374,7 +235,6 @@ export function FleetView({ onNavigateToNode }: FleetViewProps) {
|
||||
triggerUpdateAll={updateStatus.triggerUpdateAll}
|
||||
/>
|
||||
|
||||
{/* Confirm dialog for local node update */}
|
||||
<LocalUpdateConfirmDialog
|
||||
open={updateStatus.localUpdateConfirm !== null}
|
||||
onOpenChange={(open) => { if (!open) updateStatus.setLocalUpdateConfirm(null); }}
|
||||
|
||||
@@ -15,7 +15,6 @@ interface UseFleetLabelsOptions {
|
||||
export function useFleetLabels({ isPaid, nodes }: UseFleetLabelsOptions) {
|
||||
const [fleetPalette, setFleetPalette] = useState<FleetPaletteEntry[]>([]);
|
||||
const [fleetStackLabelMap, setFleetStackLabelMap] = useState<Record<number, Record<string, StackLabel[]>>>({});
|
||||
const [labelFilters, setLabelFilters] = useState<Set<string>>(new Set());
|
||||
|
||||
const fetchLabelsForNodes = useCallback(async (fleetNodes: FleetNode[]) => {
|
||||
if (!isPaid || fleetNodes.length === 0) return;
|
||||
@@ -68,5 +67,5 @@ export function useFleetLabels({ isPaid, nodes }: UseFleetLabelsOptions) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isPaid, onlineNodeKey, fetchLabelsForNodes]);
|
||||
|
||||
return { fleetPalette, fleetStackLabelMap, labelFilters, setLabelFilters };
|
||||
return { fleetPalette, fleetStackLabelMap };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { useFleetLabels, labelPaletteKey } from './useFleetLabels';
|
||||
import { isCritical, getNodeCpu, getNodeMem, getNodeDisk } from '../nodeUtils';
|
||||
import type { FleetNode, ViewMode, FleetPreferences, NodeUpdateStatus } from '../types';
|
||||
|
||||
interface MastheadStats {
|
||||
nodeCount: number;
|
||||
onlineCount: number;
|
||||
criticalCount: number;
|
||||
totalContainers: number;
|
||||
totalContainersAll: number;
|
||||
avgCpuNum: number;
|
||||
worstCpu: { name: string; percent: number } | null;
|
||||
totalMemUsed: number;
|
||||
totalMemTotal: number;
|
||||
}
|
||||
|
||||
interface UseFleetOverviewOptions {
|
||||
isPaid: boolean;
|
||||
prefs: FleetPreferences;
|
||||
updatePrefs: (updates: Partial<FleetPreferences>) => void;
|
||||
updateStatuses: NodeUpdateStatus[];
|
||||
}
|
||||
|
||||
export function useFleetOverview({ isPaid, prefs, updatePrefs, updateStatuses }: UseFleetOverviewOptions) {
|
||||
const [nodes, setNodes] = useState<FleetNode[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [lastSyncAt, setLastSyncAt] = useState<number | null>(null);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
const [labelFilters, setLabelFilters] = useState<Set<string>>(new Set());
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const { fleetPalette, fleetStackLabelMap } = useFleetLabels({ isPaid, nodes });
|
||||
|
||||
const fetchOverview = useCallback(async (showRefresh = false) => {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
if (showRefresh) setRefreshing(true);
|
||||
try {
|
||||
const res = await apiFetch('/fleet/overview', { localOnly: true, signal: controller.signal });
|
||||
if (res.ok) {
|
||||
setNodes(await res.json());
|
||||
setLastSyncAt(Date.now());
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') return;
|
||||
console.error('Failed to fetch fleet overview:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onlineNodes = useMemo(() => nodes.filter(n => n.status === 'online'), [nodes]);
|
||||
|
||||
const mastheadStats = useMemo((): MastheadStats => {
|
||||
const onlineCount = onlineNodes.length;
|
||||
const criticalCount = onlineNodes.filter(isCritical).length;
|
||||
const totalContainers = nodes.reduce((sum, n) => sum + (n.stats?.active ?? 0), 0);
|
||||
const totalContainersAll = nodes.reduce((sum, n) => sum + (n.stats?.total ?? 0), 0);
|
||||
const avgCpuNum = onlineNodes.length > 0
|
||||
? onlineNodes.reduce((sum, n) => sum + getNodeCpu(n), 0) / onlineNodes.length
|
||||
: 0;
|
||||
const worstCpuNode = onlineNodes.length > 0
|
||||
? onlineNodes.reduce((worst, n) => getNodeCpu(n) > getNodeCpu(worst) ? n : worst, onlineNodes[0])
|
||||
: null;
|
||||
const worstCpu = worstCpuNode
|
||||
? { name: worstCpuNode.name, percent: getNodeCpu(worstCpuNode) }
|
||||
: null;
|
||||
const totalMemUsed = onlineNodes.reduce((sum, n) => sum + (n.systemStats?.memory.used ?? 0), 0);
|
||||
const totalMemTotal = onlineNodes.reduce((sum, n) => sum + (n.systemStats?.memory.total ?? 0), 0);
|
||||
return {
|
||||
nodeCount: nodes.length,
|
||||
onlineCount,
|
||||
criticalCount,
|
||||
totalContainers,
|
||||
totalContainersAll,
|
||||
avgCpuNum,
|
||||
worstCpu,
|
||||
totalMemUsed,
|
||||
totalMemTotal,
|
||||
};
|
||||
}, [nodes, onlineNodes]);
|
||||
|
||||
const updateStatusMap = useMemo(
|
||||
() => new Map(updateStatuses.map(s => [s.nodeId, s])),
|
||||
[updateStatuses]
|
||||
);
|
||||
|
||||
const processedNodes = useMemo(() => {
|
||||
let filtered = [...nodes];
|
||||
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(n =>
|
||||
n.name.toLowerCase().includes(q) ||
|
||||
n.stacks?.some(s => s.toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
|
||||
if (isPaid) {
|
||||
if (prefs.filterStatus === 'online') filtered = filtered.filter(n => n.status === 'online');
|
||||
if (prefs.filterStatus === 'offline') filtered = filtered.filter(n => n.status !== 'online');
|
||||
if (prefs.filterType === 'local') filtered = filtered.filter(n => n.type === 'local');
|
||||
if (prefs.filterType === 'remote') filtered = filtered.filter(n => n.type !== 'local');
|
||||
if (prefs.filterCritical) filtered = filtered.filter(isCritical);
|
||||
|
||||
if (labelFilters.size > 0) {
|
||||
filtered = filtered.filter(n => {
|
||||
const nodeStackLabels = fleetStackLabelMap[n.id] ?? {};
|
||||
return n.stacks?.some(s => {
|
||||
const sLabels = nodeStackLabels[s] ?? [];
|
||||
return sLabels.some(l => labelFilters.has(labelPaletteKey(l.name, l.color)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
let cmp = 0;
|
||||
switch (prefs.sortBy) {
|
||||
case 'name':
|
||||
cmp = a.name.localeCompare(b.name);
|
||||
break;
|
||||
case 'cpu':
|
||||
cmp = getNodeCpu(b) - getNodeCpu(a);
|
||||
break;
|
||||
case 'memory':
|
||||
cmp = getNodeMem(b) - getNodeMem(a);
|
||||
break;
|
||||
case 'containers':
|
||||
cmp = (b.stats?.active ?? 0) - (a.stats?.active ?? 0);
|
||||
break;
|
||||
case 'status':
|
||||
cmp = (a.status === 'online' ? 0 : 1) - (b.status === 'online' ? 0 : 1);
|
||||
break;
|
||||
}
|
||||
return prefs.sortDir === 'desc' ? -cmp : cmp;
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [nodes, searchQuery, isPaid, prefs, labelFilters, fleetStackLabelMap]);
|
||||
|
||||
const localNode = useMemo(
|
||||
() => processedNodes.find(n => n.type === 'local') ?? null,
|
||||
[processedNodes]
|
||||
);
|
||||
const remoteNodes = useMemo(
|
||||
() => processedNodes.filter(n => n.type !== 'local'),
|
||||
[processedNodes]
|
||||
);
|
||||
const topologyNodes = useMemo(
|
||||
() => processedNodes.map(n => ({
|
||||
id: n.id,
|
||||
name: n.name,
|
||||
type: n.type,
|
||||
status: n.status,
|
||||
cpuPercent: getNodeCpu(n),
|
||||
memPercent: getNodeMem(n),
|
||||
diskPercent: getNodeDisk(n),
|
||||
stackCount: n.stacks?.length ?? 0,
|
||||
runningCount: n.stats?.active ?? 0,
|
||||
critical: n.status === 'online' && isCritical(n),
|
||||
})),
|
||||
[processedNodes]
|
||||
);
|
||||
const allNodes = useMemo(
|
||||
() => (localNode ? [localNode, ...remoteNodes] : remoteNodes),
|
||||
[localNode, remoteNodes]
|
||||
);
|
||||
|
||||
const activeFilterCount = useMemo(() => {
|
||||
let count = 0;
|
||||
if (prefs.filterStatus !== 'all') count++;
|
||||
if (prefs.filterType !== 'all') count++;
|
||||
if (prefs.filterCritical) count++;
|
||||
count += labelFilters.size;
|
||||
return count;
|
||||
}, [prefs, labelFilters]);
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
updatePrefs({ filterStatus: 'all', filterType: 'all', filterCritical: false });
|
||||
setLabelFilters(new Set());
|
||||
}, [updatePrefs]);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
loading,
|
||||
refreshing,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
lastSyncAt,
|
||||
viewMode,
|
||||
setViewMode,
|
||||
labelFilters,
|
||||
setLabelFilters,
|
||||
fetchOverview,
|
||||
processedNodes,
|
||||
localNode,
|
||||
remoteNodes,
|
||||
topologyNodes,
|
||||
allNodes,
|
||||
mastheadStats,
|
||||
updateStatusMap,
|
||||
fleetPalette,
|
||||
fleetStackLabelMap,
|
||||
activeFilterCount,
|
||||
clearFilters,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user