Commit 98e42f80 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'monitoring-dashboard-add-annotations-configs' into 'master'

Add annotations config generator

See merge request gitlab-org/gitlab!29085
parents 71f1e807 daa53290
import { graphTypes, symbolSizes } from '../../constants';
import { graphTypes, symbolSizes, colorValues } from '../../constants';
/**
* Annotations and deployments are decoration layers on
......@@ -40,33 +40,50 @@ export const annotationsYAxis = {
formatter: () => {},
},
};
/**
* This util method check if a particular series data point
* is of annotation type. Annotations are generally scatter
* plot charts
* Fetched list of annotations are parsed into a
* format the eCharts accepts to draw markLines
*
* If Annotation is a single line, the `from` property
* has a value and the `to` is null. Because annotations
* only supports lines the from value does not exist yet.
*
*
* @param {String} type series component type
* @returns {Boolean}
* @param {Object} annotation object
* @returns {Object} markLine object
*/
export const isAnnotation = type => type === graphTypes.annotationsData;
export const parseAnnotations = ({
from: annotationFrom = '',
color = colorValues.primaryColor,
}) => ({
xAxis: annotationFrom,
lineStyle: {
color,
},
});
/**
* This method currently supports only deployments. After
* https://gitlab.com/gitlab-org/gitlab/-/issues/211418 annotations
* support will be added in this method.
* This method currently generates deployments and annotations
* but are not used in the chart. The method calling
* generateAnnotationsSeries will not pass annotations until
* https://gitlab.com/gitlab-org/gitlab/-/issues/211330 is
* implemented.
*
* This method is extracted out of the charts so that
* annotation lines can be easily supported in
* the future.
*
* In order to make hover work, hidden annotation data points
* are created along with the markLines. These data points have
* the necessart metadata that is used to display in the tooltip.
*
* @param {Array} deployments deployments data
* @returns {Object} annotation series object
*/
export const generateAnnotationsSeries = (deployments = []) => {
if (!deployments.length) {
return [];
}
const data = deployments.map(deployment => {
export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } = {}) => {
// deployment data points
const deploymentsData = deployments.map(deployment => {
return {
name: 'deployments',
value: [deployment.createdAt, annotationsYAxisCoords.pos],
......@@ -78,9 +95,27 @@ export const generateAnnotationsSeries = (deployments = []) => {
};
});
// annotation data points
const annotationsData = annotations.map(annotation => {
return {
name: 'annotations',
value: [annotation.from, annotationsYAxisCoords.pos],
symbol: 'none',
description: annotation.description,
};
});
// annotation markLine option
const markLine = {
symbol: 'none',
silent: true,
data: annotations.map(parseAnnotations),
};
return {
type: graphTypes.annotationsData,
yAxisIndex: 1, // annotationsYAxis index
data,
data: [...deploymentsData, ...annotationsData],
markLine,
};
};
......@@ -6,9 +6,9 @@ import dateFormat from 'dateformat';
import { s__, __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
import { chartHeight, lineTypes, lineWidths, dateFormats } from '../../constants';
import { chartHeight, lineTypes, lineWidths, dateFormats, tooltipTypes } from '../../constants';
import { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { annotationsYAxis, generateAnnotationsSeries, isAnnotation } from './annotations';
import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
......@@ -20,6 +20,7 @@ const events = {
};
export default {
tooltipTypes,
components: {
GlAreaChart,
GlLineChart,
......@@ -88,10 +89,10 @@ export default {
data() {
return {
tooltip: {
type: '',
title: '',
content: [],
commitUrl: '',
isDeployment: false,
sha: '',
},
width: 0,
......@@ -137,7 +138,13 @@ export default {
}, []);
},
chartOptionSeries() {
return (this.option.series || []).concat(generateAnnotationsSeries(this.recentDeployments));
// After https://gitlab.com/gitlab-org/gitlab/-/issues/211330 is implemented,
// this method will have access to annotations data
return (this.option.series || []).concat(
generateAnnotationsSeries({
deployments: this.recentDeployments,
}),
);
},
chartOptions() {
const { yAxis, xAxis } = this.option;
......@@ -246,6 +253,9 @@ export default {
formatLegendLabel(query) {
return `${query.label}`;
},
isTooltipOfType(tooltipType, defaultType) {
return tooltipType === defaultType;
},
formatTooltipText(params) {
this.tooltip.title = dateFormat(params.value, dateFormats.default);
this.tooltip.content = [];
......@@ -253,13 +263,18 @@ export default {
params.seriesData.forEach(dataPoint => {
if (dataPoint.value) {
const [xVal, yVal] = dataPoint.value;
this.tooltip.isDeployment = isAnnotation(dataPoint.componentSubType);
if (this.tooltip.isDeployment) {
this.tooltip.type = dataPoint.name;
if (this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.deployments)) {
const [deploy] = this.recentDeployments.filter(
deployment => deployment.createdAt === xVal,
);
this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl;
} else if (
this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.annotations)
) {
const { data } = dataPoint;
this.tooltip.content.push(data?.description);
} else {
const { seriesName, color, dataIndex } = dataPoint;
......@@ -288,7 +303,6 @@ export default {
onChartUpdated(eChart) {
[this.primaryColor] = eChart.getOption().color;
},
onChartCreated(eChart) {
// Emit a datazoom event that corresponds to the eChart
// `datazoom` event.
......@@ -346,7 +360,7 @@ export default {
@created="onChartCreated"
@updated="onChartUpdated"
>
<template v-if="tooltip.isDeployment">
<template v-if="isTooltipOfType(tooltip.type, this.$options.tooltipTypes.deployments)">
<template slot="tooltipTitle">
{{ __('Deployed') }}
</template>
......@@ -355,29 +369,35 @@ export default {
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
</template>
<template v-else-if="isTooltipOfType(tooltip.type, this.$options.tooltipTypes.annotations)">
<template slot="tooltipTitle">
<div class="text-nowrap">
{{ tooltip.title }}
</div>
</template>
<div slot="tooltipContent" class="d-flex align-items-center">
{{ tooltip.content.join('\n') }}
</div>
</template>
<template v-else>
<template slot="tooltipTitle">
<slot name="tooltipTitle">
<div class="text-nowrap">
{{ tooltip.title }}
</div>
</slot>
<div class="text-nowrap">
{{ tooltip.title }}
</div>
</template>
<template slot="tooltipContent">
<slot name="tooltipContent" :tooltip="tooltip">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ content.value }}
</div>
<template slot="tooltipContent" :tooltip="tooltip">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ content.value }}
</div>
</slot>
</div>
</template>
</template>
</component>
......
......@@ -115,3 +115,12 @@ export const NOT_IN_DB_PREFIX = 'NO_DB';
* Used as a value for the 'states' query filter
*/
export const ENVIRONMENT_AVAILABLE_STATE = 'available';
/**
* Time series charts have different types of
* tooltip based on the hovered data point.
*/
export const tooltipTypes = {
deployments: 'deployments',
annotations: 'annotations',
};
import { generateAnnotationsSeries } from '~/monitoring/components/charts/annotations';
import { deploymentData } from '../../mock_data';
import { deploymentData, annotationsData } from '../../mock_data';
describe('annotations spec', () => {
describe('generateAnnotationsSeries', () => {
it('default options', () => {
it('with default options', () => {
const annotations = generateAnnotationsSeries();
expect(annotations).toEqual([]);
expect(annotations).toEqual(
expect.objectContaining({
type: 'scatter',
yAxisIndex: 1,
data: [],
markLine: {
data: [],
symbol: 'none',
silent: true,
},
}),
);
});
it('with deployments', () => {
const annotations = generateAnnotationsSeries(deploymentData);
it('when only deployments data is passed', () => {
const annotations = generateAnnotationsSeries({ deployments: deploymentData });
expect(annotations).toEqual(
expect.objectContaining({
type: 'scatter',
yAxisIndex: 1,
data: expect.any(Array),
markLine: {
data: [],
symbol: 'none',
silent: true,
},
}),
);
annotations.data.forEach(annotation => {
expect(annotation).toEqual(expect.any(Object));
});
expect(annotations.data).toHaveLength(deploymentData.length);
});
it('when only annotations data is passed', () => {
const annotations = generateAnnotationsSeries({
annotations: annotationsData,
});
expect(annotations).toEqual(
expect.objectContaining({
type: 'scatter',
yAxisIndex: 1,
data: expect.any(Array),
markLine: expect.any(Object),
}),
);
annotations.markLine.data.forEach(annotation => {
expect(annotation).toEqual(expect.any(Object));
});
expect(annotations.data).toHaveLength(annotationsData.length);
expect(annotations.markLine.data).toHaveLength(annotationsData.length);
});
it('when deploments and annotations data is passed', () => {
const annotations = generateAnnotationsSeries({
deployments: deploymentData,
annotations: annotationsData,
});
expect(annotations).toEqual(
expect.objectContaining({
type: 'scatter',
yAxisIndex: 1,
data: expect.any(Array),
markLine: expect.any(Object),
}),
);
annotations.markLine.data.forEach(annotation => {
expect(annotation).toEqual(expect.any(Object));
});
expect(annotations.data).toHaveLength(deploymentData.length + annotationsData.length);
});
});
});
......@@ -169,6 +169,7 @@ describe('Time series component', () => {
componentSubType: type,
value: [mockDate, 5.55555],
dataIndex: 0,
...(type === 'scatter' && { name: 'deployments' }),
},
],
value: mockDate,
......@@ -225,6 +226,10 @@ describe('Time series component', () => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
});
it('set tooltip type to deployments', () => {
expect(timeSeriesChart.vm.tooltip.type).toBe('deployments');
});
it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
});
......@@ -521,7 +526,11 @@ describe('Time series component', () => {
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
beforeEach(done => {
timeSeriesAreaChart.vm.tooltip.isDeployment = true;
timeSeriesAreaChart.setData({
tooltip: {
type: 'deployments',
},
});
timeSeriesAreaChart.vm.$nextTick(done);
});
......
......@@ -210,6 +210,30 @@ export const deploymentData = [
},
];
export const annotationsData = [
{
id: 'gid://gitlab/Metrics::Dashboard::Annotation/1',
from: '2020-04-01T12:51:58.373Z',
to: null,
panelId: null,
description: 'This is a test annotation',
},
{
id: 'gid://gitlab/Metrics::Dashboard::Annotation/2',
description: 'test annotation 2',
from: '2020-04-02T12:51:58.373Z',
to: null,
panelId: null,
},
{
id: 'gid://gitlab/Metrics::Dashboard::Annotation/3',
description: 'test annotation 3',
from: '2020-04-04T12:51:58.373Z',
to: null,
panelId: null,
},
];
export const metricsNewGroupsAPIResponse = [
{
group: 'System metrics (Kubernetes)',
......
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