import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { ViewMode, TopologyNode, TopologyEdge, NetworkInfo, Host, NodeType } from '../types'; export type Orientation = 'LR' | 'TB'; export type StatusFilter = 'all' | 'running' | 'stopped'; // All node types for default filter const ALL_NODE_TYPES: NodeType[] = [ 'gateway', 'vlan', 'wifi', 'host_physical', 'host_vm', 'host_container', 'service', 'volume', 'mount', 'path' ]; interface TopologyState { nodes: TopologyNode[]; edges: TopologyEdge[]; selectedNodeId: string | null; viewMode: ViewMode; orientation: Orientation; searchQuery: string; typeFilters: NodeType[]; statusFilter: StatusFilter; leftPanelOpen: boolean; rightPanelOpen: boolean; lastUpdated: Date | null; isLoading: boolean; networkInfo: NetworkInfo | null; hosts: Host[]; setNodes: (nodes: TopologyNode[]) => void; setEdges: (edges: TopologyEdge[]) => void; setSelectedNode: (nodeId: string | null) => void; setViewMode: (mode: ViewMode) => void; setOrientation: (orientation: Orientation) => void; setSearchQuery: (query: string) => void; toggleTypeFilter: (type: NodeType) => void; setStatusFilter: (filter: StatusFilter) => void; toggleLeftPanel: () => void; toggleRightPanel: () => void; setLastUpdated: (date: Date) => void; setIsLoading: (loading: boolean) => void; setNetworkInfo: (info: NetworkInfo) => void; setHosts: (hosts: Host[]) => void; getSelectedNode: () => TopologyNode | null; getChildNodes: () => TopologyNode[]; getFilteredNodes: () => TopologyNode[]; } export const useTopologyStore = create()( persist( (set, get) => ({ nodes: [], edges: [], selectedNodeId: null, viewMode: 'full', orientation: 'LR', searchQuery: '', typeFilters: ALL_NODE_TYPES, statusFilter: 'all', leftPanelOpen: true, rightPanelOpen: true, lastUpdated: null, isLoading: false, networkInfo: null, hosts: [], setNodes: (nodes) => set({ nodes }), setEdges: (edges) => set({ edges }), setSelectedNode: (nodeId) => set({ selectedNodeId: nodeId }), setViewMode: (mode) => set({ viewMode: mode }), setOrientation: (orientation) => set({ orientation }), setSearchQuery: (query) => set({ searchQuery: query }), toggleTypeFilter: (type) => set((state) => { const exists = state.typeFilters.includes(type); return { typeFilters: exists ? state.typeFilters.filter(t => t !== type) : [...state.typeFilters, type] }; }), setStatusFilter: (filter) => set({ statusFilter: filter }), toggleLeftPanel: () => set((state) => ({ leftPanelOpen: !state.leftPanelOpen })), toggleRightPanel: () => set((state) => ({ rightPanelOpen: !state.rightPanelOpen })), setLastUpdated: (date) => set({ lastUpdated: date }), setIsLoading: (loading) => set({ isLoading: loading }), setNetworkInfo: (info) => set({ networkInfo: info }), setHosts: (hosts) => set({ hosts }), getSelectedNode: () => { const { nodes, selectedNodeId } = get(); return nodes.find(n => n.id === selectedNodeId) || null; }, getChildNodes: () => { const { nodes, selectedNodeId } = get(); if (!selectedNodeId) return []; const selectedNode = nodes.find(n => n.id === selectedNodeId); if (!selectedNode) return []; return nodes.filter(n => n.data.parentId === selectedNodeId); }, getFilteredNodes: () => { const { nodes, viewMode, searchQuery, typeFilters, statusFilter } = get(); let filtered = nodes; if (searchQuery) { const query = searchQuery.toLowerCase(); filtered = filtered.filter(n => n.name.toLowerCase().includes(query) || n.data.ip?.toLowerCase().includes(query) ); } if (statusFilter !== 'all') { filtered = filtered.filter(n => n.data.status === statusFilter); } if (typeFilters.length > 0 && typeFilters.length < ALL_NODE_TYPES.length) { filtered = filtered.filter(n => typeFilters.includes(n.type)); } let allowedTypes: NodeType[] = []; if (viewMode === 'network') { allowedTypes = ['gateway', 'vlan', 'wifi', 'host_physical', 'host_vm', 'host_container']; } else if (viewMode === 'host') { allowedTypes = ['gateway', 'vlan', 'wifi', 'host_physical', 'host_vm', 'host_container']; } else if (viewMode === 'service') { allowedTypes = ['host_physical', 'host_vm', 'host_container', 'service', 'volume']; } else if (viewMode === 'filesystem') { allowedTypes = ['volume', 'mount', 'path']; } if (allowedTypes.length > 0) { const nodeMap = new Map(nodes.map(n => [n.id, n])); const includeSet = new Set(); nodes.forEach(node => { if (allowedTypes.includes(node.type)) { includeSet.add(node.id); let current: TopologyNode | undefined = node; while (current?.data?.parentId) { const parentId = current.data.parentId; includeSet.add(parentId); current = nodeMap.get(parentId); if (!current) break; } } }); filtered = filtered.filter(n => includeSet.has(n.id)); } return filtered; } }), { name: 'homelab-topology-settings', version: 1, storage: createJSONStorage(() => localStorage), partialize: (state: TopologyState) => ({ viewMode: state.viewMode, orientation: state.orientation, searchQuery: state.searchQuery, typeFilters: state.typeFilters, statusFilter: state.statusFilter, leftPanelOpen: state.leftPanelOpen, rightPanelOpen: state.rightPanelOpen }) }));