mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 09:46:47 +00:00
refactor(frontend): extract useFleetPreferences + useFleetLabels from FleetView (F5-6) (#920)
* refactor(frontend): extract useFleetPreferences + useFleetLabels from FleetView (F5-6) Move localStorage preferences and label palette/assignment fetching out of the FleetView shell into dedicated hooks under FleetView/hooks/: - useFleetPreferences: wraps PREFS_KEY, loadPreferences, savePreferences, and the prefs useState + updatePrefs callback. Defaults are merged on load so stale stored values cannot produce missing keys. Save is a side-effect-free useEffect rather than a setState updater call, consistent with React purity contract. - useFleetLabels: wraps fleetPalette, fleetStackLabelMap, labelFilters state, fetchLabelsForNodes callback, and the onlineNodeKey-gated fetch effect. The onlineNodeKey derivation is memoized. labelPaletteKey is exported for the shell processedNodes useMemo until F5-8 absorbs it. Shell useState: 17 to 13. useEffect: 5 to 4. useCallback: 10 to 8. * fix(frontend): add comments to empty catch blocks in useFleetPreferences Empty catch blocks trigger the no-empty lint rule. Add explanatory comments to both catch sites to satisfy the rule while keeping the intent clear.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { fetchForNode } from '@/lib/api';
|
||||
import type { FleetNode, FleetPaletteEntry } from '../types';
|
||||
import type { Label as StackLabel, LabelColor } from '../../label-types';
|
||||
|
||||
export function labelPaletteKey(name: string, color: LabelColor): string {
|
||||
return `${name.trim().toLowerCase()}|${color}`;
|
||||
}
|
||||
|
||||
interface UseFleetLabelsOptions {
|
||||
isPaid: boolean;
|
||||
nodes: FleetNode[];
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const paletteMap = new Map<string, FleetPaletteEntry>();
|
||||
const stackLabelMap: Record<number, Record<string, StackLabel[]>> = {};
|
||||
|
||||
await Promise.allSettled(fleetNodes.map(async (node) => {
|
||||
if (node.status !== 'online') return;
|
||||
try {
|
||||
const [labelsRes, assignmentsRes] = await Promise.all([
|
||||
fetchForNode('/labels', node.id, { signal: AbortSignal.timeout(5000) }),
|
||||
fetchForNode('/labels/assignments', node.id, { signal: AbortSignal.timeout(5000) }),
|
||||
]);
|
||||
if (labelsRes.ok) {
|
||||
const labels = await labelsRes.json() as StackLabel[];
|
||||
for (const l of labels) {
|
||||
const key = labelPaletteKey(l.name, l.color);
|
||||
if (!paletteMap.has(key)) {
|
||||
paletteMap.set(key, { key, name: l.name, color: l.color });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (assignmentsRes.ok) {
|
||||
stackLabelMap[node.id] = await assignmentsRes.json() as Record<string, StackLabel[]>;
|
||||
}
|
||||
} catch {
|
||||
// Node unreachable or slow: skip, other nodes still contribute.
|
||||
}
|
||||
}));
|
||||
|
||||
setFleetPalette(Array.from(paletteMap.values()).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
setFleetStackLabelMap(stackLabelMap);
|
||||
}, [isPaid]);
|
||||
|
||||
// Refetch labels only when the set of online nodes actually changes,
|
||||
// not on every fetchOverview tick (which mints a new nodes ref).
|
||||
const onlineNodeKey = useMemo(
|
||||
() => nodes
|
||||
.filter(n => n.status === 'online')
|
||||
.map(n => n.id)
|
||||
.sort((a, b) => a - b)
|
||||
.join(','),
|
||||
[nodes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPaid || nodes.length === 0) return;
|
||||
fetchLabelsForNodes(nodes);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isPaid, onlineNodeKey, fetchLabelsForNodes]);
|
||||
|
||||
return { fleetPalette, fleetStackLabelMap, labelFilters, setLabelFilters };
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import type { FleetPreferences } from '../types';
|
||||
|
||||
const PREFS_KEY = 'sencho-fleet-preferences';
|
||||
|
||||
const DEFAULT_PREFS: FleetPreferences = {
|
||||
sortBy: 'name', sortDir: 'asc', filterStatus: 'all', filterType: 'all', filterCritical: false,
|
||||
};
|
||||
|
||||
function loadPreferences(): FleetPreferences {
|
||||
try {
|
||||
const stored = localStorage.getItem(PREFS_KEY);
|
||||
if (stored) return { ...DEFAULT_PREFS, ...(JSON.parse(stored) as Partial<FleetPreferences>) };
|
||||
} catch { /* corrupted or missing — use defaults */ }
|
||||
return { ...DEFAULT_PREFS };
|
||||
}
|
||||
|
||||
function savePreferences(prefs: FleetPreferences) {
|
||||
try {
|
||||
localStorage.setItem(PREFS_KEY, JSON.stringify(prefs));
|
||||
} catch { /* non-fatal: localStorage unavailable or quota exceeded */ }
|
||||
}
|
||||
|
||||
export function useFleetPreferences() {
|
||||
const [prefs, setPrefs] = useState<FleetPreferences>(loadPreferences);
|
||||
|
||||
const updatePrefs = useCallback((update: Partial<FleetPreferences>) => {
|
||||
setPrefs(prev => ({ ...prev, ...update }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
savePreferences(prefs);
|
||||
}, [prefs]);
|
||||
|
||||
return { prefs, updatePrefs };
|
||||
}
|
||||
Reference in New Issue
Block a user