diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 2e5f8a09fc1ba8642957d75feead5ee499379c09..2b135b4f96fc60707fba6a3626edf44052ed3215 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -188,5 +188,29 @@ require('vendor/latinise'); gl.text.slugify = function(str) { return str.trim().toLowerCase().latinise(); }; + gl.text.formatRelevantDigits = function(number) { + var digitsLeft = ''; + var relevantDigits = 0; + if (isNaN(Number(number))) { + return 0; + } else { + digitsLeft = number.split('.')[0]; + switch (digitsLeft.length) { + case 1: + relevantDigits = 3; + break; + case 2: + relevantDigits = 2; + break; + case 3: + relevantDigits = 1; + break; + default: + relevantDigits = 4; + break; + } + return Number(number).toFixed(relevantDigits); + } + }; })(window); }).call(window); diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index 844a0785bc9a20057830d97ea0ac1103e304a85e..eb9e93dba0985d75dc31decd8e3b1a1de0d144f4 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable no-new*/ +/* eslint-disable no-new */ /* global Flash */ import d3 from 'd3'; @@ -21,19 +21,19 @@ class PrometheusGraph { const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + extraAddedWidthParent; this.originalWidth = parentContainerWidth; - this.originalHeight = 400; + this.originalHeight = 330; this.width = parentContainerWidth - this.margin.left - this.margin.right; - this.height = 400 - this.margin.top - this.margin.bottom; + this.height = this.originalHeight - this.margin.top - this.margin.bottom; this.backOffRequestCounter = 0; this.configureGraph(); this.init(); } createGraph() { - Object.keys(this.data).forEach((key) => { - const value = this.data[key]; - if (value.length > 0) { - this.plotValues(value, key); + Object.keys(this.graphSpecificProperties).forEach((key) => { + const value = this.graphSpecificProperties[key]; + if (value.data.length > 0) { + this.plotValues(key); } }); } @@ -49,13 +49,16 @@ class PrometheusGraph { }); } - plotValues(valuesToPlot, key) { + plotValues(key) { const x = d3.time.scale() .range([0, this.width]); const y = d3.scale.linear() .range([this.height, 0]); + this.graphSpecificProperties[key].xScale = x; + this.graphSpecificProperties[key].yScale = y; + const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; const graphSpecifics = this.graphSpecificProperties[key]; @@ -67,13 +70,13 @@ class PrometheusGraph { .attr('transform', `translate(${this.margin.left},${this.margin.top})`); const axisLabelContainer = d3.select(prometheusGraphContainer) - .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right) - .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top) + .attr('width', this.originalWidth) + .attr('height', this.originalHeight) .append('g') .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); - x.domain(d3.extent(valuesToPlot, d => d.time)); - y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]); + 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) @@ -108,13 +111,13 @@ class PrometheusGraph { .y(d => y(d.value)); chart.append('path') - .datum(valuesToPlot) + .datum(graphSpecifics.data) .attr('d', area) .attr('class', 'metric-area') .attr('fill', graphSpecifics.area_fill_color); chart.append('path') - .datum(valuesToPlot) + .datum(graphSpecifics.data) .attr('class', 'metric-line') .attr('stroke', graphSpecifics.line_color) .attr('fill', 'none') @@ -126,7 +129,7 @@ class PrometheusGraph { .attr('class', 'prometheus-graph-overlay') .attr('width', this.width) .attr('height', this.height) - .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key)); + .on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer)); } // The legends from the metric @@ -138,10 +141,10 @@ class PrometheusGraph { .attr('stroke', '#000000') .attr('stroke-width', '1') .attr({ - x1: 0, - y1: this.originalHeight - this.marginLabelContainer.top, - x2: this.originalWidth - this.margin.right, - y2: this.originalHeight - this.marginLabelContainer.top, + x1: 10, + y1: this.originalHeight - 80, + x2: (this.originalWidth - this.margin.right) + 10, + y2: this.originalHeight - 80, }); axisLabelContainer.append('line') @@ -149,29 +152,35 @@ class PrometheusGraph { .attr('stroke', '#000000') .attr('stroke-width', '1') .attr({ - x1: 0, + x1: 10, y1: 0, - x2: 0, - y2: this.originalHeight - this.marginLabelContainer.top, + x2: 10, + y2: this.originalHeight - 80, }); + 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.marginLabelContainer.top) / 2}) rotate(-90)`) + .attr('transform', `translate(15, ${(this.originalHeight - 80) / 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 - this.marginLabelContainer.top - 20) + .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.marginLabelContainer.top) + .attr('y', this.originalHeight - 80) .attr('dy', '.35em') .text('Time'); @@ -186,7 +195,7 @@ class PrometheusGraph { .attr('height', 35); axisLabelContainer.append('text') - .attr('class', 'label-axis-text') + .attr('class', 'text-metric-title') .attr('x', this.originalWidth - 140) .attr('y', (this.originalHeight / 2) - 50) .text('Average'); @@ -197,65 +206,79 @@ class PrometheusGraph { .attr('y', (this.originalHeight / 2) - 25); } - handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) { + handleMouseOverGraph(prometheusGraphContainer) { const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); - const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]); - const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1); - const d0 = valuesToPlot[timeValueIndex - 1]; - const d1 = valuesToPlot[timeValueIndex]; - const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0; - const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value))); - const currentTimeCoordinate = x(currentData.time); - const graphSpecifics = this.graphSpecificProperties[key]; - // Remove the current selectors - d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove(); - d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove(); - d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove(); - d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove(); - - chart.append('line') - .attr('class', 'selected-metric-line') - .attr({ - x1: currentTimeCoordinate, - y1: y(0), - x2: currentTimeCoordinate, - y2: maxValueMetric, + 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 = currentGraphProps.xScale(currentData.time); + 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`).remove(); + d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove(); + + const currentChart = d3.select(currentPrometheusGraphContainer).select('g'); + currentChart.append('line') + .attr('class', 'selected-metric-line') + .attr({ + x1: currentTimeCoordinate, + y1: currentGraphProps.yScale(0), + x2: currentTimeCoordinate, + y2: maxMetricValue, + }); + + currentChart.append('circle') + .attr('class', 'circle-metric') + .attr('fill', currentGraphProps.line_color) + .attr('cx', currentTimeCoordinate) + .attr('cy', currentGraphProps.yScale(currentData.value)) + .attr('r', this.commonGraphProperties.circle_radius_metric); + + // The little box with text + const rectTextMetric = currentChart.append('g') + .attr('class', 'rect-text-metric') + .attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`); + + rectTextMetric.append('rect') + .attr('class', 'rect-metric') + .attr('x', currentTimeCoordinate + 10) + .attr('y', maxMetricValue) + .attr('width', this.commonGraphProperties.rect_text_width) + .attr('height', this.commonGraphProperties.rect_text_height); + + rectTextMetric.append('text') + .attr('class', 'text-metric') + .attr('x', currentTimeCoordinate + 35) + .attr('y', maxMetricValue + 35) + .text(timeFormat(currentData.time)); + + rectTextMetric.append('text') + .attr('class', 'text-metric-date') + .attr('x', currentTimeCoordinate + 15) + .attr('y', maxMetricValue + 15) + .text(dayFormat(currentData.time)); + + let currentMetricValue = gl.text.formatRelevantDigits(currentData.value); + if (key === 'cpu_values') { + currentMetricValue = `${currentMetricValue}%`; + } else { + currentMetricValue = `${currentMetricValue} MB`; + } + d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`) + .text(currentMetricValue); }); - - chart.append('circle') - .attr('class', 'circle-metric') - .attr('fill', graphSpecifics.line_color) - .attr('cx', currentTimeCoordinate) - .attr('cy', y(currentData.value)) - .attr('r', this.commonGraphProperties.circle_radius_metric); - - // The little box with text - const rectTextMetric = chart.append('g') - .attr('class', 'rect-text-metric') - .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`); - - rectTextMetric.append('rect') - .attr('class', 'rect-metric') - .attr('x', currentTimeCoordinate + 10) - .attr('y', maxValueMetric) - .attr('width', this.commonGraphProperties.rect_text_width) - .attr('height', this.commonGraphProperties.rect_text_height); - - rectTextMetric.append('text') - .attr('class', 'text-metric') - .attr('x', currentTimeCoordinate + 35) - .attr('y', maxValueMetric + 35) - .text(timeFormat(currentData.time)); - - rectTextMetric.append('text') - .attr('class', 'text-metric-date') - .attr('x', currentTimeCoordinate + 15) - .attr('y', maxValueMetric + 15) - .text(dayFormat(currentData.time)); - - // Update the text - d3.select(`${prometheusGraphContainer} .text-metric-usage`) - .text(currentData.value.substring(0, 8)); } configureGraph() { @@ -263,12 +286,18 @@ class PrometheusGraph { cpu_values: { area_fill_color: '#edf3fc', line_color: '#5b99f7', - graph_legend_title: 'CPU utilization (%)', + graph_legend_title: 'CPU Usage (Cores)', + data: [], + xScale: {}, + yScale: {}, }, memory_values: { area_fill_color: '#fca326', line_color: '#fc6d26', - graph_legend_title: 'Memory usage (MB)', + graph_legend_title: 'Memory Usage (MB)', + data: [], + xScale: {}, + yScale: {}, }, }; @@ -318,17 +347,17 @@ class PrometheusGraph { } transformData(metricsResponse) { - const metricTypes = {}; Object.keys(metricsResponse.metrics).forEach((key) => { if (key === 'cpu_values' || key === 'memory_values') { const metricValues = (metricsResponse.metrics[key])[0]; - metricTypes[key] = metricValues.values.map(metric => ({ - time: new Date(metric[0] * 1000), - value: metric[1], - })); + if (typeof metricValues !== 'undefined') { + this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ + time: new Date(metric[0] * 1000), + value: metric[1], + })); + } } }); - this.data = metricTypes; } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 501db7f390bbf4153e569365b86248a5abaff81e..9eb005b75a726d8f597a0406cbf557e6e4d7987b 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -292,6 +292,16 @@ text { fill: $stat-graph-axis-fill; } + + .label-axis-text, + .text-metric-usage { + fill: $black; + font-weight: 500; + } + + .legend-axis-text { + fill: $black; + } } .x-axis path, diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml index bf0f181907339e129ece9321181a9420b209e71f..a82ef5ee5bbe2d37bfdf869e28ef5235858ddad6 100644 --- a/app/views/projects/environments/_external_url.html.haml +++ b/app/views/projects/environments/_external_url.html.haml @@ -1,3 +1,4 @@ - if environment.external_url && can?(current_user, :read_environment, environment) = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do = icon('external-link') + View deployment diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml index acbac1869fdb7fee2e1f7ffd6c7ac3f4ea0e48fe..e27281d69178a3db2f379d9eda33f1a9531de096 100644 --- a/app/views/projects/environments/_metrics_button.html.haml +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -4,3 +4,4 @@ = link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do = icon('area-chart') + Monitoring diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 3b45162df52e351cc2b2d7b97ac53cb0571f97ed..5627192c36cbf92d65f57ee4725eb3906d9cba71 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -11,7 +11,8 @@ .col-sm-6 %h3.page-title Environment: - = @environment.name + = link_to environment_path(@environment) do + = @environment.name .col-sm-6 .nav-controls diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index f463a429f65943a609f29396aab74b14dd0cd0b4..ff6aaebda22e30e904e64132f99b18460d9e022a 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -4,9 +4,9 @@ %div{ class: container_class } .top-area.adjust - .col-md-9 + .col-md-7 %h3.page-title= @environment.name - .col-md-3 + .col-md-5 .nav-controls = render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment diff --git a/changelogs/unreleased-ee/environment-performance-improvements-ee.yml b/changelogs/unreleased-ee/environment-performance-improvements-ee.yml new file mode 100644 index 0000000000000000000000000000000000000000..14c8ad114acbef54d9934704e58a3556b4dad988 --- /dev/null +++ b/changelogs/unreleased-ee/environment-performance-improvements-ee.yml @@ -0,0 +1,4 @@ +--- +title: Improved UX for the environments metrics view +merge_request: 1503 +author: diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 06b69b8ac1750e850a2e24077700c8445f6557ef..6177b84f307b576732fe850e06d4734b6e7de139 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -46,5 +46,30 @@ require('~/lib/utils/text_utility'); expect(gl.text.highCountTrim(45)).toBe(45); }); }); + describe('gl.text.formatRelevantDigits', () => { + it('returns 0 when the number is NaN', () => { + expect(gl.text.formatRelevantDigits('fail')).toBe(0); + }); + + it('returns 4 decimals when there is 4 plus digits to the left', () => { + const formattedNumber = gl.text.formatRelevantDigits('1000.1234567').split('.')[1]; + expect(formattedNumber.length).toBe(4); + }); + + it('returns 3 decimals when there is 1 digit to the left', () => { + const formattedNumber = gl.text.formatRelevantDigits('0.1234567').split('.')[1]; + expect(formattedNumber.length).toBe(3); + }); + + it('returns 2 decimals when there is 2 digits to the left', () => { + const formattedNumber = gl.text.formatRelevantDigits('10.1234567').split('.')[1]; + expect(formattedNumber.length).toBe(2); + }); + + it('returns 1 decimal when there is 3 digits to the left', () => { + const formattedNumber = gl.text.formatRelevantDigits('100.1234567').split('.')[1]; + expect(formattedNumber.length).toBe(1); + }); + }); }); })(); diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index a3c1c5e1b7c1c7a1926567fa3d762d8fd4483eb7..c2bcd9c0f7cf505e880a194f1b7ed70ec90d1c81 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -37,9 +37,11 @@ describe('PrometheusGraph', () => { it('transforms the data', () => { this.prometheusGraph.init(prometheusMockData.metrics); - expect(this.prometheusGraph.data).toBeDefined(); - expect(this.prometheusGraph.data.cpu_values.length).toBe(121); - expect(this.prometheusGraph.data.memory_values.length).toBe(121); + Object.keys(this.prometheusGraph.graphSpecificProperties, (key) => { + const graphProps = this.prometheusGraph.graphSpecificProperties[key]; + expect(graphProps.data).toBeDefined(); + expect(graphProps.data.length).toBe(121); + }); }); it('creates two graphs', () => { @@ -68,7 +70,7 @@ describe('PrometheusGraph', () => { expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined(); expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined(); expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined(); - expect($axisLabelContainer.find('rect').length).toBe(2); + expect($axisLabelContainer.find('rect').length).toBe(3); expect($axisLabelContainer.find('text').length).toBe(4); }); });