Commit fcc2b0ea authored by Jose Ivan Vargas's avatar Jose Ivan Vargas Committed by Fatih Acet

Add heatmap chart support

This adds the support to add heatmap charts to the monitoring
dashboard, via the use of the custom dashboards feature
parent eadb7263
<script>
import { GlHeatmap } from '@gitlab/ui/dist/charts';
import dateformat from 'dateformat';
import PrometheusHeader from '../shared/prometheus_header.vue';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
import { graphDataValidatorForValues } from '../../utils';
export default {
components: {
GlHeatmap,
ResizableChartContainer,
PrometheusHeader,
},
props: {
graphData: {
type: Object,
required: true,
validator: graphDataValidatorForValues.bind(null, false),
},
containerWidth: {
type: Number,
required: true,
},
},
computed: {
chartData() {
return this.queries.result.reduce(
(acc, result, i) => [...acc, ...result.values.map((value, j) => [i, j, value[1]])],
[],
);
},
xAxisName() {
return this.graphData.x_label || '';
},
yAxisName() {
return this.graphData.y_label || '';
},
xAxisLabels() {
return this.queries.result.map(res => Object.values(res.metric)[0]);
},
yAxisLabels() {
return this.result.values.map(val => {
const [yLabel] = val;
return dateformat(new Date(yLabel), 'HH:MM:ss');
});
},
result() {
return this.queries.result[0];
},
queries() {
return this.graphData.queries[0];
},
},
};
</script>
<template>
<div class="prometheus-graph col-12 col-lg-6">
<prometheus-header :graph-title="graphData.title" />
<resizable-chart-container>
<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"
:width="containerWidth"
/>
</resizable-chart-container>
</div>
</template>
...@@ -13,6 +13,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -13,6 +13,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue'; import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorAnomalyChart from './charts/anomaly.vue'; import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorSingleStatChart from './charts/single_stat.vue'; import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorHeatmapChart from './charts/heatmap.vue';
import MonitorEmptyChart from './charts/empty_chart.vue'; import MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import { downloadCSVOptions, generateLinkToChartOptions } from '../utils'; import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
...@@ -20,6 +21,7 @@ import { downloadCSVOptions, generateLinkToChartOptions } from '../utils'; ...@@ -20,6 +21,7 @@ import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
export default { export default {
components: { components: {
MonitorSingleStatChart, MonitorSingleStatChart,
MonitorHeatmapChart,
MonitorEmptyChart, MonitorEmptyChart,
Icon, Icon,
GlDropdown, GlDropdown,
...@@ -99,6 +101,11 @@ export default { ...@@ -99,6 +101,11 @@ export default {
v-if="isPanelType('single-stat') && graphDataHasMetrics" v-if="isPanelType('single-stat') && graphDataHasMetrics"
:graph-data="graphData" :graph-data="graphData"
/> />
<monitor-heatmap-chart
v-else-if="isPanelType('heatmap') && graphDataHasMetrics"
:graph-data="graphData"
:container-width="dashboardWidth"
/>
<component <component
:is="monitorChartComponent" :is="monitorChartComponent"
v-else-if="graphDataHasMetrics" v-else-if="graphDataHasMetrics"
......
<script>
export default {
props: {
graphTitle: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="prometheus-graph-header">
<h5 class="prometheus-graph-title js-graph-title">{{ graphTitle }}</h5>
</div>
</template>
---
title: Add heatmap chart support
merge_request: 32424
author:
type: added
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];
expect(row.length).toBe(3);
expect(heatmapChart.vm.chartData.length).toBe(30);
});
it('returns a series of labels for the x axis', () => {
const { xAxisLabels } = heatmapChart.vm;
expect(xAxisLabels.length).toBe(5);
});
it('returns a series of labels for the y axis', () => {
const { yAxisLabels } = heatmapChart.vm;
expect(yAxisLabels.length).toBe(6);
});
});
});
});
...@@ -1013,3 +1013,82 @@ export const graphDataPrometheusQueryRange = { ...@@ -1013,3 +1013,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],
],
},
],
},
],
};
import { shallowMount } from '@vue/test-utils';
import PrometheusHeader from '~/monitoring/components/shared/prometheus_header.vue';
describe('Prometheus Header component', () => {
let prometheusHeader;
beforeEach(() => {
prometheusHeader = shallowMount(PrometheusHeader, {
propsData: {
graphTitle: 'graph header',
},
});
});
afterEach(() => {
prometheusHeader.destroy();
});
describe('Prometheus header component', () => {
it('should show a title', () => {
const title = prometheusHeader.vm.$el.querySelector('.js-graph-title').textContent;
expect(title).toBe('graph header');
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment