Commit 2c8dced5 authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla

Add bar chart support to monitoring dashboard

This MR implements the recently added gitlab ui
bar charts to the monitoring dashboard
parent f552c848
<script>
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlBarChart } from '@gitlab/ui/dist/charts';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import { chartHeight } from '../../constants';
import { barChartsDataParser, graphDataValidatorForValues } from '../../utils';
export default {
components: {
GlBarChart,
},
directives: {
GlResizeObserverDirective,
},
props: {
graphData: {
type: Object,
required: true,
validator: graphDataValidatorForValues.bind(null, false),
},
},
data() {
return {
width: 0,
height: chartHeight,
svgs: {},
};
},
computed: {
chartData() {
return barChartsDataParser(this.graphData.metrics);
},
chartOptions() {
return {
dataZoom: [this.dataZoomConfig],
};
},
xAxisTitle() {
const { xLabel = '' } = this.graphData;
return xLabel;
},
yAxisTitle() {
const { y_label = '' } = this.graphData;
return y_label; // eslint-disable-line babel/camelcase
},
xAxisType() {
const { x_type = 'value' } = this.graphData;
return x_type; // eslint-disable-line babel/camelcase
},
dataZoomConfig() {
const handleIcon = this.svgs['scroll-handle'];
return handleIcon ? { handleIcon } : {};
},
},
created() {
this.setSvg('scroll-handle');
},
methods: {
formatLegendLabel(query) {
return `${query.label}`;
},
onResize() {
if (!this.$refs.barChart) return;
const { width } = this.$refs.barChart.$el.getBoundingClientRect();
this.width = width;
},
setSvg(name) {
getSvgIconPathContent(name)
.then(path => {
if (path) {
this.$set(this.svgs, name, `path://${path}`);
}
})
.catch(e => {
// eslint-disable-next-line no-console, @gitlab/require-i18n-strings
console.error('SVG could not be rendered correctly: ', e);
});
},
},
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize">
<gl-bar-chart
ref="barChart"
v-bind="$attrs"
:data="chartData"
:option="chartOptions"
:width="width"
:height="height"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
:x-axis-type="xAxisType"
/>
</div>
</template>
......@@ -18,6 +18,7 @@ import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorHeatmapChart from './charts/heatmap.vue';
import MonitorColumnChart from './charts/column.vue';
import MonitorBarChart from './charts/bar.vue';
import MonitorStackedColumnChart from './charts/stacked_column.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
......@@ -31,6 +32,7 @@ export default {
components: {
MonitorSingleStatChart,
MonitorColumnChart,
MonitorBarChart,
MonitorHeatmapChart,
MonitorStackedColumnChart,
MonitorEmptyChart,
......@@ -259,6 +261,10 @@ export default {
v-else-if="isPanelType('heatmap') && graphDataHasMetrics"
:graph-data="graphData"
/>
<monitor-bar-chart
v-else-if="isPanelType('bar') && graphDataHasMetrics"
:graph-data="graphData"
/>
<monitor-column-chart
v-else-if="isPanelType('column') && graphDataHasMetrics"
:graph-data="graphData"
......
......@@ -46,7 +46,6 @@ export const metricStates = {
};
export const sidebarAnimationDuration = 300; // milliseconds.
export const chartHeight = 300;
export const graphTypes = {
......
......@@ -73,14 +73,21 @@ const mapToMetricsViewModel = (metrics, defaultLabel) =>
}));
/**
* Maps an axis view model
* Maps X-axis view model
*
* @param {Object} axis
*/
const mapXAxisToViewModel = ({ name = '' }) => ({ name });
/**
* Maps Y-axis view model
*
* Defaults to a 2 digit precision and `number` format. It only allows
* formats in the SUPPORTED_FORMATS array.
*
* @param {Object} axis
*/
const mapToAxisViewModel = ({ name = '', format = SUPPORTED_FORMATS.number, precision = 2 }) => {
const mapYAxisToViewModel = ({ name = '', format = SUPPORTED_FORMATS.number, precision = 2 }) => {
return {
name,
format: SUPPORTED_FORMATS[format] || SUPPORTED_FORMATS.number,
......@@ -94,15 +101,30 @@ const mapToAxisViewModel = ({ name = '', format = SUPPORTED_FORMATS.number, prec
* @param {Object} panel - Metrics panel
* @returns {Object}
*/
const mapToPanelViewModel = ({ title = '', type, y_label, y_axis = {}, metrics = [] }) => {
const mapPanelToViewModel = ({
title = '',
type,
x_axis = {},
x_label,
y_label,
y_axis = {},
metrics = [],
}) => {
// Both `x_axis.name` and `x_label` are supported for now
// https://gitlab.com/gitlab-org/gitlab/issues/210521
const xAxis = mapXAxisToViewModel({ name: x_label, ...x_axis }); // eslint-disable-line babel/camelcase
// Both `y_axis.name` and `y_label` are supported for now
// https://gitlab.com/gitlab-org/gitlab/issues/208385
const yAxis = mapToAxisViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase
const yAxis = mapYAxisToViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase
return {
title,
type,
xLabel: xAxis.name,
y_label: yAxis.name, // Changing y_label to yLabel is pending https://gitlab.com/gitlab-org/gitlab/issues/207198
yAxis,
xAxis,
metrics: mapToMetricsViewModel(metrics, yAxis.name),
};
};
......@@ -117,7 +139,7 @@ const mapToPanelGroupViewModel = ({ group = '', panels = [] }, i) => {
return {
key: `${slugify(group || 'default')}-${i}`,
group,
panels: panels.map(mapToPanelViewModel),
panels: panels.map(mapPanelToViewModel),
};
};
......
......@@ -132,4 +132,63 @@ export const timeRangeToUrl = (timeRange, url = window.location.href) => {
return mergeUrlParams(params, toUrl);
};
/**
* Get the metric value from first data point.
* Currently only used for bar charts
*
* @param {Array} values data points
* @returns {Number}
*/
const metricValueMapper = values => values[0]?.[1];
/**
* Get the metric name from metric object
* Currently only used for bar charts
* e.g. { handler: '/query' }
* { method: 'get' }
*
* @param {Object} metric metric object
* @returns {String}
*/
const metricNameMapper = metric => Object.values(metric)?.[0];
/**
* Parse metric object to extract metric value and name in
* [<metric-value>, <metric-name>] format.
* Currently only used for bar charts
*
* @param {Object} param0 metric object
* @returns {Array}
*/
const resultMapper = ({ metric, values = [] }) => [
metricValueMapper(values),
metricNameMapper(metric),
];
/**
* Bar charts graph data parser to massage data from
* backend to a format acceptable by bar charts component
* in GitLab UI
*
* e.g.
* {
* SLO: [
* [98, 'api'],
* [99, 'web'],
* [99, 'database']
* ]
* }
*
* @param {Array} data series information
* @returns {Object}
*/
export const barChartsDataParser = (data = []) =>
data?.reduce(
(acc, { result = [], label }) => ({
...acc,
[label]: result.map(resultMapper),
}),
{},
);
export default {};
---
title: Add bar chart support to monitoring dashboard
merge_request: 27155
author:
type: added
import { shallowMount } from '@vue/test-utils';
import { GlBarChart } from '@gitlab/ui/dist/charts';
import Bar from '~/monitoring/components/charts/bar.vue';
import { barMockData } from '../../mock_data';
jest.mock('~/lib/utils/icon_utils', () => ({
getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
}));
describe('Bar component', () => {
let barChart;
let store;
beforeEach(() => {
barChart = shallowMount(Bar, {
propsData: {
graphData: barMockData,
},
store,
});
});
afterEach(() => {
barChart.destroy();
});
describe('wrapped components', () => {
describe('GitLab UI bar chart', () => {
let glbarChart;
let chartData;
beforeEach(() => {
glbarChart = barChart.find(GlBarChart);
chartData = barChart.vm.chartData[barMockData.metrics[0].label];
});
it('is a Vue instance', () => {
expect(glbarChart.isVueInstance()).toBe(true);
});
it('should display a label on the x axis', () => {
expect(glbarChart.vm.xAxisTitle).toBe(barMockData.xLabel);
});
it('should return chartData as array of arrays', () => {
expect(chartData).toBeInstanceOf(Array);
chartData.forEach(item => {
expect(item).toBeInstanceOf(Array);
});
});
});
});
});
......@@ -703,3 +703,50 @@ export const stackedColumnMockedData = {
},
],
};
export const barMockData = {
title: 'SLA Trends - Primary Services',
type: 'bar-chart',
xLabel: 'service',
y_label: 'percentile',
metrics: [
{
id: 'sla_trends_primary_services',
series_name: 'group 1',
metric_id: 'undefined_sla_trends_primary_services',
metricId: 'undefined_sla_trends_primary_services',
query_range:
'avg(avg_over_time(slo_observation_status{environment="gprd", stage=~"main|", type=~"api|web|git|registry|sidekiq|ci-runners"}[1d])) by (type)',
unit: 'Percentile',
label: 'SLA',
prometheus_endpoint_path:
'/gitlab-com/metrics-dogfooding/-/environments/266/prometheus/api/v1/query_range?query=clamp_min%28clamp_max%28avg%28avg_over_time%28slo_observation_status%7Benvironment%3D%22gprd%22%2C+stage%3D~%22main%7C%22%2C+type%3D~%22api%7Cweb%7Cgit%7Cregistry%7Csidekiq%7Cci-runners%22%7D%5B1d%5D%29%29+by+%28type%29%2C1%29%2C0%29',
result: [
{
metric: { type: 'api' },
values: [[1583995208, '0.9935198135198128']],
},
{
metric: { type: 'git' },
values: [[1583995208, '0.9975296513504401']],
},
{
metric: { type: 'registry' },
values: [[1583995208, '0.9994716394716395']],
},
{
metric: { type: 'sidekiq' },
values: [[1583995208, '0.9948251748251747']],
},
{
metric: { type: 'web' },
values: [[1583995208, '0.9535664335664336']],
},
{
metric: { type: 'postgresql_database' },
values: [[1583995208, '0.9335664335664336']],
},
],
},
],
};
......@@ -25,6 +25,10 @@ describe('mapToDashboardViewModel', () => {
panels: [
{
title: 'Title A',
xLabel: '',
xAxis: {
name: '',
},
type: 'chart-type',
y_label: 'Y Label A',
metrics: [],
......@@ -44,6 +48,10 @@ describe('mapToDashboardViewModel', () => {
{
title: 'Title A',
type: 'chart-type',
xLabel: '',
xAxis: {
name: '',
},
y_label: 'Y Label A',
yAxis: {
name: 'Y Label A',
......@@ -114,6 +122,28 @@ describe('mapToDashboardViewModel', () => {
const getMappedPanel = () => mapToDashboardViewModel(dashboard).panelGroups[0].panels[0];
it('panel with x_label', () => {
setupWithPanel({
title: panelTitle,
x_label: 'x label',
});
expect(getMappedPanel()).toEqual({
title: panelTitle,
xLabel: 'x label',
xAxis: {
name: 'x label',
},
y_label: '',
yAxis: {
name: '',
format: SUPPORTED_FORMATS.number,
precision: 2,
},
metrics: [],
});
});
it('group y_axis defaults', () => {
setupWithPanel({
title: panelTitle,
......@@ -121,7 +151,11 @@ describe('mapToDashboardViewModel', () => {
expect(getMappedPanel()).toEqual({
title: panelTitle,
xLabel: '',
y_label: '',
xAxis: {
name: '',
},
yAxis: {
name: '',
format: SUPPORTED_FORMATS.number,
......
......@@ -6,6 +6,7 @@ import {
graphDataPrometheusQuery,
graphDataPrometheusQueryRange,
anomalyMockGraphData,
barMockData,
} from './mock_data';
jest.mock('~/lib/utils/url_utility');
......@@ -210,4 +211,67 @@ describe('monitoring/utils', () => {
expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: `${seconds}` }, fromUrl);
});
});
describe('barChartsDataParser', () => {
const singleMetricExpected = {
SLA: [
['0.9935198135198128', 'api'],
['0.9975296513504401', 'git'],
['0.9994716394716395', 'registry'],
['0.9948251748251747', 'sidekiq'],
['0.9535664335664336', 'web'],
['0.9335664335664336', 'postgresql_database'],
],
};
const multipleMetricExpected = {
...singleMetricExpected,
SLA_2: Object.values(singleMetricExpected)[0],
};
const barMockDataWithMultipleMetrics = {
...barMockData,
metrics: [
barMockData.metrics[0],
{
...barMockData.metrics[0],
label: 'SLA_2',
},
],
};
[
{
input: { metrics: undefined },
output: {},
testCase: 'barChartsDataParser returns {} with undefined',
},
{
input: { metrics: null },
output: {},
testCase: 'barChartsDataParser returns {} with null',
},
{
input: { metrics: [] },
output: {},
testCase: 'barChartsDataParser returns {} with []',
},
{
input: barMockData,
output: singleMetricExpected,
testCase: 'barChartsDataParser returns single series object with single metrics',
},
{
input: barMockDataWithMultipleMetrics,
output: multipleMetricExpected,
testCase: 'barChartsDataParser returns multiple series object with multiple metrics',
},
].forEach(({ input, output, testCase }) => {
it(testCase, () => {
expect(monitoringUtils.barChartsDataParser(input.metrics)).toEqual(
expect.objectContaining(output),
);
});
});
});
});
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