Skip to content

Commit e74b6e9

Browse files
jotakjpinsonneau
andauthored
NETOBSERV-621 split summary metrics, limit 5 per chart (#237)
* NETOBSERV-621 split summary metrics, show less Show more stats in side panel The in/out textual stats are becoming less useful if we split in/out metrics in different tabs, however, more stats can be displayed regardless the "metric function" being defined, so we now display at the same time total, avg and latest (We don't display max at least for now .. more work would be necessary to get the max out of multiple timeseries) Fix metrics panel css Fix layout issues with stats * fix rebase conflicts * Merge in/out metrics back in one tab * Remove unnecessary flex * metrics justify content space evenly Co-authored-by: Julien Pinsonneau <[email protected]>
1 parent 2088ae6 commit e74b6e9

File tree

9 files changed

+346
-215
lines changed

9 files changed

+346
-215
lines changed

web/locales/en/plugin__netobserv-plugin.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,12 @@
174174
"Name": "Name",
175175
"IP": "IP",
176176
"No information available for this content. Change scope to get more details.": "No information available for this content. Change scope to get more details.",
177-
"Source to destination:": "Source to destination:",
178-
"In:": "In:",
179-
"Destination to source:": "Destination to source:",
180-
"Out:": "Out:",
181-
"Both:": "Both:",
182-
"{{type}} rate": "{{type}} rate",
177+
"Stats": "Stats",
178+
"Top 5 rates": "Top 5 rates",
179+
"A -> B": "A -> B",
180+
"In": "In",
181+
"B -> A": "B -> A",
182+
"Out": "Out",
183183
"Edge": "Edge",
184184
"Unable to get topology": "Unable to get topology",
185185
"Query is slow": "Query is slow",

web/src/components/metrics/__tests__/metrics-content.spec.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ describe('<MetricsContent />', () => {
1111
title: 'chart-test',
1212
metricType: 'bytes',
1313
metrics: metrics.map(m => ({ ...m, fullName: 'whatever', shortName: 'whatever', isInternal: false })),
14-
showTitle: true,
1514
smallerTexts: false,
1615
limit: 5,
1716
tooltipsTruncate: true
1817
};
1918
it('should render component', async () => {
2019
const wrapper = mount(<MetricsContent {...props} />);
2120
expect(wrapper.find(MetricsContent)).toBeTruthy();
22-
expect(wrapper.find('#metrics-title').last().text()).toBe('chart-test');
21+
expect(wrapper.find(Chart).last().props().ariaTitle).toBe('chart-test');
2322
});
2423
it('should render bar', async () => {
2524
const wrapper = mount(<MetricsContent {...props} showBar={true} />);

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

Lines changed: 48 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
ChartStack,
1111
ChartThemeColor
1212
} from '@patternfly/react-charts';
13-
import { Text, TextContent, TextVariants } from '@patternfly/react-core';
1413
import * as React from 'react';
1514
import { NamedMetric } from '../../api/loki';
1615
import { MetricType } from '../../model/flow-query';
@@ -32,8 +31,6 @@ export type MetricsContentProps = {
3231
metricType: MetricType;
3332
metrics: NamedMetric[];
3433
limit: number;
35-
counters?: JSX.Element;
36-
showTitle?: boolean;
3734
showBar?: boolean;
3835
showArea?: boolean;
3936
showScatter?: boolean;
@@ -48,8 +45,6 @@ export const MetricsContent: React.FC<MetricsContentProps> = ({
4845
metricType,
4946
metrics,
5047
limit,
51-
counters,
52-
showTitle,
5348
showBar,
5449
showArea,
5550
showScatter,
@@ -82,62 +77,54 @@ export const MetricsContent: React.FC<MetricsContentProps> = ({
8277
}, [containerRef, dimensions]);
8378

8479
return (
85-
<TextContent id="metrics" className="metrics-content-div">
86-
{showTitle && (
87-
<Text id="metrics-title" component={TextVariants.h4}>
88-
{title}
89-
</Text>
90-
)}
91-
{counters}
92-
<div id={`chart-${id}`} className="metrics-content-div" ref={containerRef}>
93-
<Chart
94-
themeColor={ChartThemeColor.multiUnordered}
95-
ariaTitle={title}
96-
containerComponent={chartVoronoi(legendData, metricType)}
97-
legendData={legendData}
98-
legendOrientation={'horizontal'}
99-
legendPosition="bottom-left"
100-
legendAllowWrap={true}
101-
legendComponent={legentComponent}
102-
//TODO: fix refresh on selection change to enable animation
103-
//animate={true}
104-
scale={{ x: 'time', y: showBar ? 'linear' : 'sqrt' }}
105-
width={dimensions.width}
106-
height={dimensions.height}
107-
domainPadding={{ x: 0, y: 0 }}
108-
padding={{
109-
bottom: (itemsPerRow && itemsPerRow > 1 ? legendData.length / 2 + 1 : legendData.length) * 25 + 75,
110-
left: 90,
111-
right: 50,
112-
top: 50
113-
}}
114-
>
115-
<ChartAxis fixLabelOverlap />
116-
<ChartAxis dependentAxis showGrid fixLabelOverlap tickFormat={y => getFormattedRateValue(y, metricType)} />
117-
{showBar && (
118-
<ChartStack>
119-
{topKDatapoints.map((datapoints, idx) => (
120-
<ChartBar name={`bar-${idx}`} key={`bar-${idx}`} data={datapoints} />
121-
))}
122-
</ChartStack>
123-
)}
124-
{showArea && (
125-
<ChartGroup>
126-
{topKDatapoints.map((datapoints, idx) => (
127-
<ChartArea name={`area-${idx}`} key={`area-${idx}`} data={datapoints} interpolation="monotoneX" />
128-
))}
129-
</ChartGroup>
130-
)}
131-
{showScatter && (
132-
<ChartGroup>
133-
{topKDatapoints.map((datapoints, idx) => (
134-
<ChartScatter name={`scatter-${idx}`} key={`scatter-${idx}`} data={datapoints} />
135-
))}
136-
</ChartGroup>
137-
)}
138-
</Chart>
139-
</div>
140-
</TextContent>
80+
<div id={`chart-${id}`} className="metrics-content-div" ref={containerRef}>
81+
<Chart
82+
themeColor={ChartThemeColor.multiUnordered}
83+
ariaTitle={title}
84+
containerComponent={chartVoronoi(legendData, metricType)}
85+
legendData={legendData}
86+
legendOrientation={'horizontal'}
87+
legendPosition="bottom-left"
88+
legendAllowWrap={true}
89+
legendComponent={legentComponent}
90+
//TODO: fix refresh on selection change to enable animation
91+
//animate={true}
92+
scale={{ x: 'time', y: showBar ? 'linear' : 'sqrt' }}
93+
width={dimensions.width}
94+
height={dimensions.height}
95+
domainPadding={{ x: 0, y: 0 }}
96+
padding={{
97+
bottom: (itemsPerRow && itemsPerRow > 1 ? legendData.length / 2 + 1 : legendData.length) * 25 + 75,
98+
left: 90,
99+
right: 50,
100+
top: 50
101+
}}
102+
>
103+
<ChartAxis fixLabelOverlap />
104+
<ChartAxis dependentAxis showGrid fixLabelOverlap tickFormat={y => getFormattedRateValue(y, metricType)} />
105+
{showBar && (
106+
<ChartStack>
107+
{topKDatapoints.map((datapoints, idx) => (
108+
<ChartBar name={`bar-${idx}`} key={`bar-${idx}`} data={datapoints} />
109+
))}
110+
</ChartStack>
111+
)}
112+
{showArea && (
113+
<ChartGroup>
114+
{topKDatapoints.map((datapoints, idx) => (
115+
<ChartArea name={`area-${idx}`} key={`area-${idx}`} data={datapoints} interpolation="monotoneX" />
116+
))}
117+
</ChartGroup>
118+
)}
119+
{showScatter && (
120+
<ChartGroup>
121+
{topKDatapoints.map((datapoints, idx) => (
122+
<ChartScatter name={`scatter-${idx}`} key={`scatter-${idx}`} data={datapoints} />
123+
))}
124+
</ChartGroup>
125+
)}
126+
</Chart>
127+
</div>
141128
);
142129
};
143130

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

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { mount, shallow } from 'enzyme';
44
import * as React from 'react';
55
import { Filter } from '../../../model/filters';
66
import { TopologyMetrics } from '../../../api/loki';
7-
import { MetricFunction, MetricScope, MetricType } from '../../../model/flow-query';
8-
import { ElementPanel, ElementPanelDetailsContent, ElementPanelMetricsContent } from '../element-panel';
7+
import { MetricScope, MetricType } from '../../../model/flow-query';
8+
import { ElementPanel, ElementPanelDetailsContent } from '../element-panel';
99
import { dataSample } from '../__tests-data__/metrics';
1010
import { NodeData } from '../../../model/topology';
11+
import { ElementPanelMetrics } from '../element-panel-metrics';
1112
import { createPeer } from '../../../utils/metrics';
1213
import { TruncateLength } from '../../../components/dropdowns/truncate-dropdown';
1314

@@ -34,7 +35,6 @@ describe('<ElementPanel />', () => {
3435
const mocks = {
3536
element: getNode('Pod', 'loki-distributor-loki-76598c8449-csmh2', '10.129.0.15'),
3637
metrics: dataSample as TopologyMetrics[],
37-
metricFunction: 'sum' as MetricFunction,
3838
metricType: 'bytes' as MetricType,
3939
metricScope: 'resource' as MetricScope,
4040
filters: [] as Filter[],
@@ -71,20 +71,47 @@ describe('<ElementPanel />', () => {
7171
expect(wrapper.find('#destination-content').last().text()).toBe('ServiceIP172.30.0.10');
7272
});
7373

74-
it('should render <ElementPanelMetricsContent />', async () => {
75-
const wrapper = mount(<ElementPanelMetricsContent {...mocks} />);
76-
expect(wrapper.find(ElementPanelMetricsContent)).toBeTruthy();
74+
it('should render <ElementPanelMetricsContent /> node', async () => {
75+
const wrapper = mount(
76+
<ElementPanelMetrics
77+
metricType={mocks.metricType}
78+
metrics={mocks.metrics}
79+
aData={mocks.element.getData()!}
80+
truncateLength={TruncateLength.M}
81+
isGroup={false}
82+
/>
83+
);
84+
expect(wrapper.find(ElementPanelMetrics)).toBeTruthy();
7785

7886
//check node metrics
79-
expect(wrapper.find('#inCount').last().text()).toBe('94.7 MB');
80-
expect(wrapper.find('#outCount').last().text()).toBe('4.1 MB');
81-
expect(wrapper.find('#total').last().text()).toBe('98.8 MB');
87+
expect(wrapper.find('#metrics-stats-total-in').last().text()).toBe('94.7 MB');
88+
expect(wrapper.find('#metrics-stats-avg-in').last().text()).toBe('332.4 kBps');
89+
expect(wrapper.find('#metrics-stats-latest-in').last().text()).toBe('0 Bps');
90+
expect(wrapper.find('#metrics-stats-total-out').last().text()).toBe('4.1 MB');
91+
expect(wrapper.find('#metrics-stats-avg-out').last().text()).toBe('14.3 kBps');
92+
expect(wrapper.find('#metrics-stats-latest-out').last().text()).toBe('0 Bps');
93+
});
8294

83-
//update to edge
84-
wrapper.setProps({ ...mocks, element: getEdge() });
85-
expect(wrapper.find('#inCount').last().text()).toBe('1.1 MB');
86-
expect(wrapper.find('#outCount').last().text()).toBe('4.5 MB');
87-
expect(wrapper.find('#total').last().text()).toBe('5.6 MB');
95+
it('should render <ElementPanelMetricsContent /> edge a->b', async () => {
96+
const edge = getEdge();
97+
const wrapper = mount(
98+
<ElementPanelMetrics
99+
metricType={mocks.metricType}
100+
metrics={mocks.metrics}
101+
aData={edge.getSource().getData()}
102+
bData={edge.getTarget().getData()}
103+
truncateLength={TruncateLength.M}
104+
isGroup={false}
105+
/>
106+
);
107+
expect(wrapper.find(ElementPanelMetrics)).toBeTruthy();
108+
109+
expect(wrapper.find('#metrics-stats-total-in').last().text()).toBe('1.1 MB');
110+
expect(wrapper.find('#metrics-stats-avg-in').last().text()).toBe('3.9 kBps');
111+
expect(wrapper.find('#metrics-stats-latest-in').last().text()).toBe('0 Bps');
112+
expect(wrapper.find('#metrics-stats-total-out').last().text()).toBe('4.5 MB');
113+
expect(wrapper.find('#metrics-stats-avg-out').last().text()).toBe('15.9 kBps');
114+
expect(wrapper.find('#metrics-stats-latest-out').last().text()).toBe('0 Bps');
88115
});
89116

90117
it('should filter <ElementPanelDetailsContent />', async () => {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { Flex, FlexItem, Radio, Text, TextContent, TextVariants } from '@patternfly/react-core';
4+
import { MetricType } from '../../model/flow-query';
5+
import { TopologyMetrics } from '../../api/loki';
6+
import { decorated, getStat, NodeData } from '../../model/topology';
7+
import { MetricsContent } from '../metrics/metrics-content';
8+
import { matchPeer } from '../../utils/metrics';
9+
import { toNamedMetric } from '../metrics/metrics-helper';
10+
import { ElementPanelStats } from './element-panel-stats';
11+
import { TruncateLength } from '../dropdowns/truncate-dropdown';
12+
13+
type MetricsRadio = 'in' | 'out' | 'both';
14+
15+
export const ElementPanelMetrics: React.FC<{
16+
aData: NodeData;
17+
bData?: NodeData;
18+
isGroup: boolean;
19+
metrics: TopologyMetrics[];
20+
metricType: MetricType;
21+
truncateLength: TruncateLength;
22+
}> = ({ aData, bData, isGroup, metrics, metricType, truncateLength }) => {
23+
const { t } = useTranslation('plugin__netobserv-plugin');
24+
const [metricsRadio, setMetricsRadio] = React.useState<MetricsRadio>('both');
25+
26+
const titleStats = t('Stats');
27+
const titleChart = t('Top 5 rates');
28+
29+
let id = '';
30+
let metricsIn: TopologyMetrics[] = [];
31+
let metricsOut: TopologyMetrics[] = [];
32+
let metricsBoth: TopologyMetrics[] = [];
33+
34+
if (bData) {
35+
// Edge selected
36+
id = `edge-${aData.peer.id}-${bData!.peer.id}`;
37+
metricsIn = metrics.filter(m => matchPeer(aData, m.source) && matchPeer(bData, m.destination));
38+
metricsOut = metrics.filter(m => matchPeer(bData, m.source) && matchPeer(aData, m.destination));
39+
metricsBoth = [...metricsIn, ...metricsOut];
40+
} else {
41+
// Node or group selected
42+
id = `node-${decorated(aData).id}`;
43+
metricsIn = metrics.filter(m => m.source.id !== m.destination.id && matchPeer(aData, m.destination));
44+
metricsOut = metrics.filter(m => m.source.id !== m.destination.id && matchPeer(aData, m.source));
45+
// Note that metricsBoth is not always the concat of in+out:
46+
// when a group is selected, there might be an overlap of in and out, so we don't want to count them twice
47+
metricsBoth = metrics.filter(
48+
m => m.source.id !== m.destination.id && (matchPeer(aData, m.source) || matchPeer(aData, m.destination))
49+
);
50+
}
51+
const focusNode = bData ? undefined : aData;
52+
const top5 = (metricsRadio === 'in' ? metricsIn : metricsRadio === 'out' ? metricsOut : metricsBoth)
53+
.map(m => toNamedMetric(t, m, truncateLength, false, false, isGroup ? undefined : focusNode))
54+
.sort((a, b) => getStat(b.stats, 'sum') - getStat(a.stats, 'sum'));
55+
56+
return (
57+
<div className="element-metrics-container">
58+
<TextContent>
59+
<Text id="metrics-stats-title" component={TextVariants.h4}>
60+
{titleStats}
61+
</Text>
62+
<ElementPanelStats
63+
metricType={metricType}
64+
metricsIn={metricsIn}
65+
metricsOut={metricsOut}
66+
metricsBoth={metricsBoth}
67+
isEdge={!!bData}
68+
/>
69+
<Text id="metrics-chart-title" component={TextVariants.h4}>
70+
{titleChart}
71+
</Text>
72+
<Flex className="metrics-justify-content">
73+
<FlexItem>
74+
<Radio
75+
isChecked={metricsRadio === 'in'}
76+
name="radio-in"
77+
onChange={() => setMetricsRadio('in')}
78+
label={bData ? t('A -> B') : t('In')}
79+
id="radio-in"
80+
/>
81+
</FlexItem>
82+
<FlexItem>
83+
<Radio
84+
isChecked={metricsRadio === 'out'}
85+
name="radio-out"
86+
onChange={() => setMetricsRadio('out')}
87+
label={bData ? t('B -> A') : t('Out')}
88+
id="radio-out"
89+
/>
90+
</FlexItem>
91+
<FlexItem>
92+
<Radio
93+
isChecked={metricsRadio === 'both'}
94+
name="radio-both"
95+
onChange={() => setMetricsRadio('both')}
96+
label={t('Both')}
97+
id="radio-both"
98+
/>
99+
</FlexItem>
100+
</Flex>
101+
</TextContent>
102+
<MetricsContent
103+
id={id}
104+
title={titleChart}
105+
metricType={metricType}
106+
metrics={top5}
107+
limit={5}
108+
showArea
109+
showScatter
110+
tooltipsTruncate={true}
111+
/>
112+
</div>
113+
);
114+
};

0 commit comments

Comments
 (0)