diff options
author | Phil Hughes <me@iamphill.com> | 2017-09-05 11:02:53 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2017-09-05 11:02:53 +0100 |
commit | f041c3896fa4c7d6f6f57e5c661b1c4b8a8e2654 (patch) | |
tree | f69843f44a73fe4876e623e5b3026a6eb289b41b /app/assets | |
parent | a27c0013009ee36bc796a6b12c36b6320d05cce2 (diff) | |
parent | 6523ba1d5a0d73692eeeb675e17e7a05fa5a0e9f (diff) | |
download | gitlab-ce-f041c3896fa4c7d6f6f57e5c661b1c4b8a8e2654.tar.gz |
Merge branch 'master' into breadcrumbs-improvements
Diffstat (limited to 'app/assets')
10 files changed, 287 insertions, 152 deletions
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 6f6da9e1463..9c785f4ada8 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -3,11 +3,12 @@ import GraphLegend from './graph/legend.vue'; import GraphFlag from './graph/flag.vue'; import GraphDeployment from './graph/deployment.vue'; + import monitoringPaths from './monitoring_paths.vue'; import MonitoringMixin from '../mixins/monitoring_mixins'; import eventHub from '../event_hub'; import measurements from '../utils/measurements'; - import { formatRelevantDigits } from '../../lib/utils/number_utils'; import { timeScaleFormat } from '../utils/date_time_formatters'; + import createTimeSeries from '../utils/multiple_time_series'; import bp from '../../breakpoints'; const bisectDate = d3.bisector(d => d.time).left; @@ -36,32 +37,29 @@ data() { return { + baseGraphHeight: 450, + baseGraphWidth: 600, graphHeight: 450, graphWidth: 600, graphHeightOffset: 120, - xScale: {}, - yScale: {}, margin: {}, - data: [], unitOfDisplay: '', areaColorRgb: '#8fbce8', lineColorRgb: '#1f78d1', yAxisLabel: '', legendTitle: '', reducedDeploymentData: [], - area: '', - line: '', measurements: measurements.large, currentData: { time: new Date(), value: 0, }, - currentYCoordinate: 0, + currentDataIndex: 0, currentXCoordinate: 0, currentFlagPosition: 0, - metricUsage: '', showFlag: false, showDeployInfo: true, + timeSeries: [], }; }, @@ -69,16 +67,17 @@ GraphLegend, GraphFlag, GraphDeployment, + monitoringPaths, }, computed: { outterViewBox() { - return `0 0 ${this.graphWidth} ${this.graphHeight}`; + return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; }, innerViewBox() { - if ((this.graphWidth - 150) > 0) { - return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`; + if ((this.baseGraphWidth - 150) > 0) { + return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; } return '0 0 0 0'; }, @@ -89,7 +88,7 @@ paddingBottomRootSvg() { return { - paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`, + paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`, }; }, }, @@ -104,17 +103,16 @@ this.margin = measurements.small.margin; this.measurements = measurements.small; } - this.data = query.result[0].values; this.unitOfDisplay = query.unit || ''; this.yAxisLabel = this.graphData.y_label || 'Values'; this.legendTitle = query.label || '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(); - } + this.baseGraphHeight = this.graphHeight; + this.baseGraphWidth = this.graphWidth; + this.renderAxesPaths(); + this.formatDeployments(); }, handleMouseOverGraph(e) { @@ -123,16 +121,17 @@ 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]; + const firstTimeSeries = this.timeSeries[0]; + const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); + const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); + const d0 = firstTimeSeries.values[overlayIndex - 1]; + const d1 = firstTimeSeries.values[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)); + this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1); + this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(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; @@ -145,17 +144,25 @@ } else { this.showFlag = true; } - - this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`; }, renderAxesPaths() { + this.timeSeries = createTimeSeries(this.graphData.queries[0].result, + this.graphWidth, + this.graphHeight, + this.graphHeightOffset); + + if (this.timeSeries.length > 3) { + this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20; + } + const axisXScale = d3.time.scale() .range([0, this.graphWidth]); - this.yScale = d3.scale.linear() + const axisYScale = 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))]); + + axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time)); + axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]); const xAxis = d3.svg.axis() .scale(axisXScale) @@ -164,7 +171,7 @@ .orient('bottom'); const yAxis = d3.svg.axis() - .scale(this.yScale) + .scale(axisYScale) .ticks(measurements.yTicks) .orient('left'); @@ -180,25 +187,6 @@ .attr('class', 'axis-tick'); } // Avoid adding the class to the first tick, to prevent coloring }); // 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); }, }, @@ -245,30 +233,25 @@ :graph-height="graphHeight" :margin="margin" :measurements="measurements" - :area-color-rgb="areaColorRgb" :legend-title="legendTitle" :y-axis-label="yAxisLabel" - :metric-usage="metricUsage" + :time-series="timeSeries" + :unit-of-display="unitOfDisplay" + :current-data-index="currentDataIndex" /> <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> - <graph-deployment + <monitoring-paths + v-for="(path, index) in timeSeries" + :key="index" + :generated-line-path="path.linePath" + :generated-area-path="path.areaPath" + :line-color="path.lineColor" + :area-color="path.areaColor" + /> + <monitoring-deployment :show-deploy-info="showDeployInfo" :deployment-data="reducedDeploymentData" :graph-height="graphHeight" @@ -277,7 +260,6 @@ <graph-flag v-if="showFlag" :current-x-coordinate="currentXCoordinate" - :current-y-coordinate="currentYCoordinate" :current-data="currentData" :current-flag-position="currentFlagPosition" :graph-height="graphHeight" diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index c4d4647d240..a98e3d06c18 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -7,10 +7,6 @@ type: Number, required: true, }, - currentYCoordinate: { - type: Number, - required: true, - }, currentFlagPosition: { type: Number, required: true, @@ -60,16 +56,7 @@ :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 + <svg class="rect-text-metric" :x="currentFlagPosition" y="0"> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index d08f9cbffd4..a43dad8e601 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -1,4 +1,6 @@ <script> + import { formatRelevantDigits } from '../../../lib/utils/number_utils'; + export default { props: { graphWidth: { @@ -17,10 +19,6 @@ type: Object, required: true, }, - areaColorRgb: { - type: String, - required: true, - }, legendTitle: { type: String, required: true, @@ -29,15 +27,25 @@ type: String, required: true, }, - metricUsage: { + timeSeries: { + type: Array, + required: true, + }, + unitOfDisplay: { type: String, required: true, }, + currentDataIndex: { + type: Number, + required: true, + }, }, data() { return { yLabelWidth: 0, yLabelHeight: 0, + seriesXPosition: 0, + metricUsageXPosition: 0, }; }, computed: { @@ -63,10 +71,28 @@ yPosition() { return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0; }, + + }, + methods: { + translateLegendGroup(index) { + return `translate(0, ${12 * (index)})`; + }, + + formatMetricUsage(series) { + return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`; + }, }, mounted() { this.$nextTick(() => { const bbox = this.$refs.ylabel.getBBox(); + this.metricUsageXPosition = 0; + this.seriesXPosition = 0; + if (this.$refs.legendTitleSvg != null) { + this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width; + } + if (this.$refs.seriesTitleSvg != null) { + this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width; + } this.yLabelWidth = bbox.width + 10; // Added some padding this.yLabelHeight = bbox.height + 5; }); @@ -121,24 +147,33 @@ 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 - 25"> - {{legendTitle}} - </text> - <text - class="text-metric-usage" - x="50" - :y="graphHeight - 10"> - {{metricUsage}} - </text> + <g class="legend-group" + v-for="(series, index) in timeSeries" + :key="index" + :transform="translateLegendGroup(index)"> + <rect + :fill="series.areaColor" + :width="measurements.legends.width" + :height="measurements.legends.height" + x="20" + :y="graphHeight - measurements.legendOffset"> + </rect> + <text + v-if="timeSeries.length > 1" + class="legend-metric-title" + ref="legendTitleSvg" + x="38" + :y="graphHeight - 30"> + {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}} + </text> + <text + v-else + class="legend-metric-title" + ref="legendTitleSvg" + x="38" + :y="graphHeight - 30"> + {{legendTitle}} {{formatMetricUsage(series)}} + </text> + </g> </g> </template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_paths.vue b/app/assets/javascripts/monitoring/components/monitoring_paths.vue new file mode 100644 index 00000000000..043f1bf66bb --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_paths.vue @@ -0,0 +1,40 @@ +<script> + export default { + props: { + generatedLinePath: { + type: String, + required: true, + }, + generatedAreaPath: { + type: String, + required: true, + }, + lineColor: { + type: String, + required: true, + }, + areaColor: { + type: String, + required: true, + }, + }, + }; +</script> +<template> + <g> + <path + class="metric-area" + :d="generatedAreaPath" + :fill="areaColor" + transform="translate(-5, 20)"> + </path> + <path + class="metric-line" + :d="generatedLinePath" + :stroke="lineColor" + fill="none" + stroke-width="1" + transform="translate(-5, 20)"> + </path> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index 8e62fa63f13..345a0b37a76 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -21,9 +21,9 @@ const mixins = { formatDeployments() { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { const time = new Date(deployment.created_at); - const xPos = Math.floor(this.xScale(time)); + const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time)); - time.setSeconds(this.data[0].time.getSeconds()); + time.setSeconds(this.timeSeries[0].values[0].time.getSeconds()); if (xPos >= 0) { deploymentDataArray.push({ diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 737c964f12e..0a4cdd88044 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -1,46 +1,52 @@ import _ from 'underscore'; -class MonitoringStore { +function sortMetrics(metrics) { + return _.chain(metrics).sortBy('weight').sortBy('title').value(); +} + +function normalizeMetrics(metrics) { + return metrics.map(metric => ({ + ...metric, + queries: metric.queries.map(query => ({ + ...query, + result: query.result.map(result => ({ + ...result, + values: result.values.map(([timestamp, value]) => ({ + time: new Date(timestamp * 1000), + value, + })), + })), + })), + })); +} + +function collate(array, rows = 2) { + const collatedArray = []; + let row = []; + array.forEach((value, index) => { + row.push(value); + if ((index + 1) % rows === 0) { + collatedArray.push(row); + row = []; + } + }); + if (row.length > 0) { + collatedArray.push(row); + } + return collatedArray; +} + +export default 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; - }); + this.groups = groups.map(group => ({ + ...group, + metrics: collate(normalizeMetrics(sortMetrics(group.metrics))), + })); } storeDeploymentData(deploymentData = []) { @@ -57,5 +63,3 @@ class MonitoringStore { return metricsCount; } } - -export default MonitoringStore; diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js index 62cd19c86e1..ee3c45efacc 100644 --- a/app/assets/javascripts/monitoring/utils/measurements.js +++ b/app/assets/javascripts/monitoring/utils/measurements.js @@ -7,15 +7,15 @@ export default { left: 40, }, legends: { - width: 15, - height: 25, + width: 10, + height: 3, }, backgroundLegend: { width: 30, height: 50, }, axisLabelLineOffset: -20, - legendOffset: 35, + legendOffset: 33, }, large: { // This covers both md and lg screen sizes margin: { @@ -25,15 +25,15 @@ export default { left: 80, }, legends: { - width: 20, - height: 30, + width: 15, + height: 3, }, backgroundLegend: { width: 30, height: 150, }, axisLabelLineOffset: 20, - legendOffset: 38, + legendOffset: 36, }, xTicks: 8, yTicks: 3, diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js new file mode 100644 index 00000000000..05d551e917c --- /dev/null +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -0,0 +1,80 @@ +import d3 from 'd3'; +import _ from 'underscore'; + +export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) { + const maxValues = seriesData.map((timeSeries, index) => { + const maxValue = d3.max(timeSeries.values.map(d => d.value)); + return { + maxValue, + index, + }; + }); + + const maxValueFromSeries = _.max(maxValues, val => val.maxValue); + + let timeSeriesNumber = 1; + let lineColor = '#1f78d1'; + let areaColor = '#8fbce8'; + return seriesData.map((timeSeries) => { + const timeSeriesScaleX = d3.time.scale() + .range([0, graphWidth - 70]); + + const timeSeriesScaleY = d3.scale.linear() + .range([graphHeight - graphHeightOffset, 0]); + + timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time)); + timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); + + const lineFunction = d3.svg.line() + .x(d => timeSeriesScaleX(d.time)) + .y(d => timeSeriesScaleY(d.value)); + + const areaFunction = d3.svg.area() + .x(d => timeSeriesScaleX(d.time)) + .y0(graphHeight - graphHeightOffset) + .y1(d => timeSeriesScaleY(d.value)) + .interpolate('linear'); + + switch (timeSeriesNumber) { + case 1: + lineColor = '#1f78d1'; + areaColor = '#8fbce8'; + break; + case 2: + lineColor = '#fc9403'; + areaColor = '#feca81'; + break; + case 3: + lineColor = '#db3b21'; + areaColor = '#ed9d90'; + break; + case 4: + lineColor = '#1aaa55'; + areaColor = '#8dd5aa'; + break; + case 5: + lineColor = '#6666c4'; + areaColor = '#d1d1f0'; + break; + default: + lineColor = '#1f78d1'; + areaColor = '#8fbce8'; + break; + } + + if (timeSeriesNumber <= 5) { + timeSeriesNumber = timeSeriesNumber += 1; + } else { + timeSeriesNumber = 1; + } + + return { + linePath: lineFunction(timeSeries.values), + areaPath: areaFunction(timeSeries.values), + timeSeriesScaleX, + values: timeSeries.values, + lineColor, + areaColor, + }; + }); +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index c5c890b835f..e4f82fe091a 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -272,6 +272,7 @@ body[data-page="projects:new"] #select2-drop, body[data-page="projects:merge_requests:edit"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop, body[data-page="profiles:show"] #select2-drop, +body[data-page="projects:issues:show"] #select2-drop, body[data-page="projects:blob:edit"] #select2-drop { &.select2-drop { border: 1px solid $dropdown-border-color; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e7c830cbc69..a52ac0d53e7 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -169,7 +169,7 @@ } .metric-area { - opacity: 0.8; + opacity: 0.25; } .prometheus-graph-overlay { @@ -251,8 +251,14 @@ font-weight: $gl-font-weight-bold; } - .label-axis-text, - .text-metric-usage { + .label-axis-text { + fill: $black; + font-weight: $gl-font-weight-normal; + font-size: 10px; + } + + .text-metric-usage, + .legend-metric-title { fill: $black; font-weight: $gl-font-weight-normal; font-size: 12px; |