Commit 71403cdf authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla Committed by Kushal Pandya

Fetch annotations for monitoring charts

Monitoring dashboard fetches all annotations
associated to it. This change is behind
metrics_dashboard_annotations feature flag until
the backend is implemented
parent 46e43451
...@@ -910,3 +910,18 @@ export const setCookie = (name, value) => Cookies.set(name, value, { expires: 36 ...@@ -910,3 +910,18 @@ export const setCookie = (name, value) => Cookies.set(name, value, { expires: 36
export const getCookie = name => Cookies.get(name); export const getCookie = name => Cookies.get(name);
export const removeCookie = name => Cookies.remove(name); export const removeCookie = name => Cookies.remove(name);
/**
* Returns the status of a feature flag.
* Currently, there is no way to access feature
* flags in Vuex other than directly tapping into
* window.gon.
*
* This should only be used on Vuex. If feature flags
* need to be accessed in Vue components consider
* using the Vue feature flag mixin.
*
* @param {String} flag Feature flag
* @returns {Boolean} on/off
*/
export const isFeatureFlagEnabled = flag => window.gon.features?.[flag];
...@@ -55,6 +55,11 @@ export default { ...@@ -55,6 +55,11 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
annotations: {
type: Array,
required: false,
default: () => [],
},
projectPath: { projectPath: {
type: String, type: String,
required: false, required: false,
...@@ -143,6 +148,7 @@ export default { ...@@ -143,6 +148,7 @@ export default {
return (this.option.series || []).concat( return (this.option.series || []).concat(
generateAnnotationsSeries({ generateAnnotationsSeries({
deployments: this.recentDeployments, deployments: this.recentDeployments,
annotations: this.annotations,
}), }),
); );
}, },
......
...@@ -213,7 +213,6 @@ export default { ...@@ -213,7 +213,6 @@ export default {
'dashboard', 'dashboard',
'emptyState', 'emptyState',
'showEmptyState', 'showEmptyState',
'deploymentData',
'useDashboardEndpoint', 'useDashboardEndpoint',
'allDashboards', 'allDashboards',
'additionalPanelTypesEnabled', 'additionalPanelTypesEnabled',
......
...@@ -89,6 +89,9 @@ export default { ...@@ -89,6 +89,9 @@ export default {
deploymentData(state) { deploymentData(state) {
return state[this.namespace].deploymentData; return state[this.namespace].deploymentData;
}, },
annotations(state) {
return state[this.namespace].annotations;
},
projectPath(state) { projectPath(state) {
return state[this.namespace].projectPath; return state[this.namespace].projectPath;
}, },
...@@ -310,6 +313,7 @@ export default { ...@@ -310,6 +313,7 @@ export default {
ref="timeChart" ref="timeChart"
:graph-data="graphData" :graph-data="graphData"
:deployment-data="deploymentData" :deployment-data="deploymentData"
:annotations="annotations"
:project-path="projectPath" :project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)" :thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId" :group-id="groupId"
......
query getAnnotations($projectPath: ID!) {
environment(name: $environmentName) {
metricDashboard(id: $dashboardId) {
annotations: nodes {
id
description
from
to
panelId
}
}
}
}
...@@ -6,8 +6,13 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range'; ...@@ -6,8 +6,13 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils'; import { gqClient, parseEnvironmentsResponse, 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 statusCodes from '../../lib/utils/http_status'; import statusCodes from '../../lib/utils/http_status';
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils'; import {
backOff,
convertObjectPropsToCamelCase,
isFeatureFlagEnabled,
} 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 } from '../constants';
...@@ -80,6 +85,14 @@ export const setShowErrorBanner = ({ commit }, enabled) => { ...@@ -80,6 +85,14 @@ export const setShowErrorBanner = ({ commit }, enabled) => {
export const fetchData = ({ dispatch }) => { export const fetchData = ({ dispatch }) => {
dispatch('fetchEnvironmentsData'); dispatch('fetchEnvironmentsData');
dispatch('fetchDashboard'); dispatch('fetchDashboard');
/**
* Annotations data is not yet fetched. This will be
* ready after the BE piece is implemented.
* https://gitlab.com/gitlab-org/gitlab/-/issues/211330
*/
if (isFeatureFlagEnabled('metrics_dashboard_annotations')) {
dispatch('fetchAnnotations');
}
}; };
// Metrics dashboard // Metrics dashboard
...@@ -269,6 +282,40 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => { ...@@ -269,6 +282,40 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE); commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
}; };
export const fetchAnnotations = ({ state, dispatch }) => {
dispatch('requestAnnotations');
return gqClient
.mutate({
mutation: getAnnotations,
variables: {
projectPath: removeLeadingSlash(state.projectPath),
dashboardId: state.currentDashboard,
environmentName: state.currentEnvironmentName,
},
})
.then(resp => resp.data?.project?.environment?.metricDashboard?.annotations)
.then(annotations => {
if (!annotations) {
createFlash(s__('Metrics|There was an error fetching annotations. Please try again.'));
}
dispatch('receiveAnnotationsSuccess', annotations);
})
.catch(err => {
Sentry.captureException(err);
dispatch('receiveAnnotationsFailure');
createFlash(s__('Metrics|There was an error getting annotations information.'));
});
};
// While this commit does not update the state it will
// eventually be useful to show a loading state
export const requestAnnotations = ({ commit }) => commit(types.REQUEST_ANNOTATIONS);
export const receiveAnnotationsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_ANNOTATIONS_SUCCESS, data);
export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_ANNOTATIONS_FAILURE);
// Dashboard manipulation // Dashboard manipulation
/** /**
......
...@@ -3,6 +3,11 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD'; ...@@ -3,6 +3,11 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS'; export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE'; export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
// Annotations
export const REQUEST_ANNOTATIONS = 'REQUEST_ANNOTATIONS';
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
export const RECEIVE_ANNOTATIONS_FAILURE = 'RECEIVE_ANNOTATIONS_FAILURE';
// Git project deployments // Git project deployments
export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA'; export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA';
export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS'; export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS';
......
...@@ -92,6 +92,16 @@ export default { ...@@ -92,6 +92,16 @@ export default {
state.environments = []; state.environments = [];
}, },
/**
* Annotations
*/
[types.RECEIVE_ANNOTATIONS_SUCCESS](state, annotations) {
state.annotations = annotations;
},
[types.RECEIVE_ANNOTATIONS_FAILURE](state) {
state.annotations = [];
},
/** /**
* Individual panel/metric results * Individual panel/metric results
*/ */
......
...@@ -20,6 +20,7 @@ export default () => ({ ...@@ -20,6 +20,7 @@ export default () => ({
allDashboards: [], allDashboards: [],
// Other project data // Other project data
annotations: [],
deploymentData: [], deploymentData: [],
environments: [], environments: [],
environmentsSearchTerm: '', environmentsSearchTerm: '',
......
...@@ -14,6 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -14,6 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? } before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts) push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:metrics_dashboard_annotations)
end end
after_action :expire_etag_cache, only: [:cancel_auto_stop] after_action :expire_etag_cache, only: [:cancel_auto_stop]
......
...@@ -12941,9 +12941,15 @@ msgstr "" ...@@ -12941,9 +12941,15 @@ msgstr ""
msgid "Metrics|There was an error creating the dashboard. %{error}" msgid "Metrics|There was an error creating the dashboard. %{error}"
msgstr "" msgstr ""
msgid "Metrics|There was an error fetching annotations. Please try again."
msgstr ""
msgid "Metrics|There was an error fetching the environments data, please try again" msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr "" msgstr ""
msgid "Metrics|There was an error getting annotations information."
msgstr ""
msgid "Metrics|There was an error getting deployment information." msgid "Metrics|There was an error getting deployment information."
msgstr "" msgstr ""
......
...@@ -50,6 +50,7 @@ describe('Time series component', () => { ...@@ -50,6 +50,7 @@ describe('Time series component', () => {
propsData: { propsData: {
graphData: { ...graphData, type }, graphData: { ...graphData, type },
deploymentData: store.state.monitoringDashboard.deploymentData, deploymentData: store.state.monitoringDashboard.deploymentData,
annotations: store.state.monitoringDashboard.annotations,
projectPath: `${mockHost}${mockProjectDir}`, projectPath: `${mockHost}${mockProjectDir}`,
}, },
store, store,
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
fetchDeploymentsData, fetchDeploymentsData,
fetchEnvironmentsData, fetchEnvironmentsData,
fetchDashboardData, fetchDashboardData,
fetchAnnotations,
fetchPrometheusMetric, fetchPrometheusMetric,
setInitialState, setInitialState,
filterEnvironments, filterEnvironments,
...@@ -24,10 +25,12 @@ import { ...@@ -24,10 +25,12 @@ import {
} from '~/monitoring/stores/actions'; } from '~/monitoring/stores/actions';
import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils'; import { gqClient, parseEnvironmentsResponse } 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 storeState from '~/monitoring/stores/state'; import storeState from '~/monitoring/stores/state';
import { import {
deploymentData, deploymentData,
environmentData, environmentData,
annotationsData,
metricsDashboardResponse, metricsDashboardResponse,
metricsDashboardViewModel, metricsDashboardViewModel,
dashboardGitResponse, dashboardGitResponse,
...@@ -120,17 +123,15 @@ describe('Monitoring store actions', () => { ...@@ -120,17 +123,15 @@ describe('Monitoring store actions', () => {
}); });
it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => { it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue( jest.spyOn(gqClient, 'mutate').mockReturnValue({
Promise.resolve({ data: {
data: { project: {
project: { data: {
data: { environments: [],
environments: [],
},
}, },
}, },
}), },
); });
return testAction( return testAction(
filterEnvironments, filterEnvironments,
...@@ -180,17 +181,15 @@ describe('Monitoring store actions', () => { ...@@ -180,17 +181,15 @@ describe('Monitoring store actions', () => {
}); });
it('dispatches receiveEnvironmentsDataSuccess on success', () => { it('dispatches receiveEnvironmentsDataSuccess on success', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue( jest.spyOn(gqClient, 'mutate').mockResolvedValue({
Promise.resolve({ data: {
data: { project: {
project: { data: {
data: { environments: environmentData,
environments: environmentData,
},
}, },
}, },
}), },
); });
return testAction( return testAction(
fetchEnvironmentsData, fetchEnvironmentsData,
...@@ -208,7 +207,7 @@ describe('Monitoring store actions', () => { ...@@ -208,7 +207,7 @@ describe('Monitoring store actions', () => {
}); });
it('dispatches receiveEnvironmentsDataFailure on error', () => { it('dispatches receiveEnvironmentsDataFailure on error', () => {
jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject()); jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
return testAction( return testAction(
fetchEnvironmentsData, fetchEnvironmentsData,
...@@ -220,6 +219,80 @@ describe('Monitoring store actions', () => { ...@@ -220,6 +219,80 @@ describe('Monitoring store actions', () => {
}); });
}); });
describe('fetchAnnotations', () => {
const { state } = store;
state.projectPath = 'gitlab-org/gitlab-test';
state.currentEnvironmentName = 'production';
state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
afterEach(() => {
resetStore(store);
});
it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
const mockMutate = jest.spyOn(gqClient, 'mutate');
const mutationVariables = {
mutation: getAnnotations,
variables: {
projectPath: state.projectPath,
environmentName: state.currentEnvironmentName,
dashboardId: state.currentDashboard,
},
};
mockMutate.mockResolvedValue({
data: {
project: {
environment: {
metricDashboard: {
annotations: annotationsData,
},
},
},
},
});
return testAction(
fetchAnnotations,
null,
state,
[],
[
{ type: 'requestAnnotations' },
{ type: 'receiveAnnotationsSuccess', payload: annotationsData },
],
() => {
expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
},
);
});
it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
const mockMutate = jest.spyOn(gqClient, 'mutate');
const mutationVariables = {
mutation: getAnnotations,
variables: {
projectPath: state.projectPath,
environmentName: state.currentEnvironmentName,
dashboardId: state.currentDashboard,
},
};
mockMutate.mockRejectedValue({});
return testAction(
fetchAnnotations,
null,
state,
[],
[{ type: 'requestAnnotations' }, { type: 'receiveAnnotationsFailure' }],
() => {
expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
},
);
});
});
describe('Set initial state', () => { describe('Set initial state', () => {
let mockedState; let mockedState;
beforeEach(() => { beforeEach(() => {
......
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