Skip to content
Snippets Groups Projects
Commit 6b8040dc authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 7b875aa3
No related branches found
No related tags found
No related merge requests found
Showing
with 375 additions and 144 deletions
Loading
Loading
@@ -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';
Loading
Loading
@@ -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: {
Loading
Loading
@@ -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) {
Loading
Loading
@@ -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) {
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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 () => {};
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';
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) {
Loading
Loading
@@ -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;
}
},
};
Loading
Loading
@@ -2,4 +2,6 @@ export default () => ({
errors: [],
externalUrl: '',
loading: true,
indexPath: '',
recentSearches: [],
});
<script>
import _ from 'underscore';
import { mapActions, mapState } from 'vuex';
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import {
GlButton,
Loading
Loading
@@ -99,6 +99,10 @@ export default {
type: String,
required: true,
},
emptyNoDataSmallSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
Loading
Loading
@@ -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]
Loading
Loading
@@ -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));
Loading
Loading
@@ -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
Loading
Loading
@@ -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"
/>
Loading
Loading
<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';
Loading
Loading
@@ -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 [];
Loading
Loading
@@ -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(() => {
Loading
Loading
Loading
Loading
@@ -37,6 +37,10 @@ export default {
type: String,
required: true,
},
emptyNoDataSmallSvgPath: {
type: String,
required: true,
},
emptyUnableToConnectSvgPath: {
type: String,
required: true,
Loading
Loading
@@ -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'),
Loading
Loading
Loading
Loading
@@ -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">
Loading
Loading
@@ -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>
Loading
Loading
@@ -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;
 
Loading
Loading
@@ -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'));
}
}
});
};
Loading
Loading
@@ -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 = [];
Loading
Loading
@@ -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');
Loading
Loading
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 () => {};
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';
 
Loading
Loading
@@ -12,6 +13,7 @@ export const createStore = () =>
monitoringDashboard: {
namespaced: true,
actions,
getters,
mutations,
state,
},
Loading
Loading
Loading
Loading
@@ -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));
Loading
Loading
Loading
Loading
@@ -13,7 +13,6 @@ export default () => ({
},
deploymentData: [],
environments: [],
metricsWithData: [],
allDashboards: [],
currentDashboard: null,
projectPath: null,
Loading
Loading
Loading
Loading
@@ -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: {
Loading
Loading
@@ -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>
Loading
Loading
@@ -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>
Loading
Loading
Loading
Loading
@@ -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"
Loading
Loading
Loading
Loading
@@ -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 || [];
Loading
Loading
Loading
Loading
@@ -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) {
Loading
Loading
Loading
Loading
@@ -214,8 +214,8 @@
padding-left: 0;
height: $input-height - 2;
line-height: inherit;
border-color: transparent;
 
&,
&:focus,
&:hover {
outline: none;
Loading
Loading
Loading
Loading
@@ -67,7 +67,6 @@
.prometheus-graph-group {
display: flex;
flex-wrap: wrap;
margin-top: $gl-padding-8;
}
 
.prometheus-graph {
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment