Commit daa53290 authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla

Add annotations config generator

This MR adds a method that accepts
annotations to generate eCharts
markLines. Until the annotations fetch
issue is implemented, this method will
not have annotations information
parent 858c3de9
import { graphTypes, symbolSizes } from '../../constants'; import { graphTypes, symbolSizes, colorValues } from '../../constants';
/** /**
* Annotations and deployments are decoration layers on * Annotations and deployments are decoration layers on
...@@ -40,33 +40,50 @@ export const annotationsYAxis = { ...@@ -40,33 +40,50 @@ export const annotationsYAxis = {
formatter: () => {}, formatter: () => {},
}, },
}; };
/** /**
* This util method check if a particular series data point * Fetched list of annotations are parsed into a
* is of annotation type. Annotations are generally scatter * format the eCharts accepts to draw markLines
* plot charts *
* 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 * @param {Object} annotation object
* @returns {Boolean} * @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 * This method currently generates deployments and annotations
* https://gitlab.com/gitlab-org/gitlab/-/issues/211418 annotations * but are not used in the chart. The method calling
* support will be added in this method. * 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 * This method is extracted out of the charts so that
* annotation lines can be easily supported in * annotation lines can be easily supported in
* the future. * 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 * @param {Array} deployments deployments data
* @returns {Object} annotation series object * @returns {Object} annotation series object
*/ */
export const generateAnnotationsSeries = (deployments = []) => { export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } = {}) => {
if (!deployments.length) { // deployment data points
return []; const deploymentsData = deployments.map(deployment => {
}
const data = deployments.map(deployment => {
return { return {
name: 'deployments', name: 'deployments',
value: [deployment.createdAt, annotationsYAxisCoords.pos], value: [deployment.createdAt, annotationsYAxisCoords.pos],
...@@ -78,9 +95,27 @@ export const generateAnnotationsSeries = (deployments = []) => { ...@@ -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 { return {
type: graphTypes.annotationsData, type: graphTypes.annotationsData,
yAxisIndex: 1, // annotationsYAxis index yAxisIndex: 1, // annotationsYAxis index
data, data: [...deploymentsData, ...annotationsData],
markLine,
}; };
}; };
...@@ -6,9 +6,9 @@ import dateFormat from 'dateformat'; ...@@ -6,9 +6,9 @@ import dateFormat from 'dateformat';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue'; 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 { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { annotationsYAxis, generateAnnotationsSeries, isAnnotation } from './annotations'; import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
import { makeDataSeries } from '~/helpers/monitor_helper'; import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils'; import { graphDataValidatorForValues } from '../../utils';
...@@ -20,6 +20,7 @@ const events = { ...@@ -20,6 +20,7 @@ const events = {
}; };
export default { export default {
tooltipTypes,
components: { components: {
GlAreaChart, GlAreaChart,
GlLineChart, GlLineChart,
...@@ -88,10 +89,10 @@ export default { ...@@ -88,10 +89,10 @@ export default {
data() { data() {
return { return {
tooltip: { tooltip: {
type: '',
title: '', title: '',
content: [], content: [],
commitUrl: '', commitUrl: '',
isDeployment: false,
sha: '', sha: '',
}, },
width: 0, width: 0,
...@@ -137,7 +138,13 @@ export default { ...@@ -137,7 +138,13 @@ export default {
}, []); }, []);
}, },
chartOptionSeries() { 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() { chartOptions() {
const { yAxis, xAxis } = this.option; const { yAxis, xAxis } = this.option;
...@@ -246,6 +253,9 @@ export default { ...@@ -246,6 +253,9 @@ export default {
formatLegendLabel(query) { formatLegendLabel(query) {
return `${query.label}`; return `${query.label}`;
}, },
isTooltipOfType(tooltipType, defaultType) {
return tooltipType === defaultType;
},
formatTooltipText(params) { formatTooltipText(params) {
this.tooltip.title = dateFormat(params.value, dateFormats.default); this.tooltip.title = dateFormat(params.value, dateFormats.default);
this.tooltip.content = []; this.tooltip.content = [];
...@@ -253,13 +263,18 @@ export default { ...@@ -253,13 +263,18 @@ export default {
params.seriesData.forEach(dataPoint => { params.seriesData.forEach(dataPoint => {
if (dataPoint.value) { if (dataPoint.value) {
const [xVal, yVal] = dataPoint.value; const [xVal, yVal] = dataPoint.value;
this.tooltip.isDeployment = isAnnotation(dataPoint.componentSubType); this.tooltip.type = dataPoint.name;
if (this.tooltip.isDeployment) { if (this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.deployments)) {
const [deploy] = this.recentDeployments.filter( const [deploy] = this.recentDeployments.filter(
deployment => deployment.createdAt === xVal, deployment => deployment.createdAt === xVal,
); );
this.tooltip.sha = deploy.sha.substring(0, 8); this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl; 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 { } else {
const { seriesName, color, dataIndex } = dataPoint; const { seriesName, color, dataIndex } = dataPoint;
...@@ -288,7 +303,6 @@ export default { ...@@ -288,7 +303,6 @@ export default {
onChartUpdated(eChart) { onChartUpdated(eChart) {
[this.primaryColor] = eChart.getOption().color; [this.primaryColor] = eChart.getOption().color;
}, },
onChartCreated(eChart) { onChartCreated(eChart) {
// Emit a datazoom event that corresponds to the eChart // Emit a datazoom event that corresponds to the eChart
// `datazoom` event. // `datazoom` event.
...@@ -346,7 +360,7 @@ export default { ...@@ -346,7 +360,7 @@ export default {
@created="onChartCreated" @created="onChartCreated"
@updated="onChartUpdated" @updated="onChartUpdated"
> >
<template v-if="tooltip.isDeployment"> <template v-if="isTooltipOfType(tooltip.type, this.$options.tooltipTypes.deployments)">
<template slot="tooltipTitle"> <template slot="tooltipTitle">
{{ __('Deployed') }} {{ __('Deployed') }}
</template> </template>
...@@ -355,16 +369,23 @@ export default { ...@@ -355,16 +369,23 @@ export default {
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div> </div>
</template> </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 v-else>
<template slot="tooltipTitle"> <template slot="tooltipTitle">
<slot name="tooltipTitle">
<div class="text-nowrap"> <div class="text-nowrap">
{{ tooltip.title }} {{ tooltip.title }}
</div> </div>
</slot>
</template> </template>
<template slot="tooltipContent"> <template slot="tooltipContent" :tooltip="tooltip">
<slot name="tooltipContent" :tooltip="tooltip">
<div <div
v-for="(content, key) in tooltip.content" v-for="(content, key) in tooltip.content"
:key="key" :key="key"
...@@ -377,7 +398,6 @@ export default { ...@@ -377,7 +398,6 @@ export default {
{{ content.value }} {{ content.value }}
</div> </div>
</div> </div>
</slot>
</template> </template>
</template> </template>
</component> </component>
......
...@@ -115,3 +115,12 @@ export const NOT_IN_DB_PREFIX = 'NO_DB'; ...@@ -115,3 +115,12 @@ export const NOT_IN_DB_PREFIX = 'NO_DB';
* Used as a value for the 'states' query filter * Used as a value for the 'states' query filter
*/ */
export const ENVIRONMENT_AVAILABLE_STATE = 'available'; 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 { generateAnnotationsSeries } from '~/monitoring/components/charts/annotations';
import { deploymentData } from '../../mock_data'; import { deploymentData, annotationsData } from '../../mock_data';
describe('annotations spec', () => { describe('annotations spec', () => {
describe('generateAnnotationsSeries', () => { describe('generateAnnotationsSeries', () => {
it('default options', () => { it('with default options', () => {
const annotations = generateAnnotationsSeries(); 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', () => { it('when only deployments data is passed', () => {
const annotations = generateAnnotationsSeries(deploymentData); const annotations = generateAnnotationsSeries({ deployments: deploymentData });
expect(annotations).toEqual( expect(annotations).toEqual(
expect.objectContaining({ expect.objectContaining({
type: 'scatter', type: 'scatter',
yAxisIndex: 1, yAxisIndex: 1,
data: expect.any(Array), data: expect.any(Array),
markLine: {
data: [],
symbol: 'none',
silent: true,
},
}), }),
); );
annotations.data.forEach(annotation => { annotations.data.forEach(annotation => {
expect(annotation).toEqual(expect.any(Object)); 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', () => { ...@@ -169,6 +169,7 @@ describe('Time series component', () => {
componentSubType: type, componentSubType: type,
value: [mockDate, 5.55555], value: [mockDate, 5.55555],
dataIndex: 0, dataIndex: 0,
...(type === 'scatter' && { name: 'deployments' }),
}, },
], ],
value: mockDate, value: mockDate,
...@@ -225,6 +226,10 @@ describe('Time series component', () => { ...@@ -225,6 +226,10 @@ describe('Time series component', () => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter')); timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
}); });
it('set tooltip type to deployments', () => {
expect(timeSeriesChart.vm.tooltip.type).toBe('deployments');
});
it('formats tooltip title', () => { it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM'); expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
}); });
...@@ -521,7 +526,11 @@ describe('Time series component', () => { ...@@ -521,7 +526,11 @@ describe('Time series component', () => {
const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`; const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
beforeEach(done => { beforeEach(done => {
timeSeriesAreaChart.vm.tooltip.isDeployment = true; timeSeriesAreaChart.setData({
tooltip: {
type: 'deployments',
},
});
timeSeriesAreaChart.vm.$nextTick(done); timeSeriesAreaChart.vm.$nextTick(done);
}); });
......
...@@ -210,6 +210,30 @@ export const deploymentData = [ ...@@ -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 = [ export const metricsNewGroupsAPIResponse = [
{ {
group: 'System metrics (Kubernetes)', 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