Commit 871bf96c authored by Phil Hughes's avatar Phil Hughes Committed by Jose Ivan Vargas Lopez

Merge branch 'additional-metrics-dashboard' into '28717-additional-metrics-review-branch'

Additional metrics dashboard

See merge request !11740
parent 676c5594
<script>
/* global Flash */
import _ from 'underscore';
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service';
import monitoringRow from './monitoring_row.vue';
import monitoringState from './monitoring_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
export default {
data() {
const metricsData = document.querySelector('#prometheus-graphs').dataset;
const store = new MonitoringStore();
return {
store,
state: 'gettingStarted',
hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
endpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
showEmptyState: true,
backOffRequestCounter: 0,
updateAspectRatio: false,
updatedAspectRatios: 0,
resizeThrottled: {},
};
},
components: {
monitoringRow,
monitoringState,
},
methods: {
getGraphsData() {
const maxNumberOfRequests = 3;
this.state = 'loading';
gl.utils.backOff((next, stop) => {
this.service.get().then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
} else {
stop(new Error('Failed to connect to the prometheus server'));
}
} else {
stop(resp);
}
}).catch(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.state = 'unableToConnect';
return false;
}
return resp.json();
})
.then((metricGroupsData) => {
if (!metricGroupsData) return false;
this.store.storeMetrics(metricGroupsData.data);
return this.getDeploymentData();
})
.then((deploymentData) => {
if (deploymentData !== false) {
this.store.storeDeploymentData(deploymentData.deployments);
this.showEmptyState = false;
}
return {};
})
.catch(() => {
this.state = 'unableToConnect';
});
},
getDeploymentData() {
return this.service.getDeploymentData(this.deploymentEndpoint)
.then(resp => resp.json())
.catch(() => new Flash('Error getting deployment information.'));
},
resize() {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
this.updatedAspectRatios = this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
}
},
},
created() {
this.service = new MonitoringService(this.endpoint);
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
this.resizeThrottled = _.throttle(this.resize, 600);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
}
},
};
</script>
<template>
<div
class="prometheus-graphs"
v-if="!showEmptyState">
<div
class="row"
v-for="(groupData, index) in store.groups"
:key="index">
<div
class="col-md-12">
<div
class="panel panel-default prometheus-panel">
<div
class="panel-heading">
<h4>{{groupData.group}}</h4>
</div>
<div
class="panel-body">
<monitoring-row
v-for="(row, index) in groupData.metrics"
:key="index"
:row-data="row"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
</div>
</div>
</div>
</div>
</div>
<monitoring-state
:selected-state="state"
:documentation-path="documentationPath"
:settings-path="settingsPath"
v-else
/>
</template>
<script>
/* global Breakpoints */
import d3 from 'd3';
import monitoringLegends from './monitoring_legends.vue';
import monitoringFlag from './monitoring_flag.vue';
import monitoringDeployment from './monitoring_deployment.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
const bisectDate = d3.bisector(d => d.time).left;
export default {
props: {
columnData: {
type: Object,
required: true,
},
classType: {
type: String,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
},
mixins: [MonitoringMixin],
data() {
return {
graphHeight: 500,
graphWidth: 600,
graphHeightOffset: 120,
xScale: {},
yScale: {},
margin: {},
data: [],
breakpointHandler: Breakpoints.get(),
unitOfDisplay: '',
areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1',
yAxisLabel: '',
legendTitle: '',
reducedDeploymentData: [],
area: '',
line: '',
measurements: measurements.large,
currentData: {
time: new Date(),
value: 0,
},
currentYCoordinate: 0,
currentXCoordinate: 0,
currentFlagPosition: 0,
metricUsage: '',
showFlag: false,
showDeployInfo: true,
};
},
components: {
monitoringLegends,
monitoringFlag,
monitoringDeployment,
},
computed: {
outterViewBox() {
return `0 0 ${this.graphWidth} ${this.graphHeight}`;
},
innerViewBox() {
if ((this.graphWidth - 150) > 0) {
return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`;
}
return '0 0 0 0';
},
axisTransform() {
return `translate(70, ${this.graphHeight - 100})`;
},
paddingBottomRootSvg() {
return (Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0;
},
},
methods: {
draw() {
const breakpointSize = this.breakpointHandler.getBreakpointSize();
const query = this.columnData.queries[0];
this.margin = measurements.large.margin;
if (breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
this.margin = measurements.small.margin;
this.measurements = measurements.small;
}
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || 'N/A';
this.yAxisLabel = this.columnData.y_axis || 'Values';
this.legendTitle = query.legend || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
if (this.data !== undefined) {
this.renderAxesPaths();
this.formatDeployments();
}
},
handleMouseOverGraph(e) {
let point = this.$refs.graphData.createSVGPoint();
point.x = e.clientX;
point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x = point.x += 7;
const timeValueOverlay = this.xScale.invert(point.x);
const overlayIndex = bisectDate(this.data, timeValueOverlay, 1);
const d0 = this.data[overlayIndex - 1];
const d1 = this.data[overlayIndex];
if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0;
this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x);
this.currentYCoordinate = this.yScale(this.currentData.value);
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
if (currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
},
renderAxesPaths() {
const axisXScale = d3.time.scale()
.range([0, this.graphWidth]);
this.yScale = d3.scale.linear()
.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))]);
const xAxis = d3.svg.axis()
.scale(axisXScale)
.ticks(measurements.ticks)
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(this.yScale)
.ticks(measurements.ticks)
.orient('left');
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
const width = this.graphWidth;
d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
.selectAll('.tick')
.each(function createTickLines() {
d3.select(this).select('line').attr('x2', width);
}); // 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);
},
},
watch: {
updateAspectRatio() {
if (this.updateAspectRatio) {
this.graphHeight = 500;
this.graphWidth = 600;
this.measurements = measurements.large;
this.draw();
eventHub.$emit('toggleAspectRatio');
}
},
},
mounted() {
this.draw();
},
};
</script>
<template>
<div
:class="classType">
<h5
class="text-center">
{{columnData.title}}
</h5>
<div
class="prometheus-svg-container">
<svg
:viewBox="outterViewBox"
:style="{ 'padding-bottom': paddingBottomRootSvg }"
ref="baseSvg">
<g
class="x-axis"
:transform="axisTransform">
</g>
<g
class="y-axis"
transform="translate(70, 20)">
</g>
<monitoring-legends
:graph-width="graphWidth"
:graph-height="graphHeight"
:margin="margin"
:measurements="measurements"
:area-color-rgb="areaColorRgb"
:legend-title="legendTitle"
:y-axis-label="yAxisLabel"
:metric-usage="metricUsage"
/>
<svg
class="graph-data"
:viewBox="innerViewBox"
ref="graphData">
<path
class="metric-area"
:d="area"
:fill="areaColorRgb"
transform="translate(-5, 20)">
</path>
<path
class="metric-line"
:d="line"
:stroke="lineColorRgb"
fill="none"
stroke-width="2"
transform="translate(-5, 20)">
</path>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<monitoring-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<monitoring-flag
v-if="showFlag"
:current-x-coordinate="currentXCoordinate"
:current-y-coordinate="currentYCoordinate"
:current-data="currentData"
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
</svg>
</svg>
</div>
</div>
</template>
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
export default {
props: {
showDeployInfo: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
},
computed: {
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
},
methods: {
refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
},
formatTime(deploymentTime) {
return timeFormat(deploymentTime);
},
formatDate(deploymentTime) {
return dateFormat(deploymentTime);
},
nameDeploymentClass(deployment) {
return `deploy-info-${deployment.id}`;
},
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
},
},
};
</script>
<template>
<g
class="deploy-info"
v-if="showDeployInfo">
<g
v-for="(deployment, index) in deploymentData"
:key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
<rect
x="0"
y="0"
:height="calculatedHeight"
width="3"
fill="url(#shadow-gradient)">
</rect>
<line
class="deployment-line"
x1="0"
y1="0"
x2="0"
:y2="calculatedHeight"
stroke="#000">
</line>
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
x="3"
y="0"
width="92"
height="60">
<rect
class="rect-text-metric deploy-info-rect rect-metric"
x="1"
y="1"
rx="2"
width="90"
height="58">
</rect>
<g
transform="translate(5, 2)">
<text
class="deploy-info-text text-metric-bold">
{{refText(deployment)}}
</text>
</g>
<text
class="deploy-info-text"
y="18"
transform="translate(5, 2)">
{{formatDate(deployment.time)}}
</text>
<text
class="deploy-info-text text-metric-bold"
y="38"
transform="translate(5, 2)">
{{formatTime(deployment.time)}}
</text>
</svg>
</g>
<svg
height="0"
width="0">
<defs>
<linearGradient
id="shadow-gradient">
<stop
offset="0%"
stop-color="#000"
stop-opacity="0.4">
</stop>
<stop
offset="100%"
stop-color="#000"
stop-opacity="0">
</stop>
</linearGradient>
</defs>
</svg>
</g>
</template>
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
export default {
props: {
currentXCoordinate: {
type: Number,
required: true,
},
currentYCoordinate: {
type: Number,
required: true,
},
currentFlagPosition: {
type: Number,
required: true,
},
currentData: {
type: Object,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
},
data() {
return {
circleColorRgb: '#8fbce8',
};
},
computed: {
formatTime() {
return timeFormat(this.currentData.time);
},
formatDate() {
return dateFormat(this.currentData.time);
},
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
},
};
</script>
<template>
<g class="mouse-over-flag">
<line
class="selected-metric-line"
:x1="currentXCoordinate"
:y1="0"
:x2="currentXCoordinate"
:y2="calculatedHeight"
transform="translate(-5, 20)">
</line>
<circle
class="circle-metric"
:fill="circleColorRgb"
stroke="#000"
:cx="currentXCoordinate"
:cy="currentYCoordinate"
r="5"
transform="translate(-5, 20)">
</circle>
<svg
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
<rect
class="rect-metric"
x="4"
y="1"
rx="2"
width="90"
height="40"
transform="translate(-3, 20)">
</rect>
<text
class="text-metric text-metric-bold"
x="8"
y="35"
transform="translate(-5, 20)">
{{formatTime}}
</text>
<text
class="text-metric-date"
x="8"
y="15"
transform="translate(-5, 20)">
{{formatDate}}
</text>
</svg>
</g>
</template>
<script>
export default {
props: {
graphWidth: {
type: Number,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
margin: {
type: Object,
required: true,
},
measurements: {
type: Object,
required: true,
},
areaColorRgb: {
type: String,
required: true,
},
legendTitle: {
type: String,
required: true,
},
yAxisLabel: {
type: String,
required: true,
},
metricUsage: {
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) / 2)
+ (this.yLabelWidth / 2) + 10 || 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>
<line
class="label-y-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
y1="0"
:x2="10"
:y2="yPosition">
</line>
<rect
class="rect-axis-text"
:transform="rectTransform"
:width="yLabelWidth"
:height="yLabelHeight">
</rect>
<text
class="label-axis-text y-label-text"
text-anchor="middle"
:transform="textTransform"
ref="ylabel">
{{yAxisLabel}}
</text>
<rect
class="rect-axis-text"
:x="xPosition + 50"
:y="graphHeight - 80"
width="50"
height="50">
</rect>
<text
class="label-axis-text"
:x="xPosition + 60"
:y="yPosition"
dy=".35em">
Time
</text>
<rect
:fill="areaColorRgb"
:width="measurements.legends.width"
:height="measurements.legends.height"
x="20"
:y="graphHeight - measurements.legendOffset">
</rect>
<text
class="text-metric-title"
x="50"
:y="graphHeight - 40">
{{legendTitle}}
</text>
<text
class="text-metric-usage"
x="50"
:y="graphHeight - 25">
{{metricUsage}}
</text>
</g>
</template>
<script>
import monitoringColumn from './monitoring_column.vue';
export default {
props: {
rowData: {
type: Array,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
},
components: {
monitoringColumn,
},
computed: {
bootstrapClass() {
return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12';
},
},
};
</script>
<template>
<div
class="prometheus-row row">
<monitoring-column
v-for="(column, index) in rowData"
:column-data="column"
:class-type="bootstrapClass"
:key="index"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="deploymentData"
/>
</div>
</template>
<script>
import gettingStartedSvg from 'empty_states/monitoring/_getting_started.svg';
import loadingSvg from 'empty_states/monitoring/_loading.svg';
import unableToConnectSvg from 'empty_states/monitoring/_unable_to_connect.svg';
export default {
props: {
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: false,
default: '',
},
selectedState: {
type: String,
required: true,
},
},
data() {
return {
states: {
gettingStarted: {
svg: gettingStartedSvg,
title: 'Get started with performance monitoring',
description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.',
buttonText: 'Configure Prometheus',
},
loading: {
svg: loadingSvg,
title: 'Waiting for performance data',
description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.',
buttonText: 'View documentation',
},
unableToConnect: {
svg: unableToConnectSvg,
title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation',
},
},
};
},
computed: {
currentState() {
return this.states[this.selectedState];
},
buttonPath() {
if (this.selectedState === 'gettingStarted') {
return this.settingsPath;
}
return this.documentationPath;
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
},
},
};
</script>
<template>
<div
class="prometheus-state">
<div
class="row">
<div
class="col-md-4 col-md-offset-4 state-svg"
v-html="currentState.svg">
</div>
</div>
<div
class="row">
<div
class="col-md-6 col-md-offset-3">
<h4
class="text-center state-title">
{{currentState.title}}
</h4>
</div>
</div>
<div
class="row">
<div
class="col-md-6 col-md-offset-3">
<div
class="description-text text-center state-description">
{{currentState.description}}
<a
:href="settingsPath"
v-if="showButtonDescription">
Prometheus server
</a>
</div>
</div>
</div>
<div
class="row state-button-section">
<div
class="col-md-4 col-md-offset-4 text-center state-button">
<a
class="btn btn-success"
:href="buttonPath">
{{currentState.buttonText}}
</a>
</div>
</div>
</div>
</template>
/* global Flash */
import d3 from 'd3';
import {
dateFormat,
timeFormat,
} from './constants';
export default class Deployments {
constructor(width, height) {
this.width = width;
this.height = height;
this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
this.createGradientDef();
}
init(chartData) {
this.chartData = chartData;
this.x = d3.time.scale().range([0, this.width]);
this.x.domain(d3.extent(this.chartData, d => d.time));
this.charts = d3.selectAll('.prometheus-graph');
this.getData();
}
getData() {
$.ajax({
url: this.endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error getting deployment information.'))
.done((data) => {
this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.x(time));
time.setSeconds(this.chartData[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
});
}
return deploymentDataArray;
}, []);
this.plotData();
});
}
plotData() {
this.charts.each((d, i) => {
const svg = d3.select(this.charts[0][i]);
const chart = svg.select('.graph-container');
const key = svg.node().getAttribute('graph-type');
this.createLine(chart, key);
this.createDeployInfoBox(chart, key);
});
}
createGradientDef() {
const defs = d3.select('body')
.append('svg')
.attr({
height: 0,
width: 0,
})
.append('defs');
defs.append('linearGradient')
.attr({
id: 'shadow-gradient',
})
.append('stop')
.attr({
offset: '0%',
'stop-color': '#000',
'stop-opacity': 0.4,
})
.select(this.selectParentNode)
.append('stop')
.attr({
offset: '100%',
'stop-color': '#000',
'stop-opacity': 0,
});
}
createLine(chart, key) {
chart.append('g')
.attr({
class: 'deploy-info',
})
.selectAll('.deploy-info')
.data(this.data)
.enter()
.append('g')
.attr({
class: d => `deploy-info-${d.id}-${key}`,
transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
})
.append('rect')
.attr({
x: 1,
y: 0,
height: this.height + 1,
width: 3,
fill: 'url(#shadow-gradient)',
})
.select(this.selectParentNode)
.append('line')
.attr({
class: 'deployment-line',
x1: 0,
x2: 0,
y1: 0,
y2: this.height + 1,
});
}
createDeployInfoBox(chart, key) {
chart.selectAll('.deploy-info')
.selectAll('.js-deploy-info-box')
.data(this.data)
.enter()
.select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
.append('svg')
.attr({
class: 'js-deploy-info-box hidden',
x: 3,
y: 0,
width: 92,
height: 60,
})
.append('rect')
.attr({
class: 'rect-text-metric deploy-info-rect rect-metric',
x: 1,
y: 1,
rx: 2,
width: 90,
height: 58,
})
.select(this.selectParentNode)
.append('g')
.attr({
transform: 'translate(5, 2)',
})
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
})
.text(Deployments.refText)
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text',
y: 18,
})
.text(d => dateFormat(d.time))
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
y: 38,
})
.text(d => timeFormat(d.time));
}
static toggleDeployTextbox(deploy, key, showInfoBox) {
d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
.classed('hidden', !showInfoBox);
}
mouseOverDeployInfo(mouseXPos, key) {
if (!this.data) return false;
let dataFound = false;
this.data.forEach((d) => {
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
Deployments.toggleDeployTextbox(d, key, true);
} else {
Deployments.toggleDeployTextbox(d, key, false);
}
});
return dataFound;
}
/* `this` is bound to the D3 node */
selectParentNode() {
return this.parentNode;
}
static refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
}
}
import Vue from 'vue';
export default new Vue();
const mixins = {
methods: {
mouseOverDeployInfo(mouseXPos) {
if (!this.reducedDeploymentData) return false;
let dataFound = false;
this.reducedDeploymentData = this.reducedDeploymentData.map((d) => {
const deployment = d;
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
deployment.showDeploymentFlag = true;
} else {
deployment.showDeploymentFlag = false;
}
return deployment;
});
return dataFound;
},
formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.xScale(time));
time.setSeconds(this.data[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
showDeploymentFlag: false,
});
}
return deploymentDataArray;
}, []);
},
},
};
export default mixins;
import PrometheusGraph from './prometheus_graph';
import Vue from 'vue';
import Monitoring from './components/monitoring.vue';
document.addEventListener('DOMContentLoaded', function onLoad() {
document.removeEventListener('DOMContentLoaded', onLoad, false);
return new PrometheusGraph();
}, false);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs',
components: {
'monitoring-dashboard': Monitoring,
},
render: createElement => createElement('monitoring-dashboard'),
}));
This diff is collapsed.
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class MonitoringService {
constructor(endpoint) {
this.graphs = Vue.resource(endpoint);
}
get() {
return this.graphs.get();
}
// eslint-disable-next-line class-methods-use-this
getDeploymentData(endpoint) {
return Vue.http.get(endpoint);
}
}
import _ from 'underscore';
class MonitoringStore {
constructor() {
this.groups = [];
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 = []) {
this.groups = groups.map((group) => {
const currentGroup = group;
currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value();
currentGroup.metrics = this.createArrayRows(currentGroup.metrics);
return currentGroup;
});
}
storeDeploymentData(deploymentData = []) {
this.deploymentData = deploymentData;
}
getMetricsCount() {
let metricsCount = 0;
this.groups.forEach((group) => {
group.metrics.forEach((metric) => {
metricsCount = metricsCount += metric.length;
});
});
return metricsCount;
}
}
export default MonitoringStore;
export default {
small: { // Covers both xs and sm screen sizes
margin: {
top: 40,
right: 40,
bottom: 50,
left: 40,
},
legends: {
width: 15,
height: 30,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
legendOffset: 52,
},
large: { // This covers both md and lg screen sizes
margin: {
top: 80,
right: 80,
bottom: 100,
left: 80,
},
legends: {
width: 20,
height: 35,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
legendOffset: 55,
},
ticks: 3,
};
......@@ -140,23 +140,6 @@
}
}
.prometheus-graph {
text {
fill: $gl-text-color;
stroke-width: 0;
}
.label-axis-text,
.text-metric-usage {
fill: $black;
font-weight: 500;
}
.legend-axis-text {
fill: $black;
}
}
.x-axis path,
.y-axis path,
.label-x-axis-line,
......@@ -205,6 +188,7 @@
.text-metric {
font-weight: 600;
font-size: 14px;
}
.selected-metric-line {
......@@ -214,20 +198,15 @@
.deployment-line {
stroke: $black;
stroke-width: 2;
stroke-width: 1;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
}
.text-metric-bold {
font-weight: 600;
}
.prometheus-state {
margin-top: 10px;
display: none;
.state-button-section {
margin-top: 10px;
......@@ -242,3 +221,59 @@
width: 38px;
}
}
.prometheus-panel {
margin-top: 20px;
}
.prometheus-svg-container {
position: relative;
height: 0;
width: 100%;
padding: 0;
padding-bottom: 100%;
.text-metric-bold {
font-weight: 600;
}
}
.prometheus-svg-container > svg {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
text {
fill: $gl-text-color;
stroke-width: 0;
}
.label-axis-text,
.text-metric-usage {
fill: $black;
font-weight: 500;
font-size: 14px;
}
.legend-axis-text {
fill: $black;
}
.tick > text {
font-size: 14px;
}
@media (max-width: $screen-sm-max) {
.label-axis-text,
.text-metric-usage,
.legend-axis-text {
font-size: 8px;
}
.tick > text {
font-size: 8px;
}
}
}
- @no_container = true
- page_title "Metrics for environment", @environment.name
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('monitoring')
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'monitoring'
= render "projects/pipelines/head"
#js-metrics.prometheus-container{ class: container_class, data: { has_metrics: "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } }
.prometheus-container{ class: container_class }
.top-area
.row
.col-sm-6
......@@ -13,68 +14,8 @@
Environment:
= link_to @environment.name, environment_path(@environment)
.prometheus-state
.js-getting-started.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= render "shared/empty_states/monitoring/getting_started.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Get started with performance monitoring
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.
= link_to help_page_path('administration/monitoring/prometheus/index.md') do
Learn more about performance monitoring
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'), class: 'btn btn-success' do
Configure Prometheus
.js-loading.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= render "shared/empty_states/monitoring/loading.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Waiting for performance data
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to help_page_path('administration/monitoring/prometheus/index.md'), class: 'btn btn-success' do
View documentation
.js-unable-to-connect.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= render "shared/empty_states/monitoring/unable_to_connect.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Unable to connect to Prometheus server
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Ensure connectivity is available from the GitLab server to the
= link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus') do
Prometheus server
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to help_page_path('administration/monitoring/prometheus/index.md'), class:'btn btn-success' do
View documentation
#prometheus-graphs{ data: { "settings-path": edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"additional-metrics": additional_metrics_namespace_project_environment_path(@project.namespace, @project, @environment, format: :json),
"has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } }
.prometheus-graphs
.row
.col-sm-12
%h4
CPU utilization
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
%h4
Memory usage
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
......@@ -163,6 +163,7 @@ var config = {
'issue_show',
'job_details',
'merge_conflicts',
'monitoring',
'notebook_viewer',
'pdf_viewer',
'pipelines',
......
import d3 from 'd3';
import PrometheusGraph from '~/monitoring/prometheus_graph';
import Deployments from '~/monitoring/deployments';
import { prometheusMockData } from './prometheus_mock_data';
describe('Metrics deployments', () => {
const fixtureName = 'environments/metrics/metrics.html.raw';
let deployment;
let prometheusGraph;
const graphElement = () => document.querySelector('.prometheus-graph');
preloadFixtures(fixtureName);
beforeEach((done) => {
// Setup the view
loadFixtures(fixtureName);
d3.selectAll('.prometheus-graph')
.append('g')
.attr('class', 'graph-container');
prometheusGraph = new PrometheusGraph();
deployment = new Deployments(1000, 500);
spyOn(prometheusGraph, 'init');
spyOn($, 'ajax').and.callFake(() => {
const d = $.Deferred();
d.resolve({
deployments: [{
id: 1,
created_at: deployment.chartData[10].time,
sha: 'testing',
tag: false,
ref: {
name: 'testing',
},
}, {
id: 2,
created_at: deployment.chartData[15].time,
sha: '',
tag: true,
ref: {
name: 'tag',
},
}],
});
setTimeout(done);
return d.promise();
});
prometheusGraph.configureGraph();
prometheusGraph.transformData(prometheusMockData.metrics);
deployment.init(prometheusGraph.graphSpecificProperties.memory_values.data);
});
it('creates line on graph for deploment', () => {
expect(
graphElement().querySelectorAll('.deployment-line').length,
).toBe(2);
});
it('creates hidden deploy boxes', () => {
expect(
graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box').length,
).toBe(2);
});
it('hides the info boxes by default', () => {
expect(
graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
).toBe(2);
});
it('shows sha short code when tag is false', () => {
expect(
graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box').textContent.trim(),
).toContain('testin');
});
it('shows ref name when tag is true', () => {
expect(
graphElement().querySelector('.deploy-info-2-cpu_values .js-deploy-info-box').textContent.trim(),
).toContain('tag');
});
it('shows info box when moving mouse over line', () => {
deployment.mouseOverDeployInfo(deployment.data[0].xPos, 'cpu_values');
expect(
graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
).toBe(1);
expect(
graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'),
).toBeNull();
});
it('hides previously visible info box when moving mouse away', () => {
deployment.mouseOverDeployInfo(500, 'cpu_values');
expect(
graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
).toBe(2);
expect(
graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'),
).not.toBeNull();
});
describe('refText', () => {
it('returns shortened SHA', () => {
expect(
Deployments.refText({
tag: false,
sha: '123456789',
}),
).toBe('123456');
});
it('returns tag name', () => {
expect(
Deployments.refText({
tag: true,
ref: 'v1.0',
}),
).toBe('v1.0');
});
});
});
This diff is collapsed.
import Vue from 'vue';
import _ from 'underscore';
import MonitoringColumn from '~/monitoring/components/monitoring_column.vue';
import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import eventHub from '~/monitoring/event_hub';
import { deploymentData, singleRowMetrics } from './mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(MonitoringColumn);
return new Component({
propsData,
}).$mount();
};
describe('MonitoringColumn', () => {
beforeEach(() => {
spyOn(MonitoringMixins.methods, 'formatDeployments').and.callFake(function fakeFormat() {
return {};
});
});
it('has a title', () => {
const component = createComponent({
columnData: singleRowMetrics[0],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
});
expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.columnData.title);
});
it('creates a path for the line and area of the graph', (done) => {
const component = createComponent({
columnData: 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', () => {
it('axisTransform translates an element Y position depending of its height', () => {
const component = createComponent({
columnData: singleRowMetrics[0],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
});
const transformedHeight = `${component.graphHeight - 100}`;
expect(component.axisTransform.indexOf(transformedHeight))
.not.toEqual(-1);
});
it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({
columnData: singleRowMetrics[0],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
});
const viewBoxArray = component.outterViewBox.split(' ');
expect(typeof component.outterViewBox).toEqual('string');
expect(viewBoxArray[2]).toEqual(component.graphWidth.toString());
expect(viewBoxArray[3]).toEqual(component.graphHeight.toString());
});
});
it('sends an event to the eventhub when it has finished resizing', (done) => {
const component = createComponent({
columnData: singleRowMetrics[0],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
});
spyOn(eventHub, '$emit');
component.updateAspectRatio = true;
Vue.nextTick(() => {
expect(eventHub.$emit).toHaveBeenCalled();
done();
});
});
});
import Vue from 'vue';
import MonitoringState from '~/monitoring/components/monitoring_deployment.vue';
import { deploymentData } from './mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(MonitoringState);
return new Component({
propsData,
}).$mount();
};
describe('MonitoringDeployment', () => {
const reducedDeploymentData = [deploymentData[0]];
reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name;
reducedDeploymentData[0].xPos = 10;
reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at);
describe('Methods', () => {
it('refText shows the ref when a tag is available', () => {
reducedDeploymentData[0].tag = '1.0';
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.refText(reducedDeploymentData[0]),
).toEqual(reducedDeploymentData[0].ref);
});
it('refText shows the sha when no tag is available', () => {
reducedDeploymentData[0].tag = null;
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.refText(reducedDeploymentData[0]),
).toContain('f5bcd1');
});
it('nameDeploymentClass creates a class with the prefix deploy-info-', () => {
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.nameDeploymentClass(reducedDeploymentData[0]),
).toContain('deploy-info');
});
it('transformDeploymentGroup translates an available deployment', () => {
const component = createComponent({
showDeployInfo: false,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.transformDeploymentGroup(reducedDeploymentData[0]),
).toContain('translate(11, 20)');
});
it('hides the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = false;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
});
it('shows the deployment flag', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.$el.querySelector('.js-deploy-info-box').style.display,
).not.toEqual('display: none;');
});
it('shows the refText inside a text element with the deploy-info-text class', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(
component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(),
).toEqual(component.refText(reducedDeploymentData[0]));
});
it('should contain a hidden gradient', () => {
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
});
describe('Computed props', () => {
it('calculatedHeight', () => {
const component = createComponent({
showDeployInfo: true,
deploymentData: reducedDeploymentData,
graphHeight: 300,
graphHeightOffset: 120,
});
expect(component.calculatedHeight).toEqual(180);
});
});
});
});
import Vue from 'vue';
import MonitoringFlag from '~/monitoring/components/monitoring_flag.vue';
const createComponent = (propsData) => {
const Component = Vue.extend(MonitoringFlag);
return new Component({
propsData,
}).$mount();
};
function getCoordinate(component, selector, coordinate) {
const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate);
return parseInt(coordinateVal, 10);
}
describe('MonitoringFlag', () => {
it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => {
const component = createComponent({
currentXCoordinate: 200,
currentYCoordinate: 100,
currentFlagPosition: 100,
currentData: {
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
},
graphHeight: 300,
graphHeightOffset: 120,
});
expect(getCoordinate(component, '.selected-metric-line', 'x1'))
.toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.selected-metric-line', 'x2'))
.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', () => {
const component = createComponent({
currentXCoordinate: 200,
currentYCoordinate: 100,
currentFlagPosition: 100,
currentData: {
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
},
graphHeight: 300,
graphHeightOffset: 120,
});
const svg = component.$el.querySelector('.rect-text-metric');
expect(svg.tagName).toEqual('svg');
expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition);
});
describe('Computed props', () => {
it('calculatedHeight', () => {
const component = createComponent({
currentXCoordinate: 200,
currentYCoordinate: 100,
currentFlagPosition: 100,
currentData: {
time: new Date('2017-06-04T18:17:33.501Z'),
value: '1.49609375',
},
graphHeight: 300,
graphHeightOffset: 120,
});
expect(component.calculatedHeight).toEqual(180);
});
});
});
import Vue from 'vue';
import MonitoringLegends from '~/monitoring/components/monitoring_legends.vue';
import measurements from '~/monitoring/utils/measurements';
const createComponent = (propsData) => {
const Component = Vue.extend(MonitoringLegends);
return new Component({
propsData,
}).$mount();
};
function getTextFromNode(component, selector) {
return component.$el.querySelector(selector).firstChild.nodeValue.trim();
}
describe('MonitoringLegends', () => {
describe('Computed props', () => {
it('textTransform', () => {
const component = createComponent({
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)');
});
it('xPosition', () => {
const component = createComponent({
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.xPosition).toEqual(180);
});
it('yPosition', () => {
const component = createComponent({
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.yPosition).toEqual(240);
});
it('rectTransform', () => {
const component = createComponent({
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)');
});
});
it('has 2 rect-axis-text rect svg elements', () => {
const component = createComponent({
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
});
it('contains text to signal the usage, title and time', () => {
const component = createComponent({
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(getTextFromNode(component, '.text-metric-title')).toEqual(component.legendTitle);
expect(getTextFromNode(component, '.text-metric-usage')).toEqual(component.metricUsage);
expect(getTextFromNode(component, '.label-axis-text')).toEqual(component.yAxisLabel);
});
});
import Vue from 'vue';
import MonitoringRow from '~/monitoring/components/monitoring_row.vue';
import { deploymentData, singleRowMetrics } from './mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(MonitoringRow);
return new Component({
propsData,
}).$mount();
};
describe('MonitoringRow', () => {
describe('Computed props', () => {
it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => {
const component = createComponent({
rowData: singleRowMetrics,
updateAspectRatio: false,
deploymentData,
});
expect(component.bootstrapClass).toEqual('col-md-6');
});
it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => {
const component = createComponent({
rowData: [singleRowMetrics[0]],
updateAspectRatio: false,
deploymentData,
});
expect(component.bootstrapClass).toEqual('col-md-12');
});
});
it('has one column', () => {
const component = createComponent({
rowData: singleRowMetrics,
updateAspectRatio: false,
deploymentData,
});
expect(component.$el.querySelectorAll('.prometheus-svg-container').length)
.toEqual(component.rowData.length);
});
it('has two columns', () => {
const component = createComponent({
rowData: singleRowMetrics,
updateAspectRatio: false,
deploymentData,
});
expect(component.$el.querySelectorAll('.col-md-6').length)
.toEqual(component.rowData.length);
});
});
import Vue from 'vue';
import Monitoring from '~/monitoring/components/monitoring.vue';
import { MonitorMockInterceptor } from './mock_data';
describe('Monitoring', () => {
const fixtureName = 'environments/metrics/metrics.html.raw';
let MonitoringComponent;
let component;
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
MonitoringComponent = Vue.extend(Monitoring);
});
describe('no metrics are available yet', () => {
it('shows a getting started empty state when no metrics are present', () => {
component = new MonitoringComponent({
el: document.querySelector('#prometheus-graphs'),
});
component.$mount();
expect(component.$el.querySelector('#prometheus-graphs')).toBe(null);
expect(component.state).toEqual('gettingStarted');
});
});
describe('requests information to the server', () => {
beforeEach(() => {
document.querySelector('#prometheus-graphs').setAttribute('data-has-metrics', 'true');
Vue.http.interceptors.push(MonitorMockInterceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, MonitorMockInterceptor);
});
it('shows up a loading state', (done) => {
component = new MonitoringComponent({
el: document.querySelector('#prometheus-graphs'),
});
component.$mount();
Vue.nextTick(() => {
expect(component.state).toEqual('loading');
done();
});
});
});
});
import Vue from 'vue';
import MonitoringState from '~/monitoring/components/monitoring_state.vue';
import { statePaths } from './mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(MonitoringState);
return new Component({
propsData,
}).$mount();
};
function getTextFromNode(component, selector) {
return component.$el.querySelector(selector).firstChild.nodeValue.trim();
}
describe('MonitoringState', () => {
describe('Computed props', () => {
it('currentState', () => {
const component = createComponent({
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
});
expect(component.currentState).toBe(component.states.gettingStarted);
});
it('buttonPath returns settings path for the state "gettingStarted"', () => {
const component = createComponent({
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
});
expect(component.buttonPath).toEqual(statePaths.settingsPath);
expect(component.buttonPath).not.toEqual(statePaths.documentationPath);
});
it('buttonPath returns documentation path for any of the other states', () => {
const component = createComponent({
selectedState: 'loading',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
});
expect(component.buttonPath).toEqual(statePaths.documentationPath);
expect(component.buttonPath).not.toEqual(statePaths.settingsPath);
});
it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
const component = createComponent({
selectedState: 'unableToConnect',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
});
expect(component.showButtonDescription).toEqual(true);
});
it('showButtonDescription returns the description without a link for any other state', () => {
const component = createComponent({
selectedState: 'loading',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
});
expect(component.showButtonDescription).toEqual(false);
});
});
it('should show the gettingStarted state', () => {
const component = createComponent({
selectedState: 'gettingStarted',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
});
expect(component.$el.querySelector('svg')).toBeDefined();
expect(getTextFromNode(component, '.state-title')).toEqual(component.states.gettingStarted.title);
expect(getTextFromNode(component, '.state-description')).toEqual(component.states.gettingStarted.description);
expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.gettingStarted.buttonText);
});
it('should show the loading state', () => {
const component = createComponent({
selectedState: 'loading',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
});
expect(component.$el.querySelector('svg')).toBeDefined();
expect(getTextFromNode(component, '.state-title')).toEqual(component.states.loading.title);
expect(getTextFromNode(component, '.state-description')).toEqual(component.states.loading.description);
expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.loading.buttonText);
});
it('should show the unableToConnect state', () => {
const component = createComponent({
selectedState: 'unableToConnect',
settingsPath: statePaths.settingsPath,
documentationPath: statePaths.documentationPath,
});
expect(component.$el.querySelector('svg')).toBeDefined();
expect(getTextFromNode(component, '.state-title')).toEqual(component.states.unableToConnect.title);
expect(component.$el.querySelector('.state-description a')).toBeDefined();
expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.unableToConnect.buttonText);
});
});
import MonitoringStore from '~/monitoring/stores/monitoring_store';
import MonitoringMock, { deploymentData } from './mock_data';
describe('MonitoringStore', () => {
this.store = new MonitoringStore();
this.store.storeMetrics(MonitoringMock.data);
it('contains one group that contains two queries sorted by priority in one row', () => {
expect(this.store.groups).toBeDefined();
expect(this.store.groups.length).toEqual(1);
expect(this.store.groups[0].metrics.length).toEqual(1);
});
it('gets the metrics count for every group', () => {
expect(this.store.getMetricsCount()).toEqual(2);
});
it('contains deployment data', () => {
this.store.storeDeploymentData(deploymentData);
expect(this.store.deploymentData).toBeDefined();
expect(this.store.deploymentData.length).toEqual(3);
expect(typeof this.store.deploymentData[0]).toEqual('object');
});
});
import 'jquery';
import PrometheusGraph from '~/monitoring/prometheus_graph';
import { prometheusMockData } from './prometheus_mock_data';
describe('PrometheusGraph', () => {
const fixtureName = 'environments/metrics/metrics.html.raw';
const prometheusGraphContainer = '.prometheus-graph';
const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`;
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
$('.prometheus-container').data('has-metrics', 'true');
this.prometheusGraph = new PrometheusGraph();
const self = this;
const fakeInit = (metricsResponse) => {
self.prometheusGraph.transformData(metricsResponse);
self.prometheusGraph.createGraph();
};
spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit);
});
it('initializes graph properties', () => {
// Test for the measurements
expect(this.prometheusGraph.margin).toBeDefined();
expect(this.prometheusGraph.marginLabelContainer).toBeDefined();
expect(this.prometheusGraph.originalWidth).toBeDefined();
expect(this.prometheusGraph.originalHeight).toBeDefined();
expect(this.prometheusGraph.height).toBeDefined();
expect(this.prometheusGraph.width).toBeDefined();
expect(this.prometheusGraph.backOffRequestCounter).toBeDefined();
// Test for the graph properties (colors, radius, etc.)
expect(this.prometheusGraph.graphSpecificProperties).toBeDefined();
expect(this.prometheusGraph.commonGraphProperties).toBeDefined();
});
it('transforms the data', () => {
this.prometheusGraph.init(prometheusMockData.metrics);
Object.keys(this.prometheusGraph.graphSpecificProperties, (key) => {
const graphProps = this.prometheusGraph.graphSpecificProperties[key];
expect(graphProps.data).toBeDefined();
expect(graphProps.data.length).toBe(121);
});
});
it('creates two graphs', () => {
this.prometheusGraph.init(prometheusMockData.metrics);
expect($(prometheusGraphContainer).length).toBe(2);
});
describe('Graph contents', () => {
beforeEach(() => {
this.prometheusGraph.init(prometheusMockData.metrics);
});
it('has axis, an area, a line and a overlay', () => {
const $graphContainer = $(prometheusGraphContents).find('.x-axis').parent();
expect($graphContainer.find('.x-axis')).toBeDefined();
expect($graphContainer.find('.y-axis')).toBeDefined();
expect($graphContainer.find('.prometheus-graph-overlay')).toBeDefined();
expect($graphContainer.find('.metric-line')).toBeDefined();
expect($graphContainer.find('.metric-area')).toBeDefined();
});
it('has legends, labels and an extra axis that labels the metrics', () => {
const $prometheusGraphContents = $(prometheusGraphContents);
const $axisLabelContainer = $(prometheusGraphContents).find('.label-x-axis-line').parent();
expect($prometheusGraphContents.find('.label-x-axis-line')).toBeDefined();
expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined();
expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined();
expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined();
expect($axisLabelContainer.find('rect').length).toBe(3);
expect($axisLabelContainer.find('text').length).toBe(4);
});
});
});
describe('PrometheusGraphs UX states', () => {
const fixtureName = 'environments/metrics/metrics.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
this.prometheusGraph = new PrometheusGraph();
});
it('shows a specified state', () => {
this.prometheusGraph.state = '.js-getting-started';
this.prometheusGraph.updateState();
const $state = $('.js-getting-started');
expect($state).toBeDefined();
expect($('.state-title', $state)).toBeDefined();
expect($('.state-svg', $state)).toBeDefined();
expect($('.state-description', $state)).toBeDefined();
expect($('.state-button', $state)).toBeDefined();
});
});
This diff is collapsed.
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