Commit f44737e4 authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Clement Ho

Support multiple queries per chart on metrics dash

Adding support for metrics alerts disabled multiple query support.
To avoid a data model refactor, this enables the visual of multiple
queries per chart on the front end, combining queries based on
metric group, title, and y-axis label.

This also adds support for adding and editing alerts based on the
query selected rather than the single metric associated with the chart.
parent a5eb29c1
...@@ -42,10 +42,10 @@ export default { ...@@ -42,10 +42,10 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
alertData: { thresholds: {
type: Object, type: Array,
required: false, required: false,
default: () => ({}), default: () => [],
}, },
}, },
data() { data() {
...@@ -64,6 +64,9 @@ export default { ...@@ -64,6 +64,9 @@ export default {
}, },
computed: { computed: {
chartData() { chartData() {
// Transforms & supplements query data to render appropriate labels & styles
// Input: [{ queryAttributes1 }, { queryAttributes2 }]
// Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
return this.graphData.queries.reduce((acc, query) => { return this.graphData.queries.reduce((acc, query) => {
const { appearance } = query; const { appearance } = query;
const lineType = const lineType =
...@@ -121,6 +124,9 @@ export default { ...@@ -121,6 +124,9 @@ export default {
}, },
earliestDatapoint() { earliestDatapoint() {
return this.chartData.reduce((acc, series) => { return this.chartData.reduce((acc, series) => {
if (!series.data.length) {
return acc;
}
const [[timestamp]] = series.data.sort(([a], [b]) => { const [[timestamp]] = series.data.sort(([a], [b]) => {
if (a < b) { if (a < b) {
return -1; return -1;
...@@ -235,7 +241,7 @@ export default { ...@@ -235,7 +241,7 @@ export default {
:data="chartData" :data="chartData"
:option="chartOptions" :option="chartOptions"
:format-tooltip-text="formatTooltipText" :format-tooltip-text="formatTooltipText"
:thresholds="alertData" :thresholds="thresholds"
:width="width" :width="width"
:height="height" :height="height"
@updated="onChartUpdated" @updated="onChartUpdated"
......
<script> <script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import _ from 'underscore';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import '~/vue_shared/mixins/is_ee'; import '~/vue_shared/mixins/is_ee';
...@@ -142,8 +143,13 @@ export default { ...@@ -142,8 +143,13 @@ export default {
} }
}, },
methods: { methods: {
getGraphAlerts(graphId) { getGraphAlerts(queries) {
return this.alertData ? this.alertData[graphId] || {} : {}; if (!this.allAlerts) return {};
const metricIdsForChart = queries.map(q => q.metricId);
return _.pick(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
},
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
}, },
getGraphsData() { getGraphsData() {
this.state = 'loading'; this.state = 'loading';
...@@ -199,17 +205,15 @@ export default { ...@@ -199,17 +205,15 @@ export default {
:key="graphIndex" :key="graphIndex"
:graph-data="graphData" :graph-data="graphData"
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
:alert-data="getGraphAlerts(graphData.id)" :thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth" :container-width="elWidth"
group-id="monitor-area-chart" group-id="monitor-area-chart"
> >
<alert-widget <alert-widget
v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData.id" v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData"
:alerts-endpoint="alertsEndpoint" :alerts-endpoint="alertsEndpoint"
:label="getGraphLabel(graphData)" :relevant-queries="graphData.queries"
:current-alerts="getQueryAlerts(graphData)" :alerts-to-manage="getGraphAlerts(graphData.queries)"
:custom-metric-id="graphData.id"
:alert-data="alertData[graphData.id]"
@setAlerts="setAlerts" @setAlerts="setAlerts"
/> />
</monitor-area-chart> </monitor-area-chart>
......
...@@ -27,10 +27,47 @@ function removeTimeSeriesNoData(queries) { ...@@ -27,10 +27,47 @@ function removeTimeSeriesNoData(queries) {
return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []); return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
} }
// Metrics and queries are currently stored 1:1, so `queries` is an array of length one.
// We want to group queries onto a single chart by title & y-axis label.
// This function will no longer be required when metrics:queries are 1:many,
// though there is no consequence if the function stays in use.
// @param metrics [Array<Object>]
// Ex) [
// { id: 1, title: 'title', y_label: 'MB', queries: [{ ...query1Attrs }] },
// { id: 2, title: 'title', y_label: 'MB', queries: [{ ...query2Attrs }] },
// { id: 3, title: 'new title', y_label: 'MB', queries: [{ ...query3Attrs }] }
// ]
// @return [Array<Object>]
// Ex) [
// { title: 'title', y_label: 'MB', queries: [{ metricId: 1, ...query1Attrs },
// { metricId: 2, ...query2Attrs }] },
// { title: 'new title', y_label: 'MB', queries: [{ metricId: 3, ...query3Attrs }]}
// ]
function groupQueriesByChartInfo(metrics) {
const metricsByChart = metrics.reduce((accumulator, metric) => {
const { id, queries, ...chart } = metric;
const chartKey = `${chart.title}|${chart.y_label}`;
accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] };
queries.forEach(queryAttrs =>
accumulator[chartKey].queries.push({ metricId: id.toString(), ...queryAttrs }),
);
return accumulator;
}, {});
return Object.values(metricsByChart);
}
function normalizeMetrics(metrics) { function normalizeMetrics(metrics) {
return metrics.map(metric => { const groupedMetrics = groupQueriesByChartInfo(metrics);
return groupedMetrics.map(metric => {
const queries = metric.queries.map(query => ({ const queries = metric.queries.map(query => ({
...query, ...query,
// custom metrics do not require a label, so we should ensure this attribute is defined
label: query.label || metric.y_label,
result: query.result.map(result => ({ result: query.result.map(result => ({
...result, ...result,
values: result.values.map(([timestamp, value]) => [ values: result.values.map(([timestamp, value]) => [
......
---
title: Support multiple queries per chart on metrics dash
merge_request: 25758
author:
type: added
...@@ -65,7 +65,7 @@ describe('Area component', () => { ...@@ -65,7 +65,7 @@ describe('Area component', () => {
expect(props.data).toBe(areaChart.vm.chartData); expect(props.data).toBe(areaChart.vm.chartData);
expect(props.option).toBe(areaChart.vm.chartOptions); expect(props.option).toBe(areaChart.vm.chartOptions);
expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText); expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText);
expect(props.thresholds).toBe(areaChart.props('alertData')); expect(props.thresholds).toBe(areaChart.vm.thresholds);
}); });
it('recieves a tooltip title', () => { it('recieves a tooltip title', () => {
...@@ -105,12 +105,13 @@ describe('Area component', () => { ...@@ -105,12 +105,13 @@ describe('Area component', () => {
seriesName: areaChart.vm.chartData[0].name, seriesName: areaChart.vm.chartData[0].name,
componentSubType: type, componentSubType: type,
value: [mockDate, 5.55555], value: [mockDate, 5.55555],
seriesIndex: 0,
}, },
], ],
value: mockDate, value: mockDate,
}); });
describe('series is of line type', () => { describe('when series is of line type', () => {
beforeEach(() => { beforeEach(() => {
areaChart.vm.formatTooltipText(generateSeriesData('line')); areaChart.vm.formatTooltipText(generateSeriesData('line'));
}); });
...@@ -131,7 +132,7 @@ describe('Area component', () => { ...@@ -131,7 +132,7 @@ describe('Area component', () => {
}); });
}); });
describe('series is of scatter type', () => { describe('when series is of scatter type', () => {
beforeEach(() => { beforeEach(() => {
areaChart.vm.formatTooltipText(generateSeriesData('scatter')); areaChart.vm.formatTooltipText(generateSeriesData('scatter'));
}); });
......
...@@ -663,10 +663,10 @@ ...@@ -663,10 +663,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.58.0.tgz#bb05263ff2eb7ca09a25cd14d0b1a932d2ea9c2f" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.58.0.tgz#bb05263ff2eb7ca09a25cd14d0b1a932d2ea9c2f"
integrity sha512-RlWSjjBT4lMIFuNC1ziCO1nws9zqZtxCjhrqK2DxDDTgp2W0At9M/BFkHp8RHyMCrO3g1fHTrLPUgzr5oR3Epg== integrity sha512-RlWSjjBT4lMIFuNC1ziCO1nws9zqZtxCjhrqK2DxDDTgp2W0At9M/BFkHp8RHyMCrO3g1fHTrLPUgzr5oR3Epg==
"@gitlab/ui@^3.0.0": "@gitlab/ui@^3.0.1":
version "3.0.0" version "3.0.2"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.0.0.tgz#33ca2808dbd4395e69a366a219d1edc1f3dbccd5" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.0.2.tgz#29a17699751261657487b939c651c0f93264df2a"
integrity sha512-pDEa2k6ln5GE/N2z0V7dNEeFtSTW0p9ipO2/N9q6QMxO7fhhOhpMC0QVbdIljKTbglspDWI5v6BcqUjzYri5Pg== integrity sha512-JZhcS5cDxtpxopTc55UWvUbZAwKvxygYHT9I01QmUtKgaKIJlnjBj8zkcg1xHazX7raSjjtjqfDEla39a+luuQ==
dependencies: dependencies:
"@babel/standalone" "^7.0.0" "@babel/standalone" "^7.0.0"
bootstrap-vue "^2.0.0-rc.11" bootstrap-vue "^2.0.0-rc.11"
......
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