diff options
author | Jose Vargas <jvargas@gitlab.com> | 2019-08-28 17:18:03 -0500 |
---|---|---|
committer | Jose Vargas <jvargas@gitlab.com> | 2019-09-09 12:15:55 -0500 |
commit | f21258035f3bdbdab002f4701ff0577a180dad56 (patch) | |
tree | baed169a57403dce168c0bd4151ef3f180b70d86 | |
parent | fd515cca50a35f7f39d8dd134e69c1b275ba632f (diff) | |
download | gitlab-ce-jivanvl-add-support-heatmap-charts.tar.gz |
Add heatmap chart supportjivanvl-add-support-heatmap-charts
This adds the support to add heatmap charts to the monitoring
dashboard, via the use of the custom dashboards feature
5 files changed, 280 insertions, 0 deletions
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue new file mode 100644 index 00000000000..c5d200c7488 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue @@ -0,0 +1,114 @@ +<script> +import { GlHeatmap } from '@gitlab/ui/dist/charts'; +import dateformat from 'dateformat'; +import { debounceByAnimationFrame } from '~/lib/utils/common_utils'; +import { chartHeight } from '../../constants'; +import { graphDataValidatorForValues } from '../../utils'; + +export default { + components: { + GlHeatmap, + }, + props: { + graphData: { + type: Object, + required: true, + validator: graphDataValidatorForValues.bind(null, false), + }, + containerWidth: { + type: Number, + required: true, + }, + }, + data() { + return { + debouncedResize: {}, + height: chartHeight, + width: 0, + }; + }, + computed: { + chartData() { + const [queries] = this.graphData.queries; + const yDim = queries.result[0].values.length; + const data = []; + for (let j = 0; j < yDim; j += 1) { + for (let i = 0; i < queries.result.length; i += 1) { + const value = queries.result[i].values[j]; + + data.push([i, j, value[1]]); + } + } + + return data; + }, + xAxisName() { + const xLabel = this.graphData.x_label; + + return xLabel != null ? xLabel : ''; + }, + yAxisName() { + const yLabel = this.graphData.y_label; + + return yLabel != null ? yLabel : ''; + }, + xAxisLabels() { + const [queries] = this.graphData.queries; + const axisLabels = queries.result.reduce((acc, res) => { + const [keyMetric] = Object.keys(res.metric); + const keyValue = res.metric[keyMetric]; + + return acc.concat(keyValue); + }, []); + + return axisLabels; + }, + yAxisLabels() { + const [queries] = this.graphData.queries; + const axisLabels = queries.result[0].values.reduce((acc, val) => { + const [yLabel] = val; + const convertedDate = new Date(yLabel); + + return acc.concat(dateformat(convertedDate, 'HH:MM:ss')); + }, []); + + return axisLabels; + }, + }, + watch: { + containerWidth: 'onResize', + }, + beforeDestroy() { + window.removeEventListener('resize', this.debouncedResize); + }, + created() { + this.debouncedResize = debounceByAnimationFrame(this.onResize); + window.addEventListener('resize', this.debouncedResize); + }, + methods: { + onResize() { + if (!this.$refs.heatmap) return; + const { width } = this.$refs.heatmap.$el.getBoundingClientRect(); + this.width = width; + }, + }, +}; +</script> +<template> + <div class="prometheus-graph col-12 col-lg-6"> + <div class="prometheus-graph-header"> + <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5> + </div> + <gl-heatmap + ref="heatmapChart" + v-bind="$attrs" + :data-series="chartData" + :x-axis-name="xAxisName" + :y-axis-name="yAxisName" + :x-axis-labels="xAxisLabels" + :y-axis-labels="yAxisLabels" + :height="height" + :width="width" + /> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 73ff651d510..0a783bd5290 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -12,12 +12,14 @@ import { import Icon from '~/vue_shared/components/icon.vue'; import MonitorTimeSeriesChart from './charts/time_series.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; +import MonitorHeatmapChart from './charts/heatmap.vue'; import MonitorEmptyChart from './charts/empty_chart.vue'; export default { components: { MonitorSingleStatChart, MonitorTimeSeriesChart, + MonitorHeatmapChart, MonitorEmptyChart, Icon, GlDropdown, @@ -92,6 +94,11 @@ export default { v-if="isPanelType('single-stat') && graphDataHasMetrics" :graph-data="graphData" /> + <monitor-heatmap-chart + v-else-if="isPanelType('heatmap') && graphDataHasMetrics" + :graph-data="graphData" + :container-width="dashboardWidth" + /> <monitor-time-series-chart v-else-if="graphDataHasMetrics" :graph-data="graphData" diff --git a/changelogs/unreleased/jivanvl-add-support-heatmap-charts.yml b/changelogs/unreleased/jivanvl-add-support-heatmap-charts.yml new file mode 100644 index 00000000000..e9a13868559 --- /dev/null +++ b/changelogs/unreleased/jivanvl-add-support-heatmap-charts.yml @@ -0,0 +1,5 @@ +--- +title: Add heatmap chart support +merge_request: 32424 +author: +type: added diff --git a/spec/javascripts/monitoring/charts/heatmap_spec.js b/spec/javascripts/monitoring/charts/heatmap_spec.js new file mode 100644 index 00000000000..3cfaaf8e5ec --- /dev/null +++ b/spec/javascripts/monitoring/charts/heatmap_spec.js @@ -0,0 +1,75 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlHeatmap } from '@gitlab/ui/dist/charts'; +import Heatmap from '~/monitoring/components/charts/heatmap.vue'; +import { graphDataPrometheusQueryRangeMultiTrack } from '../mock_data'; + +describe('Heatmap component', () => { + let heatmapChart; + let store; + + beforeEach(() => { + heatmapChart = shallowMount(Heatmap, { + propsData: { + graphData: graphDataPrometheusQueryRangeMultiTrack, + containerWidth: 100, + }, + store, + }); + }); + + afterEach(() => { + heatmapChart.destroy(); + }); + + describe('wrapped components', () => { + describe('GitLab UI heatmap chart', () => { + let glHeatmapChart; + + beforeEach(() => { + glHeatmapChart = heatmapChart.find(GlHeatmap); + }); + + it('is a Vue instance', () => { + expect(glHeatmapChart.isVueInstance()).toBe(true); + }); + + it('should display a label on the x axis', () => { + expect(heatmapChart.vm.xAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.x_label); + }); + + it('should display a label on the y axis', () => { + expect(heatmapChart.vm.yAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.y_label); + }); + + // According to the echarts docs https://echarts.apache.org/en/option.html#series-heatmap.data + // each row of the heatmap chart is represented by an array inside another parent array + // e.g. [[0, 0, 10]], the format represents the column, the row and finally the value + // corresponding to the cell + + it('should return chartData with a length of x by y, with a length of 3 per array', () => { + const row = heatmapChart.vm.chartData[0]; + const queryResult = graphDataPrometheusQueryRangeMultiTrack.queries[0].result; + const xDim = queryResult.length; + const yDim = queryResult[0].values.length; + const totalLengthArray = xDim * yDim; + + expect(row.length).toBe(3); + expect(heatmapChart.vm.chartData.length).toBe(totalLengthArray); + }); + + it('returns a series of labels for the x axis', () => { + const { xAxisLabels } = heatmapChart.vm; + const queryResult = graphDataPrometheusQueryRangeMultiTrack.queries[0].result; + + expect(xAxisLabels.length).toBe(queryResult.length); + }); + + it('returns a series of labels for the y axis', () => { + const { yAxisLabels } = heatmapChart.vm; + const queryResult = graphDataPrometheusQueryRangeMultiTrack.queries[0].result; + + expect(yAxisLabels.length).toBe(queryResult[0].values.length); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 17e7314e214..175eb6365aa 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -1009,3 +1009,82 @@ export const graphDataPrometheusQueryRange = { }, ], }; + +export const graphDataPrometheusQueryRangeMultiTrack = { + title: 'Super Chart A3', + type: 'heatmap', + weight: 3, + x_label: 'Status Code', + y_label: 'Time', + metrics: [], + queries: [ + { + metricId: '1', + id: 'response_metrics_nginx_ingress_throughput_status_code', + query_range: + 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)', + unit: 'req / sec', + label: 'Status Code', + metric_id: 1, + prometheus_endpoint_path: + '/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29', + result: [ + { + metric: { status_code: '1xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 0], + ['2019-08-30T16:00:00.000Z', 2], + ['2019-08-30T17:00:00.000Z', 0], + ['2019-08-30T18:00:00.000Z', 0], + ['2019-08-30T19:00:00.000Z', 0], + ['2019-08-30T20:00:00.000Z', 3], + ], + }, + { + metric: { status_code: '2xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 1], + ['2019-08-30T16:00:00.000Z', 3], + ['2019-08-30T17:00:00.000Z', 6], + ['2019-08-30T18:00:00.000Z', 10], + ['2019-08-30T19:00:00.000Z', 8], + ['2019-08-30T20:00:00.000Z', 6], + ], + }, + { + metric: { status_code: '3xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 1], + ['2019-08-30T16:00:00.000Z', 2], + ['2019-08-30T17:00:00.000Z', 3], + ['2019-08-30T18:00:00.000Z', 3], + ['2019-08-30T19:00:00.000Z', 2], + ['2019-08-30T20:00:00.000Z', 1], + ], + }, + { + metric: { status_code: '4xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 2], + ['2019-08-30T16:00:00.000Z', 0], + ['2019-08-30T17:00:00.000Z', 0], + ['2019-08-30T18:00:00.000Z', 2], + ['2019-08-30T19:00:00.000Z', 0], + ['2019-08-30T20:00:00.000Z', 2], + ], + }, + { + metric: { status_code: '5xx' }, + values: [ + ['2019-08-30T15:00:00.000Z', 0], + ['2019-08-30T16:00:00.000Z', 1], + ['2019-08-30T17:00:00.000Z', 0], + ['2019-08-30T18:00:00.000Z', 0], + ['2019-08-30T19:00:00.000Z', 0], + ['2019-08-30T20:00:00.000Z', 2], + ], + }, + ], + }, + ], +}; |