Skip to content

Commit 5d61168

Browse files
authored
Merge pull request #246 from jotak/more-summary-filters
NETOBSERV-474 summary filters by source or dest
2 parents 786607b + 68239b8 commit 5d61168

File tree

18 files changed

+645
-601
lines changed

18 files changed

+645
-601
lines changed

web/locales/en/plugin__netobserv-plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@
172172
"Unpin this element": "Unpin this element",
173173
"Pin this element": "Pin this element",
174174
"Name": "Name",
175-
"Node Name": "Node Name",
176175
"IP": "IP",
177176
"No information available for this content. Change scope to get more details.": "No information available for this content. Change scope to get more details.",
178177
"Source to destination:": "Source to destination:",
@@ -182,7 +181,6 @@
182181
"Both:": "Both:",
183182
"{{type}} rate": "{{type}} rate",
184183
"Edge": "Edge",
185-
"Unknown": "Unknown",
186184
"Unable to get topology": "Unable to get topology",
187185
"Query is slow": "Query is slow",
188186
"Overview": "Overview",
@@ -233,12 +231,14 @@
233231
"Query summary": "Query summary",
234232
"Find in view": "Find in view",
235233
"External": "External",
234+
"Unknown": "Unknown",
236235
"Names": "Names",
237236
"Kinds": "Kinds",
238237
"Owner Kinds": "Owner Kinds",
239238
"Ports": "Ports",
240239
"MAC": "MAC",
241240
"Node IP": "Node IP",
241+
"Node Name": "Node Name",
242242
"Kubernetes Objects": "Kubernetes Objects",
243243
"Owner Kubernetes Objects": "Owner Kubernetes Objects",
244244
"IPs & Ports": "IPs & Ports",

web/src/api/loki.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,21 @@ export interface RawTopologyMetrics {
6363
values: [number, unknown][];
6464
}
6565

66+
export interface NameAndType {
67+
name: string;
68+
type: string;
69+
}
70+
6671
export interface TopologyMetricPeer {
72+
id: string;
6773
addr?: string;
68-
name?: string;
6974
namespace?: string;
70-
ownerName?: string;
71-
ownerType?: string;
72-
type?: string;
75+
owner?: NameAndType;
76+
resource?: NameAndType;
7377
hostName?: string;
74-
displayName?: string;
78+
resourceKind?: string;
79+
isAmbiguous: boolean;
80+
getDisplayName: (inclNamespace: boolean, disambiguate: boolean) => string | undefined;
7581
}
7682

7783
export type TopologyMetrics = {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.pf-c-options-menu.summary-filter-menu ul {
2+
padding-left: 0;
3+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { Checkbox, 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+
import './summary-filter-button.css';
10+
11+
export interface SummaryFilterButtonProps {
12+
id: string;
13+
filterType: NodeType;
14+
fields: Partial<TopologyMetricPeer>;
15+
activeFilters: Filter[];
16+
setFilters: (filters: Filter[]) => void;
17+
}
18+
19+
const srcFilter: FilterDir = 'src';
20+
const dstFilter: FilterDir = 'dst';
21+
const anyFilter: FilterDir = 'any';
22+
23+
export const SummaryFilterButton: React.FC<SummaryFilterButtonProps> = ({
24+
id,
25+
filterType,
26+
fields,
27+
activeFilters,
28+
setFilters
29+
}) => {
30+
const { t } = useTranslation('plugin__netobserv-plugin');
31+
const [isOpen, setIsOpen] = React.useState(false);
32+
33+
const selected = [srcFilter, dstFilter, anyFilter].filter(dir =>
34+
isElementFiltered(filterType, fields, dir, activeFilters, t)
35+
);
36+
37+
const onSelect = (dir: FilterDir, e: React.BaseSyntheticEvent) => {
38+
toggleElementFilter(filterType, fields, dir, selected.includes(dir), activeFilters, setFilters, t);
39+
e.preventDefault();
40+
};
41+
42+
const menuItem = (id: FilterDir, label: string) => (
43+
<OptionsMenuItem id={id} key={id} onSelect={e => onSelect(id, e!)}>
44+
<Checkbox
45+
id={id + '-checkbox'}
46+
label={label}
47+
isChecked={selected.includes(id)}
48+
onChange={(_, e) => onSelect(id, e)}
49+
/>
50+
</OptionsMenuItem>
51+
);
52+
53+
return (
54+
<OptionsMenu
55+
id={id}
56+
className={'summary-filter-menu'}
57+
data-test={id}
58+
toggle={<OptionsMenuToggle toggleTemplate={<FilterIcon />} onToggle={setIsOpen} hideCaret />}
59+
menuItems={[menuItem('src', t('Source')), menuItem('dst', t('Destination')), menuItem('any', t('Common'))]}
60+
isOpen={isOpen}
61+
position={OptionsMenuPosition.right}
62+
isPlain
63+
/>
64+
);
65+
};

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { NamedMetric, TopologyMetricPeer, TopologyMetrics } from '../../api/loki
55
import { MetricScope, MetricType } from '../../model/flow-query';
66
import { NodeData } from '../../model/topology';
77
import { getDateFromUnix, getFormattedDate } from '../../utils/datetime';
8-
import { getFormattedRateValue, matchPeer } from '../../utils/metrics';
8+
import { getFormattedRateValue, isUnknownPeer, matchPeer } from '../../utils/metrics';
99
import { TruncateLength } from '../dropdowns/truncate-dropdown';
1010

1111
export type LegendDataItem = {
@@ -81,10 +81,13 @@ const getPeerName = (
8181
t: TFunction,
8282
peer: TopologyMetricPeer,
8383
scope: MetricScope,
84-
truncateLength: TruncateLength = TruncateLength.OFF
84+
truncateLength: TruncateLength,
85+
inclNamespace: boolean,
86+
disambiguate: boolean
8587
): string => {
86-
if (peer.displayName) {
87-
return truncateParts(peer.displayName, truncateLength);
88+
const name = peer.getDisplayName(inclNamespace, disambiguate);
89+
if (name) {
90+
return truncateParts(name, truncateLength);
8891
}
8992
if (scope === 'app') {
9093
// No peer distinction here
@@ -103,15 +106,17 @@ const getPeerName = (
103106
export const toNamedMetric = (
104107
t: TFunction,
105108
m: TopologyMetrics,
106-
data?: NodeData,
107-
truncateLength: TruncateLength = TruncateLength.OFF
109+
truncateLength: TruncateLength,
110+
inclNamespace: boolean,
111+
disambiguate: boolean,
112+
data?: NodeData
108113
): NamedMetric => {
109-
const srcName = getPeerName(t, m.source, m.scope, truncateLength);
110-
const srcFullName = getPeerName(t, m.source, m.scope);
111-
const dstName = getPeerName(t, m.destination, m.scope, truncateLength);
112-
const dstFullName = getPeerName(t, m.destination, m.scope);
114+
const srcName = getPeerName(t, m.source, m.scope, truncateLength, inclNamespace, disambiguate);
115+
const srcFullName = getPeerName(t, m.source, m.scope, TruncateLength.OFF, inclNamespace, disambiguate);
116+
const dstName = getPeerName(t, m.destination, m.scope, truncateLength, inclNamespace, disambiguate);
117+
const dstFullName = getPeerName(t, m.destination, m.scope, TruncateLength.OFF, inclNamespace, disambiguate);
113118
if (srcFullName === dstFullName) {
114-
if (m.source.displayName) {
119+
if (!isUnknownPeer(m.source)) {
115120
// E.g: namespace "netobserv" to "netobserv"
116121
return {
117122
...m,

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, true, true));
104+
const namedTotalMetric = toNamedMetric(t, totalMetric, truncateLength, false, false);
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/2d/topology-content.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,11 @@ export const TopologyContent: React.FC<{
164164

165165
const onFilter = React.useCallback(
166166
(data: Decorated<ElementData>) => {
167-
const isFiltered = isElementFiltered(data, filters, t);
168-
toggleElementFilter(data, isFiltered, filters, setFilters, t);
169-
setSelectedIds([data.id]);
167+
if (data.nodeType && data.peer) {
168+
const isFiltered = isElementFiltered(data.nodeType, data.peer, 'any', filters, t);
169+
toggleElementFilter(data.nodeType, data.peer, 'any', isFiltered, filters, setFilters, t);
170+
setSelectedIds([data.id]);
171+
}
170172
},
171173
[filters, setFilters, t]
172174
);

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+
peer: TopologyMetricPeer;
15+
activeFilters: Filter[];
16+
setFilters: (filters: Filter[]) => void;
17+
}> = ({ id, label, filterType, forcedText, peer, 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 peer={peer} />}
24+
</FlexItem>
25+
<FlexItem>
26+
<SummaryFilterButton
27+
id={id + '-filter'}
28+
activeFilters={activeFilters}
29+
filterType={filterType}
30+
fields={peer}
31+
setFilters={setFilters}
32+
/>
33+
</FlexItem>
34+
</Flex>
35+
</TextContent>
36+
);
37+
};

0 commit comments

Comments
 (0)