diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-17 15:08:15 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-17 15:08:15 +0000 |
commit | c2b98d3dbd47ab92c79c702276fe9130d9a28036 (patch) | |
tree | bf4071f551fdc12c22b23b2bb66483064e7b9ea9 /app/assets | |
parent | badb9c1deacbea601b02f88811b7e123589d9251 (diff) | |
download | gitlab-ce-c2b98d3dbd47ab92c79c702276fe9130d9a28036.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
12 files changed, 227 insertions, 44 deletions
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 734a9a89c72..675552e6c2b 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -1,5 +1,6 @@ <script> import icon from '~/vue_shared/components/icon.vue'; +import { GlBadge } from '@gitlab/ui'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { ITEM_TYPE, @@ -8,13 +9,16 @@ import { PROJECT_VISIBILITY_TYPE, } from '../constants'; import itemStatsValue from './item_stats_value.vue'; +import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal'; export default { components: { icon, timeAgoTooltip, itemStatsValue, + GlBadge, }, + mixins: [isProjectPendingRemoval], props: { item: { type: Object, @@ -70,6 +74,9 @@ export default { css-class="project-stars" icon-name="star" /> + <div v-if="isProjectPendingRemoval"> + <gl-badge variant="warning">{{ __('pending removal') }}</gl-badge> + </div> <div v-if="isProject" class="last-updated"> <time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" /> </div> diff --git a/app/assets/javascripts/groups/mixins/is_project_pending_removal.js b/app/assets/javascripts/groups/mixins/is_project_pending_removal.js new file mode 100644 index 00000000000..e44e5780199 --- /dev/null +++ b/app/assets/javascripts/groups/mixins/is_project_pending_removal.js @@ -0,0 +1,7 @@ +export default { + computed: { + isProjectPendingRemoval() { + return false; + }, + }, +}; diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js index 16f95d5a0cc..214ac5e3db5 100644 --- a/app/assets/javascripts/groups/store/groups_store.js +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -93,6 +93,7 @@ export default class GroupsStore { memberCount: rawGroupItem.number_users_with_delimiter, starCount: rawGroupItem.star_count, updatedAt: rawGroupItem.updated_at, + pendingRemoval: rawGroupItem.marked_for_deletion_at, }; } diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 2e7bf9a7d5a..dd69e2d6f1f 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -17,10 +17,18 @@ export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DAT export const discardAllChanges = ({ state, commit, dispatch }) => { state.changedFiles.forEach(file => { - commit(types.DISCARD_FILE_CHANGES, file.path); + if (file.tempFile || file.prevPath) dispatch('closeFile', file); if (file.tempFile) { - dispatch('closeFile', file); + dispatch('deleteEntry', file.path); + } else if (file.prevPath) { + dispatch('renameEntry', { + path: file.path, + name: file.prevName, + parentPath: file.prevParentPath, + }); + } else { + commit(types.DISCARD_FILE_CHANGES, file.path); } }); 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), }); }, |