Commit 5524a083 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'add-bar-charts-to-monitoring-dashboard' into 'master'

Add bar chart support to monitoring dashboard

See merge request gitlab-org/gitlab!27155
parents 935c7c09 2c8dced5
<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