Skip to content

Commit 72915fe

Browse files
committed
NETOBSERV-474 summary filters by source or dest
!QE NOTICE: graph node id generation changed! Allow to filter by source, dest or common via the summary panel filters This is also adding Owner as part of the information displayed per node. It modifies the whole topology node data so that the entire "TopologyMetricPeer" (returned from metrics) object is now part of it. It also makes metrics matching for groups simpler because the parent information is now contained in the id itself Also, some refactoring on element-panel to extract new components: element-field(s), peer-resource-link, summary-filter-button
1 parent 786607b commit 72915fe

File tree

16 files changed

+612
-575
lines changed

16 files changed

+612
-575
lines changed

web/locales/en/plugin__netobserv-plugin.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,6 @@
171171
"Add {{name}} filter": "Add {{name}} filter",
172172
"Unpin this element": "Unpin this element",
173173
"Pin this element": "Pin this element",
174-
"Name": "Name",
175-
"Node Name": "Node Name",
176174
"IP": "IP",
177175
"No information available for this content. Change scope to get more details.": "No information available for this content. Change scope to get more details.",
178176
"Source to destination:": "Source to destination:",
@@ -182,7 +180,6 @@
182180
"Both:": "Both:",
183181
"{{type}} rate": "{{type}} rate",
184182
"Edge": "Edge",
185-
"Unknown": "Unknown",
186183
"Unable to get topology": "Unable to get topology",
187184
"Query is slow": "Query is slow",
188185
"Overview": "Overview",
@@ -233,15 +230,18 @@
233230
"Query summary": "Query summary",
234231
"Find in view": "Find in view",
235232
"External": "External",
233+
"Unknown": "Unknown",
236234
"Names": "Names",
237235
"Kinds": "Kinds",
238236
"Owner Kinds": "Owner Kinds",
239237
"Ports": "Ports",
240238
"MAC": "MAC",
241239
"Node IP": "Node IP",
240+
"Node Name": "Node Name",
242241
"Kubernetes Objects": "Kubernetes Objects",
243242
"Owner Kubernetes Objects": "Owner Kubernetes Objects",
244243
"IPs & Ports": "IPs & Ports",
244+
"Name": "Name",
245245
"Kind": "Kind",
246246
"Owner Kind": "Owner Kind",
247247
"Port": "Port",

web/src/api/loki.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@ export interface RawTopologyMetrics {
6464
}
6565

6666
export interface TopologyMetricPeer {
67+
id: string;
6768
addr?: string;
68-
name?: string;
6969
namespace?: string;
70-
ownerName?: string;
71-
ownerType?: string;
72-
type?: string;
70+
owner?: { name: string; type: string };
71+
resource?: { name: string; type: string };
7372
hostName?: string;
73+
resourceKind?: string;
7474
displayName?: string;
7575
}
7676

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { OptionsMenu, OptionsMenuItem, OptionsMenuPosition, OptionsMenuToggle } from '@patternfly/react-core';
4+
import { FilterIcon } from '@patternfly/react-icons';
5+
import { Filter } from '../../model/filters';
6+
import { FilterDir, isElementFiltered, toggleElementFilter } from '../../model/topology';
7+
import { TopologyMetricPeer } from '../../api/loki';
8+
import { NodeType } from '../../model/flow-query';
9+
10+
export interface SummaryFilterButtonProps {
11+
id: string;
12+
filterType: NodeType;
13+
fields: Partial<TopologyMetricPeer>;
14+
activeFilters: Filter[];
15+
setFilters: (filters: Filter[]) => void;
16+
}
17+
18+
const srcFilter: FilterDir = 'src';
19+
const dstFilter: FilterDir = 'dst';
20+
const anyFilter: FilterDir = 'any';
21+
22+
export const SummaryFilterButton: React.FC<SummaryFilterButtonProps> = ({
23+
id,
24+
filterType,
25+
fields,
26+
activeFilters,
27+
setFilters
28+
}) => {
29+
const { t } = useTranslation('plugin__netobserv-plugin');
30+
const [isOpen, setIsOpen] = React.useState(false);
31+
32+
const selected = [srcFilter, dstFilter, anyFilter].filter(dir =>
33+
isElementFiltered(filterType, fields, dir, activeFilters, t)
34+
);
35+
36+
const onSelect = (dir: FilterDir) => {
37+
toggleElementFilter(filterType, fields, dir, selected.includes(dir), activeFilters, setFilters, t);
38+
};
39+
40+
const menuItem = (id: FilterDir, label: string) => (
41+
<OptionsMenuItem id={id} key={id} isSelected={selected.includes(id)} onSelect={() => onSelect(id)}>
42+
{label}
43+
</OptionsMenuItem>
44+
);
45+
46+
return (
47+
<OptionsMenu
48+
id={id}
49+
data-test={id}
50+
toggle={<OptionsMenuToggle toggleTemplate={<FilterIcon />} onToggle={setIsOpen} hideCaret />}
51+
menuItems={[menuItem('src', t('Source')), menuItem('dst', t('Destination')), menuItem('any', t('Common'))]}
52+
isOpen={isOpen}
53+
position={OptionsMenuPosition.right}
54+
isPlain
55+
/>
56+
);
57+
};

web/src/components/metrics/metrics-helper.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ const getPeerName = (
103103
export const toNamedMetric = (
104104
t: TFunction,
105105
m: TopologyMetrics,
106-
data?: NodeData,
107-
truncateLength: TruncateLength = TruncateLength.OFF
106+
truncateLength: TruncateLength,
107+
data?: NodeData
108108
): NamedMetric => {
109109
const srcName = getPeerName(t, m.source, m.scope, truncateLength);
110110
const srcFullName = getPeerName(t, m.source, m.scope);

web/src/components/netflow-overview/netflow-overview.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { useTranslation } from 'react-i18next';
1515
import { TopologyMetrics } from '../../api/loki';
1616
import { MetricType } from '../../model/flow-query';
1717
import { getStat } from '../../model/topology';
18-
import { peersEqual } from '../../utils/metrics';
1918
import { getOverviewPanelInfo, OverviewPanel, OverviewPanelId } from '../../utils/overview-panels';
2019
import { TruncateLength } from '../dropdowns/truncate-dropdown';
2120
import LokiError from '../messages/loki-error';
@@ -101,9 +100,9 @@ export const NetflowOverview: React.FC<NetflowOverviewProps> = ({
101100
//limit to top X since multiple queries can run in parallel
102101
const topKMetrics = metrics
103102
.sort((a, b) => getStat(b.stats, 'sum') - getStat(a.stats, 'sum'))
104-
.map(m => toNamedMetric(t, m, undefined, truncateLength));
105-
const namedTotalMetric = toNamedMetric(t, totalMetric, undefined, truncateLength);
106-
const noInternalTopK = topKMetrics.filter(m => !peersEqual(m.source, m.destination));
103+
.map(m => toNamedMetric(t, m, truncateLength));
104+
const namedTotalMetric = toNamedMetric(t, totalMetric, truncateLength);
105+
const noInternalTopK = topKMetrics.filter(m => m.source.id !== m.destination.id);
107106

108107
const smallerTexts = truncateLength >= TruncateLength.M;
109108

web/src/components/netflow-topology/2d/styles/styleNode.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as React from 'react';
2+
import * as _ from 'lodash';
13
import { Tooltip, TooltipPosition } from '@patternfly/react-core';
24
import {
35
CubeIcon,
@@ -32,8 +34,6 @@ import {
3234
} from '@patternfly/react-topology';
3335
import useDetailsLevel from '@patternfly/react-topology/dist/esm/hooks/useDetailsLevel';
3436
import { TFunction } from 'i18next';
35-
import * as _ from 'lodash';
36-
import * as React from 'react';
3737
import { useTranslation } from 'react-i18next';
3838
import { Decorated, NodeData } from '../../../../model/topology';
3939
import BaseNode from '../components/node';
@@ -88,7 +88,7 @@ const renderIcon = (data: Decorated<NodeData>, element: NodePeer): React.ReactNo
8888
const iconSize =
8989
(shape === NodeShape.trapezoid ? width : Math.min(width, height)) -
9090
(shape === NodeShape.stadium ? 5 : ICON_PADDING) * 2;
91-
const Component = getTypeIcon(data.resourceKind);
91+
const Component = getTypeIcon(data.peer.resourceKind);
9292

9393
return (
9494
<g transform={`translate(${(width - iconSize) / 2}, ${(height - iconSize) / 2})`}>
@@ -200,21 +200,21 @@ const renderDecorators = (
200200
element,
201201
TopologyQuadrant.lowerRight,
202202
<LevelDownAltIcon />,
203-
t('Step into this {{name}}', { name: data.resourceKind?.toLowerCase() }),
203+
t('Step into this {{name}}', { name: data.peer.resourceKind?.toLowerCase() }),
204204
false,
205205
onStepIntoClick,
206206
getShapeDecoratorCenter,
207207
MEDIUM_DECORATOR_PADDING
208208
)}
209-
{(data.namespace || data.name || data.addr || data.host) &&
209+
{(data.peer.namespace || data.peer.resource || data.peer.owner || data.peer.addr || data.peer.hostName) &&
210210
renderClickableDecorator(
211211
t,
212212
element,
213213
TopologyQuadrant.lowerLeft,
214214
isFiltered ? <TimesIcon /> : <FilterIcon />,
215215
isFiltered
216-
? t('Remove {{name}} filter', { name: data.resourceKind?.toLowerCase() })
217-
: t('Add {{name}} filter', { name: data.resourceKind?.toLowerCase() }),
216+
? t('Remove {{name}} filter', { name: data.peer.resourceKind?.toLowerCase() })
217+
: t('Add {{name}} filter', { name: data.peer.resourceKind?.toLowerCase() }),
218218
isFiltered,
219219
onFilterClick,
220220
getShapeDecoratorCenter,

web/src/components/netflow-topology/__tests__/element-panel.spec.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, DrawerCloseButton } from '@patternfly/react-core';
1+
import { DrawerCloseButton, OptionsMenuToggle } from '@patternfly/react-core';
22
import { BaseEdge, BaseNode, NodeModel } from '@patternfly/react-topology';
33
import { mount, shallow } from 'enzyme';
44
import * as React from 'react';
@@ -8,16 +8,18 @@ import { MetricFunction, MetricScope, MetricType } from '../../../model/flow-que
88
import { ElementPanel, ElementPanelDetailsContent, ElementPanelMetricsContent } from '../element-panel';
99
import { dataSample } from '../__tests-data__/metrics';
1010
import { NodeData } from '../../../model/topology';
11+
import { createPeer } from '../../../utils/metrics';
1112
import { TruncateLength } from '../../../components/dropdowns/truncate-dropdown';
1213

1314
describe('<ElementPanel />', () => {
1415
const getNode = (kind: string, name: string, addr: string) => {
1516
const bn = new BaseNode<NodeModel, NodeData>();
1617
bn.setData({
1718
nodeType: 'resource',
18-
resourceKind: kind,
19-
name,
20-
addr
19+
peer: createPeer({
20+
addr: addr,
21+
resource: { name, type: kind }
22+
})
2123
});
2224
return bn;
2325
};
@@ -61,7 +63,7 @@ describe('<ElementPanel />', () => {
6163
expect(wrapper.find(ElementPanelDetailsContent)).toBeTruthy();
6264

6365
//check node infos
64-
expect(wrapper.find('#addressValue').last().text()).toBe('10.129.0.15');
66+
expect(wrapper.find('#node-info-address').last().text()).toBe('IP10.129.0.15');
6567

6668
//update to edge
6769
wrapper.setProps({ ...mocks, element: getEdge() });
@@ -87,9 +89,11 @@ describe('<ElementPanel />', () => {
8789

8890
it('should filter <ElementPanelDetailsContent />', async () => {
8991
const wrapper = mount(<ElementPanelDetailsContent {...mocks} />);
90-
const filterButtons = wrapper.find(Button);
92+
const ipFilters = wrapper.find(OptionsMenuToggle).last();
9193
// Two buttons: first for pod filter, second for IP filter
92-
filterButtons.last().simulate('click');
94+
ipFilters.last().simulate('click');
95+
expect(wrapper.find('li').length).toBe(3);
96+
wrapper.find('[id="any"]').at(0).simulate('click');
9397
expect(mocks.setFilters).toHaveBeenCalledWith([
9498
{
9599
def: expect.any(Object),
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Flex, FlexItem, Text, TextContent, TextVariants } from '@patternfly/react-core';
2+
import * as React from 'react';
3+
import { NodeType } from '../../model/flow-query';
4+
import { TopologyMetricPeer } from '../../api/loki';
5+
import { Filter } from '../../model/filters';
6+
import { SummaryFilterButton } from '../filters/summary-filter-button';
7+
import { PeerResourceLink } from './peer-resource-link';
8+
9+
export const ElementField: React.FC<{
10+
id: string;
11+
label: string;
12+
filterType: NodeType;
13+
forcedText?: string;
14+
fields: Partial<TopologyMetricPeer>;
15+
activeFilters: Filter[];
16+
setFilters: (filters: Filter[]) => void;
17+
}> = ({ id, label, filterType, forcedText, fields, activeFilters, setFilters }) => {
18+
return (
19+
<TextContent id={id} className="record-field-container">
20+
<Text component={TextVariants.h4}>{label}</Text>
21+
<Flex>
22+
<FlexItem flex={{ default: 'flex_1' }}>
23+
{forcedText ? <Text>{forcedText}</Text> : <PeerResourceLink fields={fields} />}
24+
</FlexItem>
25+
<FlexItem>
26+
<SummaryFilterButton
27+
id={id + '-filter'}
28+
activeFilters={activeFilters}
29+
filterType={filterType}
30+
fields={fields}
31+
setFilters={setFilters}
32+
/>
33+
</FlexItem>
34+
</Flex>
35+
</TextContent>
36+
);
37+
};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as React from 'react';
2+
import { Text, TextContent, TextVariants } from '@patternfly/react-core';
3+
import { useTranslation } from 'react-i18next';
4+
import { Filter } from '../../model/filters';
5+
import { NodeData } from '../../model/topology';
6+
import { ElementField } from './element-field';
7+
8+
export const ElementFields: React.FC<{
9+
id: string;
10+
data: NodeData;
11+
forceFirstAsText?: boolean;
12+
activeFilters: Filter[];
13+
setFilters: (filters: Filter[]) => void;
14+
}> = ({ id, data, forceFirstAsText, activeFilters, setFilters }) => {
15+
const { t } = useTranslation('plugin__netobserv-plugin');
16+
17+
const fragments = [];
18+
let forceAsText = forceFirstAsText;
19+
let forceLabel = forceFirstAsText ? t('Name') : undefined;
20+
if (data.peer.resource) {
21+
fragments.push(
22+
<ElementField
23+
id={id + '-resource'}
24+
key={id + '-resource'}
25+
label={forceLabel || data.peer.resource.type}
26+
forcedText={forceAsText ? data.peer.resource.name : undefined}
27+
activeFilters={activeFilters}
28+
filterType={'resource'}
29+
fields={data.peer}
30+
setFilters={setFilters}
31+
/>
32+
);
33+
forceLabel = forceAsText = undefined;
34+
}
35+
if (data.peer.owner && data.peer.owner.type !== data.peer.resource?.type) {
36+
fragments.push(
37+
<ElementField
38+
id={id + '-owner'}
39+
key={id + '-owner'}
40+
label={forceLabel || data.peer.owner.type}
41+
forcedText={forceAsText ? data.peer.owner.name : undefined}
42+
activeFilters={activeFilters}
43+
filterType={'owner'}
44+
fields={{ owner: data.peer.owner, namespace: data.peer.namespace }}
45+
setFilters={setFilters}
46+
/>
47+
);
48+
forceLabel = forceAsText = undefined;
49+
}
50+
if (data.peer.namespace) {
51+
fragments.push(
52+
<ElementField
53+
id={id + '-namespace'}
54+
key={id + '-namespace'}
55+
label={forceLabel || t('Namespace')}
56+
forcedText={forceAsText ? data.peer.namespace : undefined}
57+
activeFilters={activeFilters}
58+
filterType={'namespace'}
59+
fields={{ namespace: data.peer.namespace }}
60+
setFilters={setFilters}
61+
/>
62+
);
63+
forceLabel = forceAsText = undefined;
64+
}
65+
if (data.peer.hostName) {
66+
fragments.push(
67+
<ElementField
68+
id={id + '-host'}
69+
key={id + '-host'}
70+
label={forceLabel || t('Node')}
71+
forcedText={forceAsText ? data.peer.hostName : undefined}
72+
activeFilters={activeFilters}
73+
filterType={'host'}
74+
fields={{ hostName: data.peer.hostName }}
75+
setFilters={setFilters}
76+
/>
77+
);
78+
forceLabel = forceAsText = undefined;
79+
}
80+
if (data.peer.addr) {
81+
fragments.push(
82+
<ElementField
83+
id={id + '-address'}
84+
key={id + '-address'}
85+
label={t('IP')}
86+
activeFilters={activeFilters}
87+
filterType={'resource'}
88+
fields={{ addr: data.peer.addr }}
89+
setFilters={setFilters}
90+
/>
91+
);
92+
}
93+
94+
return (
95+
<>
96+
{fragments.length > 0 ? (
97+
fragments
98+
) : (
99+
<TextContent id={id + '-no-infos'} className="record-field-container">
100+
{
101+
<Text component={TextVariants.p}>
102+
{t('No information available for this content. Change scope to get more details.')}
103+
</Text>
104+
}
105+
</TextContent>
106+
)}
107+
</>
108+
);
109+
};

0 commit comments

Comments
 (0)