diff options
Diffstat (limited to 'app/assets/javascripts/monitoring/stores')
9 files changed, 188 insertions, 69 deletions
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index a441882a47d..16a685305dc 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/browser'; import * as types from './mutation_types'; import axios from '~/lib/utils/axios_utils'; -import createFlash from '~/flash'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { gqClient, @@ -13,16 +13,14 @@ import trackDashboardLoad from '../monitoring_tracking_helper'; import getEnvironments from '../queries/getEnvironments.query.graphql'; import getAnnotations from '../queries/getAnnotations.query.graphql'; import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql'; -import statusCodes from '../../lib/utils/http_status'; -import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils'; +import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils'; import { s__, sprintf } from '../../locale'; +import { getDashboard, getPrometheusQueryData } from '../requests'; -import { - PROMETHEUS_TIMEOUT, - ENVIRONMENT_AVAILABLE_STATE, - DEFAULT_DASHBOARD_PATH, - VARIABLE_TYPES, -} from '../constants'; +import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants'; + +const axiosCancelToken = axios.CancelToken; +let cancelTokenSource; function prometheusMetricQueryParams(timeRange) { const { start, end } = convertToFixedRange(timeRange); @@ -38,29 +36,18 @@ function prometheusMetricQueryParams(timeRange) { }; } -function backOffRequest(makeRequestCallback) { - return backOff((next, stop) => { - makeRequestCallback() - .then(resp => { - if (resp.status === statusCodes.NO_CONTENT) { - next(); - } else { - stop(resp); - } - }) - .catch(stop); - }, PROMETHEUS_TIMEOUT); -} - -function getPrometheusQueryData(prometheusEndpoint, params) { - return backOffRequest(() => axios.get(prometheusEndpoint, { params })) - .then(res => res.data) - .then(response => { - if (response.status === 'error') { - throw new Error(response.error); - } - return response.data; - }); +/** + * Extract error messages from API or HTTP request errors. + * + * - API errors are in `error.response.data.message` + * - HTTP (axios) errors are in `error.messsage` + * + * @param {Object} error + * @returns {String} User friendly error message + */ +function extractErrorMessage(error) { + const message = error?.response?.data?.message; + return message ?? error.message; } // Setup @@ -126,8 +113,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => { params.dashboard = getters.fullDashboardPath; } - return backOffRequest(() => axios.get(state.dashboardEndpoint, { params })) - .then(resp => resp.data) + return getDashboard(state.dashboardEndpoint, params) .then(response => { dispatch('receiveMetricsDashboardSuccess', { response }); /** @@ -329,7 +315,7 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => { export const fetchAnnotations = ({ state, dispatch, getters }) => { const { start } = convertToFixedRange(state.timeRange); - const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH; + const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH; return gqClient .mutate({ mutation: getAnnotations, @@ -362,12 +348,12 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) => { /** - * Normally, the default dashboard won't throw any validation warnings. + * Normally, the overview dashboard won't throw any validation warnings. * - * However, if a bug sneaks into the default dashboard making it invalid, + * However, if a bug sneaks into the overview dashboard making it invalid, * this might come handy for our clients */ - const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH; + const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH; return gqClient .mutate({ mutation: getDashboardValidationWarnings, @@ -484,12 +470,10 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery if (variable.type === VARIABLE_TYPES.metric_label_values) { const { prometheusEndpointPath, label } = variable.options; - const optionsRequest = backOffRequest(() => - axios.get(prometheusEndpointPath, { - params: { start_time, end_time }, - }), - ) - .then(({ data }) => data.data) + const optionsRequest = getPrometheusQueryData(prometheusEndpointPath, { + start_time, + end_time, + }) .then(data => { commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data }); }) @@ -507,5 +491,59 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery return Promise.all(optionsRequests); }; -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; +// Panel Builder + +export const setPanelPreviewTimeRange = ({ commit }, timeRange) => { + commit(types.SET_PANEL_PREVIEW_TIME_RANGE, timeRange); +}; + +export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) => { + if (!panelPreviewYml) { + return null; + } + + commit(types.SET_PANEL_PREVIEW_IS_SHOWN, true); + commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml); + + return axios + .post(state.panelPreviewEndpoint, { panel_yaml: panelPreviewYml }) + .then(({ data }) => { + commit(types.RECEIVE_PANEL_PREVIEW_SUCCESS, data); + + dispatch('fetchPanelPreviewMetrics'); + }) + .catch(error => { + commit(types.RECEIVE_PANEL_PREVIEW_FAILURE, extractErrorMessage(error)); + }); +}; + +export const fetchPanelPreviewMetrics = ({ state, commit }) => { + if (cancelTokenSource) { + cancelTokenSource.cancel(); + } + cancelTokenSource = axiosCancelToken.source(); + + const defaultQueryParams = prometheusMetricQueryParams(state.panelPreviewTimeRange); + + state.panelPreviewGraphData.metrics.forEach((metric, index) => { + commit(types.REQUEST_PANEL_PREVIEW_METRIC_RESULT, { index }); + + const params = { ...defaultQueryParams }; + if (metric.step) { + params.step = metric.step; + } + return getPrometheusQueryData(metric.prometheusEndpointPath, params, { + cancelToken: cancelTokenSource.token, + }) + .then(data => { + commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data }); + }) + .catch(error => { + Sentry.captureException(error); + + commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE, { index, error }); + // Continue to throw error so the panel builder can notify using createFlash + throw error; + }); + }); +}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/actions.js b/app/assets/javascripts/monitoring/stores/embed_group/actions.js index cbe0950d954..4a7572bdbd9 100644 --- a/app/assets/javascripts/monitoring/stores/embed_group/actions.js +++ b/app/assets/javascripts/monitoring/stores/embed_group/actions.js @@ -1,5 +1,4 @@ import * as types from './mutation_types'; +// eslint-disable-next-line import/prefer-default-export export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data); - -export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js index 9b08cf762c1..096d8d03096 100644 --- a/app/assets/javascripts/monitoring/stores/embed_group/getters.js +++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js @@ -1,4 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export export const metricsWithData = (state, getters, rootState, rootGetters) => state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length); - -export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js index e7a425d3623..7fd3f0f8647 100644 --- a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js @@ -1,3 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export export const ADD_MODULE = 'ADD_MODULE'; - -export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js index 3aa711a0509..8ed83cf02fe 100644 --- a/app/assets/javascripts/monitoring/stores/getters.js +++ b/app/assets/javascripts/monitoring/stores/getters.js @@ -170,6 +170,3 @@ export const getCustomVariablesParams = state => */ export const fullDashboardPath = state => normalizeCustomDashboardPath(state.currentDashboard, state.customDashboardBasePath); - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index d408628fc4d..1d7279912cc 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -46,3 +46,17 @@ export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS'; export const SET_ENVIRONMENTS_FILTER = 'SET_ENVIRONMENTS_FILTER'; export const SET_EXPANDED_PANEL = 'SET_EXPANDED_PANEL'; + +// Panel preview +export const REQUEST_PANEL_PREVIEW = 'REQUEST_PANEL_PREVIEW'; +export const RECEIVE_PANEL_PREVIEW_SUCCESS = 'RECEIVE_PANEL_PREVIEW_SUCCESS'; +export const RECEIVE_PANEL_PREVIEW_FAILURE = 'RECEIVE_PANEL_PREVIEW_FAILURE'; + +export const REQUEST_PANEL_PREVIEW_METRIC_RESULT = 'REQUEST_PANEL_PREVIEW_METRIC_RESULT'; +export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS = + 'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS'; +export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE = + 'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE'; + +export const SET_PANEL_PREVIEW_TIME_RANGE = 'SET_PANEL_PREVIEW_TIME_RANGE'; +export const SET_PANEL_PREVIEW_IS_SHOWN = 'SET_PANEL_PREVIEW_IS_SHOWN'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 744441c8935..09a5861b475 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -1,9 +1,9 @@ import Vue from 'vue'; import { pick } from 'lodash'; import * as types from './mutation_types'; -import { mapToDashboardViewModel, normalizeQueryResponseData } from './utils'; +import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils'; import httpStatusCodes from '~/lib/utils/http_status'; -import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils'; +import { BACKOFF_TIMEOUT } from '~/lib/utils/common_utils'; import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants'; import { optionsFromSeriesData } from './variable_mapping'; @@ -53,6 +53,14 @@ const emptyStateFromError = error => { return metricStates.UNKNOWN_ERROR; }; +export const metricStateFromData = data => { + if (data?.result?.length) { + const result = normalizeQueryResponseData(data); + return { state: metricStates.OK, result: Object.freeze(result) }; + } + return { state: metricStates.NO_DATA, result: null }; +}; + export default { /** * Dashboard panels structure and global state @@ -154,17 +162,11 @@ export default { }, [types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, data }) { const metric = findMetricInDashboard(metricId, state.dashboard); - metric.loading = false; + const metricState = metricStateFromData(data); - if (!data.result || data.result.length === 0) { - metric.state = metricStates.NO_DATA; - metric.result = null; - } else { - const result = normalizeQueryResponseData(data); - - metric.state = metricStates.OK; - metric.result = Object.freeze(result); - } + metric.loading = false; + metric.state = metricState.state; + metric.result = metricState.result; }, [types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) { const metric = findMetricInDashboard(metricId, state.dashboard); @@ -218,4 +220,54 @@ export default { // Add new options with assign to ensure Vue reactivity Object.assign(variable.options, { values }); }, + + [types.REQUEST_PANEL_PREVIEW](state, panelPreviewYml) { + state.panelPreviewIsLoading = true; + + state.panelPreviewYml = panelPreviewYml; + state.panelPreviewGraphData = null; + state.panelPreviewError = null; + }, + [types.RECEIVE_PANEL_PREVIEW_SUCCESS](state, payload) { + state.panelPreviewIsLoading = false; + + state.panelPreviewGraphData = mapPanelToViewModel(payload); + state.panelPreviewError = null; + }, + [types.RECEIVE_PANEL_PREVIEW_FAILURE](state, error) { + state.panelPreviewIsLoading = false; + + state.panelPreviewGraphData = null; + state.panelPreviewError = error; + }, + + [types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](state, { index }) { + const metric = state.panelPreviewGraphData.metrics[index]; + + metric.loading = true; + if (!metric.result) { + metric.state = metricStates.LOADING; + } + }, + [types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS](state, { index, data }) { + const metric = state.panelPreviewGraphData.metrics[index]; + const metricState = metricStateFromData(data); + + metric.loading = false; + metric.state = metricState.state; + metric.result = metricState.result; + }, + [types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](state, { index, error }) { + const metric = state.panelPreviewGraphData.metrics[index]; + + metric.loading = false; + metric.state = emptyStateFromError(error); + metric.result = null; + }, + [types.SET_PANEL_PREVIEW_TIME_RANGE](state, timeRange) { + state.panelPreviewTimeRange = timeRange; + }, + [types.SET_PANEL_PREVIEW_IS_SHOWN](state, isPreviewShown) { + state.panelPreviewIsShown = isPreviewShown; + }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index 89738756ffe..ef8b1adb624 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -1,12 +1,14 @@ import invalidUrl from '~/lib/utils/invalid_url'; import { timezones } from '../format_date'; import { dashboardEmptyStates } from '../constants'; +import { defaultTimeRange } from '~/vue_shared/constants'; export default () => ({ // API endpoints deploymentsEndpoint: null, dashboardEndpoint: invalidUrl, dashboardsEndpoint: invalidUrl, + panelPreviewEndpoint: invalidUrl, // Dashboard request parameters timeRange: null, @@ -59,6 +61,15 @@ export default () => ({ * via the dashboard yml file. */ links: [], + + // Panel editor / builder + panelPreviewYml: '', + panelPreviewIsLoading: false, + panelPreviewGraphData: null, + panelPreviewError: null, + panelPreviewTimeRange: defaultTimeRange, + panelPreviewIsShown: false, + // Other project data dashboardTimezone: timezones.LOCAL, annotations: [], @@ -69,9 +80,11 @@ export default () => ({ currentEnvironmentName: null, // GitLab paths to other pages + externalDashboardUrl: '', projectPath: null, operationsSettingsPath: '', logsPath: invalidUrl, + addDashboardDocumentationPath: '', // static paths customDashboardBasePath: '', diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 51562593ee8..df7f22e622f 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -176,7 +176,11 @@ export const mapPanelToViewModel = ({ field, metrics = [], links = [], + min_value, max_value, + split, + thresholds, + format, }) => { // Both `x_axis.name` and `x_label` are supported for now // https://gitlab.com/gitlab-org/gitlab/issues/210521 @@ -195,7 +199,11 @@ export const mapPanelToViewModel = ({ yAxis, xAxis, field, + minValue: min_value, maxValue: max_value, + split, + thresholds, + format, links: links.map(mapLinksToViewModel), metrics: mapToMetricsViewModel(metrics), }; @@ -465,9 +473,9 @@ export const addPrefixToCustomVariableParams = name => `variables[${name}]`; * metrics dashboard to work with custom dashboard file names instead * of the entire path. * - * If dashboard is empty, it is the default dashboard. + * If dashboard is empty, it is the overview dashboard. * If dashboard is set, it usually is a custom dashboard unless - * explicitly it is set to default dashboard path. + * explicitly it is set to overview dashboard path. * * @param {String} dashboard dashboard path * @param {String} dashboardPrefix custom dashboard directory prefix |