Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-11 12:08:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-11 12:08:12 +0300
commit6b8040dc25fdc5fe614c3796a147517dd50bc7d8 (patch)
tree1930c21748fc632a7900659a71fcb7248097879f /app
parent7b875aa3fd1645e2e881997256ba94c6cb73ab3d (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue210
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js16
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutation_types.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js36
-rw-r--r--app/assets/javascripts/error_tracking/store/list/state.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue99
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue9
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue29
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js38
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js32
-rw-r--r--app/assets/javascripts/monitoring/stores/index.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js1
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss1
-rw-r--r--app/helpers/environments_helper.rb1
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/merge_request.rb6
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb4
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml2
25 files changed, 387 insertions, 148 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 9d8e5396dea..5cd68687329 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -3,11 +3,17 @@ import { mapActions, mapState } from 'vuex';
import {
GlEmptyState,
GlButton,
+ GlIcon,
GlLink,
GlLoadingIcon,
GlTable,
- GlSearchBoxByClick,
+ GlFormInput,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlTooltipDirective,
} from '@gitlab/ui';
+import AccessorUtils from '~/lib/utils/accessor';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
@@ -24,14 +30,19 @@ export default {
components: {
GlEmptyState,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlIcon,
GlLink,
GlLoadingIcon,
GlTable,
- GlSearchBoxByClick,
+ GlFormInput,
Icon,
TimeAgo,
},
directives: {
+ GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
props: {
@@ -56,13 +67,14 @@ export default {
required: true,
},
},
+ hasLocalStorage: AccessorUtils.isLocalStorageAccessSafe(),
data() {
return {
errorSearchQuery: '',
};
},
computed: {
- ...mapState('list', ['errors', 'externalUrl', 'loading']),
+ ...mapState('list', ['errors', 'externalUrl', 'loading', 'recentSearches']),
},
created() {
if (this.errorTrackingEnabled) {
@@ -70,9 +82,23 @@ export default {
}
},
methods: {
- ...mapActions('list', ['startPolling', 'restartPolling']),
+ ...mapActions('list', [
+ 'startPolling',
+ 'restartPolling',
+ 'addRecentSearch',
+ 'clearRecentSearches',
+ 'loadRecentSearches',
+ 'setIndexPath',
+ ]),
filterErrors() {
- this.startPolling(`${this.indexPath}?search_term=${this.errorSearchQuery}`);
+ const searchTerm = this.errorSearchQuery.trim();
+ this.addRecentSearch(searchTerm);
+
+ this.startPolling(`${this.indexPath}?search_term=${searchTerm}`);
+ },
+ setSearchText(text) {
+ this.errorSearchQuery = text;
+ this.filterErrors();
},
trackViewInSentryOptions,
getDetailsLink(errorId) {
@@ -85,81 +111,119 @@ export default {
<template>
<div>
<div v-if="errorTrackingEnabled">
- <div>
- <div class="d-flex flex-row justify-content-around bg-secondary border">
- <gl-search-box-by-click
- v-model="errorSearchQuery"
- class="col-lg-10 m-3 p-0"
- :placeholder="__('Search or filter results...')"
- type="search"
- autofocus
- @submit="filterErrors"
- />
- <gl-button
- v-track-event="trackViewInSentryOptions(externalUrl)"
- class="m-3"
- variant="primary"
- :href="externalUrl"
- target="_blank"
+ <div class="d-flex flex-row justify-content-around bg-secondary border p-3">
+ <div class="filtered-search-box">
+ <gl-dropdown
+ :text="__('Recent searches')"
+ class="filtered-search-history-dropdown-wrapper d-none d-md-block"
+ toggle-class="filtered-search-history-dropdown-toggle-button"
+ :disabled="loading"
>
- {{ __('View in Sentry') }}
- <icon name="external-link" class="flex-shrink-0" />
- </gl-button>
- </div>
-
- <div v-if="loading" class="py-3">
- <gl-loading-icon size="md" />
+ <div v-if="!$options.hasLocalStorage" class="px-3">
+ {{ __('This feature requires local storage to be enabled') }}
+ </div>
+ <template v-else-if="recentSearches.length > 0">
+ <gl-dropdown-item
+ v-for="searchQuery in recentSearches"
+ :key="searchQuery"
+ @click="setSearchText(searchQuery)"
+ >{{ searchQuery }}</gl-dropdown-item
+ >
+ <gl-dropdown-divider />
+ <gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches">{{
+ __('Clear recent searches')
+ }}</gl-dropdown-item>
+ </template>
+ <div v-else class="px-3">{{ __("You don't have any recent searches") }}</div>
+ </gl-dropdown>
+ <div class="filtered-search-input-container flex-fill">
+ <gl-form-input
+ v-model="errorSearchQuery"
+ class="pl-2 filtered-search"
+ :disabled="loading"
+ :placeholder="__('Search or filter results…')"
+ autofocus
+ @keyup.enter.native="filterErrors"
+ />
+ </div>
+ <div class="gl-search-box-by-type-right-icons">
+ <gl-button
+ v-if="errorSearchQuery.length > 0"
+ v-gl-tooltip.hover
+ :title="__('Clear')"
+ class="clear-search text-secondary"
+ name="clear"
+ @click="errorSearchQuery = ''"
+ >
+ <gl-icon name="close" :size="12" />
+ </gl-button>
+ </div>
</div>
- <gl-table
- v-else
- class="mt-3"
- :items="errors"
- :fields="$options.fields"
- :show-empty="true"
- fixed
- stacked="sm"
+ <gl-button
+ v-track-event="trackViewInSentryOptions(externalUrl)"
+ class="ml-3"
+ variant="primary"
+ :href="externalUrl"
+ target="_blank"
>
- <template slot="HEAD_events" slot-scope="data">
- <div class="text-md-right">{{ data.label }}</div>
- </template>
- <template slot="HEAD_users" slot-scope="data">
- <div class="text-md-right">{{ data.label }}</div>
- </template>
- <template slot="error" slot-scope="errors">
- <div class="d-flex flex-column">
- <gl-link class="d-flex text-dark" :href="getDetailsLink(errors.item.id)">
- <strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
- </gl-link>
- <span class="text-secondary text-truncate">
- {{ errors.item.culprit }}
- </span>
- </div>
- </template>
+ {{ __('View in Sentry') }}
+ <icon name="external-link" class="flex-shrink-0" />
+ </gl-button>
+ </div>
- <template slot="events" slot-scope="errors">
- <div class="text-md-right">{{ errors.item.count }}</div>
- </template>
+ <div v-if="loading" class="py-3">
+ <gl-loading-icon size="md" />
+ </div>
- <template slot="users" slot-scope="errors">
- <div class="text-md-right">{{ errors.item.userCount }}</div>
- </template>
+ <gl-table
+ v-else
+ class="mt-3"
+ :items="errors"
+ :fields="$options.fields"
+ :show-empty="true"
+ fixed
+ stacked="sm"
+ >
+ <template slot="HEAD_events" slot-scope="data">
+ <div class="text-md-right">{{ data.label }}</div>
+ </template>
+ <template slot="HEAD_users" slot-scope="data">
+ <div class="text-md-right">{{ data.label }}</div>
+ </template>
+ <template slot="error" slot-scope="errors">
+ <div class="d-flex flex-column">
+ <gl-link class="d-flex text-dark" :href="getDetailsLink(errors.item.id)">
+ <strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
+ </gl-link>
+ <span class="text-secondary text-truncate">
+ {{ errors.item.culprit }}
+ </span>
+ </div>
+ </template>
- <template slot="lastSeen" slot-scope="errors">
- <div class="d-flex align-items-center">
- <time-ago :time="errors.item.lastSeen" class="text-secondary" />
- </div>
- </template>
- <template slot="empty">
- <div ref="empty">
- {{ __('No errors to display.') }}
- <gl-link class="js-try-again" @click="restartPolling">
- {{ __('Check again') }}
- </gl-link>
- </div>
- </template>
- </gl-table>
- </div>
+ <template slot="events" slot-scope="errors">
+ <div class="text-md-right">{{ errors.item.count }}</div>
+ </template>
+
+ <template slot="users" slot-scope="errors">
+ <div class="text-md-right">{{ errors.item.userCount }}</div>
+ </template>
+
+ <template slot="lastSeen" slot-scope="errors">
+ <div class="d-flex align-items-center">
+ <time-ago :time="errors.item.lastSeen" class="text-secondary" />
+ </div>
+ </template>
+ <template slot="empty">
+ <div ref="empty">
+ {{ __('No errors to display.') }}
+ <gl-link class="js-try-again" @click="restartPolling">
+ {{ __('Check again') }}
+ </gl-link>
+ </div>
+ </template>
+ </gl-table>
</div>
<div v-else-if="userCanEnableErrorTracking">
<gl-empty-state
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index 401fef5983e..13b15549d81 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -51,4 +51,20 @@ export function restartPolling({ commit }) {
if (eTagPoll) eTagPoll.restart();
}
+export function setIndexPath({ commit }, path) {
+ commit(types.SET_INDEX_PATH, path);
+}
+
+export function loadRecentSearches({ commit }) {
+ commit(types.LOAD_RECENT_SEARCHES);
+}
+
+export function addRecentSearch({ commit }, searchQuery) {
+ commit(types.ADD_RECENT_SEARCH, searchQuery);
+}
+
+export function clearRecentSearches({ commit }) {
+ commit(types.CLEAR_RECENT_SEARCHES);
+}
+
export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/list/mutation_types.js b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
index f9d77a6b08e..4199e8d5cda 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutation_types.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
@@ -1,3 +1,7 @@
export const SET_ERRORS = 'SET_ERRORS';
export const SET_EXTERNAL_URL = 'SET_EXTERNAL_URL';
+export const SET_INDEX_PATH = 'SET_INDEX_PATH';
export const SET_LOADING = 'SET_LOADING';
+export const ADD_RECENT_SEARCH = 'ADD_RECENT_SEARCH';
+export const CLEAR_RECENT_SEARCHES = 'CLEAR_RECENT_SEARCHES';
+export const LOAD_RECENT_SEARCHES = 'LOAD_RECENT_SEARCHES';
diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index e4bd81db9c9..18404d3b0af 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
@@ -1,5 +1,6 @@
import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import AccessorUtils from '~/lib/utils/accessor';
export default {
[types.SET_ERRORS](state, data) {
@@ -11,4 +12,39 @@ export default {
[types.SET_LOADING](state, loading) {
state.loading = loading;
},
+ [types.SET_INDEX_PATH](state, path) {
+ state.indexPath = path;
+ },
+ [types.ADD_RECENT_SEARCH](state, searchTerm) {
+ if (searchTerm.length === 0) {
+ return;
+ }
+ // remove any existing item, then add it to the start of the list
+ const recentSearches = state.recentSearches.filter(s => s !== searchTerm);
+ recentSearches.unshift(searchTerm);
+ // only keep the last 5
+ state.recentSearches = recentSearches.slice(0, 5);
+
+ if (AccessorUtils.isLocalStorageAccessSafe()) {
+ localStorage.setItem(
+ `recent-searches${state.indexPath}`,
+ JSON.stringify(state.recentSearches),
+ );
+ }
+ },
+ [types.CLEAR_RECENT_SEARCHES](state) {
+ state.recentSearches = [];
+ if (AccessorUtils.isLocalStorageAccessSafe()) {
+ localStorage.removeItem(`recent-searches${state.indexPath}`);
+ }
+ },
+ [types.LOAD_RECENT_SEARCHES](state) {
+ const recentSearches = localStorage.getItem(`recent-searches${state.indexPath}`) || [];
+ try {
+ state.recentSearches = JSON.parse(recentSearches);
+ } catch (e) {
+ state.recentSearches = [];
+ throw e;
+ }
+ },
};
diff --git a/app/assets/javascripts/error_tracking/store/list/state.js b/app/assets/javascripts/error_tracking/store/list/state.js
index d371350ef0e..f1f0369e5f3 100644
--- a/app/assets/javascripts/error_tracking/store/list/state.js
+++ b/app/assets/javascripts/error_tracking/store/list/state.js
@@ -2,4 +2,6 @@ export default () => ({
errors: [],
externalUrl: '',
loading: true,
+ indexPath: '',
+ recentSearches: [],
});
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index fb6f5dc73b8..2a9321f6733 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import {
GlButton,
@@ -99,6 +99,10 @@ export default {
type: String,
required: true,
},
+ emptyNoDataSmallSvgPath: {
+ type: String,
+ required: true,
+ },
emptyUnableToConnectSvgPath: {
type: String,
required: true,
@@ -176,11 +180,11 @@ export default {
'showEmptyState',
'environments',
'deploymentData',
- 'metricsWithData',
'useDashboardEndpoint',
'allDashboards',
'additionalPanelTypesEnabled',
]),
+ ...mapGetters('monitoringDashboard', ['metricsWithData']),
firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
@@ -280,13 +284,8 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
- chartsWithData(panels) {
- return panels.filter(panel =>
- panel.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
- );
- },
groupHasData(group) {
- return this.chartsWithData(group.panels).length > 0;
+ return this.metricsWithData(group.key).length > 0;
},
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
@@ -447,42 +446,61 @@ export default {
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
- :collapse-group="groupHasData(groupData)"
+ :collapse-group="!groupHasData(groupData)"
>
- <vue-draggable
- :value="groupData.panels"
- group="metrics-dashboard"
- :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
- :disabled="!isRearrangingPanels"
- @input="updatePanels(groupData.key, $event)"
- >
- <div
- v-for="(graphData, graphIndex) in groupData.panels"
- :key="`panel-type-${graphIndex}`"
- class="col-12 col-lg-6 px-2 mb-2 draggable"
- :class="{ 'draggable-enabled': isRearrangingPanels }"
+ <div v-if="groupHasData(groupData)">
+ <vue-draggable
+ :value="groupData.panels"
+ group="metrics-dashboard"
+ :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
+ :disabled="!isRearrangingPanels"
+ @input="updatePanels(groupData.key, $event)"
>
- <div class="position-relative draggable-panel js-draggable-panel">
- <div
- v-if="isRearrangingPanels"
- class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
- @click="removePanel(groupData.key, groupData.panels, graphIndex)"
- >
- <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
- ><icon name="close"
- /></a>
- </div>
+ <div
+ v-for="(graphData, graphIndex) in groupData.panels"
+ :key="`panel-type-${graphIndex}`"
+ class="col-12 col-lg-6 px-2 mb-2 draggable"
+ :class="{ 'draggable-enabled': isRearrangingPanels }"
+ >
+ <div class="position-relative draggable-panel js-draggable-panel">
+ <div
+ v-if="isRearrangingPanels"
+ class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
+ @click="removePanel(groupData.key, groupData.panels, graphIndex)"
+ >
+ <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
+ ><icon name="close"
+ /></a>
+ </div>
- <panel-type
- :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
- :graph-data="graphData"
- :alerts-endpoint="alertsEndpoint"
- :prometheus-alerts-available="prometheusAlertsAvailable"
- :index="`${index}-${graphIndex}`"
- />
+ <panel-type
+ :clipboard-text="
+ generateLink(groupData.group, graphData.title, graphData.y_label)
+ "
+ :graph-data="graphData"
+ :alerts-endpoint="alertsEndpoint"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
+ :index="`${index}-${graphIndex}`"
+ />
+ </div>
</div>
- </div>
- </vue-draggable>
+ </vue-draggable>
+ </div>
+ <div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
+ <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"
+ />
+ </div>
</graph-group>
</div>
<empty-state
@@ -494,6 +512,7 @@ export default {
: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="smallEmptyState"
/>
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index a5c933a0071..dae1fbad547 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import GraphGroup from './graph_group.vue';
@@ -35,7 +35,8 @@ export default {
};
},
computed: {
- ...mapState('monitoringDashboard', ['dashboard', 'metricsWithData']),
+ ...mapState('monitoringDashboard', ['dashboard']),
+ ...mapGetters('monitoringDashboard', ['metricsWithData']),
charts() {
if (!this.dashboard || !this.dashboard.panel_groups) {
return [];
@@ -73,7 +74,7 @@ export default {
'setShowErrorBanner',
]),
chartHasData(chart) {
- return chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id));
+ return chart.metrics.some(metric => this.metricsWithData().includes(metric.metric_id));
},
onSidebarMutation() {
setTimeout(() => {
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 1bb40447a3e..ab8c9712ce4 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -37,6 +37,10 @@ export default {
type: String,
required: true,
},
+ emptyNoDataSmallSvgPath: {
+ type: String,
+ required: true,
+ },
emptyUnableToConnectSvgPath: {
type: String,
required: true,
@@ -80,6 +84,11 @@ 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/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index e01324372a7..5a7981b6534 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -15,31 +15,44 @@ export default {
required: false,
default: true,
},
+ /**
+ * Initial value of collapse on mount.
+ */
collapseGroup: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
},
data() {
return {
- showGroup: true,
+ isCollapsed: this.collapseGroup,
};
},
computed: {
caretIcon() {
- return this.collapseGroup && this.showGroup ? 'angle-down' : 'angle-right';
+ return this.isCollapsed ? 'angle-right' : 'angle-down';
+ },
+ },
+ watch: {
+ collapseGroup(val) {
+ // Respond to changes in collapseGroup but do not
+ // collapse it once was opened by the user.
+ if (this.showPanels && !val) {
+ this.isCollapsed = false;
+ }
},
},
methods: {
collapse() {
- this.showGroup = !this.showGroup;
+ this.isCollapsed = !this.isCollapsed;
},
},
};
</script>
<template>
- <div v-if="showPanels" class="card prometheus-panel">
+ <div v-if="showPanels" ref="graph-group" class="card prometheus-panel">
<div class="card-header d-flex align-items-center">
<h4 class="flex-grow-1">{{ name }}</h4>
<a role="button" class="js-graph-group-toggle" @click="collapse">
@@ -47,12 +60,12 @@ export default {
</a>
</div>
<div
- v-if="collapseGroup"
- v-show="collapseGroup && showGroup"
+ v-show="!isCollapsed"
+ ref="graph-group-content"
class="card-body prometheus-graph-group p-0"
>
<slot></slot>
</div>
</div>
- <div v-else class="prometheus-graph-group"><slot></slot></div>
+ <div v-else ref="graph-group-content" class="prometheus-graph-group"><slot></slot></div>
</template>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 3612e4d173f..268d9d636b1 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -4,7 +4,7 @@ import createFlash from '~/flash';
import trackDashboardLoad from '../monitoring_tracking_helper';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
-import { s__ } from '../../locale';
+import { s__, sprintf } from '../../locale';
const TWO_MINUTES = 120000;
@@ -74,17 +74,21 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
.then(resp => resp.data)
.then(response => dispatch('receiveMetricsDashboardSuccess', { response, params }))
- .then(() => {
- const dashboardType = state.currentDashboard === '' ? 'default' : 'custom';
- return trackDashboardLoad({
- label: `${dashboardType}_metrics_dashboard`,
- value: state.metricsWithData.length,
- });
- })
- .catch(error => {
- dispatch('receiveMetricsDashboardFailure', error);
- if (state.setShowErrorBanner) {
- createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ .catch(e => {
+ dispatch('receiveMetricsDashboardFailure', e);
+ if (state.showErrorBanner) {
+ if (e.response.data && e.response.data.message) {
+ const { message } = e.response.data;
+ createFlash(
+ sprintf(
+ s__('Metrics|There was an error while retrieving metrics. %{message}'),
+ { message },
+ false,
+ ),
+ );
+ } else {
+ createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ }
}
});
};
@@ -126,7 +130,7 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
});
};
-export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
+export const fetchPrometheusMetrics = ({ state, commit, dispatch, getters }, params) => {
commit(types.REQUEST_METRICS_DATA);
const promises = [];
@@ -140,9 +144,11 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
return Promise.all(promises)
.then(() => {
- if (state.metricsWithData.length === 0) {
- commit(types.SET_NO_DATA_EMPTY_STATE);
- }
+ const dashboardType = state.currentDashboard === '' ? 'default' : 'custom';
+ trackDashboardLoad({
+ label: `${dashboardType}_metrics_dashboard`,
+ value: getters.metricsWithData().length,
+ });
})
.catch(() => {
createFlash(s__(`Metrics|There was an error while retrieving metrics`), 'warning');
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
new file mode 100644
index 00000000000..3eddd52705d
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -0,0 +1,32 @@
+const metricsIdsInPanel = panel =>
+ panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
+
+/**
+ * Getter to obtain the list of metric ids that have data
+ *
+ * Useful to understand which parts of the dashboard should
+ * be displayed. It is a Vuex Method-Style Access getter.
+ *
+ * @param {Object} state
+ * @returns {Function} A function that returns an array of
+ * metrics in the dashboard that contain results, optionally
+ * filtered by group key.
+ */
+export const metricsWithData = state => groupKey => {
+ let groups = state.dashboard.panel_groups;
+ if (groupKey) {
+ groups = groups.filter(group => group.key === groupKey);
+ }
+
+ const res = [];
+ groups.forEach(group => {
+ group.panels.forEach(panel => {
+ res.push(...metricsIdsInPanel(panel));
+ });
+ });
+
+ return res;
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/index.js b/app/assets/javascripts/monitoring/stores/index.js
index d58398c54ae..c1c466b7cf0 100644
--- a/app/assets/javascripts/monitoring/stores/index.js
+++ b/app/assets/javascripts/monitoring/stores/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
import state from './state';
@@ -12,6 +13,7 @@ export const createStore = () =>
monitoringDashboard: {
namespaced: true,
actions,
+ getters,
mutations,
state,
},
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index bfa76aa7cea..db5ec4e9e2b 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -67,7 +67,6 @@ export default {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
if (metric.metric_id === metricId) {
- state.metricsWithData.push(metricId);
// ensure dates/numbers are correctly formatted for charts
const normalizedResults = result.map(normalizeQueryResult);
Vue.set(metric, 'result', Object.freeze(normalizedResults));
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index e3300967022..88f333aeb80 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -13,7 +13,6 @@ export default () => ({
},
deploymentData: [],
environments: [],
- metricsWithData: [],
allDashboards: [],
currentDashboard: null,
projectPath: null,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 4ff8447485f..42db1935123 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -28,6 +28,10 @@ export default {
type: Object,
required: true,
},
+ pipelineCoverageDelta: {
+ type: String,
+ required: false,
+ },
// This prop needs to be camelCase, html attributes are case insensive
// https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case
hasCi: {
@@ -92,6 +96,16 @@ export default {
showSourceBranch() {
return Boolean(this.pipeline.ref.branch);
},
+ coverageDeltaClass() {
+ const delta = this.pipelineCoverageDelta;
+ if (delta && parseFloat(delta) > 0) {
+ return 'text-success';
+ }
+ if (delta && parseFloat(delta) < 0) {
+ return 'text-danger';
+ }
+ return '';
+ },
},
};
</script>
@@ -142,6 +156,14 @@ export default {
</div>
<div v-if="pipeline.coverage" class="coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
+
+ <span
+ v-if="pipelineCoverageDelta"
+ class="js-pipeline-coverage-delta"
+ :class="coverageDeltaClass"
+ >
+ ({{ pipelineCoverageDelta }}%)
+ </span>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index c8b26889076..90fb254ecca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -76,6 +76,7 @@ export default {
<mr-widget-container>
<mr-widget-pipeline
:pipeline="pipeline"
+ :pipeline-coverage-delta="mr.pipelineCoverageDelta"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
:source-branch="branch"
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 1a25edee9b8..c7949fa264e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -42,6 +42,7 @@ export default class MergeRequestStore {
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
+ this.pipelineCoverageDelta = data.pipeline_coverage_delta;
this.mergePipeline = data.merge_pipeline || {};
this.deployments = this.deployments || data.deployments || [];
this.postMergeDeployments = this.postMergeDeployments || [];
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 9a15505dd25..4cbb2f5ba71 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -515,6 +515,12 @@ img.emoji {
cursor: pointer;
}
+// this needs to use "!important" due to some very specific styles
+// around buttons
+.cursor-default {
+ cursor: default !important;
+}
+
// Make buttons/dropdowns full-width on mobile
.full-width-mobile {
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 2d826064569..1c252584047 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -214,8 +214,8 @@
padding-left: 0;
height: $input-height - 2;
line-height: inherit;
- border-color: transparent;
+ &,
&:focus,
&:hover {
outline: none;
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index 154e505f7a4..e20e58e21cf 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -67,7 +67,6 @@
.prometheus-graph-group {
display: flex;
flex-wrap: wrap;
- margin-top: $gl-padding-8;
}
.prometheus-graph {
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index f57d0fa19d4..59972118ae3 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -26,6 +26,7 @@ module EnvironmentsHelper
"empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'),
"empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'),
+ "empty-no-data-small-svg-path" => image_path('illustrations/chart-empty-state-small.svg'),
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
"dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 4ba0317b580..e764b6c56b0 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,9 +5,6 @@ class ApplicationSetting < ApplicationRecord
include CacheMarkdownField
include TokenAuthenticatable
include ChronicDurationAttribute
- include IgnorableColumns
-
- ignore_columns :pendo_enabled, :pendo_url, remove_after: '2019-12-01', remove_with: '12.6'
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 240c143abba..bbc54987407 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1423,6 +1423,12 @@ class MergeRequest < ApplicationRecord
true
end
+ def pipeline_coverage_delta
+ if base_pipeline&.coverage && head_pipeline&.coverage
+ '%.2f' % (head_pipeline.coverage.to_f - base_pipeline.coverage.to_f)
+ end
+ end
+
def base_pipeline
@base_pipeline ||= project.ci_pipelines
.order(id: :desc)
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 028de38e42a..a45026ea016 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -57,6 +57,10 @@ class MergeRequestPollWidgetEntity < Grape::Entity
presenter(merge_request).ci_status
end
+ expose :pipeline_coverage_delta do |merge_request|
+ presenter(merge_request).pipeline_coverage_delta
+ end
+
expose :cancel_auto_merge_path do |merge_request|
presenter(merge_request).cancel_auto_merge_path
end
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index b00c95c3cd7..3fe6f0a6640 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -7,7 +7,7 @@
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)}
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
- window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/index.md', anchor: 'troubleshooting')}';
+ window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.html', anchor: 'security-approvals-in-merge-requests-ultimate')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';