diff --git a/web/src/components/netflow-topology/2d/topology-content.tsx b/web/src/components/netflow-topology/2d/topology-content.tsx index d34ac8d6b..78ac57f58 100644 --- a/web/src/components/netflow-topology/2d/topology-content.tsx +++ b/web/src/components/netflow-topology/2d/topology-content.tsx @@ -27,7 +27,9 @@ import { ElementData, FilterDir, generateDataModel, + getStepIntoNext, GraphElementPeer, + isDirElementFiltered, LayoutName, NodeData, toggleDirElementFilter, @@ -55,6 +57,7 @@ export interface TopologyContentProps { metricType: MetricType; metricScope: FlowScope; setMetricScope: (ms: FlowScope) => void; + allowedScopes: FlowScope[]; metrics: TopologyMetrics[]; droppedMetrics: TopologyMetrics[]; options: TopologyOptions; @@ -75,6 +78,7 @@ export const TopologyContent: React.FC = ({ metricType, metricScope, setMetricScope, + allowedScopes, metrics, droppedMetrics, options, @@ -195,43 +199,40 @@ export const TopologyContent: React.FC = ({ const onStepInto = React.useCallback( (data: Decorated) => { - let scope: MetricScopeOptions; let groupTypes: TopologyGroupTypes; switch (metricScope) { case MetricScopeOptions.CLUSTER: - scope = MetricScopeOptions.ZONE; groupTypes = TopologyGroupTypes.clusters; break; case MetricScopeOptions.ZONE: - scope = MetricScopeOptions.HOST; groupTypes = TopologyGroupTypes.zones; break; case MetricScopeOptions.HOST: - scope = MetricScopeOptions.NAMESPACE; - groupTypes = TopologyGroupTypes.none; + groupTypes = TopologyGroupTypes.hosts; break; case MetricScopeOptions.NAMESPACE: - scope = MetricScopeOptions.OWNER; groupTypes = TopologyGroupTypes.namespaces; break; default: - scope = MetricScopeOptions.RESOURCE; groupTypes = TopologyGroupTypes.owners; } - if (data.nodeType && data.peer) { + const scope = getStepIntoNext(metricScope, allowedScopes); + if (data.nodeType && data.peer && scope) { setMetricScope(scope); setOptions({ ...options, groupTypes }); - toggleDirElementFilter( - data.nodeType, - data.peer, - 'src', - true, - filters.list, - list => { - setFilters({ list: list, backAndForth: true }); - }, - filterDefinitions - ); + if (!isDirElementFiltered(data.nodeType, data.peer, 'src', filters.list, filterDefinitions)) { + toggleDirElementFilter( + data.nodeType, + data.peer, + 'src', + false, + filters.list, + list => { + setFilters({ list: list, backAndForth: true }); + }, + filterDefinitions + ); + } setSelectedIds([data.id]); //clear search onChangeSearch(); @@ -239,7 +240,17 @@ export const TopologyContent: React.FC = ({ onSelect(undefined); } }, - [metricScope, setMetricScope, setOptions, options, filters.list, filterDefinitions, onSelect, setFilters] + [ + metricScope, + setMetricScope, + allowedScopes, + setOptions, + options, + filters.list, + filterDefinitions, + onSelect, + setFilters + ] ); const onHover = React.useCallback((data: Decorated) => { @@ -344,6 +355,7 @@ export const TopologyContent: React.FC = ({ droppedMetrics, getOptions(), metricScope, + allowedScopes, searchEvent?.searchValue || '', highlightedId, filters, @@ -386,6 +398,7 @@ export const TopologyContent: React.FC = ({ selectedIds, getOptions, metricScope, + allowedScopes, searchEvent?.searchValue, filters, t, diff --git a/web/src/components/netflow-topology/netflow-topology.tsx b/web/src/components/netflow-topology/netflow-topology.tsx index eace60439..67d247f65 100644 --- a/web/src/components/netflow-topology/netflow-topology.tsx +++ b/web/src/components/netflow-topology/netflow-topology.tsx @@ -109,6 +109,7 @@ export const NetflowTopology: React.FC = ({ metricType={metricType} metricScope={metricScope} setMetricScope={setMetricScope} + allowedScopes={allowedScopes} metrics={displayedMetrics} droppedMetrics={droppedMetrics} options={options} diff --git a/web/src/components/netflow-traffic.tsx b/web/src/components/netflow-traffic.tsx index d004d3b92..e730700de 100644 --- a/web/src/components/netflow-traffic.tsx +++ b/web/src/components/netflow-traffic.tsx @@ -242,7 +242,7 @@ export const NetflowTraffic: React.FC = ({ forcedFilters, i const [lastTop, setLastTop] = useLocalStorage(localStorageLastTopKey, topValues[0]); const [range, setRange] = React.useState(getRangeFromURL()); const [histogramRange, setHistogramRange] = React.useState(); - const [metricScope, setMetricScope] = useLocalStorage(localStorageMetricScopeKey, 'namespace'); + const [metricScope, _setMetricScope] = useLocalStorage(localStorageMetricScopeKey, 'namespace'); const [topologyMetricFunction, setTopologyMetricFunction] = useLocalStorage( localStorageMetricFunctionKey, defaultMetricFunction @@ -435,6 +435,18 @@ export const NetflowTraffic: React.FC = ({ forcedFilters, i [config, getQuickFilters] ); + const setMetricScope = React.useCallback( + (scope: FlowScope) => { + _setMetricScope(scope); + // Invalidate groups if necessary, when metrics scope changed + const groups = getGroupsForScope(scope as MetricScopeOptions); + if (!groups.includes(topologyOptions.groupTypes)) { + setTopologyOptions({ ...topologyOptions, groupTypes: TopologyGroupTypes.none }); + } + }, + [_setMetricScope, topologyOptions, setTopologyOptions] + ); + const getTopologyMetrics = React.useCallback(() => { switch (topologyMetricType) { case 'Bytes': @@ -1261,14 +1273,6 @@ export const NetflowTraffic: React.FC = ({ forcedFilters, i // eslint-disable-next-line react-hooks/exhaustive-deps }, [filters]); - //invalidate groups if necessary, when metrics scope changed - React.useEffect(() => { - const groups = getGroupsForScope(metricScope as MetricScopeOptions); - if (!groups.includes(topologyOptions.groupTypes)) { - setTopologyOptions({ ...topologyOptions, groupTypes: TopologyGroupTypes.none }); - } - }, [metricScope, topologyOptions, setTopologyOptions]); - const clearFilters = React.useCallback(() => { if (forcedFilters) { navigate(netflowTrafficPath); diff --git a/web/src/model/topology.ts b/web/src/model/topology.ts index 04d2fc180..b93e7bdda 100644 --- a/web/src/model/topology.ts +++ b/web/src/model/topology.ts @@ -102,6 +102,31 @@ export const isGroupEnabled = (group: TopologyGroupTypes, enabledScopes: FlowSco ); }; +export const getStepIntoNext = (current: FlowScope, allowedScopes: FlowScope[]): FlowScope | undefined => { + let next: FlowScope | undefined = undefined; + switch (current) { + case 'cluster': + next = 'zone'; + break; + case 'zone': + next = 'host'; + break; + case 'host': + next = 'resource'; + break; + case 'namespace': + next = 'owner'; + break; + case 'owner': + next = 'resource'; + break; + } + if (!next || allowedScopes.includes(next)) { + return next; + } + return getStepIntoNext(next, allowedScopes); +}; + export interface TopologyOptions { maxEdgeStat: number; nodeBadges?: boolean; @@ -456,6 +481,7 @@ export const generateDataModel = ( droppedMetrics: TopologyMetrics[], options: TopologyOptions, metricScope: FlowScope, + allowedScopes: FlowScope[], searchValue: string, highlightedId: string, filters: Filters, @@ -656,23 +682,22 @@ export const generateDataModel = ( }; const peerToNodeData = (p: TopologyMetricPeer): NodeData => { + const canStepInto = getStepIntoNext(metricScope, allowedScopes) !== undefined; switch (metricScope) { case 'cluster': return _.isEmpty(p.clusterName) ? { peer: p, nodeType: 'unknown' } - : { peer: p, nodeType: 'cluster', canStepInto: true }; + : { peer: p, nodeType: 'cluster', canStepInto }; case 'zone': - return _.isEmpty(p.zone) ? { peer: p, nodeType: 'unknown' } : { peer: p, nodeType: 'zone', canStepInto: true }; + return _.isEmpty(p.zone) ? { peer: p, nodeType: 'unknown' } : { peer: p, nodeType: 'zone', canStepInto }; case 'host': - return _.isEmpty(p.hostName) - ? { peer: p, nodeType: 'unknown' } - : { peer: p, nodeType: 'host', canStepInto: true }; + return _.isEmpty(p.hostName) ? { peer: p, nodeType: 'unknown' } : { peer: p, nodeType: 'host', canStepInto }; case 'namespace': return _.isEmpty(p.namespace) ? { peer: p, nodeType: 'unknown' } - : { peer: p, nodeType: 'namespace', canStepInto: true }; + : { peer: p, nodeType: 'namespace', canStepInto }; case 'owner': - return p.owner ? { peer: p, nodeType: 'owner', canStepInto: true } : { peer: p, nodeType: 'unknown' }; + return p.owner ? { peer: p, nodeType: 'owner', canStepInto } : { peer: p, nodeType: 'unknown' }; case 'resource': default: return { peer: p, nodeType: 'resource' };