diff --git a/web/src/components/netflow-traffic.tsx b/web/src/components/netflow-traffic.tsx index 04aa9def7..15f036efc 100644 --- a/web/src/components/netflow-traffic.tsx +++ b/web/src/components/netflow-traffic.tsx @@ -10,6 +10,7 @@ import { Filters, getDisabledFiltersRecord, getEnabledFilters } from '../model/f import { filtersToString, FlowQuery, MetricType } from '../model/flow-query'; import { netflowTrafficModel } from '../model/netflow-traffic'; import { parseQuickFilters } from '../model/quick-filters'; +import { resolveGroupTypes } from '../model/scope'; import { getFetchFunctions as getBackAndForthFetch } from '../utils/back-and-forth'; import { ColumnsId, getDefaultColumns } from '../utils/columns'; import { loadConfig } from '../utils/config'; @@ -289,7 +290,9 @@ export const NetflowTraffic: React.FC = ({ query.aggregateBy = model.metricScope; if (model.selectedViewId === 'topology') { query.type = model.topologyMetricType; - query.groups = model.topologyOptions.groupTypes !== 'none' ? model.topologyOptions.groupTypes : undefined; + const scopes = getAvailableScopes(); + const resolvedGroup = resolveGroupTypes(model.topologyOptions.groupTypes, model.metricScope, scopes); + query.groups = resolvedGroup !== 'none' ? resolvedGroup : undefined; } else if (model.selectedViewId === 'overview') { query.limit = topValues.includes(model.limit) ? model.limit : topValues[0]; query.groups = undefined; @@ -309,7 +312,8 @@ export const NetflowTraffic: React.FC = ({ model.selectedViewId, model.topologyMetricType, model.metricScope, - model.topologyOptions.groupTypes + model.topologyOptions.groupTypes, + getAvailableScopes ]); const getFetchFunctions = React.useCallback(() => { diff --git a/web/src/components/tabs/netflow-topology/2d/topology-content.tsx b/web/src/components/tabs/netflow-topology/2d/topology-content.tsx index 130b53f44..495e51fa3 100644 --- a/web/src/components/tabs/netflow-topology/2d/topology-content.tsx +++ b/web/src/components/tabs/netflow-topology/2d/topology-content.tsx @@ -207,7 +207,7 @@ export const TopologyContent: React.FC = ({ const onStepInto = React.useCallback( (data: Decorated) => { - const groupTypes: TopologyGroupTypes = metricScope; + const groupTypes: TopologyGroupTypes = options.groupTypes === 'auto' ? 'auto' : metricScope; const scope = getStepInto( metricScope, scopes.map(sc => sc.id) diff --git a/web/src/model/__tests__/topology.spec.ts b/web/src/model/__tests__/topology.spec.ts index 2434727e3..3798212cb 100644 --- a/web/src/model/__tests__/topology.spec.ts +++ b/web/src/model/__tests__/topology.spec.ts @@ -1,6 +1,6 @@ import { ScopeDefSample } from '../../components/__tests-data__/scopes'; import { ContextSingleton } from '../../utils/context'; -import { getGroupName, getGroupsForScope, getStepInto } from '../scope'; +import { getGroupName, getGroupsForScope, getStepInto, resolveGroupTypes } from '../scope'; describe('Check enabled groups', () => { beforeEach(() => { @@ -9,14 +9,15 @@ describe('Check enabled groups', () => { it('should get group from scope', () => { let groups = getGroupsForScope('cluster', ScopeDefSample); - expect(groups).toEqual(['none']); + expect(groups).toEqual(['none', 'auto']); groups = getGroupsForScope('host', ScopeDefSample); - expect(groups).toEqual(['none', 'clusters', 'zones', 'clusters+zones']); + expect(groups).toEqual(['none', 'auto', 'clusters', 'zones', 'clusters+zones']); groups = getGroupsForScope('owner', ScopeDefSample); expect(groups).toEqual([ 'none', + 'auto', 'clusters', 'clusters+zones', 'clusters+hosts', @@ -30,6 +31,23 @@ describe('Check enabled groups', () => { ]); }); + it('should resolve auto group', () => { + let group = resolveGroupTypes('auto', 'resource', ScopeDefSample); + expect(group).toEqual('namespaces'); + + group = resolveGroupTypes('auto', 'owner', ScopeDefSample); + expect(group).toEqual('namespaces'); + + group = resolveGroupTypes('auto', 'namespace', ScopeDefSample); + expect(group).toEqual('none'); + + group = resolveGroupTypes('auto', 'host', ScopeDefSample); + expect(group).toEqual('none'); + + group = resolveGroupTypes('hosts', 'resource', ScopeDefSample); + expect(group).toEqual('hosts'); + }); + it('should get group name', () => { let name = getGroupName('hosts', ScopeDefSample, (s: string) => s); expect(name).toEqual('Node'); diff --git a/web/src/model/netflow-traffic.ts b/web/src/model/netflow-traffic.ts index 38808cef0..131962408 100644 --- a/web/src/model/netflow-traffic.ts +++ b/web/src/model/netflow-traffic.ts @@ -150,7 +150,7 @@ export function netflowTrafficModel() { // Invalidate groups if necessary, when metrics scope changed const groups = getGroupsForScope(scope, config.scopes); if (!groups.includes(topologyOptions.groupTypes)) { - setTopologyOptions({ ...topologyOptions, groupTypes: 'none' }); + setTopologyOptions({ ...topologyOptions, groupTypes: 'auto' }); } }, [setMetricScope, config.scopes, topologyOptions, setTopologyOptions] diff --git a/web/src/model/scope.ts b/web/src/model/scope.ts index 679ab9043..bdf8a82f1 100644 --- a/web/src/model/scope.ts +++ b/web/src/model/scope.ts @@ -22,18 +22,36 @@ export const getScopeName = (sc: ScopeConfigDef | undefined, t: (k: string) => s return sc.name || sc.id; }; -export const getGroupsForScope = (scopeId: FlowScope, scopes: ScopeConfigDef[]) => { +export const getGroupsForScope = (scopeId: FlowScope, scopes: ScopeConfigDef[]): TopologyGroupTypes[] => { const scope = scopes.find(sc => sc.id === scopeId); if (scope?.groups?.length) { const availableParts = scopes.map(sc => `${sc.id}s`); - return ['none', ...scope.groups.filter(gp => gp.split('+').every(part => availableParts.includes(part)))]; + return ['none', 'auto', ...scope.groups.filter(gp => gp.split('+').every(part => availableParts.includes(part)))]; } - return ['none']; + return ['none', 'auto']; +}; + +export const resolveGroupTypes = ( + inGroupTypes: TopologyGroupTypes, + scopeId: FlowScope, + scopes: ScopeConfigDef[] +): TopologyGroupTypes => { + if (inGroupTypes === 'auto') { + const groups = getGroupsForScope(scopeId, scopes); + if (groups.includes('namespaces')) { + return 'namespaces'; + } + // More logic can be added here for more default behaviours + return 'none'; + } + return inGroupTypes; }; export const getGroupName = (group: TopologyGroupTypes, scopes: ScopeConfigDef[], t: (k: string) => string) => { if (group === 'none') { return t('None'); + } else if (group === 'auto') { + return t('Auto'); } else if (group.includes('+')) { return group .split('+') diff --git a/web/src/model/topology.ts b/web/src/model/topology.ts index 8b08c75ca..8de38d863 100644 --- a/web/src/model/topology.ts +++ b/web/src/model/topology.ts @@ -25,7 +25,7 @@ import { createPeer, getFormattedValue } from '../utils/metrics'; import { defaultMetricFunction, defaultMetricType } from '../utils/router'; import { FlowScope, Groups, MetricFunction, MetricType, NodeType, StatFunction } from './flow-query'; import { getStat } from './metrics'; -import { getStepInto, isDirectionnal, ScopeConfigDef } from './scope'; +import { getStepInto, isDirectionnal, resolveGroupTypes, ScopeConfigDef } from './scope'; export enum LayoutName { threeD = '3d', @@ -40,7 +40,7 @@ export enum LayoutName { grid = 'Grid' } -export type TopologyGroupTypes = 'none' | Groups; +export type TopologyGroupTypes = 'none' | 'auto' | Groups; export interface TopologyOptions { maxEdgeStat: number; @@ -66,7 +66,7 @@ export const DefaultOptions: TopologyOptions = { startCollapsed: false, truncateLength: TruncateLength.M, layout: LayoutName.colaNoForce, - groupTypes: 'none', + groupTypes: 'auto', lowScale: 0.3, medScale: 0.5, metricFunction: defaultMetricFunction, @@ -537,11 +537,12 @@ export const generateDataModel = ( const addPossibleGroups = (peer: TopologyMetricPeer): NodeModel | undefined => { // groups are all possible scopes except last one const parentScopes = ContextSingleton.getScopes().slice(0, -1); + const resolvedGroups = resolveGroupTypes(options.groupTypes, metricScope, scopes); // build parent tree from biggest to smallest group let parent: NodeModel | undefined = undefined; parentScopes.forEach(sc => { - if (options.groupTypes.includes(`${sc.id}s`) && !_.isEmpty(peer[sc.id])) { + if (resolvedGroups.includes(`${sc.id}s`) && !_.isEmpty(peer[sc.id])) { parent = addGroup( { [sc.id]: peer[sc.id], namespace: ['namespace', 'owner'].includes(sc.id) ? peer.namespace : undefined }, sc.id,