Commit 83eb2626 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'additional-time-series-charts' into 'master'

Added support for additional time series charts

Closes #33345

See merge request !13159
parents fb9f1e3a 0257dbda
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
import GraphLegend from './graph/legend.vue'; import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue'; import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue'; import GraphDeployment from './graph/deployment.vue';
import monitoringPaths from './monitoring_paths.vue';
import MonitoringMixin from '../mixins/monitoring_mixins'; import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
import { timeScaleFormat } from '../utils/date_time_formatters'; import { timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints'; import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left; const bisectDate = d3.bisector(d => d.time).left;
...@@ -36,32 +37,29 @@ ...@@ -36,32 +37,29 @@
data() { data() {
return { return {
baseGraphHeight: 450,
baseGraphWidth: 600,
graphHeight: 450, graphHeight: 450,
graphWidth: 600, graphWidth: 600,
graphHeightOffset: 120, graphHeightOffset: 120,
xScale: {},
yScale: {},
margin: {}, margin: {},
data: [],
unitOfDisplay: '', unitOfDisplay: '',
areaColorRgb: '#8fbce8', areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1', lineColorRgb: '#1f78d1',
yAxisLabel: '', yAxisLabel: '',
legendTitle: '', legendTitle: '',
reducedDeploymentData: [], reducedDeploymentData: [],
area: '',
line: '',
measurements: measurements.large, measurements: measurements.large,
currentData: { currentData: {
time: new Date(), time: new Date(),
value: 0, value: 0,
}, },
currentYCoordinate: 0, currentDataIndex: 0,
currentXCoordinate: 0, currentXCoordinate: 0,
currentFlagPosition: 0, currentFlagPosition: 0,
metricUsage: '',
showFlag: false, showFlag: false,
showDeployInfo: true, showDeployInfo: true,
timeSeries: [],
}; };
}, },
...@@ -69,16 +67,17 @@ ...@@ -69,16 +67,17 @@
GraphLegend, GraphLegend,
GraphFlag, GraphFlag,
GraphDeployment, GraphDeployment,
monitoringPaths,
}, },
computed: { computed: {
outterViewBox() { outterViewBox() {
return `0 0 ${this.graphWidth} ${this.graphHeight}`; return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
}, },
innerViewBox() { innerViewBox() {
if ((this.graphWidth - 150) > 0) { if ((this.baseGraphWidth - 150) > 0) {
return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`; return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
} }
return '0 0 0 0'; return '0 0 0 0';
}, },
...@@ -89,7 +88,7 @@ ...@@ -89,7 +88,7 @@
paddingBottomRootSvg() { paddingBottomRootSvg() {
return { return {
paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`, paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
}; };
}, },
}, },
...@@ -104,17 +103,16 @@ ...@@ -104,17 +103,16 @@
this.margin = measurements.small.margin; this.margin = measurements.small.margin;
this.measurements = measurements.small; this.measurements = measurements.small;
} }
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || ''; this.unitOfDisplay = query.unit || '';
this.yAxisLabel = this.graphData.y_label || 'Values'; this.yAxisLabel = this.graphData.y_label || 'Values';
this.legendTitle = query.label || 'Average'; this.legendTitle = query.label || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth - this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right; this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
if (this.data !== undefined) { this.baseGraphHeight = this.graphHeight;
this.renderAxesPaths(); this.baseGraphWidth = this.graphWidth;
this.formatDeployments(); this.renderAxesPaths();
} this.formatDeployments();
}, },
handleMouseOverGraph(e) { handleMouseOverGraph(e) {
...@@ -123,16 +121,17 @@ ...@@ -123,16 +121,17 @@
point.y = e.clientY; point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x = point.x += 7; point.x = point.x += 7;
const timeValueOverlay = this.xScale.invert(point.x); const firstTimeSeries = this.timeSeries[0];
const overlayIndex = bisectDate(this.data, timeValueOverlay, 1); const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const d0 = this.data[overlayIndex - 1]; const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
const d1 = this.data[overlayIndex]; const d0 = firstTimeSeries.values[overlayIndex - 1];
const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return; if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0; this.currentData = evalTime ? d1 : d0;
this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time)); this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x); const currentDeployXPos = this.mouseOverDeployInfo(point.x);
this.currentYCoordinate = this.yScale(this.currentData.value);
if (this.currentXCoordinate > (this.graphWidth - 200)) { if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103; this.currentFlagPosition = this.currentXCoordinate - 103;
...@@ -145,17 +144,25 @@ ...@@ -145,17 +144,25 @@
} else { } else {
this.showFlag = true; this.showFlag = true;
} }
this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
}, },
renderAxesPaths() { renderAxesPaths() {
this.timeSeries = createTimeSeries(this.graphData.queries[0].result,
this.graphWidth,
this.graphHeight,
this.graphHeightOffset);
if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
const axisXScale = d3.time.scale() const axisXScale = d3.time.scale()
.range([0, this.graphWidth]); .range([0, this.graphWidth]);
this.yScale = d3.scale.linear() const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]); .range([this.graphHeight - this.graphHeightOffset, 0]);
axisXScale.domain(d3.extent(this.data, d => d.time));
this.yScale.domain([0, d3.max(this.data.map(d => d.value))]); axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time));
axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]);
const xAxis = d3.svg.axis() const xAxis = d3.svg.axis()
.scale(axisXScale) .scale(axisXScale)
...@@ -164,7 +171,7 @@ ...@@ -164,7 +171,7 @@
.orient('bottom'); .orient('bottom');
const yAxis = d3.svg.axis() const yAxis = d3.svg.axis()
.scale(this.yScale) .scale(axisYScale)
.ticks(measurements.yTicks) .ticks(measurements.yTicks)
.orient('left'); .orient('left');
...@@ -180,25 +187,6 @@ ...@@ -180,25 +187,6 @@
.attr('class', 'axis-tick'); .attr('class', 'axis-tick');
} // Avoid adding the class to the first tick, to prevent coloring } // Avoid adding the class to the first tick, to prevent coloring
}); // This will select all of the ticks once they're rendered }); // This will select all of the ticks once they're rendered
this.xScale = d3.time.scale()
.range([0, this.graphWidth - 70]);
this.xScale.domain(d3.extent(this.data, d => d.time));
const areaFunction = d3.svg.area()
.x(d => this.xScale(d.time))
.y0(this.graphHeight - this.graphHeightOffset)
.y1(d => this.yScale(d.value))
.interpolate('linear');
const lineFunction = d3.svg.line()
.x(d => this.xScale(d.time))
.y(d => this.yScale(d.value));
this.line = lineFunction(this.data);
this.area = areaFunction(this.data);
}, },
}, },
...@@ -245,30 +233,25 @@ ...@@ -245,30 +233,25 @@
:graph-height="graphHeight" :graph-height="graphHeight"
:margin="margin" :margin="margin"
:measurements="measurements" :measurements="measurements"
:area-color-rgb="areaColorRgb"
:legend-title="legendTitle" :legend-title="legendTitle"
:y-axis-label="yAxisLabel" :y-axis-label="yAxisLabel"
:metric-usage="metricUsage" :time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
/> />
<svg <svg
class="graph-data" class="graph-data"
:viewBox="innerViewBox" :viewBox="innerViewBox"
ref="graphData"> ref="graphData">
<path <monitoring-paths
class="metric-area" v-for="(path, index) in timeSeries"
:d="area" :key="index"
:fill="areaColorRgb" :generated-line-path="path.linePath"
transform="translate(-5, 20)"> :generated-area-path="path.areaPath"
</path> :line-color="path.lineColor"
<path :area-color="path.areaColor"
class="metric-line" />
:d="line" <monitoring-deployment
:stroke="lineColorRgb"
fill="none"
stroke-width="2"
transform="translate(-5, 20)">
</path>
<graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
:graph-height="graphHeight" :graph-height="graphHeight"
...@@ -277,7 +260,6 @@ ...@@ -277,7 +260,6 @@
<graph-flag <graph-flag
v-if="showFlag" v-if="showFlag"
:current-x-coordinate="currentXCoordinate" :current-x-coordinate="currentXCoordinate"
:current-y-coordinate="currentYCoordinate"
:current-data="currentData" :current-data="currentData"
:current-flag-position="currentFlagPosition" :current-flag-position="currentFlagPosition"
:graph-height="graphHeight" :graph-height="graphHeight"
......
...@@ -7,10 +7,6 @@ ...@@ -7,10 +7,6 @@
type: Number, type: Number,
required: true, required: true,
}, },
currentYCoordinate: {
type: Number,
required: true,
},
currentFlagPosition: { currentFlagPosition: {
type: Number, type: Number,
required: true, required: true,
...@@ -60,16 +56,7 @@ ...@@ -60,16 +56,7 @@
:y2="calculatedHeight" :y2="calculatedHeight"
transform="translate(-5, 20)"> transform="translate(-5, 20)">
</line> </line>
<circle <svg
class="circle-metric"
:fill="circleColorRgb"
stroke="#000"
:cx="currentXCoordinate"
:cy="currentYCoordinate"
r="5"
transform="translate(-5, 20)">
</circle>
<svg
class="rect-text-metric" class="rect-text-metric"
:x="currentFlagPosition" :x="currentFlagPosition"
y="0"> y="0">
......
<script> <script>
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
export default { export default {
props: { props: {
graphWidth: { graphWidth: {
...@@ -17,10 +19,6 @@ ...@@ -17,10 +19,6 @@
type: Object, type: Object,
required: true, required: true,
}, },
areaColorRgb: {
type: String,
required: true,
},
legendTitle: { legendTitle: {
type: String, type: String,
required: true, required: true,
...@@ -29,15 +27,25 @@ ...@@ -29,15 +27,25 @@
type: String, type: String,
required: true, required: true,
}, },
metricUsage: { timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String, type: String,
required: true, required: true,
}, },
currentDataIndex: {
type: Number,
required: true,
},
}, },
data() { data() {
return { return {
yLabelWidth: 0, yLabelWidth: 0,
yLabelHeight: 0, yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
}; };
}, },
computed: { computed: {
...@@ -63,10 +71,28 @@ ...@@ -63,10 +71,28 @@
yPosition() { yPosition() {
return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0; return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
}, },
},
methods: {
translateLegendGroup(index) {
return `translate(0, ${12 * (index)})`;
},
formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
},
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox(); const bbox = this.$refs.ylabel.getBBox();
this.metricUsageXPosition = 0;
this.seriesXPosition = 0;
if (this.$refs.legendTitleSvg != null) {
this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
}
if (this.$refs.seriesTitleSvg != null) {
this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
}
this.yLabelWidth = bbox.width + 10; // Added some padding this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5; this.yLabelHeight = bbox.height + 5;
}); });
...@@ -121,24 +147,33 @@ ...@@ -121,24 +147,33 @@
dy=".35em"> dy=".35em">
Time Time
</text> </text>
<rect <g class="legend-group"
:fill="areaColorRgb" v-for="(series, index) in timeSeries"
:width="measurements.legends.width" :key="index"
:height="measurements.legends.height" :transform="translateLegendGroup(index)">
x="20" <rect
:y="graphHeight - measurements.legendOffset"> :fill="series.areaColor"
</rect> :width="measurements.legends.width"
<text :height="measurements.legends.height"
class="text-metric-title" x="20"
x="50" :y="graphHeight - measurements.legendOffset">
:y="graphHeight - 25"> </rect>
{{legendTitle}} <text
</text> v-if="timeSeries.length > 1"
<text class="legend-metric-title"
class="text-metric-usage" ref="legendTitleSvg"
x="50" x="38"
:y="graphHeight - 10"> :y="graphHeight - 30">
{{metricUsage}} {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}}
</text> </text>
<text
v-else
class="legend-metric-title"
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30">
{{legendTitle}} {{formatMetricUsage(series)}}
</text>
</g>
</g> </g>
</template> </template>
<script>
export default {
props: {
generatedLinePath: {
type: String,
required: true,
},
generatedAreaPath: {
type: String,
required: true,
},
lineColor: {
type: String,
required: true,
},
areaColor: {
type: String,
required: true,
},
},
};
</script>
<template>
<g>
<path
class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
transform="translate(-5, 20)">
</path>
<path
class="metric-line"
:d="generatedLinePath"
:stroke="lineColor"
fill="none"
stroke-width="1"
transform="translate(-5, 20)">
</path>
</g>
</template>
...@@ -21,9 +21,9 @@ const mixins = { ...@@ -21,9 +21,9 @@ const mixins = {
formatDeployments() { formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at); const time = new Date(deployment.created_at);
const xPos = Math.floor(this.xScale(time)); const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time));
time.setSeconds(this.data[0].time.getSeconds()); time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
if (xPos >= 0) { if (xPos >= 0) {
deploymentDataArray.push({ deploymentDataArray.push({
......
import _ from 'underscore'; import _ from 'underscore';
class MonitoringStore { function sortMetrics(metrics) {
return _.chain(metrics).sortBy('weight').sortBy('title').value();
}
function normalizeMetrics(metrics) {
return metrics.map(metric => ({
...metric,
queries: metric.queries.map(query => ({
...query,
result: query.result.map(result => ({
...result,
values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000),
value,
})),
})),
})),
}));
}
function collate(array, rows = 2) {
const collatedArray = [];
let row = [];
array.forEach((value, index) => {
row.push(value);
if ((index + 1) % rows === 0) {
collatedArray.push(row);
row = [];
}
});
if (row.length > 0) {
collatedArray.push(row);
}
return collatedArray;
}
export default class MonitoringStore {
constructor() { constructor() {
this.groups = []; this.groups = [];
this.deploymentData = []; this.deploymentData = [];
} }
// eslint-disable-next-line class-methods-use-this
createArrayRows(metrics = []) {
const currentMetrics = metrics;
const availableMetrics = [];
let metricsRow = [];
let index = 1;
Object.keys(currentMetrics).forEach((key) => {
const metricValues = currentMetrics[key].queries[0].result[0].values;
if (metricValues != null) {
const literalMetrics = metricValues.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
}));
currentMetrics[key].queries[0].result[0].values = literalMetrics;
metricsRow.push(currentMetrics[key]);
if (index % 2 === 0) {
availableMetrics.push(metricsRow);
metricsRow = [];
}
index = index += 1;
}
});
if (metricsRow.length > 0) {
availableMetrics.push(metricsRow);
}
return availableMetrics;
}
storeMetrics(groups = []) { storeMetrics(groups = []) {
this.groups = groups.map((group) => { this.groups = groups.map(group => ({
const currentGroup = group; ...group,
currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value(); metrics: collate(normalizeMetrics(sortMetrics(group.metrics))),
currentGroup.metrics = this.createArrayRows(currentGroup.metrics); }));
return currentGroup;
});
} }
storeDeploymentData(deploymentData = []) { storeDeploymentData(deploymentData = []) {
...@@ -57,5 +63,3 @@ class MonitoringStore { ...@@ -57,5 +63,3 @@ class MonitoringStore {
return metricsCount; return metricsCount;
} }
} }
export default MonitoringStore;
...@@ -7,15 +7,15 @@ export default { ...@@ -7,15 +7,15 @@ export default {
left: 40, left: 40,
}, },
legends: { legends: {
width: 15, width: 10,
height: 25, height: 3,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 50, height: 50,
}, },
axisLabelLineOffset: -20, axisLabelLineOffset: -20,
legendOffset: 35, legendOffset: 33,
}, },
large: { // This covers both md and lg screen sizes large: { // This covers both md and lg screen sizes
margin: { margin: {
...@@ -25,15 +25,15 @@ export default { ...@@ -25,15 +25,15 @@ export default {
left: 80, left: 80,
}, },
legends: { legends: {
width: 20, width: 15,
height: 30, height: 3,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 150, height: 150,
}, },
axisLabelLineOffset: 20, axisLabelLineOffset: 20,
legendOffset: 38, legendOffset: 36,
}, },
xTicks: 8, xTicks: 8,
yTicks: 3, yTicks: 3,
......
import d3 from 'd3';
import _ from 'underscore';
export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) {
const maxValues = seriesData.map((timeSeries, index) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value));
return {
maxValue,
index,
};
});
const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
let timeSeriesNumber = 1;
let lineColor = '#1f78d1';
let areaColor = '#8fbce8';
return seriesData.map((timeSeries) => {
const timeSeriesScaleX = d3.time.scale()
.range([0, graphWidth - 70]);
const timeSeriesScaleY = d3.scale.linear()
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const lineFunction = d3.svg.line()
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area()
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value))
.interpolate('linear');
switch (timeSeriesNumber) {
case 1:
lineColor = '#1f78d1';
areaColor = '#8fbce8';
break;
case 2:
lineColor = '#fc9403';
areaColor = '#feca81';
break;
case 3:
lineColor = '#db3b21';
areaColor = '#ed9d90';
break;
case 4:
lineColor = '#1aaa55';
areaColor = '#8dd5aa';
break;
case 5:
lineColor = '#6666c4';
areaColor = '#d1d1f0';
break;
default:
lineColor = '#1f78d1';
areaColor = '#8fbce8';
break;
}
if (timeSeriesNumber <= 5) {
timeSeriesNumber = timeSeriesNumber += 1;
} else {
timeSeriesNumber = 1;
}
return {
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
lineColor,
areaColor,
};
});
}
...@@ -169,7 +169,7 @@ ...@@ -169,7 +169,7 @@
} }
.metric-area { .metric-area {
opacity: 0.8; opacity: 0.25;
} }
.prometheus-graph-overlay { .prometheus-graph-overlay {
...@@ -251,8 +251,14 @@ ...@@ -251,8 +251,14 @@
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.label-axis-text, .label-axis-text {
.text-metric-usage { fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 10px;
}
.text-metric-usage,
.legend-metric-title {
fill: $black; fill: $black;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
font-size: 12px; font-size: 12px;
......
---
title: Added support the multiple time series for prometheus monitoring
merge_request: !36893
author:
type: changed
...@@ -32,10 +32,6 @@ describe('GraphFlag', () => { ...@@ -32,10 +32,6 @@ describe('GraphFlag', () => {
.toEqual(component.currentXCoordinate); .toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.selected-metric-line', 'x2')) expect(getCoordinate(component, '.selected-metric-line', 'x2'))
.toEqual(component.currentXCoordinate); .toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.circle-metric', 'cx'))
.toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.circle-metric', 'cy'))
.toEqual(component.currentYCoordinate);
}); });
it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import GraphLegend from '~/monitoring/components/graph/legend.vue'; import GraphLegend from '~/monitoring/components/graph/legend.vue';
import measurements from '~/monitoring/utils/measurements'; import measurements from '~/monitoring/utils/measurements';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data';
const createComponent = (propsData) => { const createComponent = (propsData) => {
const Component = Vue.extend(GraphLegend); const Component = Vue.extend(GraphLegend);
...@@ -10,6 +12,28 @@ const createComponent = (propsData) => { ...@@ -10,6 +12,28 @@ const createComponent = (propsData) => {
}).$mount(); }).$mount();
}; };
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
const defaultValuesComponent = {
graphWidth: 500,
graphHeight: 300,
graphHeightOffset: 120,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
unitOfDisplay: 'Req/Sec',
currentDataIndex: 0,
};
const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result,
defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight,
defaultValuesComponent.graphHeightOffset);
defaultValuesComponent.timeSeries = timeSeries;
function getTextFromNode(component, selector) { function getTextFromNode(component, selector) {
return component.$el.querySelector(selector).firstChild.nodeValue.trim(); return component.$el.querySelector(selector).firstChild.nodeValue.trim();
} }
...@@ -17,95 +41,67 @@ function getTextFromNode(component, selector) { ...@@ -17,95 +41,67 @@ function getTextFromNode(component, selector) {
describe('GraphLegend', () => { describe('GraphLegend', () => {
describe('Computed props', () => { describe('Computed props', () => {
it('textTransform', () => { it('textTransform', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.textTransform).toContain('translate(15, 120) rotate(-90)'); expect(component.textTransform).toContain('translate(15, 120) rotate(-90)');
}); });
it('xPosition', () => { it('xPosition', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.xPosition).toEqual(180); expect(component.xPosition).toEqual(180);
}); });
it('yPosition', () => { it('yPosition', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.yPosition).toEqual(240); expect(component.yPosition).toEqual(240);
}); });
it('rectTransform', () => { it('rectTransform', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)'); expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)');
}); });
}); });
it('has 2 rect-axis-text rect svg elements', () => { describe('methods', () => {
const component = createComponent({ it('translateLegendGroup should only change Y direction', () => {
graphWidth: 500, const component = createComponent(defaultValuesComponent);
graphHeight: 300,
margin: measurements.large.margin, const translatedCoordinate = component.translateLegendGroup(1);
measurements: measurements.large, expect(translatedCoordinate.indexOf('translate(0, ')).not.toEqual(-1);
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
}); });
it('formatMetricUsage should contain the unit of display and the current value selected via "currentDataIndex"', () => {
const component = createComponent(defaultValuesComponent);
const formattedMetricUsage = component.formatMetricUsage(timeSeries[0]);
const valueFromSeries = timeSeries[0].values[component.currentDataIndex].value;
expect(formattedMetricUsage.indexOf(component.unitOfDisplay)).not.toEqual(-1);
expect(formattedMetricUsage.indexOf(valueFromSeries)).not.toEqual(-1);
});
});
it('has 2 rect-axis-text rect svg elements', () => {
const component = createComponent(defaultValuesComponent);
expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
}); });
it('contains text to signal the usage, title and time', () => { it('contains text to signal the usage, title and time', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500, const titles = component.$el.querySelectorAll('.legend-metric-title');
graphHeight: 300,
margin: measurements.large.margin, expect(getTextFromNode(component, '.legend-metric-title').indexOf(component.legendTitle)).not.toEqual(-1);
measurements: measurements.large, expect(titles[0].textContent.indexOf('Title')).not.toEqual(-1);
areaColorRgb: '#f0f0f0', expect(titles[1].textContent.indexOf('Series')).not.toEqual(-1);
legendTitle: 'Title', expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel);
yAxisLabel: 'Values', });
metricUsage: 'Value',
}); it('should contain the same number of legend groups as the timeSeries length', () => {
const component = createComponent(defaultValuesComponent);
expect(getTextFromNode(component, '.text-metric-title')).toEqual(component.legendTitle); expect(component.$el.querySelectorAll('.legend-group').length).toEqual(component.timeSeries.length);
expect(getTextFromNode(component, '.text-metric-usage')).toEqual(component.metricUsage);
expect(getTextFromNode(component, '.label-axis-text')).toEqual(component.yAxisLabel);
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import GraphRow from '~/monitoring/components/graph_row.vue'; import GraphRow from '~/monitoring/components/graph_row.vue';
import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import { deploymentData, singleRowMetrics } from './mock_data'; import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
const createComponent = (propsData) => { const createComponent = (propsData) => {
const Component = Vue.extend(GraphRow); const Component = Vue.extend(GraphRow);
...@@ -11,15 +11,15 @@ const createComponent = (propsData) => { ...@@ -11,15 +11,15 @@ const createComponent = (propsData) => {
}).$mount(); }).$mount();
}; };
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
describe('GraphRow', () => { describe('GraphRow', () => {
beforeEach(() => { beforeEach(() => {
spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
}); });
describe('Computed props', () => { describe('Computed props', () => {
it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => { it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => {
const component = createComponent({ const component = createComponent({
rowData: singleRowMetrics, rowData: convertedMetrics,
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
}); });
...@@ -29,7 +29,7 @@ describe('GraphRow', () => { ...@@ -29,7 +29,7 @@ describe('GraphRow', () => {
it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => { it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => {
const component = createComponent({ const component = createComponent({
rowData: [singleRowMetrics[0]], rowData: [convertedMetrics[0]],
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
}); });
...@@ -40,7 +40,7 @@ describe('GraphRow', () => { ...@@ -40,7 +40,7 @@ describe('GraphRow', () => {
it('has one column', () => { it('has one column', () => {
const component = createComponent({ const component = createComponent({
rowData: singleRowMetrics, rowData: convertedMetrics,
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
}); });
...@@ -51,7 +51,7 @@ describe('GraphRow', () => { ...@@ -51,7 +51,7 @@ describe('GraphRow', () => {
it('has two columns', () => { it('has two columns', () => {
const component = createComponent({ const component = createComponent({
rowData: singleRowMetrics, rowData: convertedMetrics,
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import _ from 'underscore';
import Graph from '~/monitoring/components/graph.vue'; import Graph from '~/monitoring/components/graph.vue';
import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import eventHub from '~/monitoring/event_hub'; import eventHub from '~/monitoring/event_hub';
import { deploymentData, singleRowMetrics } from './mock_data'; import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
const createComponent = (propsData) => { const createComponent = (propsData) => {
const Component = Vue.extend(Graph); const Component = Vue.extend(Graph);
...@@ -13,6 +12,8 @@ const createComponent = (propsData) => { ...@@ -13,6 +12,8 @@ const createComponent = (propsData) => {
}).$mount(); }).$mount();
}; };
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
describe('Graph', () => { describe('Graph', () => {
beforeEach(() => { beforeEach(() => {
spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
...@@ -20,7 +21,7 @@ describe('Graph', () => { ...@@ -20,7 +21,7 @@ describe('Graph', () => {
it('has a title', () => { it('has a title', () => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
...@@ -29,29 +30,10 @@ describe('Graph', () => { ...@@ -29,29 +30,10 @@ describe('Graph', () => {
expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title); expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title);
}); });
it('creates a path for the line and area of the graph', (done) => {
const component = createComponent({
graphData: singleRowMetrics[0],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
});
Vue.nextTick(() => {
expect(component.area).toBeDefined();
expect(component.line).toBeDefined();
expect(typeof component.area).toEqual('string');
expect(typeof component.line).toEqual('string');
expect(_.isFunction(component.xScale)).toBe(true);
expect(_.isFunction(component.yScale)).toBe(true);
done();
});
});
describe('Computed props', () => { describe('Computed props', () => {
it('axisTransform translates an element Y position depending of its height', () => { it('axisTransform translates an element Y position depending of its height', () => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
...@@ -64,7 +46,7 @@ describe('Graph', () => { ...@@ -64,7 +46,7 @@ describe('Graph', () => {
it('outterViewBox gets a width and height property based on the DOM size of the element', () => { it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
...@@ -79,7 +61,7 @@ describe('Graph', () => { ...@@ -79,7 +61,7 @@ describe('Graph', () => {
it('sends an event to the eventhub when it has finished resizing', (done) => { it('sends an event to the eventhub when it has finished resizing', (done) => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
...@@ -95,7 +77,7 @@ describe('Graph', () => { ...@@ -95,7 +77,7 @@ describe('Graph', () => {
it('has a title for the y-axis and the chart legend that comes from the backend', () => { it('has a title for the y-axis and the chart legend that comes from the backend', () => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
......
This diff is collapsed.
import Vue from 'vue';
import MonitoringPaths from '~/monitoring/components/monitoring_paths.vue';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(MonitoringPaths);
return new Component({
propsData,
}).$mount();
};
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120);
describe('Monitoring Paths', () => {
it('renders two paths to represent a line and the area underneath it', () => {
const component = createComponent({
generatedLinePath: timeSeries[0].linePath,
generatedAreaPath: timeSeries[0].areaPath,
lineColor: '#ccc',
areaColor: '#fff',
});
const metricArea = component.$el.querySelector('.metric-area');
const metricLine = component.$el.querySelector('.metric-line');
expect(metricArea.getAttribute('fill')).toBe('#fff');
expect(metricArea.getAttribute('d')).toBe(timeSeries[0].areaPath);
expect(metricLine.getAttribute('stroke')).toBe('#ccc');
expect(metricLine.getAttribute('d')).toBe(timeSeries[0].linePath);
});
});
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data';
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120);
describe('Multiple time series', () => {
it('createTimeSeries returned array contains an object for each element', () => {
expect(typeof timeSeries[0].linePath).toEqual('string');
expect(typeof timeSeries[0].areaPath).toEqual('string');
expect(typeof timeSeries[0].timeSeriesScaleX).toEqual('function');
expect(typeof timeSeries[0].areaColor).toEqual('string');
expect(typeof timeSeries[0].lineColor).toEqual('string');
expect(timeSeries[0].values instanceof Array).toEqual(true);
});
it('createTimeSeries returns an array', () => {
expect(timeSeries instanceof Array).toEqual(true);
expect(timeSeries.length).toEqual(2);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment