Commit 0570f4b2 authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla

Update annotations configs

This MR adds the logic to add markLines
and markPoints for annotations within
GitLab and stop using the annotations
prop that was recently introduced in
GitLab UI. This makes it easy to interact
with annotation arrows and tooltips
parent a0d178ab
import { graphTypes, symbolSizes, colorValues } from '../../constants'; import { graphTypes, symbolSizes, colorValues, annotationsSymbolIcon } from '../../constants';
/** /**
* Annotations and deployments are decoration layers on * Annotations and deployments are decoration layers on
* top of the actual chart data. We use a scatter plot to * top of the actual chart data. We use a scatter plot to
* display this information. Each chart has its coordinate * display this information. Each chart has its coordinate
* system based on data and irresptive of the data, these * system based on data and irrespective of the data, these
* decorations have to be placed in specific locations. * decorations have to be placed in specific locations.
* For this reason, annotations have their own coordinate system, * For this reason, annotations have their own coordinate system,
* *
* As of %12.9, only deployment icons, a type of annotations, need * As of %12.9, only deployment icons, a type of annotations, need
* to be displayed on the chart. * to be displayed on the chart.
* *
* After https://gitlab.com/gitlab-org/gitlab/-/issues/211418, * Annotations and deployments co-exist in the same series as
* annotations and deployments will co-exist in the same * they logically belong together. Annotations are passed as
* series as they logically belong together. Annotations will be * markLines and markPoints while deployments are passed as
* passed as markLine objects. * data points with custom icons.
*/ */
/** /**
...@@ -49,38 +49,45 @@ export const annotationsYAxis = { ...@@ -49,38 +49,45 @@ export const annotationsYAxis = {
* has a value and the `ending_at` is null. Because annotations * has a value and the `ending_at` is null. Because annotations
* only supports lines the `ending_at` value does not exist yet. * only supports lines the `ending_at` value does not exist yet.
* *
*
* @param {Object} annotation object * @param {Object} annotation object
* @returns {Object} markLine object * @returns {Object} markLine object
*/ */
export const parseAnnotations = ({ starting_at = '', color = colorValues.primaryColor }) => ({ export const parseAnnotations = annotations =>
xAxis: starting_at, annotations.reduce(
lineStyle: { (acc, annotation) => {
color, acc.lines.push({
}, xAxis: annotation.starting_at,
}); lineStyle: {
color: colorValues.primaryColor,
},
});
acc.points.push({
name: 'annotations',
xAxis: annotation.starting_at,
yAxis: annotationsYAxisCoords.min,
tooltipData: {
title: annotation.starting_at,
content: annotation.description,
},
});
return acc;
},
{ lines: [], points: [] },
);
/** /**
* This method currently generates deployments and annotations * This method generates a decorative series that has
* but are not used in the chart. The method calling * deployments as data points with custom icons and
* generateAnnotationsSeries will not pass annotations until * annotations as markLines and markPoints
* 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 * @param {Array} deployments deployments data
* @returns {Object} annotation series object * @returns {Object} annotation series object
*/ */
export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } = {}) => { export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } = {}) => {
// deployment data points // deployment data points
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],
...@@ -98,31 +105,29 @@ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } ...@@ -98,31 +105,29 @@ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] }
}; };
}); });
// annotation data points const parsedAnnotations = parseAnnotations(annotations);
const annotationsData = annotations.map(annotation => {
return {
name: 'annotations',
value: [annotation.starting_at, annotationsYAxisCoords.pos],
// style options
symbol: 'none',
// metadata that are accessible in `formatTooltipText` method
tooltipData: {
description: annotation.description,
},
};
});
// annotation markLine option // markLine option draws the annotations dotted line
const markLine = { const markLine = {
symbol: 'none', symbol: 'none',
silent: true, silent: true,
data: annotations.map(parseAnnotations), data: parsedAnnotations.lines,
};
// markPoints are the arrows under the annotations lines
const markPoint = {
symbol: annotationsSymbolIcon,
symbolSize: '8',
symbolOffset: [0, ' 60%'],
data: parsedAnnotations.points,
}; };
return { return {
name: 'annotations',
type: graphTypes.annotationsData, type: graphTypes.annotationsData,
yAxisIndex: 1, // annotationsYAxis index yAxisIndex: 1, // annotationsYAxis index
data: [...deploymentsData, ...annotationsData], data,
markLine, markLine,
markPoint,
}; };
}; };
...@@ -6,7 +6,7 @@ import dateFormat from 'dateformat'; ...@@ -6,7 +6,7 @@ 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, tooltipTypes } from '../../constants'; import { chartHeight, lineTypes, lineWidths, dateFormats } from '../../constants';
import { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options'; import { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { annotationsYAxis, generateAnnotationsSeries } from './annotations'; import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
import { makeDataSeries } from '~/helpers/monitor_helper'; import { makeDataSeries } from '~/helpers/monitor_helper';
...@@ -20,7 +20,6 @@ const events = { ...@@ -20,7 +20,6 @@ const events = {
}; };
export default { export default {
tooltipTypes,
components: { components: {
GlAreaChart, GlAreaChart,
GlLineChart, GlLineChart,
...@@ -262,6 +261,21 @@ export default { ...@@ -262,6 +261,21 @@ export default {
isTooltipOfType(tooltipType, defaultType) { isTooltipOfType(tooltipType, defaultType) {
return tooltipType === defaultType; return tooltipType === defaultType;
}, },
/**
* This method is triggered when hovered over a single markPoint.
*
* The annotations title timestamp should match the data tooltip
* title.
*
* @params {Object} params markPoint object
* @returns {Object}
*/
formatAnnotationsTooltipText(params) {
return {
title: dateFormat(params.data?.tooltipData?.title, dateFormats.default),
content: params.data?.tooltipData?.content,
};
},
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 = [];
...@@ -270,15 +284,10 @@ export default { ...@@ -270,15 +284,10 @@ export default {
if (dataPoint.value) { if (dataPoint.value) {
const [, yVal] = dataPoint.value; const [, yVal] = dataPoint.value;
this.tooltip.type = dataPoint.name; this.tooltip.type = dataPoint.name;
if (this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.deployments)) { if (this.tooltip.type === 'deployments') {
const { data = {} } = dataPoint; const { data = {} } = dataPoint;
this.tooltip.sha = data?.tooltipData?.sha; this.tooltip.sha = data?.tooltipData?.sha;
this.tooltip.commitUrl = data?.tooltipData?.commitUrl; this.tooltip.commitUrl = data?.tooltipData?.commitUrl;
} else if (
this.isTooltipOfType(this.tooltip.type, this.$options.tooltipTypes.annotations)
) {
const { data } = dataPoint;
this.tooltip.content.push(data?.tooltipData?.description);
} else { } else {
const { seriesName, color, dataIndex } = dataPoint; const { seriesName, color, dataIndex } = dataPoint;
...@@ -356,6 +365,7 @@ export default { ...@@ -356,6 +365,7 @@ export default {
:data="chartData" :data="chartData"
:option="chartOptions" :option="chartOptions"
:format-tooltip-text="formatTooltipText" :format-tooltip-text="formatTooltipText"
:format-annotations-tooltip-text="formatAnnotationsTooltipText"
:thresholds="thresholds" :thresholds="thresholds"
:width="width" :width="width"
:height="height" :height="height"
...@@ -364,7 +374,7 @@ export default { ...@@ -364,7 +374,7 @@ export default {
@created="onChartCreated" @created="onChartCreated"
@updated="onChartUpdated" @updated="onChartUpdated"
> >
<template v-if="isTooltipOfType(tooltip.type, this.$options.tooltipTypes.deployments)"> <template v-if="tooltip.type === 'deployments'">
<template slot="tooltipTitle"> <template slot="tooltipTitle">
{{ __('Deployed') }} {{ __('Deployed') }}
</template> </template>
...@@ -373,16 +383,6 @@ export default { ...@@ -373,16 +383,6 @@ 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">
<div class="text-nowrap"> <div class="text-nowrap">
......
...@@ -120,10 +120,14 @@ export const NOT_IN_DB_PREFIX = 'NO_DB'; ...@@ -120,10 +120,14 @@ export const NOT_IN_DB_PREFIX = 'NO_DB';
export const ENVIRONMENT_AVAILABLE_STATE = 'available'; export const ENVIRONMENT_AVAILABLE_STATE = 'available';
/** /**
* Time series charts have different types of * As of %12.10, the svg icon library does not have an annotation
* tooltip based on the hovered data point. * arrow icon yet. In order to deliver annotations feature, the icon
* is hard coded until the icon is added. The below issue is
* to track the icon.
*
* https://gitlab.com/gitlab-org/gitlab-svgs/-/issues/118
*
* Once the icon is merged this can be removed.
* https://gitlab.com/gitlab-org/gitlab/-/issues/214540
*/ */
export const tooltipTypes = { export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z';
deployments: 'deployments',
annotations: 'annotations',
};
...@@ -54,6 +54,7 @@ describe('annotations spec', () => { ...@@ -54,6 +54,7 @@ describe('annotations spec', () => {
yAxisIndex: 1, yAxisIndex: 1,
data: expect.any(Array), data: expect.any(Array),
markLine: expect.any(Object), markLine: expect.any(Object),
markPoint: expect.any(Object),
}), }),
); );
...@@ -61,11 +62,12 @@ describe('annotations spec', () => { ...@@ -61,11 +62,12 @@ describe('annotations spec', () => {
expect(annotation).toEqual(expect.any(Object)); expect(annotation).toEqual(expect.any(Object));
}); });
expect(annotations.data).toHaveLength(annotationsData.length); expect(annotations.data).toHaveLength(0);
expect(annotations.markLine.data).toHaveLength(annotationsData.length); expect(annotations.markLine.data).toHaveLength(annotationsData.length);
expect(annotations.markPoint.data).toHaveLength(annotationsData.length);
}); });
it('when deploments and annotations data is passed', () => { it('when deployments and annotations data is passed', () => {
const annotations = generateAnnotationsSeries({ const annotations = generateAnnotationsSeries({
deployments: deploymentData, deployments: deploymentData,
annotations: annotationsData, annotations: annotationsData,
...@@ -77,6 +79,7 @@ describe('annotations spec', () => { ...@@ -77,6 +79,7 @@ describe('annotations spec', () => {
yAxisIndex: 1, yAxisIndex: 1,
data: expect.any(Array), data: expect.any(Array),
markLine: expect.any(Object), markLine: expect.any(Object),
markPoint: expect.any(Object),
}), }),
); );
...@@ -84,7 +87,9 @@ describe('annotations spec', () => { ...@@ -84,7 +87,9 @@ describe('annotations spec', () => {
expect(annotation).toEqual(expect.any(Object)); expect(annotation).toEqual(expect.any(Object));
}); });
expect(annotations.data).toHaveLength(deploymentData.length + annotationsData.length); expect(annotations.data).toHaveLength(deploymentData.length);
expect(annotations.markLine.data).toHaveLength(annotationsData.length);
expect(annotations.markPoint.data).toHaveLength(annotationsData.length);
}); });
}); });
}); });
...@@ -13,7 +13,7 @@ import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper'; ...@@ -13,7 +13,7 @@ import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import TimeSeries from '~/monitoring/components/charts/time_series.vue'; import TimeSeries from '~/monitoring/components/charts/time_series.vue';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { deploymentData, mockProjectDir } from '../../mock_data'; import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data';
import { import {
metricsDashboardPayload, metricsDashboardPayload,
metricsDashboardViewModel, metricsDashboardViewModel,
...@@ -278,6 +278,33 @@ describe('Time series component', () => { ...@@ -278,6 +278,33 @@ describe('Time series component', () => {
}); });
}); });
describe('formatAnnotationsTooltipText', () => {
const annotationsMetadata = {
name: 'annotations',
xAxis: annotationsData[0].from,
yAxis: 0,
tooltipData: {
title: '2020/02/19 10:01:41',
content: annotationsData[0].description,
},
};
const mockMarkPoint = {
componentType: 'markPoint',
name: 'annotations',
value: undefined,
data: annotationsMetadata,
};
it('formats tooltip title and sets tooltip content', () => {
const formattedTooltipData = timeSeriesChart.vm.formatAnnotationsTooltipText(
mockMarkPoint,
);
expect(formattedTooltipData.title).toBe('19 Feb 2020, 10:01AM');
expect(formattedTooltipData.content).toBe(annotationsMetadata.tooltipData.content);
});
});
describe('setSvg', () => { describe('setSvg', () => {
const mockSvgName = 'mockSvgName'; const mockSvgName = 'mockSvgName';
...@@ -380,6 +407,8 @@ describe('Time series component', () => { ...@@ -380,6 +407,8 @@ describe('Time series component', () => {
series: [ series: [
{ {
name: mockSeriesName, name: mockSeriesName,
type: 'line',
data: [],
}, },
], ],
}, },
......
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