Files
homelab-topology/src/store/topologyStore.ts

174 lines
5.6 KiB
TypeScript

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<TopologyState>()(
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<string>();
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
})
}));