diff options
author | Jose Ivan Vargas <jvargas@gitlab.com> | 2017-05-24 15:46:04 -0500 |
---|---|---|
committer | Jose Ivan Vargas <jvargas@gitlab.com> | 2017-05-24 15:46:04 -0500 |
commit | f7a821519e12301e7bcfb2b1f3a704f062ad6d91 (patch) | |
tree | 5eeb9e3412370afd8407d9156678ce815a566f4e | |
parent | f4965ae3cb3f1603b3710cc36561a6e9dfa61722 (diff) | |
download | gitlab-ce-support-additional-metrics-environments-dashboard.tar.gz |
Added the responsive characteristic to the d3 graphs using thesupport-additional-metrics-environments-dashboard
viewBox and padding techniques
7 files changed, 206 insertions, 139 deletions
diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/monitoring_column.vue index e28e9577990..5957bcb2ab5 100644 --- a/app/assets/javascripts/monitoring/components/monitoring_column.vue +++ b/app/assets/javascripts/monitoring/components/monitoring_column.vue @@ -1,10 +1,11 @@ <script> + /* global Breakpoints */ import d3 from 'd3'; import { dateFormat, timeFormat, } from '../constants'; - + import eventHub from '../event_hub'; const bisectDate = d3.bisector(d => d[0]).left; @@ -20,6 +21,11 @@ required: true, default: 'col-md-6', }, + updateAspectRatio: { + type: Boolean, + required: true, + default: false, + }, }, data() { return { @@ -42,21 +48,38 @@ svgContainer: {}, data: [], axisLabelContainer: {}, + breakpointHandler: Breakpoints.get(), }; }, methods: { + draw() { + const breakpointSize = this.breakpointHandler.getBreakpointSize(); + let height = 500; + if (breakpointSize === 'xs' || breakpointSize === 'sm') { + height = 300; + } + this.svgContainer = this.$el.querySelector('svg'); + this.data = (this.columnData.queries[0].result[0])[0].values; + this.width = this.svgContainer.clientWidth - + this.margin.left - this.margin.right; + this.height = height - this.margin.top - this.margin.bottom; + if (this.data !== undefined) { + this.renderAxisAndContainer(); + this.renderLabelAxisContainer(); + } + }, handleMouseOverGraph() { const rectOverlay = this.$el.querySelector('.prometheus-graph-overlay'); const currentXCoordinate = d3.mouse(rectOverlay)[0]; - const timeValueOverlay = this.xScale.invert(currentXCoordinate); + const timeValueOverlay = this.xScale2.invert(currentXCoordinate); const overlayIndex = bisectDate(this.data, timeValueOverlay, 1); const d0 = this.data[overlayIndex - 1]; const d1 = this.data[overlayIndex]; - if(d0 === undefined || d1 === undefined) return; + if (d0 === undefined || d1 === undefined) return; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; const currentData = evalTime ? d1 : d0; const currentDeployXPos = {}; - const currentTimeCoordinate = Math.floor(this.xScale(currentData[0])); + let currentTimeCoordinate = Math.floor(this.xScale2(currentData[0])); // const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key); const maxValueFromData = d3.max(this.data.map(d => d[1])); const maxMetricValue = this.yScale(maxValueFromData); @@ -65,10 +88,12 @@ graphContainer.selectAll('.selected-metric-line').remove(); graphContainer.selectAll('.circle-metric').remove(); graphContainer.selectAll('.rect-text-metric:not(.deploy-info-rect)').remove(); + graphContainer.select('.mouse-over-flag').remove(); // if (currentDeployXPos) return; - const currentChart = graphContainer.select('g'); + const currentChart = graphContainer.select('.graph-data') + .append('g').attr('class', 'mouse-over-flag'); currentChart.append('line') .attr({ @@ -77,17 +102,23 @@ y1: this.yScale(0), x2: currentTimeCoordinate, y2: maxMetricValue, - }); + }) + .attr('transform', 'translate(-5,0)'); currentChart.append('circle') .attr('class', 'circle-metric') .attr('fill', '#5b99f7') .attr('cx', currentTimeCoordinate || currentDeployXPos) .attr('cy', this.yScale(currentData[1])) - .attr('r', 5); + .attr('r', 5) + .attr('transform', 'translate(-5,0)'); // The little box with text - const rectTextMetric = graphContainer.select('g').append('svg') + if (currentTimeCoordinate >= this.width - 70 - 120) { + currentTimeCoordinate = currentTimeCoordinate -= 100; + } + + const rectTextMetric = currentChart.append('svg') .attr({ class: 'rect-text-metric', x: currentTimeCoordinate, @@ -102,6 +133,7 @@ rx: 2, width: 90, height: 40, + transform: 'translate(-5,0)', }); rectTextMetric.append('text') @@ -109,6 +141,7 @@ class: 'text-metric text-metric-bold', x: 8, y: 35, + transform: 'translate(-5,0)', }) .text(timeFormat(new Date(currentData[0] * 1000))); @@ -117,34 +150,29 @@ class: 'text-metric-date', x: 8, y: 15, + transform: 'translate(-5,0)', }) .text(dateFormat(new Date(currentData[0] * 1000))); }, renderAxisAndContainer() { + d3.select(this.$el.querySelector('.prometheus-svg-container')) + .attr({ + style: `padding-bottom: ${(Math.ceil(this.height * 100) / this.width)}%`, + }); + const chart = d3.select(this.svgContainer) - .attr("preserveAspectRatio", "xMinYMin meet") - .attr("viewBox", this.viewBoxSize) - .attr('class', 'svg-content') - .append('g') - .attr('transform', `translate(${this.margin.left},${this.margin.top})`); - - this.width = this.svgContainer.viewBox.baseVal.width - this.margin.left - this.margin.right; - this.height = this.svgContainer.viewBox.baseVal.height - this.margin.top - this.margin.bottom; + .attr('viewBox', `0 0 ${this.width} ${this.height}`); + this.xScale = d3.time.scale() .range([0, this.width]); this.yScale = d3.scale.linear() - .range([this.height, 0]); - // this.xScale.domain(d3.extent(this.data, d => d[0])); - this.xScale.domain(d3.extent(this.data, function(d) { - if (d !== undefined) { - return d[0]; - } - })); + .range([this.height - 100, 0]); + this.xScale.domain(d3.extent(this.data, d => d[0])); this.yScale.domain([0, d3.max(this.data.map(d => d[1]))]); const xAxis = d3.svg.axis() .scale(this.xScale) - .ticks(3) + .ticks(5) .orient('bottom'); const yAxis = d3.svg.axis() @@ -154,49 +182,66 @@ chart.append('g') .attr('class', 'x-axis') - .attr('transform', `translate(0,${this.height})`) + .attr('transform', `translate(70,${this.height - 100})`) .call(xAxis); + const width = this.width; chart.append('g') .attr('class', 'y-axis') - .call(yAxis); + .attr('transform', 'translate(70,0)') + .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 + + const pathGroup = chart.append('svg') + .attr('class', 'graph-data') + .attr('viewBox', `0 0 ${this.width - 150} ${this.height}`); + + this.xScale2 = d3.time.scale() + .range([0, this.width - 70]); + + this.xScale2.domain(d3.extent(this.data, d => d[0])); const area = d3.svg.area() - .x(d => this.xScale(d[0])) - .y0(this.height) + .x(d => this.xScale2(d[0])) + .y0(this.height - 100) .y1(d => this.yScale(d[1])) .interpolate('linear'); const line = d3.svg.line() - .x(d => this.xScale(d[0])) + .x(d => this.xScale2(d[0])) .y(d => this.yScale(d[1])); - chart.append('path') + pathGroup.append('path') .datum(this.data) .attr('d', area) .attr('class', 'metric-area') - .attr('fill', '#edf3fc'); + .attr('fill', '#edf3fc') + .attr('transform', 'translate(-5,0)'); - chart.append('path') + pathGroup.append('path') .datum(this.data) .attr('class', 'metric-line') .attr('stroke', '#5b99f7') .attr('fill', 'none') .attr('stroke-width', 2) - .attr('d', line); + .attr('d', line) + .attr('transform', 'translate(-5, 0)'); - //Overlay area for mouseover events - chart.append('rect') + // Overlay area for mouseover events + pathGroup.append('rect') .attr('class', 'prometheus-graph-overlay') - .attr('width', this.width) - .attr('height', this.height) + .attr('width', this.width - 70) + .attr('height', this.height - 100) + .attr('transform', 'translate(-5, 0)') .on('mousemove', this.handleMouseOverGraph); }, renderLabelAxisContainer() { const axisLabelContainer = d3.select(this.svgContainer) .append('g') - .attr('class', 'axis-label-container') - .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); + .attr('class', 'axis-label-container'); axisLabelContainer.append('line') .attr('class', 'label-x-axis-line') @@ -204,9 +249,9 @@ .attr('stroke-width', '1') .attr({ x1: 10, - y1: this.svgContainer.viewBox.baseVal.height - this.margin.top, - x2: (this.svgContainer.viewBox.baseVal.width - this.margin.right) + 10, - y2: this.svgContainer.viewBox.baseVal.height - this.margin.top, + y1: (this.height - this.margin.top) + 20, + x2: this.width + 20, + y2: (this.height - this.margin.top) + 20, }); axisLabelContainer.append('line') @@ -217,7 +262,7 @@ x1: 10, y1: 0, x2: 10, - y2: this.svgContainer.viewBox.baseVal.height - this.margin.top, + y2: (this.height - this.margin.top) + 20, }); axisLabelContainer.append('rect') @@ -230,55 +275,65 @@ axisLabelContainer.append('text') .attr('class', 'label-axis-text') .attr('text-anchor', 'middle') - .attr('transform', `translate(15, ${(this.svgContainer.viewBox.baseVal.height - this.margin.top) / 2}) rotate(-90)`) + .attr('transform', `translate(15, ${((this.height - this.margin.top) + 20) / 2}) rotate(-90)`) .text('I.O.U Title'); // TODO: Put the appropiate title axisLabelContainer.append('rect') .attr('class', 'rect-axis-text') - .attr('x', (this.svgContainer.viewBox.baseVal.width / 2) - this.margin.right) - .attr('y', this.svgContainer.viewBox.baseVal.height - 100) + .attr('x', ((this.width + 20) / 2) - this.margin.right) + .attr('y', this.height - 80) .attr('width', 30) - .attr('height', 80); + .attr('height', 50); axisLabelContainer.append('text') .attr('class', 'label-axis-text') - .attr('x', (this.svgContainer.viewBox.baseVal.width / 2) - this.margin.right) - .attr('y', this.svgContainer.viewBox.baseVal.height - this.margin.top) + .attr('x', ((this.width + 20) / 2) - this.margin.right) + .attr('y', (this.height - this.margin.top) + 20) .attr('dy', '.35em') .text('Time'); - // TODO: Move this to the bottom of the graph, these are the legends + // The legends axisLabelContainer.append('rect') - .attr('x', this.svgContainer.viewBox.baseVal.width - 170) - .attr('y', (this.svgContainer.viewBox.baseVal.height / 2) - 60) + .attr('x', 20) + .attr('y', this.height - 55) .style('fill', '#edf3fc') .attr('width', 20) .attr('height', 35); axisLabelContainer.append('text') .attr('class', 'text-metric-title') - .attr('x', this.svgContainer.viewBox.baseVal.width - 140) - .attr('y', (this.svgContainer.viewBox.baseVal.height / 2) - 50) + .attr('x', 50) + .attr('y', this.height - 40) .text('Average'); axisLabelContainer.append('text') .attr('class', 'text-metric-usage') - .attr('x', this.svgContainer.viewBox.baseVal.width - 140) - .attr('y', (this.svgContainer.viewBox.baseVal.height / 2) - 25); - } + .attr('x', 50) + .attr('y', this.height - 25) + .text('N/A'); + }, + redraw() { + // Remove event listeners and graphs, then redraw them + d3.select(this.svgContainer).select('.prometheus-graph-overlay').on('mousemove', null); + d3.select(this.svgContainer).remove(); + d3.select(this.$el).select('.prometheus-svg-container').append('svg'); + this.draw(); + }, }, + + watch: { + updateAspectRatio: { + handler() { + if (this.updateAspectRatio) { + this.redraw(); + eventHub.$emit('toggleAspectRatio'); + } + }, + }, + }, + mounted() { - this.svgContainer = this.$el.querySelector('svg'); - this.data = (this.columnData.queries[0].result[0])[0].values || (this.columnData.queries[0].result[0])[0].value; - if(this.classType === 'col-md-6') { - this.viewBoxSize = '0 0 600 350'; - } else { - this.viewBoxSize = '0 0 500 250'; - } - if (this.data !== undefined) { - this.renderAxisAndContainer(); - this.renderLabelAxisContainer(); - } + this.draw(); }, }; </script> diff --git a/app/assets/javascripts/monitoring/components/monitoring_panel.vue b/app/assets/javascripts/monitoring/components/monitoring_panel.vue deleted file mode 100644 index e69544efbbb..00000000000 --- a/app/assets/javascripts/monitoring/components/monitoring_panel.vue +++ /dev/null @@ -1,37 +0,0 @@ -<script> -import MonitoringRow from './monitoring_row.vue'; -/* -This component renders a panel for each group of the correspondent metrics -previously configured -*/ -export default { - props: { - groupData: { - type: Object, - required: true, - default: {}, - }, - }, - components: { - 'monitoring-row': MonitoringRow - } -}; -</script> -<template> - <div class="row"> - <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" - :rowData="row" - :key="index" - /> - </div> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_row.vue b/app/assets/javascripts/monitoring/components/monitoring_row.vue index db11d756b60..bd72e3fb3a3 100644 --- a/app/assets/javascripts/monitoring/components/monitoring_row.vue +++ b/app/assets/javascripts/monitoring/components/monitoring_row.vue @@ -8,13 +8,18 @@ required: true, default: () => [], }, + updateAspectRatio: { + type: Boolean, + required: true, + default: false, + }, }, components: { 'monitoring-column': MonitoringColumn, }, methods: { bootstrapClass() { - return this.rowData.length > 3 ? 'col-md-6' : 'col-md-12'; + return this.rowData.length >= 3 ? 'col-md-6' : 'col-md-12'; }, }, }; @@ -25,7 +30,8 @@ v-for="(column, index) in rowData" :columnData="column" :classType="bootstrapClass()" - :key="index" + :key="index" + :updateAspectRatio="updateAspectRatio" /> </div> </template> 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/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 6fc83d76b0c..880c6de2a99 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -7,8 +7,9 @@ import VueResource from 'vue-resource'; import d3 from 'd3'; import MonitoringService from './services/monitoring_service'; import MonitoringState from './components/monitoring_state.vue'; -import MonitoringPanel from './components/monitoring_panel.vue'; +import MonitoringRow from './components/monitoring_row.vue'; import MonitoringStore from './stores/monitoring_store'; +import eventHub from './event_hub'; import MonitoringNewAPIResponse from './monitoring_mock'; Vue.use(VueResource); @@ -37,12 +38,14 @@ document.addEventListener('DOMContentLoaded', function onLoad() { backOffRequestCounter: 0, bisectDate: d3.bisector(d => d.time).left, service: {}, + updateAspectRatio: false, + updatedAspectRatios: 0, }; }, components: { 'monitoring-state': MonitoringState, - 'monitoring-panel': MonitoringPanel, + 'monitoring-row': MonitoringRow, }, methods: { @@ -89,13 +92,31 @@ document.addEventListener('DOMContentLoaded', function onLoad() { this.showEmptyState = false; this.store.storeMetrics(MonitoringNewAPIResponse); }, - increaseValue() { - this.testValue = this.testValue += 1; + resizeThrottler() { + // ignore resize events as long as an actualResizeHandler execution is in the queue + if (!this.resizeTimeout) { + this.resizeTimeout = setTimeout(() => { + this.resizeTimeout = null; + this.updateAspectRatio = true; + }, 600); + } + }, + toggleAspectRatio() { + this.updatedAspectRatios = this.updatedAspectRatios += 1; + if (this.store.getMetricsCount() === this.updatedAspectRatios) { + this.updateAspectRatio = !this.updateAspectRatio; + this.updatedAspectRatios = 0; + } }, }, created() { this.service = new MonitoringService(); + eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + }, + + beforeDestroyed() { + eventHub.$off('toggleAspectRatio'); }, mounted() { @@ -106,19 +127,31 @@ document.addEventListener('DOMContentLoaded', function onLoad() { } else { // this.getGraphsData(); this.getGraphsDataNewApiMock(); + window.addEventListener('resize', this.resizeThrottler, false); } }, template: ` <div class="prometheus-graphs" v-if="!showEmptyState"> - <button class="btn btn-primary" @click=increaseValue> - Increase test value - </button> - <monitoring-panel - v-for="(group, index) in store.groups" - :groupData="group" - :key="index" - /> + <div class="row" + v-for="(groupData, index) in store.groups" + > + <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" + :rowData="row" + :key="index" + :updateAspectRatio="updateAspectRatio" + /> + </div> + </div> + </div> + </div> </div> <monitoring-state :isLoading=isLoading @@ -128,4 +161,3 @@ document.addEventListener('DOMContentLoaded', function onLoad() { `, }); }, false); - diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 53cca6ee9a3..0137a1c803a 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -2,9 +2,11 @@ import _ from 'underscore'; class MonitoringStore { constructor() { - this.groups = []; - this.enoughMetrics = true; - return this; + if (!MonitoringStore.singleton) { + this.groups = []; + this.enoughMetrics = true; + } + return MonitoringStore.singleton; } // TODO: Probably move this to an utility class @@ -27,7 +29,7 @@ class MonitoringStore { } storeMetrics(groups = []) { - // we're going to have to sort the groups depending on the weight of each of the graphs + // TODO: Sorted by weight add the name as another modifier this.groups = groups.map((group) => { const currentGroup = group; currentGroup.metrics = _.sortBy(group.metrics, 'weight'); @@ -35,6 +37,16 @@ class MonitoringStore { return currentGroup; }); } + + 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/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 11f034989eb..f9310bca6e3 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -232,27 +232,23 @@ } .prometheus-svg-container { - @media(max-width: $screen-sm-max) { - display: inline-block; - position: relative; - width: 100%; - padding-bottom: 60%; - vertical-align: top; - overflow: hidden; - } + position: relative; + height: 0; + width: 100%; + padding: 0; + padding-bottom: 100%; .text-metric-bold { font-weight: 600; } } -.svg-content { - @media(max-width: $screen-sm-max) { - display: inline-block; - position: absolute; - top: 0; - left: 0; - } +.prometheus-svg-container > svg { + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; text { fill: $gl-text-color; |