Skip to content

Commit

Permalink
histogram UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jpinsonneau committed Jan 17, 2023
1 parent 08a4fb5 commit 9dbcd1e
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 21 deletions.
2 changes: 2 additions & 0 deletions web/locales/en/plugin__netobserv-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@
"Add Namespace, Owner or Resource filters (which use indexed fields), or decrease limit / range, to improve the query performance": "Add Namespace, Owner or Resource filters (which use indexed fields), or decrease limit / range, to improve the query performance",
"Add more filters or decrease limit / range to improve the query performance": "Add more filters or decrease limit / range to improve the query performance",
"Network Traffic": "Network Traffic",
"Hide histogram": "Hide histogram",
"Show histogram": "Show histogram",
"Hide advanced options": "Hide advanced options",
"Show advanced options": "Show advanced options",
"Filtered sum of bytes": "Filtered sum of bytes",
Expand Down
4 changes: 2 additions & 2 deletions web/src/api/loki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ export interface StreamResult {
values: string[][];
}

export interface RecordsResult {
export class RecordsResult {
records: Record[];
stats: Stats;
}

export interface TopologyResult {
export class TopologyResult {
metrics: TopologyMetrics[];
stats: Stats;
}
Expand Down
2 changes: 2 additions & 0 deletions web/src/components/dropdowns/metric-type-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const MetricTypeDropdown: React.FC<{
return t('Packets');
case 'bytes':
return t('Bytes');
default:
throw new Error('getMetricDisplay called with invalid metricType: ' + metricType);
}
},
[t]
Expand Down
27 changes: 27 additions & 0 deletions web/src/components/metrics/brush-handle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';

export const BrushHandleComponent: React.FC<{
x?: number;
y?: number;
width?: number;
height?: number;
isDark?: boolean;
}> = ({ x, y, width, height, isDark }) => {
if (x === undefined || y === undefined || width === undefined || height === undefined) {
return null;
}

const triangleSize = 6;
const color = isDark ? '#D2D2D2' : '#0066CC';
return (
<g>
<line x1={x} x2={x} y1={y} y2={height + y} style={{ stroke: color, strokeDasharray: '5 3' }} />
<polygon
points={`${x},${y} ${x - triangleSize},${y - triangleSize} ${x + triangleSize},${y - triangleSize}`}
style={{ fill: color }}
/>
</g>
);
};

export default BrushHandleComponent;
125 changes: 125 additions & 0 deletions web/src/components/metrics/histogram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
Chart,
ChartAxis,
ChartBar,
ChartLegendTooltip,
ChartStack,
ChartThemeColor,
createContainer
} from '@patternfly/react-charts';
import * as React from 'react';
import { TimeRange } from '../../utils/datetime';
import { getDateMsInSeconds, getDateSInMiliseconds } from '../../utils/duration';
import { NamedMetric } from '../../api/loki';
import { getFormattedRateValue } from '../../utils/metrics';
import BrushHandleComponent from './brush-handle';
import './metrics-content.css';
import {
ChartDataPoint,
defaultDimensions,
Dimensions,
getHistogramRangeFromLimit,
observe,
toDatapoints
} from './metrics-helper';

export type HistogramProps = {
id: string;
title: string;
totalMetric: NamedMetric;
limit: number;
isDark: boolean;
setRange: (tr: TimeRange) => void;
};

export const Histogram: React.FC<HistogramProps> = ({ id, title, totalMetric, limit, isDark, setRange }) => {
const VoronoiContainer = createContainer('voronoi', 'brush');
const datapoints: ChartDataPoint[] = toDatapoints(totalMetric);

const containerRef = React.createRef<HTMLDivElement>();
const [dimensions, setDimensions] = React.useState<Dimensions>(defaultDimensions);
React.useEffect(() => {
observe(containerRef, dimensions, setDimensions);
}, [containerRef, dimensions]);

return (
<div id={`chart-${id}`} className="metrics-content-div" ref={containerRef}>
<Chart
themeColor={ChartThemeColor.multiUnordered}
ariaTitle={title}
containerComponent={
<VoronoiContainer
activateData={false}
cursorDimension="x"
voronoiDimension="x"
brushDimension="x"
labels={(dp: { datum: ChartDataPoint }) => {
return dp.datum.y || dp.datum.y === 0 ? getFormattedRateValue(dp.datum.y, 'count') : 'n/a';
}}
labelComponent={<ChartLegendTooltip title={(datum: ChartDataPoint) => datum.date} />}
mouseFollowTooltips={true}
voronoiPadding={0}
onBrushDomainChange={(updated?: { x?: Array<Date | number> }) => {
if (totalMetric && limit && updated?.x && updated.x.length && typeof updated.x[0] === 'object') {
const start = getDateMsInSeconds(updated.x[0].getTime());
const range = getHistogramRangeFromLimit(totalMetric, limit, start);

updated.x = [new Date(getDateSInMiliseconds(range.from)), new Date(getDateSInMiliseconds(range.to))];
}
}}
onBrushDomainChangeEnd={(domain?: { x?: Array<Date | number> }) => {
if (
domain?.x &&
domain.x.length > 1 &&
typeof domain.x[0] === 'object' &&
typeof domain.x[1] === 'object'
) {
const start = domain.x[0];
const end = domain.x[1];

if (start.getTime() < end.getTime()) {
setRange?.({ from: getDateMsInSeconds(start.getTime()), to: getDateMsInSeconds(end.getTime()) });
}
}
}}
handleComponent={<BrushHandleComponent isDark={isDark} />}
defaultBrushArea="none"
handleWidth={0}
brushStyle={{ stroke: 'transparent', fill: 'black', fillOpacity: 0.1, zIndex: 999999 }}
allowDrag={true}
allowDraw={true}
allowResize={true}
brushDomain={{ x: [0, 0], y: [0, 0] }}
/>
}
//TODO: fix refresh on selection change to enable animation
//animate={true}
scale={{ x: 'time', y: 'linear' }}
width={dimensions.width}
height={dimensions.height}
domainPadding={{ x: 50, y: 0 }}
padding={{
top: 10,
right: 10,
bottom: 35,
left: 60
}}
>
<ChartAxis fixLabelOverlap />
<ChartAxis dependentAxis showGrid fixLabelOverlap tickFormat={y => getFormattedRateValue(y, 'count')} />
<ChartStack>
<ChartBar
name={`bar-${id}`}
key={`bar-${id}`}
data={datapoints}
barWidth={(dimensions.width / datapoints.length) * 0.8}
alignment={'start'}
labelComponent={<g />}
/>
</ChartStack>
</Chart>
</div>
);
};

export default Histogram;
25 changes: 23 additions & 2 deletions web/src/components/metrics/metrics-helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from 'react';
import { NamedMetric, TopologyMetricPeer, TopologyMetrics } from '../../api/loki';
import { MetricScope, MetricType } from '../../model/flow-query';
import { NodeData } from '../../model/topology';
import { getDateFromUnix, getFormattedDate } from '../../utils/datetime';
import { getDateFromUnix, getFormattedDate, TimeRange } from '../../utils/datetime';
import { getFormattedRateValue, isUnknownPeer, matchPeer } from '../../utils/metrics';
import { TruncateLength } from '../dropdowns/truncate-dropdown';

Expand Down Expand Up @@ -54,6 +54,21 @@ export const chartVoronoi = (legendData: LegendDataItem[], metricType: MetricTyp
);
};

export const getHistogramRangeFromLimit = (totalMetric: NamedMetric, limit: number, start = 0): TimeRange => {
let limitCount = 0;

const filteredValues = totalMetric.values.filter(v => v[0] >= start && v[1] > 0);
const from = filteredValues.shift()?.[0] || 0;
let to = 0;
filteredValues.forEach(v => {
limitCount += v[1];
if (limitCount < limit) {
to = v[0];
}
});
return { from, to };
};

const truncate = (input: string, length: number) => {
if (input.length > length) {
return input.substring(0, length / 2) + '…' + input.substring(input.length - length / 2);
Expand Down Expand Up @@ -179,7 +194,13 @@ export const observe = (
height: containerRef?.current?.clientHeight || defaultDimensions.height
};

if (newDimension.width !== dimensions.width || newDimension.height !== dimensions.height) {
// in some cases, newDimension is increasing which result of infinite loop in the observer
// making graphs growing endlessly
const toleration = 10;
if (
Math.abs(newDimension.width - dimensions.width) > toleration ||
Math.abs(newDimension.height - dimensions.height) > toleration
) {
setDimensions(newDimension);
}
}
Expand Down
16 changes: 16 additions & 0 deletions web/src/components/netflow-traffic.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,28 @@ span.pf-c-button__icon.pf-m-start {
padding-right: 0;
}

/* histogram is maximum 2x standard toolbar height */
#histogram-toolbar {
max-height: 130px;
}

#histogram-toolbar,
#view-options-toolbar {
z-index: 5;
box-shadow: 0 0.125rem 0.1rem -0.0625rem rgba(3, 3, 3, 0.16);
}

#histogram-toolbar.dark,
#view-options-toolbar.dark {
box-shadow: 0 0.125rem 0.1rem -0.0625rem rgba(255, 255, 255, 0.16);
}

.histogram {
display: flex;
align-items: center;
justify-content: center;
}

/*hide some content for fullscreen mode*/
.hidden {
display: none !important;
Expand Down Expand Up @@ -152,14 +165,17 @@ span.pf-c-button__icon.pf-m-start {
margin-right: 1rem;
}

#histogram-toolbar,
#view-options-toolbar {
display: flex;
}

#histogram-toolbar>.histogram,
#view-options-toolbar>.view-options-first {
margin-left: 1.5rem;
}

#histogram-toolbar>.histogram,
#view-options-toolbar>.view-options-last {
margin-right: 1.5rem;
}
Expand Down
Loading

0 comments on commit 9dbcd1e

Please sign in to comment.