Commit 1f05f317 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'update-annotations-parse-config' into 'master'

Update annotations configs to add arrows

See merge request gitlab-org/gitlab!29392
parents 59634d7c 0570f4b2
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