Add summary statistics prometheus dashboard
This commit is contained in:
parent
9e3cdc02e6
commit
657fea8688
|
@ -3,6 +3,7 @@ import { scaleLinear, scaleTime } from 'd3-scale';
|
|||
import { axisLeft, axisBottom } from 'd3-axis';
|
||||
import { max, extent } from 'd3-array';
|
||||
import { select } from 'd3-selection';
|
||||
import GraphAxis from './graph/axis.vue';
|
||||
import GraphLegend from './graph/legend.vue';
|
||||
import GraphFlag from './graph/flag.vue';
|
||||
import GraphDeployment from './graph/deployment.vue';
|
||||
|
@ -18,10 +19,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }
|
|||
|
||||
export default {
|
||||
components: {
|
||||
GraphLegend,
|
||||
GraphAxis,
|
||||
GraphFlag,
|
||||
GraphDeployment,
|
||||
GraphPath,
|
||||
GraphLegend,
|
||||
},
|
||||
mixins: [MonitoringMixin],
|
||||
props: {
|
||||
|
@ -138,7 +140,7 @@ export default {
|
|||
this.legendTitle = query.label || 'Average';
|
||||
this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right;
|
||||
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
|
||||
this.baseGraphHeight = this.graphHeight;
|
||||
this.baseGraphHeight = this.graphHeight - 50;
|
||||
this.baseGraphWidth = this.graphWidth;
|
||||
|
||||
// pixel offsets inside the svg and outside are not 1:1
|
||||
|
@ -177,14 +179,10 @@ export default {
|
|||
this.graphHeightOffset,
|
||||
);
|
||||
|
||||
if (!this.showLegend) {
|
||||
this.baseGraphHeight -= 50;
|
||||
} else if (this.timeSeries.length > 3) {
|
||||
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
|
||||
}
|
||||
|
||||
const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
|
||||
const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]);
|
||||
const axisXScale = d3.scaleTime()
|
||||
.range([0, this.graphWidth - 70]);
|
||||
const axisYScale = d3.scaleLinear()
|
||||
.range([this.graphHeight - this.graphHeightOffset, 0]);
|
||||
|
||||
const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
|
||||
axisXScale.domain(d3.extent(allValues, d => d.time));
|
||||
|
@ -251,17 +249,12 @@ export default {
|
|||
class="y-axis"
|
||||
transform="translate(70, 20)"
|
||||
/>
|
||||
<graph-legend
|
||||
<graph-axis
|
||||
:graph-width="graphWidth"
|
||||
:graph-height="graphHeight"
|
||||
:margin="margin"
|
||||
:measurements="measurements"
|
||||
:legend-title="legendTitle"
|
||||
:y-axis-label="yAxisLabel"
|
||||
:time-series="timeSeries"
|
||||
:unit-of-display="unitOfDisplay"
|
||||
:current-data-index="currentDataIndex"
|
||||
:show-legend-group="showLegend"
|
||||
/>
|
||||
<svg
|
||||
class="graph-data"
|
||||
|
@ -306,5 +299,12 @@ export default {
|
|||
:deployment-flag-data="deploymentFlagData"
|
||||
/>
|
||||
</div>
|
||||
<graph-legend
|
||||
v-if="showLegend"
|
||||
:legend-title="legendTitle"
|
||||
:time-series="timeSeries"
|
||||
:current-data-index="currentDataIndex"
|
||||
:unit-of-display="unitOfDisplay"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
graphWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
graphHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
margin: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
measurements: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
yAxisLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yLabelWidth: 0,
|
||||
yLabelHeight: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textTransform() {
|
||||
const yCoordinate =
|
||||
(this.graphHeight -
|
||||
this.margin.top +
|
||||
this.measurements.axisLabelLineOffset) /
|
||||
2 || 0;
|
||||
|
||||
return `translate(15, ${yCoordinate}) rotate(-90)`;
|
||||
},
|
||||
|
||||
rectTransform() {
|
||||
const yCoordinate =
|
||||
(this.graphHeight -
|
||||
this.margin.top +
|
||||
this.measurements.axisLabelLineOffset) /
|
||||
2 +
|
||||
this.yLabelWidth / 2 || 0;
|
||||
|
||||
return `translate(0, ${yCoordinate}) rotate(-90)`;
|
||||
},
|
||||
|
||||
xPosition() {
|
||||
return (
|
||||
(this.graphWidth + this.measurements.axisLabelLineOffset) / 2 -
|
||||
this.margin.right || 0
|
||||
);
|
||||
},
|
||||
|
||||
yPosition() {
|
||||
return (
|
||||
this.graphHeight -
|
||||
this.margin.top +
|
||||
this.measurements.axisLabelLineOffset || 0
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const bbox = this.$refs.ylabel.getBBox();
|
||||
this.yLabelWidth = bbox.width + 10; // Added some padding
|
||||
this.yLabelHeight = bbox.height + 5;
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<g class="axis-label-container">
|
||||
<line
|
||||
class="label-x-axis-line"
|
||||
stroke="#000000"
|
||||
stroke-width="1"
|
||||
x1="10"
|
||||
:y1="yPosition"
|
||||
:x2="graphWidth + 20"
|
||||
:y2="yPosition"
|
||||
/>
|
||||
<line
|
||||
class="label-y-axis-line"
|
||||
stroke="#000000"
|
||||
stroke-width="1"
|
||||
x1="10"
|
||||
y1="0"
|
||||
:x2="10"
|
||||
:y2="yPosition"
|
||||
/>
|
||||
<rect
|
||||
class="rect-axis-text"
|
||||
:transform="rectTransform"
|
||||
:width="yLabelWidth"
|
||||
:height="yLabelHeight"
|
||||
/>
|
||||
<text
|
||||
class="label-axis-text y-label-text"
|
||||
text-anchor="middle"
|
||||
:transform="textTransform"
|
||||
ref="ylabel"
|
||||
>
|
||||
{{ yAxisLabel }}
|
||||
</text>
|
||||
<rect
|
||||
class="rect-axis-text"
|
||||
:x="xPosition + 60"
|
||||
:y="graphHeight - 80"
|
||||
width="35"
|
||||
height="50"
|
||||
/>
|
||||
<text
|
||||
class="label-axis-text x-label-text"
|
||||
:x="xPosition + 60"
|
||||
:y="yPosition"
|
||||
dy=".35em"
|
||||
>
|
||||
Time
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
|
@ -160,7 +160,7 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
<div class="popover-content">
|
||||
<table>
|
||||
<table class="prometheus-table">
|
||||
<tr
|
||||
v-for="(series, index) in timeSeries"
|
||||
:key="index"
|
||||
|
|
|
@ -1,112 +1,50 @@
|
|||
<script>
|
||||
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
|
||||
import { formatRelevantDigits } from '~/lib/utils/number_utils';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
graphWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
graphHeight: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
margin: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
measurements: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
legendTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
yAxisLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
timeSeries: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
unitOfDisplay: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
currentDataIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
showLegendGroup: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
unitOfDisplay: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
yLabelWidth: 0,
|
||||
yLabelHeight: 0,
|
||||
seriesXPosition: 0,
|
||||
metricUsageXPosition: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
textTransform() {
|
||||
const yCoordinate =
|
||||
(this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0;
|
||||
|
||||
return `translate(15, ${yCoordinate}) rotate(-90)`;
|
||||
},
|
||||
rectTransform() {
|
||||
const yCoordinate =
|
||||
(this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 +
|
||||
this.yLabelWidth / 2 || 0;
|
||||
|
||||
return `translate(0, ${yCoordinate}) rotate(-90)`;
|
||||
},
|
||||
xPosition() {
|
||||
return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0;
|
||||
},
|
||||
yPosition() {
|
||||
return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
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.yLabelHeight = bbox.height + 5;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
translateLegendGroup(index) {
|
||||
return `translate(0, ${12 * index})`;
|
||||
},
|
||||
formatMetricUsage(series) {
|
||||
const value =
|
||||
series.values[this.currentDataIndex] && series.values[this.currentDataIndex].value;
|
||||
series.values[this.currentDataIndex] &&
|
||||
series.values[this.currentDataIndex].value;
|
||||
if (isNaN(value)) {
|
||||
return '-';
|
||||
}
|
||||
return `${formatRelevantDigits(value)} ${this.unitOfDisplay}`;
|
||||
},
|
||||
|
||||
createSeriesString(index, series) {
|
||||
if (series.metricTag) {
|
||||
return `${series.metricTag} ${this.formatMetricUsage(series)}`;
|
||||
}
|
||||
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
|
||||
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(
|
||||
series,
|
||||
)}`;
|
||||
},
|
||||
|
||||
summaryMetrics(series) {
|
||||
return `Avg: ${formatRelevantDigits(series.average)} ${this.unitOfDisplay},
|
||||
Max: ${formatRelevantDigits(series.max)} ${this.unitOfDisplay}`;
|
||||
},
|
||||
|
||||
strokeDashArray(type) {
|
||||
if (type === 'dashed') return '6, 3';
|
||||
if (type === 'dotted') return '3, 3';
|
||||
|
@ -116,89 +54,38 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<g class="axis-label-container">
|
||||
<line
|
||||
class="label-x-axis-line"
|
||||
stroke="#000000"
|
||||
stroke-width="1"
|
||||
x1="10"
|
||||
:y1="yPosition"
|
||||
:x2="graphWidth + 20"
|
||||
:y2="yPosition"
|
||||
/>
|
||||
<line
|
||||
class="label-y-axis-line"
|
||||
stroke="#000000"
|
||||
stroke-width="1"
|
||||
x1="10"
|
||||
y1="0"
|
||||
:x2="10"
|
||||
:y2="yPosition"
|
||||
/>
|
||||
<rect
|
||||
class="rect-axis-text"
|
||||
:transform="rectTransform"
|
||||
:width="yLabelWidth"
|
||||
:height="yLabelHeight"
|
||||
/>
|
||||
<text
|
||||
class="label-axis-text y-label-text"
|
||||
text-anchor="middle"
|
||||
:transform="textTransform"
|
||||
ref="ylabel"
|
||||
>
|
||||
{{ yAxisLabel }}
|
||||
</text>
|
||||
<rect
|
||||
class="rect-axis-text"
|
||||
:x="xPosition + 60"
|
||||
:y="graphHeight - 80"
|
||||
width="35"
|
||||
height="50"
|
||||
/>
|
||||
<text
|
||||
class="label-axis-text x-label-text"
|
||||
:x="xPosition + 60"
|
||||
:y="yPosition"
|
||||
dy=".35em"
|
||||
>
|
||||
Time
|
||||
</text>
|
||||
<template v-if="showLegendGroup">
|
||||
<g
|
||||
class="legend-group"
|
||||
<div class="prometheus-graph-legends prepend-left-10">
|
||||
<table class="prometheus-table">
|
||||
<tr
|
||||
v-for="(series, index) in timeSeries"
|
||||
:key="index"
|
||||
:transform="translateLegendGroup(index)"
|
||||
>
|
||||
<line
|
||||
:stroke="series.lineColor"
|
||||
:stroke-width="measurements.legends.height"
|
||||
:stroke-dasharray="strokeDashArray(series.lineStyle)"
|
||||
:x1="measurements.legends.offsetX"
|
||||
:x2="measurements.legends.offsetX + measurements.legends.width"
|
||||
:y1="graphHeight - measurements.legends.offsetY"
|
||||
:y2="graphHeight - measurements.legends.offsetY"
|
||||
/>
|
||||
<text
|
||||
<td>
|
||||
<svg
|
||||
width="15"
|
||||
height="6"
|
||||
>
|
||||
<line
|
||||
:stroke-dasharray="strokeDashArray(series.lineStyle)"
|
||||
:stroke="series.lineColor"
|
||||
stroke-width="4"
|
||||
:x1="0"
|
||||
:x2="15"
|
||||
:y1="2"
|
||||
:y2="2"
|
||||
/>
|
||||
</svg>
|
||||
</td>
|
||||
<td
|
||||
class="legend-metric-title"
|
||||
v-if="timeSeries.length > 1"
|
||||
class="legend-metric-title"
|
||||
ref="legendTitleSvg"
|
||||
x="38"
|
||||
:y="graphHeight - 30"
|
||||
>
|
||||
{{ createSeriesString(index, series) }}
|
||||
</text>
|
||||
<text
|
||||
v-else
|
||||
class="legend-metric-title"
|
||||
ref="legendTitleSvg"
|
||||
x="38"
|
||||
:y="graphHeight - 30"
|
||||
>
|
||||
{{ legendTitle }} {{ formatMetricUsage(series) }}
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
||||
</g>
|
||||
{{ createSeriesString(index, series) }}, {{ summaryMetrics(series) }}
|
||||
</td>
|
||||
<td v-else>
|
||||
{{ legendTitle }} {{ formatMetricUsage(series) }}, {{ summaryMetrics(series) }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
import _ from 'underscore';
|
||||
import { scaleLinear, scaleTime } from 'd3-scale';
|
||||
import { line, area, curveLinear } from 'd3-shape';
|
||||
import { extent, max } from 'd3-array';
|
||||
import { extent, max, sum } from 'd3-array';
|
||||
import { timeMinute } from 'd3-time';
|
||||
|
||||
const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
|
||||
const d3 = {
|
||||
scaleLinear,
|
||||
scaleTime,
|
||||
line,
|
||||
area,
|
||||
curveLinear,
|
||||
extent,
|
||||
max,
|
||||
timeMinute,
|
||||
sum,
|
||||
};
|
||||
|
||||
const defaultColorPalette = {
|
||||
blue: ['#1f78d1', '#8fbce8'],
|
||||
|
@ -18,7 +28,15 @@ const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
|
|||
|
||||
const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
|
||||
|
||||
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
|
||||
function queryTimeSeries(
|
||||
query,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
graphHeightOffset,
|
||||
xDom,
|
||||
yDom,
|
||||
lineStyle,
|
||||
) {
|
||||
let usedColors = [];
|
||||
|
||||
function pickColor(name) {
|
||||
|
@ -42,11 +60,14 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
let metricTag = '';
|
||||
let lineColor = '';
|
||||
let areaColor = '';
|
||||
const timeSeriesValues = timeSeries.values.map(d => d.value);
|
||||
const maximumValue = d3.max(timeSeriesValues);
|
||||
const accum = d3.sum(timeSeriesValues);
|
||||
|
||||
const timeSeriesScaleX = d3.scaleTime()
|
||||
.range([0, graphWidth - 70]);
|
||||
const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
|
||||
|
||||
const timeSeriesScaleY = d3.scaleLinear()
|
||||
const timeSeriesScaleY = d3
|
||||
.scaleLinear()
|
||||
.range([graphHeight - graphHeightOffset, 0]);
|
||||
|
||||
timeSeriesScaleX.domain(xDom);
|
||||
|
@ -55,28 +76,35 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
|
||||
const defined = d => !isNaN(d.value) && d.value != null;
|
||||
|
||||
const lineFunction = d3.line()
|
||||
const lineFunction = d3
|
||||
.line()
|
||||
.defined(defined)
|
||||
.curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
|
||||
.x(d => timeSeriesScaleX(d.time))
|
||||
.y(d => timeSeriesScaleY(d.value));
|
||||
|
||||
const areaFunction = d3.area()
|
||||
const areaFunction = d3
|
||||
.area()
|
||||
.defined(defined)
|
||||
.curve(d3.curveLinear)
|
||||
.x(d => timeSeriesScaleX(d.time))
|
||||
.y0(graphHeight - graphHeightOffset)
|
||||
.y1(d => timeSeriesScaleY(d.value));
|
||||
|
||||
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
|
||||
const seriesCustomizationData = query.series != null &&
|
||||
const timeSeriesMetricLabel =
|
||||
timeSeries.metric[Object.keys(timeSeries.metric)[0]];
|
||||
const seriesCustomizationData =
|
||||
query.series != null &&
|
||||
_.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
|
||||
|
||||
if (seriesCustomizationData) {
|
||||
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
|
||||
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
|
||||
} else {
|
||||
metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
|
||||
metricTag =
|
||||
timeSeriesMetricLabel ||
|
||||
query.label ||
|
||||
`series ${timeSeriesNumber + 1}`;
|
||||
[lineColor, areaColor] = pickColor();
|
||||
}
|
||||
|
||||
|
@ -89,6 +117,8 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
areaPath: areaFunction(timeSeries.values),
|
||||
timeSeriesScaleX,
|
||||
values: timeSeries.values,
|
||||
max: maximumValue,
|
||||
average: accum / timeSeries.values.length,
|
||||
lineStyle,
|
||||
lineColor,
|
||||
areaColor,
|
||||
|
@ -97,10 +127,22 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
|
|||
});
|
||||
}
|
||||
|
||||
export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
|
||||
const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
|
||||
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
|
||||
), []);
|
||||
export default function createTimeSeries(
|
||||
queries,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
graphHeightOffset,
|
||||
) {
|
||||
const allValues = queries.reduce(
|
||||
(allQueryResults, query) =>
|
||||
allQueryResults.concat(
|
||||
query.result.reduce(
|
||||
(allResults, result) => allResults.concat(result.values),
|
||||
[],
|
||||
),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const xDom = d3.extent(allValues, d => d.time);
|
||||
const yDom = [0, d3.max(allValues.map(d => d.value))];
|
||||
|
@ -108,7 +150,15 @@ export default function createTimeSeries(queries, graphWidth, graphHeight, graph
|
|||
return queries.reduce((series, query, index) => {
|
||||
const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
|
||||
return series.concat(
|
||||
queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle),
|
||||
queryTimeSeries(
|
||||
query,
|
||||
graphWidth,
|
||||
graphHeight,
|
||||
graphHeightOffset,
|
||||
xDom,
|
||||
yDom,
|
||||
lineStyle,
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
}
|
||||
|
|
|
@ -273,21 +273,6 @@
|
|||
line-height: 1.2;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
|
||||
+ td {
|
||||
padding-left: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.deploy-meta-content {
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
||||
|
@ -323,6 +308,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.prometheus-table {
|
||||
border-collapse: collapse;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
|
||||
+ td {
|
||||
padding-left: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prometheus-svg-container {
|
||||
position: relative;
|
||||
height: 0;
|
||||
|
@ -330,8 +330,7 @@
|
|||
padding: 0;
|
||||
padding-bottom: 100%;
|
||||
|
||||
.text-metric-usage,
|
||||
.legend-metric-title {
|
||||
.text-metric-usage {
|
||||
fill: $black;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
font-size: 12px;
|
||||
|
@ -374,10 +373,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.text-metric-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.y-label-text,
|
||||
.x-label-text {
|
||||
fill: $gray-darkest;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add average and maximum summary statistics to the prometheus dashboard
|
||||
merge_request: 17921
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,70 @@
|
|||
import Vue from 'vue';
|
||||
import GraphAxis from '~/monitoring/components/graph/axis.vue';
|
||||
import measurements from '~/monitoring/utils/measurements';
|
||||
|
||||
const createComponent = propsData => {
|
||||
const Component = Vue.extend(GraphAxis);
|
||||
|
||||
return new Component({
|
||||
propsData,
|
||||
}).$mount();
|
||||
};
|
||||
|
||||
const defaultValuesComponent = {
|
||||
graphWidth: 500,
|
||||
graphHeight: 300,
|
||||
graphHeightOffset: 120,
|
||||
margin: measurements.large.margin,
|
||||
measurements: measurements.large,
|
||||
yAxisLabel: 'Values',
|
||||
};
|
||||
|
||||
function getTextFromNode(component, selector) {
|
||||
return component.$el.querySelector(selector).firstChild.nodeValue.trim();
|
||||
}
|
||||
|
||||
describe('Axis', () => {
|
||||
describe('Computed props', () => {
|
||||
it('textTransform', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.textTransform).toContain(
|
||||
'translate(15, 120) rotate(-90)',
|
||||
);
|
||||
});
|
||||
|
||||
it('xPosition', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.xPosition).toEqual(180);
|
||||
});
|
||||
|
||||
it('yPosition', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.yPosition).toEqual(240);
|
||||
});
|
||||
|
||||
it('rectTransform', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.rectTransform).toContain(
|
||||
'translate(0, 120) rotate(-90)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('has 2 rect-axis-text rect svg elements', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('contains text to signal the usage, title and time with multiple time series', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(getTextFromNode(component, '.y-label-text')).toEqual(
|
||||
component.yAxisLabel,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,106 +1,85 @@
|
|||
import Vue from 'vue';
|
||||
import GraphLegend from '~/monitoring/components/graph/legend.vue';
|
||||
import measurements from '~/monitoring/utils/measurements';
|
||||
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
|
||||
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import {
|
||||
singleRowMetricsMultipleSeries,
|
||||
convertDatesMultipleSeries,
|
||||
} from '../mock_data';
|
||||
|
||||
const createComponent = (propsData) => {
|
||||
const Component = Vue.extend(GraphLegend);
|
||||
const convertedMetrics = convertDatesMultipleSeries(
|
||||
singleRowMetricsMultipleSeries,
|
||||
);
|
||||
|
||||
return new Component({
|
||||
propsData,
|
||||
}).$mount();
|
||||
};
|
||||
const defaultValuesComponent = {};
|
||||
|
||||
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,
|
||||
defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight,
|
||||
defaultValuesComponent.graphHeightOffset);
|
||||
const timeSeries = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120);
|
||||
|
||||
defaultValuesComponent.timeSeries = timeSeries;
|
||||
|
||||
function getTextFromNode(component, selector) {
|
||||
return component.$el.querySelector(selector).firstChild.nodeValue.trim();
|
||||
}
|
||||
describe('Legend Component', () => {
|
||||
let vm;
|
||||
let Legend;
|
||||
|
||||
describe('GraphLegend', () => {
|
||||
describe('Computed props', () => {
|
||||
it('textTransform', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.textTransform).toContain('translate(15, 120) rotate(-90)');
|
||||
});
|
||||
|
||||
it('xPosition', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.xPosition).toEqual(180);
|
||||
});
|
||||
|
||||
it('yPosition', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.yPosition).toEqual(240);
|
||||
});
|
||||
|
||||
it('rectTransform', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)');
|
||||
});
|
||||
beforeEach(() => {
|
||||
Legend = Vue.extend(GraphLegend);
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
it('translateLegendGroup should only change Y direction', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
const translatedCoordinate = component.translateLegendGroup(1);
|
||||
expect(translatedCoordinate.indexOf('translate(0, ')).not.toEqual(-1);
|
||||
describe('Methods', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Legend, {
|
||||
legendTitle: 'legend',
|
||||
timeSeries,
|
||||
currentDataIndex: 0,
|
||||
unitOfDisplay: 'Req/Sec',
|
||||
});
|
||||
});
|
||||
|
||||
it('formatMetricUsage should contain the unit of display and the current value selected via "currentDataIndex"', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
const formattedMetricUsage = vm.formatMetricUsage(timeSeries[0]);
|
||||
const valueFromSeries = timeSeries[0].values[vm.currentDataIndex].value;
|
||||
|
||||
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(vm.unitOfDisplay)).not.toEqual(-1);
|
||||
expect(formattedMetricUsage.indexOf(valueFromSeries)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it('strokeDashArray', () => {
|
||||
const dashedArray = vm.strokeDashArray('dashed');
|
||||
const dottedArray = vm.strokeDashArray('dotted');
|
||||
|
||||
expect(dashedArray).toEqual('6, 3');
|
||||
expect(dottedArray).toEqual('3, 3');
|
||||
});
|
||||
|
||||
it('summaryMetrics gets the average and max of a series', () => {
|
||||
const summary = vm.summaryMetrics(timeSeries[0]);
|
||||
|
||||
expect(summary.indexOf('Max')).not.toEqual(-1);
|
||||
expect(summary.indexOf('Avg')).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
it('has 2 rect-axis-text rect svg elements', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
describe('View', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Legend, {
|
||||
legendTitle: 'legend',
|
||||
timeSeries,
|
||||
currentDataIndex: 0,
|
||||
unitOfDisplay: 'Req/Sec',
|
||||
});
|
||||
});
|
||||
|
||||
expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
|
||||
});
|
||||
it('should render the usage, title and time with multiple time series', () => {
|
||||
const titles = vm.$el.querySelectorAll('.legend-metric-title');
|
||||
|
||||
it('contains text to signal the usage, title and time with multiple time series', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
const titles = component.$el.querySelectorAll('.legend-metric-title');
|
||||
expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1);
|
||||
expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1);
|
||||
});
|
||||
|
||||
expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1);
|
||||
expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1);
|
||||
expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel);
|
||||
});
|
||||
|
||||
it('should contain the same number of legend groups as the timeSeries length', () => {
|
||||
const component = createComponent(defaultValuesComponent);
|
||||
|
||||
expect(component.$el.querySelectorAll('.legend-group').length).toEqual(component.timeSeries.length);
|
||||
it('should container the same number of rows in the table as time series', () => {
|
||||
expect(vm.$el.querySelectorAll('.prometheus-table tr').length).toEqual(
|
||||
vm.timeSeries.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,11 +2,15 @@ import Vue from 'vue';
|
|||
import Graph from '~/monitoring/components/graph.vue';
|
||||
import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
|
||||
import eventHub from '~/monitoring/event_hub';
|
||||
import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
|
||||
import {
|
||||
deploymentData,
|
||||
convertDatesMultipleSeries,
|
||||
singleRowMetricsMultipleSeries,
|
||||
} from './mock_data';
|
||||
|
||||
const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
|
||||
const projectPath = 'http://test.host/frontend-fixtures/environments-project';
|
||||
const createComponent = (propsData) => {
|
||||
const createComponent = propsData => {
|
||||
const Component = Vue.extend(Graph);
|
||||
|
||||
return new Component({
|
||||
|
@ -14,7 +18,9 @@ const createComponent = (propsData) => {
|
|||
}).$mount();
|
||||
};
|
||||
|
||||
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
|
||||
const convertedMetrics = convertDatesMultipleSeries(
|
||||
singleRowMetricsMultipleSeries,
|
||||
);
|
||||
|
||||
describe('Graph', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -31,7 +37,9 @@ describe('Graph', () => {
|
|||
projectPath,
|
||||
});
|
||||
|
||||
expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title);
|
||||
expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(
|
||||
component.graphData.title,
|
||||
);
|
||||
});
|
||||
|
||||
describe('Computed props', () => {
|
||||
|
@ -46,8 +54,9 @@ describe('Graph', () => {
|
|||
});
|
||||
|
||||
const transformedHeight = `${component.graphHeight - 100}`;
|
||||
expect(component.axisTransform.indexOf(transformedHeight))
|
||||
.not.toEqual(-1);
|
||||
expect(component.axisTransform.indexOf(transformedHeight)).not.toEqual(
|
||||
-1,
|
||||
);
|
||||
});
|
||||
|
||||
it('outerViewBox gets a width and height property based on the DOM size of the element', () => {
|
||||
|
@ -63,11 +72,11 @@ describe('Graph', () => {
|
|||
const viewBoxArray = component.outerViewBox.split(' ');
|
||||
expect(typeof component.outerViewBox).toEqual('string');
|
||||
expect(viewBoxArray[2]).toEqual(component.graphWidth.toString());
|
||||
expect(viewBoxArray[3]).toEqual(component.graphHeight.toString());
|
||||
expect(viewBoxArray[3]).toEqual((component.graphHeight - 50).toString());
|
||||
});
|
||||
});
|
||||
|
||||
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({
|
||||
graphData: convertedMetrics[1],
|
||||
classType: 'col-md-6',
|
||||
|
|
Loading…
Reference in New Issue