Commit 2a515ee1 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'fetch-annotations-for-monitoring-dashboard' into 'master'

Fetch annotations for monitoring dashboard

See merge request gitlab-org/gitlab!28394
parents 85fc12e6 71403cdf
...@@ -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