summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/monitoring
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-17 15:08:15 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-17 15:08:15 +0000
commitc2b98d3dbd47ab92c79c702276fe9130d9a28036 (patch)
treebf4071f551fdc12c22b23b2bb66483064e7b9ea9 /app/assets/javascripts/monitoring
parentbadb9c1deacbea601b02f88811b7e123589d9251 (diff)
downloadgitlab-ce-c2b98d3dbd47ab92c79c702276fe9130d9a28036.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/monitoring')
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue50
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue105
-rw-r--r--app/assets/javascripts/monitoring/constants.js18
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js30
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js32
8 files changed, 202 insertions, 42 deletions
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 2a9321f6733..c1ca5449ba3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -20,8 +20,10 @@ import invalidUrl from '~/lib/utils/invalid_url';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
+import GroupEmptyState from './group_empty_state.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, isValidDate, getAddMetricTrackingOptions } from '../utils';
+import { metricStates } from '../constants';
export default {
components: {
@@ -29,6 +31,7 @@ export default {
PanelType,
GraphGroup,
EmptyState,
+ GroupEmptyState,
Icon,
GlButton,
GlDropdown,
@@ -184,7 +187,7 @@ export default {
'allDashboards',
'additionalPanelTypesEnabled',
]),
- ...mapGetters('monitoringDashboard', ['metricsWithData']),
+ ...mapGetters('monitoringDashboard', ['getMetricStates']),
firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
@@ -284,12 +287,35 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
- groupHasData(group) {
- return this.metricsWithData(group.key).length > 0;
- },
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
},
+ /**
+ * Return a single empty state for a group.
+ *
+ * If all states are the same a single state is returned to be displayed
+ * Except if the state is OK, in which case the group is displayed.
+ *
+ * @param {String} groupKey - Identifier for group
+ * @returns {String} state code from `metricStates`
+ */
+ groupSingleEmptyState(groupKey) {
+ const states = this.getMetricStates(groupKey);
+ if (states.length === 1 && states[0] !== metricStates.OK) {
+ return states[0];
+ }
+ return null;
+ },
+ /**
+ * A group should be not collapsed if any metric is loaded (OK)
+ *
+ * @param {String} groupKey - Identifier for group
+ * @returns {Boolean} If the group should be collapsed
+ */
+ collapseGroup(groupKey) {
+ // Collapse group if no data is available
+ return !this.getMetricStates(groupKey).includes(metricStates.OK);
+ },
getAddMetricTrackingOptions,
},
addMetric: {
@@ -446,9 +472,9 @@ export default {
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
- :collapse-group="!groupHasData(groupData)"
+ :collapse-group="collapseGroup(groupData.key)"
>
- <div v-if="groupHasData(groupData)">
+ <div v-if="!groupSingleEmptyState(groupData.key)">
<vue-draggable
:value="groupData.panels"
group="metrics-dashboard"
@@ -487,18 +513,12 @@ export default {
</vue-draggable>
</div>
<div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
- <empty-state
+ <group-empty-state
ref="empty-group"
- selected-state="noDataGroup"
:documentation-path="documentationPath"
:settings-path="settingsPath"
- :clusters-path="clustersPath"
- :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
- :empty-loading-svg-path="emptyLoadingSvgPath"
- :empty-no-data-svg-path="emptyNoDataSvgPath"
- :empty-no-data-small-svg-path="emptyNoDataSmallSvgPath"
- :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
- :compact="true"
+ :selected-state="groupSingleEmptyState(groupData.key)"
+ :svg-path="emptyNoDataSmallSvgPath"
/>
</div>
</graph-group>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 728910dd633..d3157b731b2 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -84,11 +84,6 @@ export default {
secondaryButtonText: '',
secondaryButtonPath: '',
},
- noDataGroup: {
- svgUrl: this.emptyNoDataSmallSvgPath,
- title: __('No data to display'),
- description: __('The data source is connected, but there is no data to display.'),
- },
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: __('Unable to connect to Prometheus server'),
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
new file mode 100644
index 00000000000..dee4e5998ee
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -0,0 +1,105 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlEmptyState } from '@gitlab/ui';
+import { metricStates } from '../constants';
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ documentationPath: {
+ type: String,
+ required: true,
+ },
+ settingsPath: {
+ type: String,
+ required: true,
+ },
+ selectedState: {
+ type: String,
+ required: true,
+ },
+ svgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ const documentationLink = `<a href="${this.documentationPath}">${__('More information')}</a>`;
+ return {
+ states: {
+ [metricStates.NO_DATA]: {
+ title: __('No data to display'),
+ slottedDescription: sprintf(
+ __(
+ 'The data source is connected, but there is no data to display. %{documentationLink}',
+ ),
+ { documentationLink },
+ false,
+ ),
+ },
+ [metricStates.TIMEOUT]: {
+ title: __('Connection timed out'),
+ slottedDescription: sprintf(
+ __(
+ "Charts can't be displayed as the request for data has timed out. %{documentationLink}",
+ ),
+ { documentationLink },
+ false,
+ ),
+ },
+ [metricStates.CONNECTION_FAILED]: {
+ title: __('Connection failed'),
+ description: __(`We couldn't reach the Prometheus server.
+ Either the server no longer exists or the configuration details need updating.`),
+ buttonText: __('Verify configuration'),
+ buttonPath: this.settingsPath,
+ },
+ [metricStates.BAD_QUERY]: {
+ title: __('Query cannot be processed'),
+ slottedDescription: sprintf(
+ __(
+ `The Prometheus server responded with "bad request".
+ Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}`,
+ ),
+ { documentationLink },
+ false,
+ ),
+ buttonText: __('Verify configuration'),
+ buttonPath: this.settingsPath,
+ },
+ [metricStates.LOADING]: {
+ title: __('Waiting for performance data'),
+ description: __(`Creating graphs uses the data from the Prometheus server.
+ If this takes a long time, ensure that data is available.`),
+ },
+ [metricStates.UNKNOWN_ERROR]: {
+ title: __('An error has occurred'),
+ description: __('An error occurred while loading the data. Please try again.'),
+ },
+ },
+ };
+ },
+ computed: {
+ currentState() {
+ return this.states[this.selectedState] || this.states[metricStates.UNKNOWN_ERROR];
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="currentState.title"
+ :primary-button-text="currentState.buttonText"
+ :primary-button-link="currentState.buttonPath"
+ :description="currentState.description"
+ :svg-path="svgPath"
+ :compact="true"
+ >
+ <template v-if="currentState.slottedDescription" #description>
+ <div v-html="currentState.slottedDescription"></div>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index e613351e524..398b45b9012 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -3,9 +3,19 @@ import { __ } from '~/locale';
export const PROMETHEUS_TIMEOUT = 120000; // TWO_MINUTES
/**
- * Errors in Prometheus Queries (PromQL) for metrics
+ * States and error states in Prometheus Queries (PromQL) for metrics
*/
-export const metricsErrors = {
+export const metricStates = {
+ /**
+ * Metric data is available
+ */
+ OK: 'OK',
+
+ /**
+ * Metric data is being fetched
+ */
+ LOADING: 'LOADING',
+
/**
* Connection timed out to prometheus server
* the timeout is set to PROMETHEUS_TIMEOUT
@@ -24,12 +34,12 @@ export const metricsErrors = {
CONNECTION_FAILED: 'CONNECTION_FAILED',
/**
- * The prometheus server was reach but it cannot process
+ * The prometheus server was reached but it cannot process
* the query. This can happen for several reasons:
* - PromQL syntax is incorrect
* - An operator is not supported
*/
- BAD_DATA: 'BAD_DATA',
+ BAD_QUERY: 'BAD_QUERY',
/**
* No specific reason found for error
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index a655191b2b4..1cb82ce0083 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -132,7 +132,7 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metric_id, result });
})
.catch(error => {
- commit(types.RECEIVE_METRIC_RESULT_ERROR, { metricId: metric.metric_id, error });
+ commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metric_id, error });
// Continue to throw error so the dashboard can notify using createFlash
throw error;
});
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index 3eddd52705d..a13157c6f87 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -2,6 +2,36 @@ const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
/**
+ * Get all state for metric in the dashboard or a group. The
+ * states are not repeated so the dashboard or group can show
+ * a global state.
+ *
+ * @param {Object} state
+ * @returns {Function} A function that returns an array of
+ * states in all the metric in the dashboard or group.
+ */
+export const getMetricStates = state => groupKey => {
+ let groups = state.dashboard.panel_groups;
+ if (groupKey) {
+ groups = groups.filter(group => group.key === groupKey);
+ }
+
+ const metricStates = groups.reduce((acc, group) => {
+ group.panels.forEach(panel => {
+ panel.metrics.forEach(metric => {
+ if (metric.state) {
+ acc.push(metric.state);
+ }
+ });
+ });
+ return acc;
+ }, []);
+
+ // Deduplicate and sort array
+ return Array.from(new Set(metricStates)).sort();
+};
+
+/**
* Getter to obtain the list of metric ids that have data
*
* Useful to understand which parts of the dashboard should
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index e4e467f3d68..74068e1d846 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -12,7 +12,7 @@ export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAIL
export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT';
export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS';
-export const RECEIVE_METRIC_RESULT_ERROR = 'RECEIVE_METRIC_RESULT_ERROR';
+export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index f04c12c2ac8..16a34a6c026 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -3,7 +3,7 @@ import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
import { normalizeMetric, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
-import { metricsErrors } from '../constants';
+import { metricStates } from '../constants';
import httpStatusCodes from '~/lib/utils/http_status';
const normalizePanelMetrics = (metrics, defaultLabel) =>
@@ -41,39 +41,39 @@ const findMetricInDashboard = (metricId, dashboard) => {
* @param {Object} metric - Metric object as defined in the dashboard
* @param {Object} state - New state
* @param {Array|null} state.result - Array of results
- * @param {String} state.error - Error code from metricsErrors
+ * @param {String} state.error - Error code from metricStates
* @param {Boolean} state.loading - True if the metric is loading
*/
-const setMetricState = (metric, { result = null, error = null, loading = false }) => {
+const setMetricState = (metric, { result = null, loading = false, state = null }) => {
Vue.set(metric, 'result', result);
- Vue.set(metric, 'error', error);
Vue.set(metric, 'loading', loading);
+ Vue.set(metric, 'state', state);
};
/**
- * Maps a backened error state to a `metricsErrors` constant
+ * Maps a backened error state to a `metricStates` constant
* @param {Object} error - Error from backend response
*/
-const getMetricError = error => {
+const emptyStateFromError = error => {
if (!error) {
- return metricsErrors.UNKNOWN_ERROR;
+ return metricStates.UNKNOWN_ERROR;
}
// Special error responses
if (error.message === BACKOFF_TIMEOUT) {
- return metricsErrors.TIMEOUT;
+ return metricStates.TIMEOUT;
}
// Axios error responses
const { response } = error;
if (response && response.status === httpStatusCodes.SERVICE_UNAVAILABLE) {
- return metricsErrors.CONNECTION_FAILED;
+ return metricStates.CONNECTION_FAILED;
} else if (response && response.status === httpStatusCodes.BAD_REQUEST) {
// Note: "error.response.data.error" may contain Prometheus error information
- return metricsErrors.BAD_DATA;
+ return metricStates.BAD_QUERY;
}
- return metricsErrors.UNKNOWN_ERROR;
+ return metricStates.UNKNOWN_ERROR;
};
export default {
@@ -132,9 +132,9 @@ export default {
*/
[types.REQUEST_METRIC_RESULT](state, { metricId }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
-
setMetricState(metric, {
loading: true,
+ state: metricStates.LOADING,
});
},
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) {
@@ -146,24 +146,24 @@ export default {
const metric = findMetricInDashboard(metricId, state.dashboard);
if (!result || result.length === 0) {
- // If no data is return we still consider it an error and set it to undefined
setMetricState(metric, {
- error: metricsErrors.NO_DATA,
+ state: metricStates.NO_DATA,
});
} else {
const normalizedResults = result.map(normalizeQueryResult);
setMetricState(metric, {
result: Object.freeze(normalizedResults),
+ state: metricStates.OK,
});
}
},
- [types.RECEIVE_METRIC_RESULT_ERROR](state, { metricId, error }) {
+ [types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
if (!metricId) {
return;
}
const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
- error: getMetricError(error),
+ state: emptyStateFromError(error),
});
},