174 lines
5.6 KiB
TypeScript
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
|
|
})
|
|
}));
|