Skip to content

Commit

Permalink
histogram UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jpinsonneau committed Jan 18, 2023
1 parent 08a4fb5 commit 82836bb
Show file tree
Hide file tree
Showing 13 changed files with 361 additions and 24 deletions.
3 changes: 3 additions & 0 deletions web/locales/en/plugin__netobserv-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"Show configuration limits": "Show configuration limits",
"Copy": "Copy",
"Copied": "Copied",
"No datapoints found.": "No datapoints found.",
"(non nodes)": "(non nodes)",
"(non pods)": "(non pods)",
"internal": "internal",
Expand Down Expand Up @@ -208,6 +209,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;
7 changes: 7 additions & 0 deletions web/src/components/metrics/histogram.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.histogram-range {
position: absolute;
left: 0;
right: 0;
text-align: center;
margin: 5px;
}
141 changes: 141 additions & 0 deletions web/src/components/metrics/histogram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Chart, ChartAxis, ChartBar, ChartStack, ChartThemeColor, createContainer } from '@patternfly/react-charts';
import { Bullseye, EmptyStateBody, Spinner, Text } from '@patternfly/react-core';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { NamedMetric, TopologyMetrics } from '../../api/loki';
import { TimeRange } from '../../utils/datetime';
import { getDateMsInSeconds } from '../../utils/duration';
import { getFormattedRateValue } from '../../utils/metrics';
import { TruncateLength } from '../dropdowns/truncate-dropdown';
import BrushHandleComponent from './brush-handle';
import {
ChartDataPoint,
Dimensions,
getDomainDisplayText,
getDomainFromRange,
getHistogramRangeFromLimit,
observe,
toHistogramDatapoints,
toNamedMetric
} from './metrics-helper';
import './histogram.css';

export const VoronoiContainer = createContainer('voronoi', 'brush');

export const Histogram: React.FC<{
id: string;
totalMetric: NamedMetric;
limit: number;
isDark: boolean;
range?: TimeRange;
setRange: (tr: TimeRange) => void;
}> = ({ id, totalMetric, limit, isDark, range, setRange }) => {
const datapoints: ChartDataPoint[] = toHistogramDatapoints(totalMetric);
const defaultRange = getHistogramRangeFromLimit(totalMetric, limit);

const containerRef = React.createRef<HTMLDivElement>();
const [dimensions, setDimensions] = React.useState<Dimensions>({ width: 3000, height: 300 });
React.useEffect(() => {
observe(containerRef, dimensions, setDimensions);
}, [containerRef, dimensions]);

return (
<div id={`chart-${id}`} className="metrics-content-div" ref={containerRef}>
<Text className="histogram-range">{getDomainDisplayText(range ? range : defaultRange)}</Text>
<Chart
themeColor={ChartThemeColor.multiUnordered}
containerComponent={
<VoronoiContainer
brushDimension="x"
onBrushDomainChange={(updated?: { x?: Array<Date | number> }) => {
if (limit && updated?.x && updated.x.length && typeof updated.x[0] === 'object') {
const start = getDateMsInSeconds(updated.x[0].getTime());
const range = getHistogramRangeFromLimit(totalMetric, limit, start);

if (range.from < range.to) {
updated.x = getDomainFromRange(range);
}
}
}}
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={1}
brushStyle={{ stroke: 'transparent', fill: 'black', fillOpacity: 0.1 }}
brushDomain={{
x: getDomainFromRange(range ? range : defaultRange)
}}
/>
}
//TODO: fix refresh on selection change to enable animation
//animate={true}
scale={{ x: 'time', y: 'linear' }}
width={dimensions.width}
height={dimensions.height}
padding={{
top: 30,
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'}
/>
</ChartStack>
</Chart>
</div>
);
};

export const HistogramContainer: React.FC<{
id: string;
loading: boolean;
totalMetric: TopologyMetrics | undefined;
limit: number;
isDark: boolean;
range?: TimeRange;
setRange: (tr: TimeRange) => void;
}> = ({ id, loading, totalMetric, limit, isDark, range, setRange }) => {
const { t } = useTranslation('plugin__netobserv-plugin');

return totalMetric ? (
<Histogram
id={id}
totalMetric={toNamedMetric(t, totalMetric, TruncateLength.OFF, false, false)}
limit={limit}
isDark={isDark}
range={range}
setRange={setRange}
/>
) : loading ? (
<Bullseye data-test="loading-histogram">
<Spinner size="xl" />
</Bullseye>
) : (
<EmptyStateBody>{t('No datapoints found.')}</EmptyStateBody>
);
};

export default HistogramContainer;
68 changes: 66 additions & 2 deletions web/src/components/metrics/metrics-helper.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ChartLegendTooltip, createContainer, getResizeObserver } from '@patternfly/react-charts';
import { TFunction } from 'i18next';
import * as React from 'react';
import { getDateSInMiliseconds } from '../../utils/duration';
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 @@ -37,6 +38,19 @@ export const toDatapoints = (metric: NamedMetric): ChartDataPoint[] => {
}));
};

export const toHistogramDatapoints = (metric: NamedMetric): ChartDataPoint[] => {
const result: ChartDataPoint[] = [];
for (let i = 0; i < metric.values.length; i++) {
result.push({
name: metric.shortName,
date: getFormattedDate(getDateFromUnix(metric.values[i][0])),
x: getDateFromUnix(metric.values[i][0]),
y: Number(metric.values[i + 1]?.[1] | 0)
});
}
return result;
};

export const chartVoronoi = (legendData: LegendDataItem[], metricType: MetricType) => {
const CursorVoronoiContainer = createContainer('voronoi', 'cursor');
const tooltipData = legendData.map(item => ({ ...item, name: item.tooltipName }));
Expand All @@ -54,6 +68,50 @@ export const chartVoronoi = (legendData: LegendDataItem[], metricType: MetricTyp
);
};

export const getHistogramRangeFromLimit = (totalMetric: NamedMetric, limit: number, start?: number): TimeRange => {
let limitCount = 0,
from = 0,
to = 0;
let values: [number, number][];

if (start !== undefined) {
// get maximum range from start date before reaching the limit
values = totalMetric.values.filter(v => v[0] >= start && v[1] > 0);
from = values.shift()?.[0] || 0;
} else {
//get last range equal or higher to the limit to preview default loki behavior
values = [...totalMetric.values].reverse();
to = values.shift()?.[0] || 0;
}

for (const v of values) {
limitCount += v[1];

if (start !== undefined) {
if (limitCount < limit) {
to = v[0];
} else {
break;
}
} else {
from = v[0];
if (limitCount > limit) {
break;
}
}
}
return { from, to };
};

export const getDomainFromRange = (range: TimeRange): [Date, Date] => {
return [new Date(getDateSInMiliseconds(range.from)), new Date(getDateSInMiliseconds(range.to))];
};

export const getDomainDisplayText = (range: TimeRange): string => {
const domain = getDomainFromRange(range);
return `${getFormattedDate(domain[0])} - ${getFormattedDate(domain[1])}`;
};

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 +237,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 82836bb

Please sign in to comment.