summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/monitoring/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/monitoring/components')
-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
7 files changed, 985 insertions, 0 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>