Commit 4672e98f authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla

Update annotations fetch graphql query

This MR updates annotations fetch graphql
query on the FE and the response parsing
logic. This MR also updates all the specs
parent ee73f5f7
...@@ -45,9 +45,9 @@ export const annotationsYAxis = { ...@@ -45,9 +45,9 @@ export const annotationsYAxis = {
* Fetched list of annotations are parsed into a * Fetched list of annotations are parsed into a
* format the eCharts accepts to draw markLines * format the eCharts accepts to draw markLines
* *
* If Annotation is a single line, the `starting_at` property * If Annotation is a single line, the `startingAt` property
* has a value and the `ending_at` is null. Because annotations * has a value and the `endingAt` is null. Because annotations
* only supports lines the `ending_at` value does not exist yet. * only supports lines the `endingAt` value does not exist yet.
* *
* @param {Object} annotation object * @param {Object} annotation object
* @returns {Object} markLine object * @returns {Object} markLine object
...@@ -56,7 +56,7 @@ export const parseAnnotations = annotations => ...@@ -56,7 +56,7 @@ export const parseAnnotations = annotations =>
annotations.reduce( annotations.reduce(
(acc, annotation) => { (acc, annotation) => {
acc.lines.push({ acc.lines.push({
xAxis: annotation.starting_at, xAxis: annotation.startingAt,
lineStyle: { lineStyle: {
color: colorValues.primaryColor, color: colorValues.primaryColor,
}, },
...@@ -64,10 +64,10 @@ export const parseAnnotations = annotations => ...@@ -64,10 +64,10 @@ export const parseAnnotations = annotations =>
acc.points.push({ acc.points.push({
name: 'annotations', name: 'annotations',
xAxis: annotation.starting_at, xAxis: annotation.startingAt,
yAxis: annotationsYAxisCoords.min, yAxis: annotationsYAxisCoords.min,
tooltipData: { tooltipData: {
title: annotation.starting_at, title: annotation.startingAt,
content: annotation.description, content: annotation.description,
}, },
}); });
......
...@@ -131,3 +131,15 @@ export const ENVIRONMENT_AVAILABLE_STATE = 'available'; ...@@ -131,3 +131,15 @@ export const ENVIRONMENT_AVAILABLE_STATE = 'available';
* https://gitlab.com/gitlab-org/gitlab/-/issues/214540 * https://gitlab.com/gitlab-org/gitlab/-/issues/214540
*/ */
export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z'; export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z';
/**
* As of %12.10, dashboard path is required to create annotation.
* The FE gets the dashboard name from the URL params. It is not
* ideal to store the path this way but there is no other way to
* get this path unless annotations fetch is delayed. This could
* potentially be removed and have the backend send this to the FE.
*
* This technical debt is being tracked here
* https://gitlab.com/gitlab-org/gitlab/-/issues/214671
*/
export const DEFAULT_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
query getAnnotations($projectPath: ID!) { query getAnnotations(
environment(name: $environmentName) { $projectPath: ID!
metricDashboard(id: $dashboardId) { $environmentName: String
annotations: nodes { $dashboardPath: String!
$startingFrom: Time!
) {
project(fullPath: $projectPath) {
environments(name: $environmentName) {
nodes {
id
name
metricsDashboard(path: $dashboardPath) {
annotations(from: $startingFrom) {
nodes {
id id
description description
starting_at startingAt
ending_at endingAt
panelId panelId
} }
} }
} }
}
}
}
} }
...@@ -3,7 +3,12 @@ import * as types from './mutation_types'; ...@@ -3,7 +3,12 @@ import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils'; import {
gqClient,
parseEnvironmentsResponse,
parseAnnotationsResponse,
removeLeadingSlash,
} from './utils';
import trackDashboardLoad from '../monitoring_tracking_helper'; import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql'; import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql'; import getAnnotations from '../queries/getAnnotations.query.graphql';
...@@ -15,7 +20,11 @@ import { ...@@ -15,7 +20,11 @@ import {
} from '../../lib/utils/common_utils'; } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants'; import {
PROMETHEUS_TIMEOUT,
ENVIRONMENT_AVAILABLE_STATE,
DEFAULT_DASHBOARD_PATH,
} from '../constants';
function prometheusMetricQueryParams(timeRange) { function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange); const { start, end } = convertToFixedRange(timeRange);
...@@ -283,16 +292,21 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => { ...@@ -283,16 +292,21 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
}; };
export const fetchAnnotations = ({ state, dispatch }) => { export const fetchAnnotations = ({ state, dispatch }) => {
const { start } = convertToFixedRange(state.timeRange);
const dashboardPath =
state.currentDashboard === '' ? DEFAULT_DASHBOARD_PATH : state.currentDashboard;
return gqClient return gqClient
.mutate({ .mutate({
mutation: getAnnotations, mutation: getAnnotations,
variables: { variables: {
projectPath: removeLeadingSlash(state.projectPath), projectPath: removeLeadingSlash(state.projectPath),
dashboardId: state.currentDashboard,
environmentName: state.currentEnvironmentName, environmentName: state.currentEnvironmentName,
dashboardPath,
startingFrom: start,
}, },
}) })
.then(resp => resp.data?.project?.environment?.metricDashboard?.annotations) .then(resp => resp.data?.project?.environments?.nodes?.[0].metricsDashboard?.annotations.nodes)
.then(parseAnnotationsResponse)
.then(annotations => { .then(annotations => {
if (!annotations) { if (!annotations) {
createFlash(s__('Metrics|There was an error fetching annotations. Please try again.')); createFlash(s__('Metrics|There was an error fetching annotations. Please try again.'));
......
...@@ -57,6 +57,31 @@ export const parseEnvironmentsResponse = (response = [], projectPath) => ...@@ -57,6 +57,31 @@ export const parseEnvironmentsResponse = (response = [], projectPath) =>
}; };
}); });
/**
* Annotation API returns time in UTC. This method
* converts time to local time.
*
* startingAt always exists but endingAt does not.
* If endingAt does not exist, a threshold line is
* drawn.
*
* If endingAt exists, a threshold range is drawn.
* But this is not supported as of %12.10
*
* @param {Array} response annotations response
* @returns {Array} parsed responses
*/
export const parseAnnotationsResponse = response => {
if (!response) {
return [];
}
return response.map(annotation => ({
...annotation,
startingAt: new Date(annotation.startingAt),
endingAt: annotation.endingAt ? new Date(annotation.endingAt) : null,
}));
};
/** /**
* Maps metrics to its view model * Maps metrics to its view model
* *
......
...@@ -247,23 +247,23 @@ export const deploymentData = [ ...@@ -247,23 +247,23 @@ export const deploymentData = [
export const annotationsData = [ export const annotationsData = [
{ {
id: 'gid://gitlab/Metrics::Dashboard::Annotation/1', id: 'gid://gitlab/Metrics::Dashboard::Annotation/1',
starting_at: '2020-04-01T12:51:58.373Z', startingAt: '2020-04-12 12:51:53 UTC',
ending_at: null, endingAt: null,
panelId: null, panelId: null,
description: 'This is a test annotation', description: 'This is a test annotation',
}, },
{ {
id: 'gid://gitlab/Metrics::Dashboard::Annotation/2', id: 'gid://gitlab/Metrics::Dashboard::Annotation/2',
description: 'test annotation 2', description: 'test annotation 2',
starting_at: '2020-04-02T12:51:58.373Z', startingAt: '2020-04-13 12:51:53 UTC',
ending_at: null, endingAt: null,
panelId: null, panelId: null,
}, },
{ {
id: 'gid://gitlab/Metrics::Dashboard::Annotation/3', id: 'gid://gitlab/Metrics::Dashboard::Annotation/3',
description: 'test annotation 3', description: 'test annotation 3',
starting_at: '2020-04-04T12:51:58.373Z', startingAt: '2020-04-16 12:51:53 UTC',
ending_at: null, endingAt: null,
panelId: null, panelId: null,
}, },
]; ];
......
...@@ -23,7 +23,11 @@ import { ...@@ -23,7 +23,11 @@ import {
setGettingStartedEmptyState, setGettingStartedEmptyState,
duplicateSystemDashboard, duplicateSystemDashboard,
} from '~/monitoring/stores/actions'; } from '~/monitoring/stores/actions';
import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils'; import {
gqClient,
parseEnvironmentsResponse,
parseAnnotationsResponse,
} from '~/monitoring/stores/utils';
import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql'; import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql'; import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
import storeState from '~/monitoring/stores/state'; import storeState from '~/monitoring/stores/state';
...@@ -224,6 +228,10 @@ describe('Monitoring store actions', () => { ...@@ -224,6 +228,10 @@ describe('Monitoring store actions', () => {
describe('fetchAnnotations', () => { describe('fetchAnnotations', () => {
const { state } = store; const { state } = store;
state.timeRange = {
start: '2020-04-15T12:54:32.137Z',
end: '2020-08-15T12:54:32.137Z',
};
state.projectPath = 'gitlab-org/gitlab-test'; state.projectPath = 'gitlab-org/gitlab-test';
state.currentEnvironmentName = 'production'; state.currentEnvironmentName = 'production';
state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml'; state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
...@@ -239,19 +247,27 @@ describe('Monitoring store actions', () => { ...@@ -239,19 +247,27 @@ describe('Monitoring store actions', () => {
variables: { variables: {
projectPath: state.projectPath, projectPath: state.projectPath,
environmentName: state.currentEnvironmentName, environmentName: state.currentEnvironmentName,
dashboardId: state.currentDashboard, dashboardPath: state.currentDashboard,
startingFrom: state.timeRange.start,
}, },
}; };
const parsedResponse = parseAnnotationsResponse(annotationsData);
mockMutate.mockResolvedValue({ mockMutate.mockResolvedValue({
data: { data: {
project: { project: {
environment: { environments: {
metricDashboard: { nodes: [
annotations: annotationsData, {
metricsDashboard: {
annotations: {
nodes: parsedResponse,
}, },
}, },
}, },
],
},
},
}, },
}); });
...@@ -260,7 +276,7 @@ describe('Monitoring store actions', () => { ...@@ -260,7 +276,7 @@ describe('Monitoring store actions', () => {
null, null,
state, state,
[], [],
[{ type: 'receiveAnnotationsSuccess', payload: annotationsData }], [{ type: 'receiveAnnotationsSuccess', payload: parsedResponse }],
() => { () => {
expect(mockMutate).toHaveBeenCalledWith(mutationVariables); expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
}, },
...@@ -274,7 +290,8 @@ describe('Monitoring store actions', () => { ...@@ -274,7 +290,8 @@ describe('Monitoring store actions', () => {
variables: { variables: {
projectPath: state.projectPath, projectPath: state.projectPath,
environmentName: state.currentEnvironmentName, environmentName: state.currentEnvironmentName,
dashboardId: state.currentDashboard, dashboardPath: state.currentDashboard,
startingFrom: state.timeRange.start,
}, },
}; };
......
...@@ -2,9 +2,11 @@ import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; ...@@ -2,9 +2,11 @@ import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { import {
uniqMetricsId, uniqMetricsId,
parseEnvironmentsResponse, parseEnvironmentsResponse,
parseAnnotationsResponse,
removeLeadingSlash, removeLeadingSlash,
mapToDashboardViewModel, mapToDashboardViewModel,
} from '~/monitoring/stores/utils'; } from '~/monitoring/stores/utils';
import { annotationsData } from '../mock_data';
import { NOT_IN_DB_PREFIX } from '~/monitoring/constants'; import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
const projectPath = 'gitlab-org/gitlab-test'; const projectPath = 'gitlab-org/gitlab-test';
...@@ -376,6 +378,27 @@ describe('parseEnvironmentsResponse', () => { ...@@ -376,6 +378,27 @@ describe('parseEnvironmentsResponse', () => {
}); });
}); });
describe('parseAnnotationsResponse', () => {
const parsedAnnotationResponse = [
{
description: 'This is a test annotation',
endingAt: null,
id: 'gid://gitlab/Metrics::Dashboard::Annotation/1',
panelId: null,
startingAt: new Date('2020-04-12T12:51:53.000Z'),
},
];
it.each`
case | input | expected
${'Returns empty array for null input'} | ${null} | ${[]}
${'Returns empty array for undefined input'} | ${undefined} | ${[]}
${'Returns empty array for empty input'} | ${[]} | ${[]}
${'Returns parsed responses for annotations data'} | ${[annotationsData[0]]} | ${parsedAnnotationResponse}
`('$case', ({ input, expected }) => {
expect(parseAnnotationsResponse(input)).toEqual(expected);
});
});
describe('removeLeadingSlash', () => { describe('removeLeadingSlash', () => {
[ [
{ input: null, output: '' }, { input: null, output: '' },
......
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