Commit fe96152d authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'jivl-summary-statistics-prometheus-dashboard' into 'master'

Add summary statistics to the prometheus dashboard

Closes #43973

See merge request gitlab-org/gitlab-ce!17921
parents 9e3cdc02 d303b5ba
......@@ -7,7 +7,8 @@
* @param {String} text
* @returns {String}
*/
export const addDelimiter = text => (text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
export const addDelimiter = text =>
(text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text);
/**
* Returns '99+' for numbers bigger than 99.
......@@ -22,7 +23,8 @@ export const highCountTrim = count => (count > 99 ? '99+' : count);
* @param {String} string
* @requires {String}
*/
export const humanize = string => string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
export const humanize = string =>
string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
/**
* Adds an 's' to the end of the string when count is bigger than 0
......@@ -53,7 +55,7 @@ export const slugify = str => str.trim().toLowerCase();
* @param {Number} maxLength
* @returns {String}
*/
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
/**
* Capitalizes first character
......@@ -80,3 +82,15 @@ export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, re
* @param {*} string
*/
export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase());
/**
* Converts a sentence to lower case from the second word onwards
* e.g. Hello World => Hello world
*
* @param {*} string
*/
export const convertToSentenceCase = string => {
const splitWord = string.split(' ').map((word, index) => (index > 0 ? word.toLowerCase() : word));
return splitWord.join(' ');
};
<script>
import { scaleLinear, scaleTime } from 'd3-scale';
import { axisLeft, axisBottom } from 'd3-axis';
import _ from 'underscore';
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 +20,11 @@ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }
export default {
components: {
GraphLegend,
GraphAxis,
GraphFlag,
GraphDeployment,
GraphPath,
GraphLegend,
},
mixins: [MonitoringMixin],
props: {
......@@ -138,7 +141,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,10 +180,8 @@ 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;
if (_.findWhere(this.timeSeries, { renderCanary: true })) {
this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true }));
}
const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]);
......@@ -251,17 +252,13 @@ 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 +303,10 @@ export default {
:deployment-flag-data="deploymentFlagData"
/>
</div>
<graph-legend
v-if="showLegend"
:legend-title="legendTitle"
:time-series="timeSeries"
/>
</div>
</template>
<script>
import { convertToSentenceCase } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
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,
},
unitOfDisplay: {
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
);
},
yAxisLabelSentenceCase() {
return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`;
},
timeString() {
return s__('PrometheusDashboard|Time');
},
},
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"
>
{{ yAxisLabelSentenceCase }}
</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"
>
{{ timeString }}
</text>
</g>
</template>
<script>
import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import icon from '../../../vue_shared/components/icon.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import TrackLine from './track_line.vue';
export default {
components: {
icon,
Icon,
TrackLine,
},
props: {
currentXCoordinate: {
......@@ -107,11 +109,6 @@ export default {
}
return `series ${index + 1}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
},
};
</script>
......@@ -160,28 +157,13 @@ export default {
</div>
</div>
<div class="popover-content">
<table>
<table class="prometheus-table">
<tr
v-for="(series, index) in timeSeries"
:key="index"
>
<td>
<svg
width="15"
height="6"
>
<line
:stroke="series.lineColor"
:stroke-dasharray="strokeDashArray(series.lineStyle)"
stroke-width="4"
x1="0"
x2="15"
y1="2"
y2="2"
/>
</svg>
</td>
<td>{{ seriesMetricLabel(index, series) }}</td>
<track-line :track="series"/>
<td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td>
<td>
<strong>{{ seriesMetricValue(series) }}</strong>
</td>
......
<script>
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
import TrackLine from './track_line.vue';
import TrackInfo from './track_info.vue';
export default {
components: {
TrackLine,
TrackInfo,
},
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,
},
},
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;
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)}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
isStable(track) {
return {
'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary,
};
},
},
};
</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)"
v-if="series.shouldRenderLegend"
:class="isStable(series)"
>
<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
v-if="timeSeries.length > 1"
class="legend-metric-title"
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30"
>
{{ createSeriesString(index, series) }}
</text>
<text
v-else
<td>
<strong v-if="series.renderCanary">{{ series.trackName }}</strong>
</td>
<track-line :track="series" />
<td
class="legend-metric-title"
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30"
>
{{ legendTitle }} {{ formatMetricUsage(series) }}
</text>
</g>
</template>
</g>
v-if="timeSeries.length > 1">
<track-info
:track="series"
v-if="series.metricTag" />
<track-info
v-else
:track="series">
<strong>{{ legendTitle }}</strong> series {{ index + 1 }}
</track-info>
</td>
<td v-else>
<track-info :track="series">
<strong>{{ legendTitle }}</strong>
</track-info>
</td>
<template v-for="(track, trackIndex) in series.tracksLegend">
<track-line
:track="track"
:key="`track-line-${trackIndex}`"/>
<td :key="`track-info-${trackIndex}`">
<track-info
class="legend-metric-title"
:track="track" />
</td>
</template>
</tr>
</table>
</div>
</template>
<script>
import { formatRelevantDigits } from '~/lib/utils/number_utils';
export default {
name: 'TrackInfo',
props: {
track: {
type: Object,
required: true,
},
},
computed: {
summaryMetrics() {
return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits(
this.track.max,
)}`;
},
},
};
</script>
<template>
<span>
<slot>
<strong> {{ track.metricTag }} </strong>
</slot>
{{ summaryMetrics }}
</span>
</template>
<script>
export default {
name: 'TrackLine',
props: {
track: {
type: Object,
required: true,
},
},
computed: {
stylizedLine() {
if (this.track.lineStyle === 'dashed') return '6, 3';
if (this.track.lineStyle === 'dotted') return '3, 3';
return null;
},
},
};
</script>
<template>
<td>
<svg
width="15"
height="6">
<line
:stroke-dasharray="stylizedLine"
:stroke="track.lineColor"
stroke-width="4"
:x1="0"
:x2="15"
:y1="2"
:y2="2"
/>
</svg>
</td>
</template>
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 };
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
const d3 = {
scaleLinear,
scaleTime,
line,
area,
curveLinear,
extent,
max,
timeMinute,
sum,
};
const defaultColorPalette = {
blue: ['#1f78d1', '#8fbce8'],
......@@ -20,6 +31,8 @@ const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
let usedColors = [];
let renderCanary = false;
const timeSeriesParsed = [];
function pickColor(name) {
let pick;
......@@ -38,16 +51,23 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
return defaultColorPalette[pick];
}
return query.result.map((timeSeries, timeSeriesNumber) => {
query.result.forEach((timeSeries, timeSeriesNumber) => {
let metricTag = '';
let lineColor = '';
let areaColor = '';
let shouldRenderLegend = true;
const timeSeriesValues = timeSeries.values.map(d => d.value);
const maximumValue = d3.max(timeSeriesValues);
const accum = d3.sum(timeSeriesValues);
const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable');
if (trackName === 'Canary') {
renderCanary = true;
}
const timeSeriesScaleX = d3.scaleTime()
.range([0, graphWidth - 70]);
const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]);
const timeSeriesScaleY = d3.scaleLinear()
.range([graphHeight - graphHeightOffset, 0]);
const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.timeMinute, 60);
......@@ -55,13 +75,15 @@ 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))
......@@ -69,38 +91,62 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
.y1(d => timeSeriesScaleY(d.value));
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
const seriesCustomizationData = query.series != null &&
_.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
const seriesCustomizationData =
query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
shouldRenderLegend = false;
} else {
metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`;
[lineColor, areaColor] = pickColor();
if (timeSeriesParsed.length > 1) {
shouldRenderLegend = false;
}
}
if (query.track) {
metricTag += ` - ${query.track}`;
if (!shouldRenderLegend) {
if (!timeSeriesParsed[0].tracksLegend) {
timeSeriesParsed[0].tracksLegend = [];
}
timeSeriesParsed[0].tracksLegend.push({
max: maximumValue,
average: accum / timeSeries.values.length,
lineStyle,
lineColor,
metricTag,
});
}
return {
timeSeriesParsed.push({
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
max: maximumValue,
average: accum / timeSeries.values.length,
lineStyle,
lineColor,
areaColor,
metricTag,
};
trackName,
shouldRenderLegend,
renderCanary,
});
});
return timeSeriesParsed;
}
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 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))];
......
......@@ -767,3 +767,8 @@ $border-color-settings: #e1e1e1;
Modals
*/
$modal-body-height: 134px;
/*
Prometheus
*/
$prometheus-table-row-highlight-color: $theme-gray-100;
......@@ -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,26 @@
}
}
.prometheus-table {
border-collapse: collapse;
padding: 0;
margin: 0;
td {
vertical-align: middle;
+ td {
padding-left: 5px;
vertical-align: top;
}
}
.legend-metric-title {
font-size: 12px;
vertical-align: middle;
}
}
.prometheus-svg-container {
position: relative;
height: 0;
......@@ -330,8 +335,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 +378,6 @@
}
}
.text-metric-title {
font-size: 12px;
}
.y-label-text,
.x-label-text {
fill: $gray-darkest;
......@@ -414,3 +414,7 @@
}
}
}
.prometheus-table-row-highlight {
background-color: $prometheus-table-row-highlight-color;
}
---
title: Add average and maximum summary statistics to the prometheus dashboard
merge_request: 17921
author:
type: changed
......@@ -26,7 +26,7 @@
weight: 1
queries:
- query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})'
label: Average
label: Pod average
unit: ms
- title: "HTTP Error Rate"
y_label: "HTTP 500 Errors / Sec"
......@@ -146,7 +146,7 @@
weight: 1
queries:
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Average
label: Pod average
unit: MB
- title: "CPU Usage"
y_label: "Cores per Pod"
......@@ -155,5 +155,5 @@
weight: 1
queries:
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Average
label: Pod average
unit: "cores"
\ No newline at end of file
......@@ -65,11 +65,15 @@ describe('text_utility', () => {
describe('stripHtml', () => {
it('replaces html tag with the default replacement', () => {
expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual(
'This is a text with html.',
);
});
it('replaces html tags with the provided replacement', () => {
expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual(
'This is a text with html .',
);
});
});
......@@ -78,4 +82,10 @@ describe('text_utility', () => {
expect(textUtils.convertToCamelCase('snake_case')).toBe('snakeCase');
});
});
describe('convertToSentenceCase', () => {
it('converts Sentence Case to Sentence case', () => {
expect(textUtils.convertToSentenceCase('Hello World')).toBe('Hello world');
});
});
});
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',
unitOfDisplay: 'MB',
};
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('Values (MB)');
});
});
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 mountComponent from 'spec/helpers/vue_mount_component_helper';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(GraphLegend);
return new Component({
propsData,
}).$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 defaultValuesComponent = {};
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('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);
describe('Legend Component', () => {
let vm;
let Legend;
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('View', () => {
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);
it('should render the usage, title and time with multiple time series', () => {
const titles = vm.$el.querySelectorAll('.legend-metric-title');
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);
expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1);
expect(titles[1].textContent.indexOf('2xx')).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);
});
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(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);
});
});
});
import Vue from 'vue';
import TrackInfo from '~/monitoring/components/graph/track_info.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data';
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
const timeSeries = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120);
describe('TrackInfo component', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(TrackInfo);
});
afterEach(() => {
vm.$destroy();
});
describe('Computed props', () => {
beforeEach(() => {
vm = mountComponent(Component, { track: timeSeries[0] });
});
it('summaryMetrics', () => {
expect(vm.summaryMetrics).toEqual('Avg: 0.000 · Max: 0.000');
});
});
describe('Rendered output', () => {
beforeEach(() => {
vm = mountComponent(Component, { track: timeSeries[0] });
});
it('contains metric tag and the summary metrics', () => {
const metricTag = vm.$el.querySelector('strong');
expect(metricTag.textContent.trim()).toEqual(vm.track.metricTag);
expect(vm.$el.textContent).toContain('Avg: 0.000 · Max: 0.000');
});
});
});
import Vue from 'vue';
import TrackLine from '~/monitoring/components/graph/track_line.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data';
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
const timeSeries = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120);
describe('TrackLine component', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(TrackLine);
});
afterEach(() => {
vm.$destroy();
});
describe('Computed props', () => {
it('stylizedLine for dashed lineStyles', () => {
vm = mountComponent(Component, { track: { ...timeSeries[0], lineStyle: 'dashed' } });
expect(vm.stylizedLine).toEqual('6, 3');
});
it('stylizedLine for dotted lineStyles', () => {
vm = mountComponent(Component, { track: { ...timeSeries[0], lineStyle: 'dotted' } });
expect(vm.stylizedLine).toEqual('3, 3');
});
});
describe('Rendered output', () => {
it('has an svg with a line', () => {
vm = mountComponent(Component, { track: { ...timeSeries[0] } });
const svgEl = vm.$el.querySelector('svg');
const lineEl = vm.$el.querySelector('svg line');
expect(svgEl.getAttribute('width')).toEqual('15');
expect(svgEl.getAttribute('height')).toEqual('6');
expect(lineEl.getAttribute('stroke-width')).toEqual('4');
expect(lineEl.getAttribute('x1')).toEqual('0');
expect(lineEl.getAttribute('x2')).toEqual('15');
expect(lineEl.getAttribute('y1')).toEqual('2');
expect(lineEl.getAttribute('y2')).toEqual('2');
});
});
});
......@@ -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',
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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