summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorTim Zallmann <tzallmann@gitlab.com>2017-06-28 15:49:42 +0000
committerTim Zallmann <tzallmann@gitlab.com>2017-06-28 15:49:42 +0000
commit5870fc5daf0b96c26479ee6f7601bc6e8a243c24 (patch)
treef9a3a417bbe37fef74da7d8db83252b3c7c54acb /app/assets
parent9ef6d028a12521c1c458680bccde24ccfdddcb65 (diff)
parentdbec39ab863367479984858a0383197f36d7a416 (diff)
downloadgitlab-ce-5870fc5daf0b96c26479ee6f7601bc6e8a243c24.tar.gz
Merge branch 'cherry-pick-5ea675be' into 'master'
Merge branch 'additional-metrics-dashboard' into '28717-additional-metrics-review-branch' See merge request !12454
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring.vue157
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_column.vue291
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_deployment.vue136
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_flag.vue104
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_legends.vue144
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_row.vue41
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_state.vue112
-rw-r--r--app/assets/javascripts/monitoring/deployments.js211
-rw-r--r--app/assets/javascripts/monitoring/event_hub.js3
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js46
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js14
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js433
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js19
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js61
-rw-r--r--app/assets/javascripts/monitoring/utils/measurements.js39
-rw-r--r--app/assets/stylesheets/pages/environments.scss81
16 files changed, 1220 insertions, 672 deletions
diff --git a/app/assets/javascripts/monitoring/components/monitoring.vue b/app/assets/javascripts/monitoring/components/monitoring.vue
new file mode 100644
index 00000000000..a6a2d3119e3
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/monitoring.vue
@@ -0,0 +1,157 @@
+<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>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/monitoring_column.vue
new file mode 100644
index 00000000000..4f4792877ee
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/monitoring_column.vue
@@ -0,0 +1,291 @@
+<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>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_deployment.vue b/app/assets/javascripts/monitoring/components/monitoring_deployment.vue
new file mode 100644
index 00000000000..e6432ba3191
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/monitoring_deployment.vue
@@ -0,0 +1,136 @@
+<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>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_flag.vue b/app/assets/javascripts/monitoring/components/monitoring_flag.vue
new file mode 100644
index 00000000000..180a771415b
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/monitoring_flag.vue
@@ -0,0 +1,104 @@
+<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>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_legends.vue b/app/assets/javascripts/monitoring/components/monitoring_legends.vue
new file mode 100644
index 00000000000..b30ed3cc889
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/monitoring_legends.vue
@@ -0,0 +1,144 @@
+<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>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_row.vue b/app/assets/javascripts/monitoring/components/monitoring_row.vue
new file mode 100644
index 00000000000..e5528f17880
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/monitoring_row.vue
@@ -0,0 +1,41 @@
+<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>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_state.vue b/app/assets/javascripts/monitoring/components/monitoring_state.vue
new file mode 100644
index 00000000000..598021aa4df
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/monitoring_state.vue
@@ -0,0 +1,112 @@
+<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>
diff --git a/app/assets/javascripts/monitoring/deployments.js b/app/assets/javascripts/monitoring/deployments.js
deleted file mode 100644
index fc92ab61b31..00000000000
--- a/app/assets/javascripts/monitoring/deployments.js
+++ /dev/null
@@ -1,211 +0,0 @@
-/* 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);
- }
-}
diff --git a/app/assets/javascripts/monitoring/event_hub.js b/app/assets/javascripts/monitoring/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/monitoring/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
new file mode 100644
index 00000000000..8e62fa63f13
--- /dev/null
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -0,0 +1,46 @@
+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;
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index b3ce9310417..5d5cb56af72 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,6 +1,10 @@
-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'),
+}));
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
deleted file mode 100644
index 6af88769129..00000000000
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ /dev/null
@@ -1,433 +0,0 @@
-/* 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;
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
new file mode 100644
index 00000000000..1e9ae934853
--- /dev/null
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -0,0 +1,19 @@
+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);
+ }
+}
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
new file mode 100644
index 00000000000..737c964f12e
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -0,0 +1,61 @@
+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;
diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js
new file mode 100644
index 00000000000..a60d2522f49
--- /dev/null
+++ b/app/assets/javascripts/monitoring/utils/measurements.js
@@ -0,0 +1,39 @@
+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,
+};
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 1046ebfa2e2..a2be957655f 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -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;
+ }
+ }
+}