Skip to content
Snippets Groups Projects
Commit 871bf96c authored by Phil Hughes's avatar Phil Hughes Committed by Jose Ivan Vargas Lopez
Browse files

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

Additional metrics dashboard

See merge request !11740
parent 676c5594
No related branches found
No related tags found
No related merge requests found
Showing
with 5458 additions and 872 deletions
<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'),
}));
/* eslint-disable no-new */
/* global Flash */
import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status';
import Deployments from './deployments';
import '../lib/utils/common_utils';
import { formatRelevantDigits } from '../lib/utils/number_utils';
import '../flash';
import {
dateFormat,
timeFormat,
} from './constants';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json';
const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100;
class PrometheusGraph {
constructor() {
const $prometheusContainer = $(prometheusContainer);
const hasMetrics = $prometheusContainer.data('has-metrics');
this.docLink = $prometheusContainer.data('doc-link');
this.integrationLink = $prometheusContainer.data('prometheus-integration');
this.state = '';
$(document).ajaxError(() => {});
if (hasMetrics) {
this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
extraAddedWidthParent;
this.originalWidth = parentContainerWidth;
this.originalHeight = 330;
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.deployments = new Deployments(this.width, this.height);
this.configureGraph();
this.init();
} else {
const prevState = this.state;
this.state = '.js-getting-started';
this.updateState(prevState);
}
}
createGraph() {
Object.keys(this.graphSpecificProperties).forEach((key) => {
const value = this.graphSpecificProperties[key];
if (value.data.length > 0) {
this.plotValues(key);
}
});
}
init() {
return this.getData().then((metricsResponse) => {
let enoughData = true;
if (typeof metricsResponse === 'undefined') {
enoughData = false;
} else {
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const currentData = (metricsResponse.metrics[key])[0];
if (currentData.values.length <= 2) {
enoughData = false;
}
}
});
}
if (enoughData) {
$(prometheusStatesContainer).hide();
$(prometheusParentGraphContainer).show();
this.transformData(metricsResponse);
this.createGraph();
const firstMetricData = this.graphSpecificProperties[
Object.keys(this.graphSpecificProperties)[0]
].data;
this.deployments.init(firstMetricData);
}
});
}
plotValues(key) {
const graphSpecifics = this.graphSpecificProperties[key];
const x = d3.time.scale()
.range([0, this.width]);
const y = d3.scale.linear()
.range([this.height, 0]);
graphSpecifics.xScale = x;
graphSpecifics.yScale = y;
const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const chart = d3.select(prometheusGraphContainer)
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g')
.attr('class', 'graph-container')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer)
.attr('width', this.originalWidth)
.attr('height', this.originalHeight)
.append('g')
.attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
x.domain(d3.extent(graphSpecifics.data, d => d.time));
y.domain([0, d3.max(graphSpecifics.data.map(metricValue => metricValue.value))]);
const xAxis = d3.svg.axis()
.scale(x)
.ticks(this.commonGraphProperties.axis_no_ticks)
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width)
.outerTickSize(0)
.orient('left');
this.createAxisLabelContainers(axisLabelContainer, key);
chart.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${this.height})`)
.call(xAxis);
chart.append('g')
.attr('class', 'y-axis')
.call(yAxis);
const area = d3.svg.area()
.x(d => x(d.time))
.y0(this.height)
.y1(d => y(d.value))
.interpolate('linear');
const line = d3.svg.line()
.x(d => x(d.time))
.y(d => y(d.value));
chart.append('path')
.datum(graphSpecifics.data)
.attr('d', area)
.attr('class', 'metric-area')
.attr('fill', graphSpecifics.area_fill_color);
chart.append('path')
.datum(graphSpecifics.data)
.attr('class', 'metric-line')
.attr('stroke', graphSpecifics.line_color)
.attr('fill', 'none')
.attr('stroke-width', this.commonGraphProperties.area_stroke_width)
.attr('d', line);
// Overlay area for the mouseover events
chart.append('rect')
.attr('class', 'prometheus-graph-overlay')
.attr('width', this.width)
.attr('height', this.height)
.on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer));
}
// The legends from the metric
createAxisLabelContainers(axisLabelContainer, key) {
const graphSpecifics = this.graphSpecificProperties[key];
axisLabelContainer.append('line')
.attr('class', 'label-x-axis-line')
.attr('stroke', '#000000')
.attr('stroke-width', '1')
.attr({
x1: 10,
y1: this.originalHeight - this.margin.top,
x2: (this.originalWidth - this.margin.right) + 10,
y2: this.originalHeight - this.margin.top,
});
axisLabelContainer.append('line')
.attr('class', 'label-y-axis-line')
.attr('stroke', '#000000')
.attr('stroke-width', '1')
.attr({
x1: 10,
y1: 0,
x2: 10,
y2: this.originalHeight - this.margin.top,
});
axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text')
.attr('x', 0)
.attr('y', 50)
.attr('width', 30)
.attr('height', 150);
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('text-anchor', 'middle')
.attr('transform', `translate(15, ${(this.originalHeight - this.margin.top) / 2}) rotate(-90)`)
.text(graphSpecifics.graph_legend_title);
axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text')
.attr('x', (this.originalWidth / 2) - this.margin.right)
.attr('y', this.originalHeight - 100)
.attr('width', 30)
.attr('height', 80);
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('x', (this.originalWidth / 2) - this.margin.right)
.attr('y', this.originalHeight - this.margin.top)
.attr('dy', '.35em')
.text('Time');
// Legends
// Metric Usage
axisLabelContainer.append('rect')
.attr('x', this.originalWidth - 170)
.attr('y', (this.originalHeight / 2) - 60)
.style('fill', graphSpecifics.area_fill_color)
.attr('width', 20)
.attr('height', 35);
axisLabelContainer.append('text')
.attr('class', 'text-metric-title')
.attr('x', this.originalWidth - 140)
.attr('y', (this.originalHeight / 2) - 50)
.text('Average');
axisLabelContainer.append('text')
.attr('class', 'text-metric-usage')
.attr('x', this.originalWidth - 140)
.attr('y', (this.originalHeight / 2) - 25);
}
handleMouseOverGraph(prometheusGraphContainer) {
const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
const currentXCoordinate = d3.mouse(rectOverlay)[0];
Object.keys(this.graphSpecificProperties).forEach((key) => {
const currentGraphProps = this.graphSpecificProperties[key];
const timeValueOverlay = currentGraphProps.xScale.invert(currentXCoordinate);
const overlayIndex = bisectDate(currentGraphProps.data, timeValueOverlay, 1);
const d0 = currentGraphProps.data[overlayIndex - 1];
const d1 = currentGraphProps.data[overlayIndex];
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
const currentData = evalTime ? d1 : d0;
const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time));
const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key);
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
// Clear up all the pieces of the flag
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line')
.attr({
class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`,
x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate,
y2: maxMetricValue,
});
currentChart.append('circle')
.attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color)
.attr('cx', currentDeployXPos || currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
if (currentDeployXPos) return;
// The little box with text
const rectTextMetric = currentChart.append('svg')
.attr({
class: 'rect-text-metric',
x: currentTimeCoordinate,
y: 0,
});
rectTextMetric.append('rect')
.attr({
class: 'rect-metric',
x: 4,
y: 1,
rx: 2,
width: this.commonGraphProperties.rect_text_width,
height: this.commonGraphProperties.rect_text_height,
});
rectTextMetric.append('text')
.attr({
class: 'text-metric text-metric-bold',
x: 8,
y: 35,
})
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
.attr({
class: 'text-metric-date',
x: 8,
y: 15,
})
.text(dateFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') {
currentMetricValue = `${currentMetricValue}%`;
} else {
currentMetricValue = `${currentMetricValue} MB`;
}
d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`)
.text(currentMetricValue);
});
}
configureGraph() {
this.graphSpecificProperties = {
cpu_values: {
area_fill_color: '#edf3fc',
line_color: '#5b99f7',
graph_legend_title: 'CPU Usage (Cores)',
data: [],
xScale: {},
yScale: {},
},
memory_values: {
area_fill_color: '#fca326',
line_color: '#fc6d26',
graph_legend_title: 'Memory Usage (MB)',
data: [],
xScale: {},
yScale: {},
},
};
this.commonGraphProperties = {
area_stroke_width: 2,
median_total_characters: 8,
circle_radius_metric: 5,
rect_text_width: 90,
rect_text_height: 40,
axis_no_ticks: 3,
};
}
getData() {
const maxNumberOfRequests = 3;
this.state = '.js-loading';
this.updateState();
return gl.utils.backOff((next, stop) => {
$.ajax({
url: metricsEndpoint,
dataType: 'json',
})
.done((data, statusText, resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
} else if (this.backOffRequestCounter >= maxNumberOfRequests) {
stop(new Error('loading'));
}
} else if (!data.success) {
stop(new Error('loading'));
} else {
stop({
status: resp.status,
metrics: data,
});
}
}).fail(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
return {};
}
return resp.metrics;
})
.catch(() => {
const prevState = this.state;
this.state = '.js-unable-to-connect';
this.updateState(prevState);
});
}
transformData(metricsResponse) {
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0];
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
}));
}
});
}
updateState(prevState) {
const $statesContainer = $(prometheusStatesContainer);
$(prometheusParentGraphContainer).hide();
if (prevState) {
$(`${prevState}`, $statesContainer).addClass('hidden');
}
$(`${this.state}`, $statesContainer).removeClass('hidden');
$(prometheusStatesContainer).show();
}
}
export default PrometheusGraph;
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,
};
Loading
Loading
@@ -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,
Loading
Loading
@@ -205,6 +188,7 @@
 
.text-metric {
font-weight: 600;
font-size: 14px;
}
 
.selected-metric-line {
Loading
Loading
@@ -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;
Loading
Loading
@@ -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
Loading
Loading
@@ -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' }
Loading
Loading
@@ -163,6 +163,7 @@ var config = {
'issue_show',
'job_details',
'merge_conflicts',
'monitoring',
'notebook_viewer',
'pdf_viewer',
'pipelines',
Loading
Loading
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.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment