Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dff63567c3
commit
69f0d90aad
66 changed files with 1193 additions and 638 deletions
288
app/assets/javascripts/cycle_analytics/components/base.vue
Normal file
288
app/assets/javascripts/cycle_analytics/components/base.vue
Normal file
|
@ -0,0 +1,288 @@
|
|||
<script>
|
||||
import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
|
||||
import Cookies from 'js-cookie';
|
||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import banner from './banner.vue';
|
||||
import stageCodeComponent from './stage_code_component.vue';
|
||||
import stageComponent from './stage_component.vue';
|
||||
import stageNavItem from './stage_nav_item.vue';
|
||||
import stageReviewComponent from './stage_review_component.vue';
|
||||
import stageStagingComponent from './stage_staging_component.vue';
|
||||
import stageTestComponent from './stage_test_component.vue';
|
||||
|
||||
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
|
||||
|
||||
export default {
|
||||
name: 'CycleAnalytics',
|
||||
components: {
|
||||
GlIcon,
|
||||
GlEmptyState,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
banner,
|
||||
'stage-issue-component': stageComponent,
|
||||
'stage-plan-component': stageComponent,
|
||||
'stage-code-component': stageCodeComponent,
|
||||
'stage-test-component': stageTestComponent,
|
||||
'stage-review-component': stageReviewComponent,
|
||||
'stage-staging-component': stageStagingComponent,
|
||||
'stage-production-component': stageComponent,
|
||||
'stage-nav-item': stageNavItem,
|
||||
},
|
||||
props: {
|
||||
noDataSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noAccessSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
service: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
state: this.store.state,
|
||||
isLoading: false,
|
||||
isLoadingStage: false,
|
||||
isEmptyStage: false,
|
||||
hasError: true,
|
||||
startDate: 30,
|
||||
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentStage() {
|
||||
return this.store.currentActiveStage();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchCycleAnalyticsData();
|
||||
},
|
||||
methods: {
|
||||
handleError() {
|
||||
this.store.setErrorState(true);
|
||||
return new Flash(__('There was an error while fetching value stream analytics data.'));
|
||||
},
|
||||
handleDateSelect(startDate) {
|
||||
this.startDate = startDate;
|
||||
this.fetchCycleAnalyticsData({ startDate: this.startDate });
|
||||
},
|
||||
fetchCycleAnalyticsData(options) {
|
||||
const fetchOptions = options || { startDate: this.startDate };
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.service
|
||||
.fetchCycleAnalyticsData(fetchOptions)
|
||||
.then((response) => {
|
||||
this.store.setCycleAnalyticsData(response);
|
||||
this.selectDefaultStage();
|
||||
})
|
||||
.catch(() => {
|
||||
this.handleError();
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
selectDefaultStage() {
|
||||
const stage = this.state.stages[0];
|
||||
this.selectStage(stage);
|
||||
},
|
||||
selectStage(stage) {
|
||||
if (this.isLoadingStage) return;
|
||||
if (this.currentStage === stage) return;
|
||||
|
||||
if (!stage.isUserAllowed) {
|
||||
this.store.setActiveStage(stage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoadingStage = true;
|
||||
this.store.setStageEvents([], stage);
|
||||
this.store.setActiveStage(stage);
|
||||
|
||||
this.service
|
||||
.fetchStageData({
|
||||
stage,
|
||||
startDate: this.startDate,
|
||||
projectIds: this.selectedProjectIds,
|
||||
})
|
||||
.then((response) => {
|
||||
this.isEmptyStage = !response.events.length;
|
||||
this.store.setStageEvents(response.events, stage);
|
||||
})
|
||||
.catch(() => {
|
||||
this.isEmptyStage = true;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoadingStage = false;
|
||||
});
|
||||
},
|
||||
dismissOverviewDialog() {
|
||||
this.isOverviewDialogDismissed = true;
|
||||
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
|
||||
},
|
||||
},
|
||||
dayRangeOptions: [7, 30, 90],
|
||||
i18n: {
|
||||
dropdownText: __('Last %{days} days'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="cycle-analytics">
|
||||
<gl-loading-icon v-if="isLoading" size="lg" />
|
||||
<div v-else class="wrapper">
|
||||
<div class="card">
|
||||
<div class="card-header">{{ __('Recent Project Activity') }}</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div v-for="item in state.summary" :key="item.title" class="flex-grow text-center">
|
||||
<h3 class="header">{{ item.value }}</h3>
|
||||
<p class="text">{{ item.title }}</p>
|
||||
</div>
|
||||
<div class="flex-grow align-self-center text-center">
|
||||
<div class="js-ca-dropdown dropdown inline">
|
||||
<button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
|
||||
<span class="dropdown-label">
|
||||
<gl-sprintf :message="$options.i18n.dropdownText">
|
||||
<template #days>{{ startDate }}</template>
|
||||
</gl-sprintf>
|
||||
<gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
|
||||
</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li v-for="days in $options.dayRangeOptions" :key="`day-range-${days}`">
|
||||
<a href="#" @click.prevent="handleDateSelect(days)">
|
||||
<gl-sprintf :message="$options.i18n.dropdownText">
|
||||
<template #days>{{ days }}</template>
|
||||
</gl-sprintf>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stage-panel-container">
|
||||
<div class="card stage-panel">
|
||||
<div class="card-header border-bottom-0">
|
||||
<nav class="col-headers">
|
||||
<ul>
|
||||
<li class="stage-header pl-5">
|
||||
<span class="stage-name font-weight-bold">{{
|
||||
s__('ProjectLifecycle|Stage')
|
||||
}}</span>
|
||||
<span
|
||||
class="has-tooltip"
|
||||
data-placement="top"
|
||||
:title="__('The phase of the development lifecycle.')"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<gl-icon name="question-o" class="gl-text-gray-500" />
|
||||
</span>
|
||||
</li>
|
||||
<li class="median-header">
|
||||
<span class="stage-name font-weight-bold">{{ __('Median') }}</span>
|
||||
<span
|
||||
class="has-tooltip"
|
||||
data-placement="top"
|
||||
:title="
|
||||
__(
|
||||
'The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.',
|
||||
)
|
||||
"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<gl-icon name="question-o" class="gl-text-gray-500" />
|
||||
</span>
|
||||
</li>
|
||||
<li class="event-header pl-3">
|
||||
<span
|
||||
v-if="currentStage && currentStage.legend"
|
||||
class="stage-name font-weight-bold"
|
||||
>{{ currentStage ? __(currentStage.legend) : __('Related Issues') }}</span
|
||||
>
|
||||
<span
|
||||
class="has-tooltip"
|
||||
data-placement="top"
|
||||
:title="
|
||||
__('The collection of events added to the data gathered for that stage.')
|
||||
"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<gl-icon name="question-o" class="gl-text-gray-500" />
|
||||
</span>
|
||||
</li>
|
||||
<li class="total-time-header pr-5 text-right">
|
||||
<span class="stage-name font-weight-bold">{{ __('Time') }}</span>
|
||||
<span
|
||||
class="has-tooltip"
|
||||
data-placement="top"
|
||||
:title="__('The time taken by each data entry gathered by that stage.')"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<gl-icon name="question-o" class="gl-text-gray-500" />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="stage-panel-body">
|
||||
<nav class="stage-nav">
|
||||
<ul>
|
||||
<stage-nav-item
|
||||
v-for="stage in state.stages"
|
||||
:key="stage.title"
|
||||
:title="stage.title"
|
||||
:is-user-allowed="stage.isUserAllowed"
|
||||
:value="stage.value"
|
||||
:is-active="stage.active"
|
||||
@select="selectStage(stage)"
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="stage-events overflow-auto">
|
||||
<gl-loading-icon v-show="isLoadingStage" size="lg" />
|
||||
<template v-if="currentStage && !currentStage.isUserAllowed">
|
||||
<gl-empty-state
|
||||
class="js-empty-state"
|
||||
:title="__('You need permission.')"
|
||||
:svg-path="noAccessSvgPath"
|
||||
:description="__('Want to see the data? Please ask an administrator for access.')"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="currentStage && isEmptyStage && !isLoadingStage">
|
||||
<gl-empty-state
|
||||
class="js-empty-state"
|
||||
:description="currentStage.emptyStageText"
|
||||
:svg-path="noDataSvgPath"
|
||||
:title="__('We don\'t have enough data to show this stage.')"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="state.events.length && !isLoadingStage && !isEmptyStage">
|
||||
<component
|
||||
:is="currentStage.component"
|
||||
:stage="currentStage"
|
||||
:items="state.events"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,156 +0,0 @@
|
|||
// This is a true violation of @gitlab/no-runtime-template-compiler, as it
|
||||
// relies on app/views/projects/cycle_analytics/show.html.haml for its
|
||||
// template.
|
||||
/* eslint-disable @gitlab/no-runtime-template-compiler */
|
||||
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
|
||||
import $ from 'jquery';
|
||||
import Cookies from 'js-cookie';
|
||||
import Vue from 'vue';
|
||||
import { __ } from '~/locale';
|
||||
import { deprecatedCreateFlash as Flash } from '../flash';
|
||||
import Translate from '../vue_shared/translate';
|
||||
import banner from './components/banner.vue';
|
||||
import stageCodeComponent from './components/stage_code_component.vue';
|
||||
import stageComponent from './components/stage_component.vue';
|
||||
import stageNavItem from './components/stage_nav_item.vue';
|
||||
import stageReviewComponent from './components/stage_review_component.vue';
|
||||
import stageStagingComponent from './components/stage_staging_component.vue';
|
||||
import stageTestComponent from './components/stage_test_component.vue';
|
||||
import CycleAnalyticsService from './cycle_analytics_service';
|
||||
import CycleAnalyticsStore from './cycle_analytics_store';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
|
||||
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: '#cycle-analytics',
|
||||
name: 'CycleAnalytics',
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlLoadingIcon,
|
||||
banner,
|
||||
'stage-issue-component': stageComponent,
|
||||
'stage-plan-component': stageComponent,
|
||||
'stage-code-component': stageCodeComponent,
|
||||
'stage-test-component': stageTestComponent,
|
||||
'stage-review-component': stageReviewComponent,
|
||||
'stage-staging-component': stageStagingComponent,
|
||||
'stage-production-component': stageComponent,
|
||||
'stage-nav-item': stageNavItem,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
store: CycleAnalyticsStore,
|
||||
state: CycleAnalyticsStore.state,
|
||||
isLoading: false,
|
||||
isLoadingStage: false,
|
||||
isEmptyStage: false,
|
||||
hasError: false,
|
||||
startDate: 30,
|
||||
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
|
||||
service: this.createCycleAnalyticsService(cycleAnalyticsEl.dataset.requestPath),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentStage() {
|
||||
return this.store.currentActiveStage();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Conditional check placed here to prevent this method from being called on the
|
||||
// new Value Stream Analytics page (i.e. the new page will be initialized blank and only
|
||||
// after a group is selected the cycle analyitcs data will be fetched). Once the
|
||||
// old (current) page has been removed this entire created method as well as the
|
||||
// variable itself can be completely removed.
|
||||
// Follow up issue: https://gitlab.com/gitlab-org/gitlab-foss/issues/64490
|
||||
if (cycleAnalyticsEl.dataset.requestPath) this.fetchCycleAnalyticsData();
|
||||
},
|
||||
methods: {
|
||||
handleError() {
|
||||
this.store.setErrorState(true);
|
||||
return new Flash(__('There was an error while fetching value stream analytics data.'));
|
||||
},
|
||||
initDropdown() {
|
||||
const $dropdown = $('.js-ca-dropdown');
|
||||
const $label = $dropdown.find('.dropdown-label');
|
||||
|
||||
// eslint-disable-next-line @gitlab/no-global-event-off
|
||||
$dropdown
|
||||
.find('li a')
|
||||
.off('click')
|
||||
.on('click', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
this.startDate = $target.data('value');
|
||||
|
||||
$label.text($target.text().trim());
|
||||
this.fetchCycleAnalyticsData({ startDate: this.startDate });
|
||||
});
|
||||
},
|
||||
fetchCycleAnalyticsData(options) {
|
||||
const fetchOptions = options || { startDate: this.startDate };
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.service
|
||||
.fetchCycleAnalyticsData(fetchOptions)
|
||||
.then((response) => {
|
||||
this.store.setCycleAnalyticsData(response);
|
||||
this.selectDefaultStage();
|
||||
this.initDropdown();
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.handleError();
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
selectDefaultStage() {
|
||||
const stage = this.state.stages[0];
|
||||
this.selectStage(stage);
|
||||
},
|
||||
selectStage(stage) {
|
||||
if (this.isLoadingStage) return;
|
||||
if (this.currentStage === stage) return;
|
||||
|
||||
if (!stage.isUserAllowed) {
|
||||
this.store.setActiveStage(stage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoadingStage = true;
|
||||
this.store.setStageEvents([], stage);
|
||||
this.store.setActiveStage(stage);
|
||||
|
||||
this.service
|
||||
.fetchStageData({
|
||||
stage,
|
||||
startDate: this.startDate,
|
||||
projectIds: this.selectedProjectIds,
|
||||
})
|
||||
.then((response) => {
|
||||
this.isEmptyStage = !response.events.length;
|
||||
this.store.setStageEvents(response.events, stage);
|
||||
this.isLoadingStage = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isEmptyStage = true;
|
||||
this.isLoadingStage = false;
|
||||
});
|
||||
},
|
||||
dismissOverviewDialog() {
|
||||
this.isOverviewDialogDismissed = true;
|
||||
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1', { expires: 365 });
|
||||
},
|
||||
createCycleAnalyticsService(requestPath) {
|
||||
return new CycleAnalyticsService({
|
||||
requestPath,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
32
app/assets/javascripts/cycle_analytics/index.js
Normal file
32
app/assets/javascripts/cycle_analytics/index.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '../vue_shared/translate';
|
||||
import CycleAnalytics from './components/base.vue';
|
||||
import CycleAnalyticsService from './cycle_analytics_service';
|
||||
import CycleAnalyticsStore from './cycle_analytics_store';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
const createCycleAnalyticsService = (requestPath) =>
|
||||
new CycleAnalyticsService({
|
||||
requestPath,
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('#js-cycle-analytics');
|
||||
const { noAccessSvgPath, noDataSvgPath } = el.dataset;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
name: 'CycleAnalytics',
|
||||
render: (createElement) =>
|
||||
createElement(CycleAnalytics, {
|
||||
props: {
|
||||
noDataSvgPath,
|
||||
noAccessSvgPath,
|
||||
store: CycleAnalyticsStore,
|
||||
service: createCycleAnalyticsService(el.dataset.requestPath),
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
|
@ -1,13 +1,15 @@
|
|||
<script>
|
||||
import {
|
||||
GlEmptyState,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByClick,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { s__, __ } from '~/locale';
|
||||
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
|
||||
import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql';
|
||||
import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql';
|
||||
|
@ -16,9 +18,14 @@ import availableNamespacesQuery from '../graphql/queries/available_namespaces.qu
|
|||
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
|
||||
import ImportTableRow from './import_table_row.vue';
|
||||
|
||||
const PAGE_SIZES = [20, 50, 100];
|
||||
const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
|
@ -43,6 +50,7 @@ export default {
|
|||
return {
|
||||
filter: '',
|
||||
page: 1,
|
||||
perPage: DEFAULT_PAGE_SIZE,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -50,13 +58,17 @@ export default {
|
|||
bulkImportSourceGroups: {
|
||||
query: bulkImportSourceGroupsQuery,
|
||||
variables() {
|
||||
return { page: this.page, filter: this.filter };
|
||||
return { page: this.page, filter: this.filter, perPage: this.perPage };
|
||||
},
|
||||
},
|
||||
availableNamespaces: availableNamespacesQuery,
|
||||
},
|
||||
|
||||
computed: {
|
||||
humanizedTotal() {
|
||||
return this.paginationInfo.total >= 1000 ? __('1000+') : this.paginationInfo.total;
|
||||
},
|
||||
|
||||
hasGroups() {
|
||||
return this.bulkImportSourceGroups?.nodes?.length > 0;
|
||||
},
|
||||
|
@ -117,14 +129,20 @@ export default {
|
|||
variables: { sourceGroupId },
|
||||
});
|
||||
},
|
||||
|
||||
setPageSize(size) {
|
||||
this.perPage = size;
|
||||
},
|
||||
},
|
||||
|
||||
PAGE_SIZES,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex gl-align-items-center"
|
||||
class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
|
||||
>
|
||||
<span>
|
||||
<gl-sprintf v-if="!$apollo.loading && hasGroups" :message="statusMessage">
|
||||
|
@ -161,7 +179,7 @@ export default {
|
|||
:title="s__('BulkImport|You have no groups to import')"
|
||||
:description="s__('Check your source instance permissions.')"
|
||||
/>
|
||||
<div v-else class="gl-display-flex gl-flex-direction-column gl-align-items-center">
|
||||
<template v-else>
|
||||
<table class="gl-w-full">
|
||||
<thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1">
|
||||
<th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th>
|
||||
|
@ -183,12 +201,50 @@ export default {
|
|||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<pagination-links
|
||||
:change="setPage"
|
||||
:page-info="bulkImportSourceGroups.pageInfo"
|
||||
class="gl-mt-3"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasGroups" class="gl-display-flex gl-mt-3 gl-align-items-center">
|
||||
<pagination-links
|
||||
:change="setPage"
|
||||
:page-info="bulkImportSourceGroups.pageInfo"
|
||||
class="gl-m-0"
|
||||
/>
|
||||
<gl-dropdown category="tertiary" class="gl-ml-auto">
|
||||
<template #button-content>
|
||||
<span class="font-weight-bold">
|
||||
<gl-sprintf :message="__('%{count} items per page')">
|
||||
<template #count>
|
||||
{{ perPage }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
<gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
|
||||
</template>
|
||||
<gl-dropdown-item
|
||||
v-for="size in $options.PAGE_SIZES"
|
||||
:key="size"
|
||||
@click="setPageSize(size)"
|
||||
>
|
||||
<gl-sprintf :message="__('%{count} items per page')">
|
||||
<template #count>
|
||||
{{ size }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
<div class="gl-ml-2">
|
||||
<gl-sprintf :message="s__('BulkImport|Showing %{start}-%{end} of %{total}')">
|
||||
<template #start>
|
||||
{{ paginationInfo.start }}
|
||||
</template>
|
||||
<template #end>
|
||||
{{ paginationInfo.end }}
|
||||
</template>
|
||||
<template #total>
|
||||
{{ humanizedTotal }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle';
|
||||
import initCycleAnalytics from '~/cycle_analytics';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initCycleAnalytics);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initForm from '../../../../shared/milestones/form';
|
||||
import initForm from '~/shared/milestones/form';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => initForm());
|
||||
initForm();
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserAvatarLink,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
pipeline: {
|
||||
type: Object,
|
||||
|
@ -15,11 +17,19 @@ export default {
|
|||
user() {
|
||||
return this.pipeline.user;
|
||||
},
|
||||
classes() {
|
||||
const triggererClass = 'pipeline-triggerer';
|
||||
|
||||
if (this.glFeatures.newPipelinesTable) {
|
||||
return triggererClass;
|
||||
}
|
||||
return `table-section section-10 d-none d-md-block ${triggererClass}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="table-section section-10 d-none d-md-block pipeline-triggerer">
|
||||
<div :class="classes">
|
||||
<user-avatar-link
|
||||
v-if="user"
|
||||
:link-href="user.path"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { SCHEDULE_ORIGIN } from '../../constants';
|
||||
|
||||
export default {
|
||||
|
@ -13,6 +14,7 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: {
|
||||
targetProjectFullPath: {
|
||||
default: '',
|
||||
|
@ -47,11 +49,19 @@ export default {
|
|||
autoDevopsHelpPath() {
|
||||
return helpPagePath('topics/autodevops/index.md');
|
||||
},
|
||||
classes() {
|
||||
const tagsClass = 'pipeline-tags';
|
||||
|
||||
if (this.glFeatures.newPipelinesTable) {
|
||||
return tagsClass;
|
||||
}
|
||||
return `table-section section-10 d-none d-md-block ${tagsClass}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="table-section section-10 d-none d-md-block pipeline-tags">
|
||||
<div :class="classes">
|
||||
<gl-link
|
||||
:href="pipeline.path"
|
||||
data-testid="pipeline-url-link"
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlTable, GlTooltipDirective } from '@gitlab/ui';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import eventHub from '../../event_hub';
|
||||
import PipelineStopModal from './pipeline_stop_modal.vue';
|
||||
import PipelinesTableRowComponent from './pipelines_table_row.vue';
|
||||
|
||||
/**
|
||||
* Pipelines Table Component.
|
||||
*
|
||||
* Given an array of objects, renders a table.
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
GlTable,
|
||||
PipelinesTableRowComponent,
|
||||
PipelineStopModal,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
pipelines: {
|
||||
type: Array,
|
||||
|
@ -45,6 +43,11 @@ export default {
|
|||
cancelingPipeline: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
legacyTableClass() {
|
||||
return !this.glFeatures.newPipelinesTable ? 'ci-table' : '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
pipelines() {
|
||||
this.cancelingPipeline = null;
|
||||
|
@ -70,37 +73,42 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="ci-table">
|
||||
<div class="gl-responsive-table-row table-row-header" role="row">
|
||||
<div class="table-section section-10 js-pipeline-status" role="rowheader">
|
||||
{{ s__('Pipeline|Status') }}
|
||||
</div>
|
||||
<div class="table-section section-10 js-pipeline-info pipeline-info" role="rowheader">
|
||||
{{ s__('Pipeline|Pipeline') }}
|
||||
</div>
|
||||
<div class="table-section section-10 js-triggerer-info triggerer-info" role="rowheader">
|
||||
{{ s__('Pipeline|Triggerer') }}
|
||||
</div>
|
||||
<div class="table-section section-20 js-pipeline-commit pipeline-commit" role="rowheader">
|
||||
{{ s__('Pipeline|Commit') }}
|
||||
</div>
|
||||
<div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader">
|
||||
{{ s__('Pipeline|Stages') }}
|
||||
</div>
|
||||
<div class="table-section section-15" role="rowheader"></div>
|
||||
<div class="table-section section-20" role="rowheader">
|
||||
<slot name="table-header-actions"></slot>
|
||||
<div :class="legacyTableClass">
|
||||
<div v-if="!glFeatures.newPipelinesTable" data-testid="ci-table">
|
||||
<div class="gl-responsive-table-row table-row-header" role="row">
|
||||
<div class="table-section section-10 js-pipeline-status" role="rowheader">
|
||||
{{ s__('Pipeline|Status') }}
|
||||
</div>
|
||||
<div class="table-section section-10 js-pipeline-info pipeline-info" role="rowheader">
|
||||
{{ s__('Pipeline|Pipeline') }}
|
||||
</div>
|
||||
<div class="table-section section-10 js-triggerer-info triggerer-info" role="rowheader">
|
||||
{{ s__('Pipeline|Triggerer') }}
|
||||
</div>
|
||||
<div class="table-section section-20 js-pipeline-commit pipeline-commit" role="rowheader">
|
||||
{{ s__('Pipeline|Commit') }}
|
||||
</div>
|
||||
<div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader">
|
||||
{{ s__('Pipeline|Stages') }}
|
||||
</div>
|
||||
<div class="table-section section-15" role="rowheader"></div>
|
||||
<div class="table-section section-20" role="rowheader">
|
||||
<slot name="table-header-actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<pipelines-table-row-component
|
||||
v-for="model in pipelines"
|
||||
:key="model.id"
|
||||
:pipeline="model"
|
||||
:pipeline-schedule-url="pipelineScheduleUrl"
|
||||
:update-graph-dropdown="updateGraphDropdown"
|
||||
:view-type="viewType"
|
||||
:canceling-pipeline="cancelingPipeline"
|
||||
/>
|
||||
</div>
|
||||
<pipelines-table-row-component
|
||||
v-for="model in pipelines"
|
||||
:key="model.id"
|
||||
:pipeline="model"
|
||||
:pipeline-schedule-url="pipelineScheduleUrl"
|
||||
:update-graph-dropdown="updateGraphDropdown"
|
||||
:view-type="viewType"
|
||||
:canceling-pipeline="cancelingPipeline"
|
||||
/>
|
||||
|
||||
<gl-table v-else />
|
||||
|
||||
<pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -131,12 +131,6 @@ export default {
|
|||
commitTitle() {
|
||||
return this.pipeline?.commit?.title;
|
||||
},
|
||||
pipelineDuration() {
|
||||
return this.pipeline?.details?.duration ?? 0;
|
||||
},
|
||||
pipelineFinishedAt() {
|
||||
return this.pipeline?.details?.finished_at ?? '';
|
||||
},
|
||||
pipelineStatus() {
|
||||
return this.pipeline?.details?.status ?? {};
|
||||
},
|
||||
|
@ -231,11 +225,7 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<pipelines-timeago
|
||||
class="gl-text-right"
|
||||
:duration="pipelineDuration"
|
||||
:finished-time="pipelineFinishedAt"
|
||||
/>
|
||||
<pipelines-timeago class="gl-text-right" :pipeline="pipeline" />
|
||||
|
||||
<div
|
||||
v-if="displayPipelineActions"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
|
@ -7,23 +8,19 @@ export default {
|
|||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: { GlIcon },
|
||||
mixins: [timeagoMixin],
|
||||
mixins: [timeagoMixin, glFeatureFlagMixin()],
|
||||
props: {
|
||||
finishedTime: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
pipeline: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasDuration() {
|
||||
return this.duration > 0;
|
||||
duration() {
|
||||
return this.pipeline?.details?.duration;
|
||||
},
|
||||
hasFinishedTime() {
|
||||
return this.finishedTime !== '';
|
||||
finishedTime() {
|
||||
return this.pipeline?.details?.finished_at;
|
||||
},
|
||||
durationFormatted() {
|
||||
const date = new Date(this.duration * 1000);
|
||||
|
@ -45,20 +42,28 @@ export default {
|
|||
|
||||
return `${hh}:${mm}:${ss}`;
|
||||
},
|
||||
legacySectionClass() {
|
||||
return !this.glFeatures.newPipelinesTable ? 'table-section section-15' : '';
|
||||
},
|
||||
legacyTableMobileClass() {
|
||||
return !this.glFeatures.newPipelinesTable ? 'table-mobile-content' : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="table-section section-15">
|
||||
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div>
|
||||
<div class="table-mobile-content">
|
||||
<p v-if="hasDuration" class="duration">
|
||||
<gl-icon name="timer" class="gl-vertical-align-baseline!" />
|
||||
<div :class="legacySectionClass">
|
||||
<div v-if="!glFeatures.newPipelinesTable" class="table-mobile-header" role="rowheader">
|
||||
{{ s__('Pipeline|Duration') }}
|
||||
</div>
|
||||
<div :class="legacyTableMobileClass">
|
||||
<p v-if="duration" class="duration">
|
||||
<gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" />
|
||||
{{ durationFormatted }}
|
||||
</p>
|
||||
|
||||
<p v-if="hasFinishedTime" class="finished-at d-none d-md-block">
|
||||
<gl-icon name="calendar" class="gl-vertical-align-baseline!" />
|
||||
<p v-if="finishedTime" class="finished-at d-none d-md-block">
|
||||
<gl-icon name="calendar" class="gl-vertical-align-baseline!" :size="12" />
|
||||
|
||||
<time
|
||||
v-gl-tooltip
|
||||
|
|
|
@ -31,13 +31,18 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<div v-if="emptyUsers" data-testid="none">
|
||||
<div class="gl-display-flex gl-flex-direction-column issuable-assignees">
|
||||
<div
|
||||
v-if="emptyUsers"
|
||||
class="gl-display-flex gl-align-items-center gl-text-gray-500"
|
||||
data-testid="none"
|
||||
>
|
||||
<span> {{ __('None') }} -</span>
|
||||
<gl-button
|
||||
data-testid="assign-yourself"
|
||||
category="tertiary"
|
||||
variant="link"
|
||||
class="gl-ml-2"
|
||||
@click="$emit('assign-self')"
|
||||
>
|
||||
<span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
|
||||
|
|
|
@ -281,6 +281,9 @@ export default {
|
|||
collapseWidget() {
|
||||
this.$refs.toggle.collapse();
|
||||
},
|
||||
showDivider(list) {
|
||||
return list.length > 0 && this.isSearchEmpty;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -306,6 +309,7 @@ export default {
|
|||
<issuable-assignees
|
||||
:users="assignees"
|
||||
:issuable-type="issuableType"
|
||||
class="gl-mt-2"
|
||||
@assign-self="assignSelf"
|
||||
/>
|
||||
</template>
|
||||
|
@ -334,12 +338,14 @@ export default {
|
|||
data-testid="unassign"
|
||||
@click="selectAssignee()"
|
||||
>
|
||||
<span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'">{{
|
||||
$options.i18n.unassigned
|
||||
}}</span></gl-dropdown-item
|
||||
<span
|
||||
:class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'"
|
||||
class="gl-font-weight-bold"
|
||||
>{{ $options.i18n.unassigned }}</span
|
||||
></gl-dropdown-item
|
||||
>
|
||||
<gl-dropdown-divider data-testid="unassign-divider" />
|
||||
</template>
|
||||
<gl-dropdown-divider v-if="showDivider(selectedFiltered)" />
|
||||
<gl-dropdown-item
|
||||
v-for="item in selectedFiltered"
|
||||
:key="item.id"
|
||||
|
@ -358,8 +364,8 @@ export default {
|
|||
/>
|
||||
</gl-avatar-link>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
|
||||
<template v-if="showCurrentUser">
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item
|
||||
data-testid="unselected-participant"
|
||||
@click.stop="selectAssignee(currentUser)"
|
||||
|
@ -370,12 +376,12 @@ export default {
|
|||
:label="currentUser.name"
|
||||
:sub-label="currentUser.username"
|
||||
:src="currentUser.avatarUrl"
|
||||
class="gl-align-items-center"
|
||||
class="gl-align-items-center gl-pl-6!"
|
||||
/>
|
||||
</gl-avatar-link>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
</template>
|
||||
<gl-dropdown-divider v-if="showDivider(unselectedFiltered)" />
|
||||
<gl-dropdown-item
|
||||
v-for="unselectedUser in unselectedFiltered"
|
||||
:key="unselectedUser.id"
|
||||
|
|
|
@ -83,7 +83,7 @@ export default {
|
|||
<assignee-avatar-link :user="user" :issuable-type="issuableType" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="renderShowMoreSection" class="user-list-more">
|
||||
<div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-link"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
|
||||
|
||||
const LOADING_STATE = 'loading';
|
||||
|
@ -50,6 +51,9 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
approvedByTooltipTitle(user) {
|
||||
return sprintf(s__('MergeRequest|Approved by @%{username}'), user);
|
||||
},
|
||||
toggleShowLess() {
|
||||
this.showLess = !this.showLess;
|
||||
},
|
||||
|
@ -57,6 +61,7 @@ export default {
|
|||
this.loadingStates[userId] = LOADING_STATE;
|
||||
this.$emit('request-review', { userId, callback: this.requestReviewComplete });
|
||||
},
|
||||
|
||||
requestReviewComplete(userId, success) {
|
||||
if (success) {
|
||||
this.loadingStates[userId] = SUCCESS_STATE;
|
||||
|
@ -85,11 +90,20 @@ export default {
|
|||
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
|
||||
<div class="gl-ml-3">@{{ user.username }}</div>
|
||||
</reviewer-avatar-link>
|
||||
<gl-icon
|
||||
v-if="user.approved"
|
||||
v-gl-tooltip.left
|
||||
:size="16"
|
||||
:title="approvedByTooltipTitle(user)"
|
||||
name="status-success"
|
||||
class="float-right gl-my-2 gl-ml-2 gl-text-green-500"
|
||||
data-testid="re-approved"
|
||||
/>
|
||||
<gl-icon
|
||||
v-if="loadingStates[user.id] === $options.SUCCESS_STATE"
|
||||
:size="24"
|
||||
name="check"
|
||||
class="float-right gl-text-green-500"
|
||||
class="float-right gl-py-2 gl-mr-2 gl-text-green-500"
|
||||
data-testid="re-request-success"
|
||||
/>
|
||||
<gl-button
|
||||
|
|
|
@ -98,7 +98,7 @@ export default {
|
|||
{{ __('Edit') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content">
|
||||
<div v-show="!edit" data-testid="collapsed-content">
|
||||
<slot name="collapsed">{{ __('None') }}</slot>
|
||||
</div>
|
||||
<div v-show="edit" data-testid="expanded-content">
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
query mergeRequestSidebarDetails($fullPath: ID!, $iid: String!) {
|
||||
project(fullPath: $fullPath) {
|
||||
mergeRequest(iid: $iid) {
|
||||
iid # currently unused.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,14 @@
|
|||
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
|
||||
import sidebarDetailsIssueQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import createGqClient, { fetchPolicies } from '~/lib/graphql';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
|
||||
import sidebarDetailsMRQuery from '../queries/sidebarDetailsMR.query.graphql';
|
||||
|
||||
const queries = {
|
||||
merge_request: sidebarDetailsMRQuery,
|
||||
issue: sidebarDetailsIssueQuery,
|
||||
};
|
||||
|
||||
export const gqClient = createGqClient(
|
||||
{},
|
||||
|
@ -20,6 +26,7 @@ export default class SidebarService {
|
|||
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
|
||||
this.fullPath = endpointMap.fullPath;
|
||||
this.iid = endpointMap.iid;
|
||||
this.issuableType = endpointMap.issuableType;
|
||||
|
||||
SidebarService.singleton = this;
|
||||
}
|
||||
|
@ -31,7 +38,7 @@ export default class SidebarService {
|
|||
return Promise.all([
|
||||
axios.get(this.endpoint),
|
||||
gqClient.query({
|
||||
query: sidebarDetailsQuery,
|
||||
query: this.sidebarDetailsQuery(),
|
||||
variables: {
|
||||
fullPath: this.fullPath,
|
||||
iid: this.iid.toString(),
|
||||
|
@ -40,6 +47,10 @@ export default class SidebarService {
|
|||
]);
|
||||
}
|
||||
|
||||
sidebarDetailsQuery() {
|
||||
return queries[this.issuableType];
|
||||
}
|
||||
|
||||
update(key, data) {
|
||||
return axios.put(this.endpoint, { [key]: data });
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ export default class SidebarMediator {
|
|||
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
|
||||
fullPath: options.fullPath,
|
||||
iid: options.iid,
|
||||
issuableType: options.issuableType,
|
||||
});
|
||||
SidebarMediator.singleton = this;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownForm } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownForm, GlDropdownDivider } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdownForm,
|
||||
GlDropdown,
|
||||
GlDropdownDivider,
|
||||
},
|
||||
props: {
|
||||
headerText: {
|
||||
|
@ -20,8 +21,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown class="show" :text="text" :header-text="headerText" @toggle="$emit('toggle')">
|
||||
<slot name="search"></slot>
|
||||
<gl-dropdown class="show" :text="text" @toggle="$emit('toggle')">
|
||||
<template #header>
|
||||
<p class="gl-font-weight-bold gl-text-center gl-mt-2 gl-mb-4">{{ headerText }}</p>
|
||||
<gl-dropdown-divider />
|
||||
<slot name="search"></slot>
|
||||
</template>
|
||||
<gl-dropdown-form>
|
||||
<slot name="items"></slot>
|
||||
</gl-dropdown-form>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
|
||||
#cycle-analytics,
|
||||
.cycle-analytics {
|
||||
margin: 24px auto 0;
|
||||
position: relative;
|
||||
|
@ -316,35 +315,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-stage,
|
||||
.no-access-stage {
|
||||
text-align: center;
|
||||
width: 75%;
|
||||
margin: 0 auto;
|
||||
padding-top: 130px;
|
||||
color: var(--gray-500, $gray-500);
|
||||
|
||||
h4 {
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-stage {
|
||||
.icon-no-data {
|
||||
height: 36px;
|
||||
width: 78px;
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-access-stage {
|
||||
.icon-lock {
|
||||
height: 36px;
|
||||
width: 78px;
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:jira_for_vulnerabilities, project, type: :development, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:new_pipelines_table, project, default_enabled: :yaml)
|
||||
end
|
||||
before_action :ensure_pipeline, only: [:show]
|
||||
|
||||
|
|
|
@ -388,7 +388,8 @@ module IssuablesHelper
|
|||
iid: issuable[:iid],
|
||||
severity: issuable[:severity],
|
||||
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours,
|
||||
createNoteEmail: issuable[:create_note_email]
|
||||
createNoteEmail: issuable[:create_note_email],
|
||||
issuableType: issuable[:type]
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -1282,7 +1282,14 @@ class MergeRequest < ApplicationRecord
|
|||
# Returns the oldest multi-line commit message, or the MR title if none found
|
||||
def default_squash_commit_message
|
||||
strong_memoize(:default_squash_commit_message) do
|
||||
recent_commits.without_merge_commits.reverse_each.find(&:description?)&.safe_message || title
|
||||
first_multiline_commit&.safe_message || title
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the oldest multi-line commit
|
||||
def first_multiline_commit
|
||||
strong_memoize(:first_multiline_commit) do
|
||||
recent_commits.without_merge_commits.reverse_each.find(&:description?)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class PrometheusService < MonitoringService
|
|||
end
|
||||
|
||||
def description
|
||||
s_('PrometheusService|Time-series monitoring service')
|
||||
s_('PrometheusService|Monitor application health with Prometheus metrics and dashboards')
|
||||
end
|
||||
|
||||
def self.to_param
|
||||
|
@ -59,20 +59,23 @@ class PrometheusService < MonitoringService
|
|||
type: 'checkbox',
|
||||
name: 'manual_configuration',
|
||||
title: s_('PrometheusService|Active'),
|
||||
help: s_('PrometheusService|Select this checkbox to override the auto configuration settings with your own settings.'),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'api_url',
|
||||
title: 'API URL',
|
||||
placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
|
||||
placeholder: s_('PrometheusService|https://prometheus.example.com/'),
|
||||
help: s_('PrometheusService|The Prometheus API base URL.'),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'google_iap_audience_client_id',
|
||||
title: 'Google IAP Audience Client ID',
|
||||
placeholder: s_('PrometheusService|Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com)'),
|
||||
placeholder: s_('PrometheusService|IAP_CLIENT_ID.apps.googleusercontent.com'),
|
||||
help: s_('PrometheusService|PrometheusService|The ID of the IAP-secured resource.'),
|
||||
autocomplete: 'off',
|
||||
required: false
|
||||
},
|
||||
|
@ -80,7 +83,8 @@ class PrometheusService < MonitoringService
|
|||
type: 'textarea',
|
||||
name: 'google_iap_service_account_json',
|
||||
title: 'Google IAP Service Account JSON',
|
||||
placeholder: s_('PrometheusService|Contents of the credentials.json file of your service account, like: { "type": "service_account", "project_id": ... }'),
|
||||
placeholder: s_('PrometheusService|{ "type": "service_account", "project_id": ... }'),
|
||||
help: s_('PrometheusService|The contents of the credentials.json file of your service account.'),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
|
|
|
@ -4,6 +4,10 @@ class MergeRequestUserEntity < ::API::Entities::UserBasic
|
|||
include UserStatusTooltip
|
||||
include RequestAwareEntity
|
||||
|
||||
def self.satisfies(*methods)
|
||||
->(_, options) { methods.all? { |m| options[:merge_request].try(m) } }
|
||||
end
|
||||
|
||||
expose :can_merge do |reviewer, options|
|
||||
options[:merge_request]&.can_be_merged_by?(reviewer)
|
||||
end
|
||||
|
@ -12,11 +16,17 @@ class MergeRequestUserEntity < ::API::Entities::UserBasic
|
|||
request.current_user&.can?(:update_merge_request, options[:merge_request])
|
||||
end
|
||||
|
||||
expose :reviewed, if: -> (_, options) { options[:merge_request] && options[:merge_request].allows_reviewers? } do |reviewer, options|
|
||||
expose :reviewed, if: satisfies(:present?, :allows_reviewers?) do |reviewer, options|
|
||||
reviewer = options[:merge_request].find_reviewer(reviewer)
|
||||
|
||||
reviewer&.reviewed?
|
||||
end
|
||||
|
||||
expose :approved, if: satisfies(:present?) do |user, options|
|
||||
# This approach is preferred over MergeRequest#approved_by? since this
|
||||
# makes one query per merge request, whereas #approved_by? makes one per user
|
||||
options[:merge_request].approvals.any? { |app| app.user_id == user.id }
|
||||
end
|
||||
end
|
||||
|
||||
MergeRequestUserEntity.prepend_if_ee('EE::MergeRequestUserEntity')
|
||||
|
|
|
@ -58,6 +58,7 @@ module MergeRequests
|
|||
:compare_commits,
|
||||
:wip_title,
|
||||
:description,
|
||||
:first_multiline_commit,
|
||||
:errors,
|
||||
to: :merge_request
|
||||
|
||||
|
@ -196,7 +197,8 @@ module MergeRequests
|
|||
# interpreted as the user wants to close that issue on this project.
|
||||
#
|
||||
# For example:
|
||||
# - Issue 112 exists, title: Emoji don't show up in commit title
|
||||
# - Issue 112 exists
|
||||
# - title: Emoji don't show up in commit title
|
||||
# - Source branch is: 112-fix-mep-mep
|
||||
#
|
||||
# Will lead to:
|
||||
|
@ -205,7 +207,7 @@ module MergeRequests
|
|||
# more than one commit in the MR
|
||||
#
|
||||
def assign_title_and_description
|
||||
assign_title_and_description_from_single_commit
|
||||
assign_title_and_description_from_commits
|
||||
merge_request.title ||= title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker
|
||||
merge_request.title ||= source_branch.titleize.humanize
|
||||
merge_request.title = wip_title if compare_commits.empty?
|
||||
|
@ -240,12 +242,16 @@ module MergeRequests
|
|||
end
|
||||
end
|
||||
|
||||
def assign_title_and_description_from_single_commit
|
||||
def assign_title_and_description_from_commits
|
||||
commits = compare_commits
|
||||
|
||||
return unless commits&.count == 1
|
||||
if commits&.count == 1
|
||||
commit = commits.first
|
||||
else
|
||||
commit = first_multiline_commit
|
||||
return unless commit
|
||||
end
|
||||
|
||||
commit = commits.first
|
||||
merge_request.title ||= commit.title
|
||||
merge_request.description ||= commit.description.try(:strip)
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.empty-stage-container
|
||||
.empty-stage
|
||||
.icon-no-data
|
||||
= custom_icon ('icon_no_data')
|
||||
%h4 {{ __('We don\'t have enough data to show this stage.') }}
|
||||
%p
|
||||
{{currentStage.emptyStageText}}
|
|
@ -1,7 +0,0 @@
|
|||
.no-access-stage-container
|
||||
.no-access-stage
|
||||
.icon-lock
|
||||
= custom_icon ('icon_lock')
|
||||
%h4 {{ __('You need permission.') }}
|
||||
%p
|
||||
{{ __('Want to see the data? Please ask an administrator for access.') }}
|
|
@ -1,66 +1,6 @@
|
|||
- page_title _("Value Stream Analytics")
|
||||
- add_page_specific_style 'page_bundles/cycle_analytics'
|
||||
- svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") }
|
||||
- initial_data = { request_path: project_cycle_analytics_path(@project) }.merge!(svgs)
|
||||
|
||||
#cycle-analytics{ "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
|
||||
%gl-loading-icon{ "v-show" => "isLoading", "size" => "lg" }
|
||||
.wrapper{ "v-show" => "!isLoading && !hasError" }
|
||||
.card
|
||||
.card-header
|
||||
{{ __('Recent Project Activity') }}
|
||||
.d-flex.justify-content-between
|
||||
.flex-grow.text-center{ "v-for" => "item in state.summary" }
|
||||
%h3.header {{ item.value }}
|
||||
%p.text {{ item.title }}
|
||||
.flex-grow.align-self-center.text-center
|
||||
.dropdown.inline.js-ca-dropdown
|
||||
%button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" }
|
||||
%span.dropdown-label {{ n__('Last %d day', 'Last %d days', 30) }}
|
||||
= sprite_icon("chevron-down", css_class: "dropdown-menu-toggle-icon gl-top-3")
|
||||
%ul.dropdown-menu.dropdown-menu-right
|
||||
%li
|
||||
%a{ "href" => "#", "data-value" => "7" }
|
||||
{{ n__('Last %d day', 'Last %d days', 7) }}
|
||||
%li
|
||||
%a{ "href" => "#", "data-value" => "30" }
|
||||
{{ n__('Last %d day', 'Last %d days', 30) }}
|
||||
%li
|
||||
%a{ "href" => "#", "data-value" => "90" }
|
||||
{{ n__('Last %d day', 'Last %d days', 90) }}
|
||||
.stage-panel-container
|
||||
.card.stage-panel
|
||||
.card-header.border-bottom-0
|
||||
%nav.col-headers
|
||||
%ul
|
||||
%li.stage-header.pl-5
|
||||
%span.stage-name.font-weight-bold
|
||||
{{ s__('ProjectLifecycle|Stage') }}
|
||||
%span.has-tooltip{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" }
|
||||
= sprite_icon('question-o', css_class: 'gl-text-gray-500')
|
||||
%li.median-header
|
||||
%span.stage-name.font-weight-bold
|
||||
{{ __('Median') }}
|
||||
%span.has-tooltip{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" }
|
||||
= sprite_icon('question-o', css_class: 'gl-text-gray-500')
|
||||
%li.event-header.pl-3
|
||||
%span.stage-name.font-weight-bold{ "v-if" => "currentStage && currentStage.legend" }
|
||||
{{ currentStage ? __(currentStage.legend) : __('Related Issues') }}
|
||||
%span.has-tooltip{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" }
|
||||
= sprite_icon('question-o', css_class: 'gl-text-gray-500')
|
||||
%li.total-time-header.pr-5.text-right
|
||||
%span.stage-name.font-weight-bold
|
||||
{{ __('Time') }}
|
||||
%span.has-tooltip{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" }
|
||||
= sprite_icon('question-o', css_class: 'gl-text-gray-500')
|
||||
.stage-panel-body
|
||||
%nav.stage-nav
|
||||
%ul
|
||||
%stage-nav-item{ "v-for" => "stage in state.stages", ":key" => '`ca-stage-title-${stage.title}`', '@select' => 'selectStage(stage)', ":title" => "stage.title", ":is-user-allowed" => "stage.isUserAllowed", ":value" => "stage.value", ":is-active" => "stage.active" }
|
||||
.section.stage-events.overflow-auto
|
||||
%gl-loading-icon{ "v-show" => "isLoadingStage", "size" => "lg" }
|
||||
%template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
|
||||
= render partial: "no_access"
|
||||
%template{ "v-else" => true }
|
||||
%template{ "v-if" => "isEmptyStage && !isLoadingStage" }
|
||||
= render partial: "empty_stage"
|
||||
%template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage" }
|
||||
%component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" }
|
||||
#js-cycle-analytics{ data: initial_data }
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
- if service.manual_configuration?
|
||||
.info-well
|
||||
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
|
||||
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration.')
|
||||
- else
|
||||
.container-fluid
|
||||
.row
|
||||
|
@ -13,14 +13,14 @@
|
|||
= image_tag 'illustrations/monitoring/getting_started.svg'
|
||||
.col-sm-10
|
||||
%p.text-success.gl-mt-3
|
||||
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
|
||||
= s_('PrometheusService|GitLab is managing Prometheus on your clusters.')
|
||||
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn gl-button'
|
||||
- else
|
||||
.col-sm-2
|
||||
= image_tag 'illustrations/monitoring/loading.svg'
|
||||
.col-sm-10
|
||||
%p.gl-mt-3
|
||||
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
|
||||
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments.')
|
||||
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn gl-button btn-success'
|
||||
|
||||
%hr
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
%h4.gl-mb-3
|
||||
= s_('PrometheusService|Manual configuration')
|
||||
%p
|
||||
= s_('PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used.')
|
||||
= s_('PrometheusService|Auto configuration settings are used unless you override their values here.')
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
- if service.manual_configuration?
|
||||
.info-well.p-2.mt-2
|
||||
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
|
||||
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration.')
|
||||
- else
|
||||
.container-fluid
|
||||
.row
|
||||
|
@ -13,12 +13,12 @@
|
|||
= image_tag 'illustrations/monitoring/getting_started.svg'
|
||||
.col-sm-10
|
||||
%p.text-success.gl-mt-3
|
||||
= s_('PrometheusService|Prometheus is being automatically managed on your clusters')
|
||||
= s_('PrometheusService|GitLab manages Prometheus on your clusters.')
|
||||
= link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn'
|
||||
- else
|
||||
.col-sm-2
|
||||
= image_tag 'illustrations/monitoring/loading.svg'
|
||||
.col-sm-10
|
||||
%p.gl-mt-3
|
||||
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
|
||||
= s_('PrometheusService|Monitor your project’s environments by deploying and configuring Prometheus on your clusters.')
|
||||
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn btn-success'
|
||||
|
|
|
@ -14,4 +14,4 @@
|
|||
%b.gl-mb-3
|
||||
= s_('PrometheusService|Manual configuration')
|
||||
%p
|
||||
= s_('PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used.')
|
||||
= s_('PrometheusService|Auto configuration settings are used unless you override their values here.')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Prefill first multiline commit message for new MRs
|
||||
merge_request: 52984
|
||||
author: Max Coplan @vegerot
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Updated UI text to match style guidelines
|
||||
merge_request: 53179
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/ajk-reviewer-widget-approved.yml
Normal file
5
changelogs/unreleased/ajk-reviewer-widget-approved.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show icon next to reviewers who have approved
|
||||
merge_request: 54365
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Auto-enable admin mode on privileged environments
|
||||
merge_request: 53015
|
||||
author: Diego Louzán
|
||||
type: changed
|
8
config/feature_flags/development/new_pipelines_table.yml
Normal file
8
config/feature_flags/development/new_pipelines_table.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: new_pipelines_table
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54958
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322599
|
||||
milestone: '13.10'
|
||||
type: development
|
||||
group: group::continuous integration
|
||||
default_enabled: false
|
|
@ -1302,6 +1302,7 @@ Represents a DAST scanner profile.
|
|||
| `globalId` **{warning-solid}** | DastScannerProfileID! | **Deprecated:** Use `id`. Deprecated in 13.6. |
|
||||
| `id` | DastScannerProfileID! | ID of the DAST scanner profile. |
|
||||
| `profileName` | String | Name of the DAST scanner profile. |
|
||||
| `referencedInSecurityPolicies` | String! => Array | List of security policy names that are referencing given project. |
|
||||
| `scanType` | DastScanTypeEnum | Indicates the type of DAST scan that will run. Either a Passive Scan or an Active Scan. |
|
||||
| `showDebugMessages` | Boolean! | Indicates if debug messages should be included in DAST console output. True to include the debug messages. |
|
||||
| `spiderTimeout` | Int | The maximum number of minutes allowed for the spider to traverse the site. |
|
||||
|
@ -1348,6 +1349,7 @@ Represents a DAST Site Profile.
|
|||
| `id` | DastSiteProfileID! | ID of the site profile. |
|
||||
| `normalizedTargetUrl` | String | Normalized URL of the target to be scanned. |
|
||||
| `profileName` | String | The name of the site profile. |
|
||||
| `referencedInSecurityPolicies` | String! => Array | List of security policy names that are referencing given project. |
|
||||
| `targetUrl` | String | The URL of the target to be scanned. |
|
||||
| `userPermissions` | DastSiteProfilePermissions! | Permissions for the current user on the resource |
|
||||
| `validationStatus` | DastSiteProfileValidationStatusEnum | The current validation status of the site profile. |
|
||||
|
|
|
@ -141,7 +141,7 @@ be checked to make sure the jobs are added to the correct pipeline type. For
|
|||
example, if a merge request pipeline did not run, the jobs may have been added to
|
||||
a branch pipeline instead.
|
||||
|
||||
It's also possible that your [`workflow: rules`](yaml/README.md#workflowrules) configuration
|
||||
It's also possible that your [`workflow: rules`](yaml/README.md#workflow) configuration
|
||||
blocked the pipeline, or allowed the wrong pipeline type.
|
||||
|
||||
### A job runs unexpectedly
|
||||
|
@ -259,7 +259,7 @@ clause, multiple pipelines may run. Usually this occurs when you push a commit t
|
|||
a branch that has an open merge request associated with it.
|
||||
|
||||
To [prevent duplicate pipelines](yaml/README.md#avoid-duplicate-pipelines), use
|
||||
[`workflow: rules`](yaml/README.md#workflowrules) or rewrite your rules to control
|
||||
[`workflow: rules`](yaml/README.md#workflow) or rewrite your rules to control
|
||||
which pipelines can run.
|
||||
|
||||
### Console workaround if job using resource_group gets stuck
|
||||
|
|
|
@ -26,41 +26,43 @@ A job is defined as a list of keywords that define the job's behavior.
|
|||
|
||||
The keywords available for jobs are:
|
||||
|
||||
| Keyword | Description |
|
||||
|:---------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [`script`](#script) | Shell script that is executed by a runner. |
|
||||
| [`after_script`](#after_script) | Override a set of commands that are executed after job. |
|
||||
| [`allow_failure`](#allow_failure) | Allow job to fail. A failed job does not cause the pipeline to fail. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, and `artifacts:reports`. |
|
||||
| [`before_script`](#before_script) | Override a set of commands that are executed before job. |
|
||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, `cache:when`, and `cache:policy`. |
|
||||
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
||||
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
||||
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in`, and `environment:action`. |
|
||||
| [`except`](#onlyexcept-basic) | Limit when jobs are not created. Also available: [`except:refs`, `except:kubernetes`, `except:variables`, and `except:changes`](#onlyexcept-advanced). |
|
||||
| [`extends`](#extends) | Configuration entries that this job inherits from. |
|
||||
| [`image`](#image) | Use Docker images. Also available: `image:name` and `image:entrypoint`. |
|
||||
| [`include`](#include) | Include external YAML files. Also available: `include:local`, `include:file`, `include:template`, and `include:remote`. |
|
||||
| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
|
||||
| [`only`](#onlyexcept-basic) | Limit when jobs are created. Also available: [`only:refs`, `only:kubernetes`, `only:variables`, and `only:changes`](#onlyexcept-advanced). |
|
||||
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
|
||||
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
|
||||
| [`release`](#release) | Instructs the runner to generate a [Release](../../user/project/releases/index.md) object. |
|
||||
| [`resource_group`](#resource_group) | Limit job concurrency. |
|
||||
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
|
||||
| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. |
|
||||
| [`services`](#services) | Use Docker services images. Also available: `services:name`, `services:alias`, `services:entrypoint`, and `services:command`. |
|
||||
| [`stage`](#stage) | Defines a job stage (default: `test`). |
|
||||
| [`tags`](#tags) | List of tags that are used to select a runner. |
|
||||
| [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
|
||||
| [`trigger`](#trigger) | Defines a downstream pipeline trigger. |
|
||||
| [`variables`](#variables) | Define job variables on a job level. |
|
||||
| [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. |
|
||||
| Keyword | Description |
|
||||
| :-----------------------------------|:------------|
|
||||
| [`after_script`](#after_script) | Override a set of commands that are executed after job. |
|
||||
| [`allow_failure`](#allow_failure) | Allow job to fail. A failed job does not cause the pipeline to fail. |
|
||||
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. |
|
||||
| [`before_script`](#before_script) | Override a set of commands that are executed before job. |
|
||||
| [`cache`](#cache) | List of files that should be cached between subsequent runs. |
|
||||
| [`coverage`](#coverage) | Code coverage settings for a given job. |
|
||||
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
|
||||
| [`environment`](#environment) | Name of an environment to which the job deploys. |
|
||||
| [`except`](#onlyexcept-basic) | Limit when jobs are not created. |
|
||||
| [`extends`](#extends) | Configuration entries that this job inherits from. |
|
||||
| [`image`](#image) | Use Docker images. |
|
||||
| [`include`](#include) | Include external YAML files. |
|
||||
| [`inherit`](#inherit) | Select which global defaults all jobs inherit. |
|
||||
| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
|
||||
| [`needs`](#needs) | Execute jobs earlier than the stage ordering. |
|
||||
| [`only`](#onlyexcept-basic) | Limit when jobs are created. |
|
||||
| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
|
||||
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
|
||||
| [`release`](#release) | Instructs the runner to generate a [Release](../../user/project/releases/index.md) object. |
|
||||
| [`resource_group`](#resource_group) | Limit job concurrency. |
|
||||
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
|
||||
| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. |
|
||||
| [`script`](#script) | Shell script that is executed by a runner. |
|
||||
| [`secrets`](#secrets) | The CI/CD secrets the job needs. |
|
||||
| [`services`](#services) | Use Docker services images. |
|
||||
| [`stage`](#stage) | Defines a job stage. |
|
||||
| [`tags`](#tags) | List of tags that are used to select a runner. |
|
||||
| [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
|
||||
| [`trigger`](#trigger) | Defines a downstream pipeline trigger. |
|
||||
| [`variables`](#variables) | Define job variables on a job level. |
|
||||
| [`when`](#when) | When to run job. |
|
||||
|
||||
### Unavailable names for jobs
|
||||
|
||||
Each job must have a unique name, but there are a few reserved `keywords` that
|
||||
can't be used as job names:
|
||||
You can't use these keywords as job names:
|
||||
|
||||
- `image`
|
||||
- `services`
|
||||
|
@ -72,38 +74,27 @@ can't be used as job names:
|
|||
- `cache`
|
||||
- `include`
|
||||
|
||||
### Reserved keywords
|
||||
### Custom default keyword values
|
||||
|
||||
If you get a validation error when you use specific values (for example, `true` or `false`), try to:
|
||||
You can set global defaults for some keywords. Jobs that do not define one or more
|
||||
of the listed keywords use the value defined in the `default:` section.
|
||||
|
||||
- Quote them.
|
||||
- Change them to a different form. For example, `/bin/true`.
|
||||
The following job keywords can be defined inside a `default:` section:
|
||||
|
||||
## Global keywords
|
||||
|
||||
Some keywords are defined at a global level and affect all jobs in the pipeline.
|
||||
|
||||
### Global defaults
|
||||
|
||||
Some keywords can be set globally as the default for all jobs with the
|
||||
`default:` keyword. Default keywords can then be overridden by job-specific
|
||||
configuration.
|
||||
|
||||
The following job keywords can be defined inside a `default:` block:
|
||||
|
||||
- [`image`](#image)
|
||||
- [`services`](#services)
|
||||
- [`before_script`](#before_script)
|
||||
- [`after_script`](#after_script)
|
||||
- [`tags`](#tags)
|
||||
- [`cache`](#cache)
|
||||
- [`artifacts`](#artifacts)
|
||||
- [`retry`](#retry)
|
||||
- [`timeout`](#timeout)
|
||||
- [`before_script`](#before_script)
|
||||
- [`cache`](#cache)
|
||||
- [`image`](#image)
|
||||
- [`interruptible`](#interruptible)
|
||||
- [`retry`](#retry)
|
||||
- [`services`](#services)
|
||||
- [`tags`](#tags)
|
||||
- [`timeout`](#timeout)
|
||||
|
||||
In the following example, the `ruby:2.5` image is set as the default for all
|
||||
jobs except the `rspec 2.6` job, which uses the `ruby:2.6` image:
|
||||
This example sets the `ruby:2.5` image as the default for all jobs in the pipeline.
|
||||
The `rspec 2.6` job does not use the default, because it overrides the default with
|
||||
a job-specific `image:` section:
|
||||
|
||||
```yaml
|
||||
default:
|
||||
|
@ -117,87 +108,16 @@ rspec 2.6:
|
|||
script: bundle exec rspec
|
||||
```
|
||||
|
||||
#### `inherit`
|
||||
## Global keywords
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207484) in GitLab 12.9.
|
||||
Some keywords are not defined within a job. These keywords control pipeline behavior
|
||||
or import additional pipeline configuration:
|
||||
|
||||
You can disable inheritance of globally defined defaults
|
||||
and variables with the `inherit:` keyword.
|
||||
|
||||
To enable or disable the inheritance of all `default:` or `variables:` keywords, use:
|
||||
|
||||
- `default: true` or `default: false`
|
||||
- `variables: true` or `variables: false`
|
||||
|
||||
To inherit only a subset of `default:` keywords or `variables:`, specify what
|
||||
you wish to inherit. Anything not listed is **not** inherited. Use
|
||||
one of the following formats:
|
||||
|
||||
```yaml
|
||||
inherit:
|
||||
default: [keyword1, keyword2]
|
||||
variables: [VARIABLE1, VARIABLE2]
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```yaml
|
||||
inherit:
|
||||
default:
|
||||
- keyword1
|
||||
- keyword2
|
||||
variables:
|
||||
- VARIABLE1
|
||||
- VARIABLE2
|
||||
```
|
||||
|
||||
In the example below:
|
||||
|
||||
- `rubocop`:
|
||||
- inherits: Nothing.
|
||||
- `rspec`:
|
||||
- inherits: the default `image` and the `WEBHOOK_URL` variable.
|
||||
- does **not** inherit: the default `before_script` and the `DOMAIN` variable.
|
||||
- `capybara`:
|
||||
- inherits: the default `before_script` and `image`.
|
||||
- does **not** inherit: the `DOMAIN` and `WEBHOOK_URL` variables.
|
||||
- `karma`:
|
||||
- inherits: the default `image` and `before_script`, and the `DOMAIN` variable.
|
||||
- does **not** inherit: `WEBHOOK_URL` variable.
|
||||
|
||||
```yaml
|
||||
default:
|
||||
image: 'ruby:2.4'
|
||||
before_script:
|
||||
- echo Hello World
|
||||
|
||||
variables:
|
||||
DOMAIN: example.com
|
||||
WEBHOOK_URL: https://my-webhook.example.com
|
||||
|
||||
rubocop:
|
||||
inherit:
|
||||
default: false
|
||||
variables: false
|
||||
script: bundle exec rubocop
|
||||
|
||||
rspec:
|
||||
inherit:
|
||||
default: [image]
|
||||
variables: [WEBHOOK_URL]
|
||||
script: bundle exec rspec
|
||||
|
||||
capybara:
|
||||
inherit:
|
||||
variables: false
|
||||
script: bundle exec capybara
|
||||
|
||||
karma:
|
||||
inherit:
|
||||
default: true
|
||||
variables: [DOMAIN]
|
||||
script: karma
|
||||
```
|
||||
| Keyword | Description |
|
||||
|-------------------------|:------------|
|
||||
| [`stages`](#stages) | The names and order of the pipeline stages. |
|
||||
| [`workflow`](#workflow) | Control what types of pipeline run. |
|
||||
| [`include`](#include) | Import configuration from other YAML files. |
|
||||
|
||||
### `stages`
|
||||
|
||||
|
@ -235,7 +155,7 @@ If a job does not specify a [`stage`](#stage), the job is assigned the `test` st
|
|||
To make a job start earlier and ignore the stage order, use
|
||||
the [`needs`](#needs) keyword.
|
||||
|
||||
### `workflow:rules`
|
||||
### `workflow`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29654) in GitLab 12.5
|
||||
|
||||
|
@ -1183,7 +1103,7 @@ causes duplicated pipelines.
|
|||
|
||||
There are multiple ways to avoid duplicate pipelines:
|
||||
|
||||
- Use [`workflow: rules`](#workflowrules) to specify which types of pipelines
|
||||
- Use [`workflow`](#workflow) to specify which types of pipelines
|
||||
can run.
|
||||
- Rewrite the rules to run the job only in very specific cases,
|
||||
and avoid a final `when:` rule:
|
||||
|
@ -2347,7 +2267,7 @@ The valid values of `when` are:
|
|||
Added in GitLab 11.14.
|
||||
1. `never`:
|
||||
- With [`rules`](#rules), don't execute job.
|
||||
- With [`workflow:rules`](#workflowrules), don't run pipeline.
|
||||
- With [`workflow`](#workflow), don't run pipeline.
|
||||
|
||||
For example:
|
||||
|
||||
|
@ -4301,6 +4221,88 @@ pages:
|
|||
|
||||
Read more on [GitLab Pages user documentation](../../user/project/pages/index.md).
|
||||
|
||||
### `inherit`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207484) in GitLab 12.9.
|
||||
|
||||
You can disable inheritance of globally defined defaults
|
||||
and variables with the `inherit:` keyword.
|
||||
|
||||
To enable or disable the inheritance of all `default:` or `variables:` keywords, use:
|
||||
|
||||
- `default: true` or `default: false`
|
||||
- `variables: true` or `variables: false`
|
||||
|
||||
To inherit only a subset of `default:` keywords or `variables:`, specify what
|
||||
you wish to inherit. Anything not listed is **not** inherited. Use
|
||||
one of the following formats:
|
||||
|
||||
```yaml
|
||||
inherit:
|
||||
default: [keyword1, keyword2]
|
||||
variables: [VARIABLE1, VARIABLE2]
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```yaml
|
||||
inherit:
|
||||
default:
|
||||
- keyword1
|
||||
- keyword2
|
||||
variables:
|
||||
- VARIABLE1
|
||||
- VARIABLE2
|
||||
```
|
||||
|
||||
In the example below:
|
||||
|
||||
- `rubocop`:
|
||||
- inherits: Nothing.
|
||||
- `rspec`:
|
||||
- inherits: the default `image` and the `WEBHOOK_URL` variable.
|
||||
- does **not** inherit: the default `before_script` and the `DOMAIN` variable.
|
||||
- `capybara`:
|
||||
- inherits: the default `before_script` and `image`.
|
||||
- does **not** inherit: the `DOMAIN` and `WEBHOOK_URL` variables.
|
||||
- `karma`:
|
||||
- inherits: the default `image` and `before_script`, and the `DOMAIN` variable.
|
||||
- does **not** inherit: `WEBHOOK_URL` variable.
|
||||
|
||||
```yaml
|
||||
default:
|
||||
image: 'ruby:2.4'
|
||||
before_script:
|
||||
- echo Hello World
|
||||
|
||||
variables:
|
||||
DOMAIN: example.com
|
||||
WEBHOOK_URL: https://my-webhook.example.com
|
||||
|
||||
rubocop:
|
||||
inherit:
|
||||
default: false
|
||||
variables: false
|
||||
script: bundle exec rubocop
|
||||
|
||||
rspec:
|
||||
inherit:
|
||||
default: [image]
|
||||
variables: [WEBHOOK_URL]
|
||||
script: bundle exec rspec
|
||||
|
||||
capybara:
|
||||
inherit:
|
||||
variables: false
|
||||
script: bundle exec capybara
|
||||
|
||||
karma:
|
||||
inherit:
|
||||
default: true
|
||||
variables: [DOMAIN]
|
||||
script: karma
|
||||
```
|
||||
|
||||
## `variables`
|
||||
|
||||
> Introduced in GitLab Runner v0.5.0.
|
||||
|
@ -4720,7 +4722,7 @@ Defining `image`, `services`, `cache`, `before_script`, and
|
|||
`after_script` globally is deprecated. Support could be removed
|
||||
from a future release.
|
||||
|
||||
Use [`default:`](#global-defaults) instead. For example:
|
||||
Use [`default:`](#custom-default-keyword-values) instead. For example:
|
||||
|
||||
```yaml
|
||||
default:
|
||||
|
|
|
@ -145,3 +145,34 @@ job:
|
|||
- Write-Host $TXT_RED"This text is red,"$TXT_CLEAR" but this text isn't"$TXT_RED" however this text is red again."
|
||||
- Write-Host "This text is not colored"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `Syntax is incorrect` in scripts that use `:`
|
||||
|
||||
If you use a colon (`:`) in a script, GitLab might output:
|
||||
|
||||
- `Syntax is incorrect`
|
||||
- `script config should be a string or a nested array of strings up to 10 levels deep`
|
||||
|
||||
For example, if you use `"PRIVATE-TOKEN: ${PRIVATE_TOKEN}"` as part of a cURL command:
|
||||
|
||||
```yaml
|
||||
pages-job:
|
||||
stage: deploy
|
||||
script:
|
||||
- curl --header 'PRIVATE-TOKEN: ${PRIVATE_TOKEN}' "https://gitlab.example.com/api/v4/projects"
|
||||
```
|
||||
|
||||
The YAML parser thinks the `:` defines a YAML keyword, and outputs the
|
||||
`Syntax is incorrect` error.
|
||||
|
||||
To use commands that contain a colon, you should wrap the whole command
|
||||
in single quotes. You might need to change existing single quotes (`'`) into double quotes (`"`):
|
||||
|
||||
```yaml
|
||||
pages-job:
|
||||
stage: deploy
|
||||
script:
|
||||
- 'curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://gitlab.example.com/api/v4/projects"'
|
||||
```
|
||||
|
|
|
@ -19,7 +19,7 @@ as much as possible.
|
|||
|
||||
## Overview
|
||||
|
||||
Pipelines for the GitLab project are created using the [`workflow:rules` keyword](../ci/yaml/README.md#workflowrules)
|
||||
Pipelines for the GitLab project are created using the [`workflow:rules` keyword](../ci/yaml/README.md#workflow)
|
||||
feature of the GitLab CI/CD.
|
||||
|
||||
Pipelines are always created for the following scenarios:
|
||||
|
|
|
@ -5,7 +5,7 @@ group: Access
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Groups
|
||||
# Groups **(FREE)**
|
||||
|
||||
In GitLab, you can put related projects together in a group.
|
||||
|
||||
|
@ -479,7 +479,7 @@ username, you can create a new group and transfer projects to it.
|
|||
|
||||
You can change settings that are specific to repositories in your group.
|
||||
|
||||
#### Custom initial branch name **(FREE)**
|
||||
#### Custom initial branch name
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43290) in GitLab 13.6.
|
||||
|
||||
|
|
|
@ -267,7 +267,7 @@ code_quality:
|
|||
- if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
|
||||
```
|
||||
|
||||
If you are using merge request pipelines, your `rules` (or [`workflow: rules`](../../../ci/yaml/README.md#workflowrules))
|
||||
If you are using merge request pipelines, your `rules` (or [`workflow: rules`](../../../ci/yaml/README.md#workflow))
|
||||
might look like this example:
|
||||
|
||||
```yaml
|
||||
|
|
|
@ -30,11 +30,16 @@ button and start a merge request from there.
|
|||
|
||||
## New Merge Request page
|
||||
|
||||
On the **New Merge Request** page, start by filling in the title
|
||||
and description for the merge request. If there are already
|
||||
commits on the branch, the title is prefilled with the first
|
||||
line of the first commit message, and the description is
|
||||
prefilled with any additional lines in the commit message.
|
||||
On the **New Merge Request** page, start by filling in the title and description
|
||||
for the merge request. If commits already exist on the branch, GitLab suggests a
|
||||
merge request title for you:
|
||||
|
||||
- **If a multi-line commit message exists**: GitLab adds the first line of the
|
||||
first multi-line commit message as the title. Any additional lines in that
|
||||
commit message become the description.
|
||||
- **If no multi-line commit message exists**: GitLab adds the branch name as the
|
||||
title, and leaves the description blank.
|
||||
|
||||
The title is the only field that is mandatory in all cases.
|
||||
|
||||
From there, you can fill it with information (title, description,
|
||||
|
|
|
@ -164,6 +164,13 @@ the author of the merge request can request a new review from the reviewer:
|
|||
GitLab creates a new [to-do item](../../todos.md) for the reviewer, and sends
|
||||
them a notification email.
|
||||
|
||||
#### Approval status
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292936) in GitLab 13.10.
|
||||
|
||||
If a user in the reviewer list has approved the merge request, a green tick symbol is
|
||||
shown to the right of their name.
|
||||
|
||||
### Merge requests to close issues
|
||||
|
||||
If the merge request is being created to resolve an issue, you can
|
||||
|
|
|
@ -7,10 +7,23 @@ type: index, reference
|
|||
|
||||
# Merge requests **(FREE)**
|
||||
|
||||
A Merge Request (**MR**) is a _request_ to _merge_ one branch into another.
|
||||
Whenever you need to merge one branch into another branch with GitLab, you'll
|
||||
need to create a merge request (MR).
|
||||
|
||||
Use merge requests to visualize and collaborate on proposed changes
|
||||
to source code.
|
||||
Using merge requests, you can visualize and collaborate on proposed changes to
|
||||
source code. Merge requests display information about the proposed code changes,
|
||||
including:
|
||||
|
||||
- A description of the request.
|
||||
- Code changes and inline code reviews.
|
||||
- Information about CI/CD pipelines.
|
||||
- A comment section for discussion threads.
|
||||
- The list of commits.
|
||||
|
||||
Based on your workflow, after review you can merge a merge request into its
|
||||
target branch.
|
||||
|
||||
To get started, read the [introduction to merge requests](getting_started.md).
|
||||
|
||||
## Use cases
|
||||
|
||||
|
@ -39,18 +52,6 @@ B. Consider you're a web developer writing a webpage for your company's website:
|
|||
1. Once approved, your merge request is [squashed and merged](squash_and_merge.md), and [deployed to staging with GitLab Pages](https://about.gitlab.com/blog/2021/02/05/ci-deployment-and-environments/)
|
||||
1. Your production team [cherry picks](cherry_pick_changes.md) the merge commit into production
|
||||
|
||||
## Overview
|
||||
|
||||
Merge requests (aka "MRs") display a great deal of information about the changes proposed.
|
||||
The body of an MR contains its description, along with its widget (displaying information
|
||||
about CI/CD pipelines, when present), followed by the discussion threads of the people
|
||||
collaborating with that MR.
|
||||
|
||||
MRs also contain navigation tabs from which you can see the discussion happening on the thread,
|
||||
the list of commits, the list of pipelines and jobs, the code changes, and inline code reviews.
|
||||
|
||||
To get started, read the [introduction to merge requests](getting_started.md).
|
||||
|
||||
## Merge request navigation tabs at the top
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33813) in GitLab 12.6. This positioning is experimental.
|
||||
|
|
|
@ -635,7 +635,7 @@ module API
|
|||
required: true,
|
||||
name: :google_iap_audience_client_id,
|
||||
type: String,
|
||||
desc: 'Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com)'
|
||||
desc: 'Client ID of the IAP-secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com)'
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
|
|
|
@ -7,6 +7,8 @@ module BulkImports
|
|||
include Gitlab::ClassAttributes
|
||||
include Runner
|
||||
|
||||
NotAllowedError = Class.new(StandardError)
|
||||
|
||||
def initialize(context)
|
||||
@context = context
|
||||
end
|
||||
|
|
|
@ -77,7 +77,7 @@ module Gitlab
|
|||
return false unless user
|
||||
|
||||
Gitlab::SafeRequestStore.fetch(admin_mode_rs_key) do
|
||||
user.admin? && session_with_admin_mode?
|
||||
user.admin? && (privileged_runtime? || session_with_admin_mode?)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -154,6 +154,11 @@ module Gitlab
|
|||
Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
|
||||
Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
|
||||
end
|
||||
|
||||
# Runtimes which imply shell access get admin mode automatically, see Gitlab::Runtime
|
||||
def privileged_runtime?
|
||||
Gitlab::Runtime.rake? || Gitlab::Runtime.rails_runner? || Gitlab::Runtime.console?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -430,6 +430,9 @@ msgid_plural "%{count} issues selected"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%{count} items per page"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{count} merge request selected"
|
||||
msgid_plural "%{count} merge requests selected"
|
||||
msgstr[0] ""
|
||||
|
@ -1193,6 +1196,9 @@ msgstr ""
|
|||
msgid "10-19 contributions"
|
||||
msgstr ""
|
||||
|
||||
msgid "1000+"
|
||||
msgstr ""
|
||||
|
||||
msgid "1st contribution!"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5128,6 +5134,9 @@ msgstr ""
|
|||
msgid "BulkImport|No parent"
|
||||
msgstr ""
|
||||
|
||||
msgid "BulkImport|Showing %{start}-%{end} of %{total}"
|
||||
msgstr ""
|
||||
|
||||
msgid "BulkImport|Showing %{start}-%{end} of %{total} from %{link}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -17399,6 +17408,9 @@ msgid_plural "Last %d days"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Last %{days} days"
|
||||
msgstr ""
|
||||
|
||||
msgid "Last 2 weeks"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18871,6 +18883,9 @@ msgstr ""
|
|||
msgid "MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequest|Approved by @%{username}"
|
||||
msgstr ""
|
||||
|
||||
msgid "MergeRequest|Compare %{target} and %{source}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23977,10 +23992,10 @@ msgstr ""
|
|||
msgid "PrometheusService|Auto configuration"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
|
||||
msgid "PrometheusService|Auto configuration settings are used unless you override their values here."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com)"
|
||||
msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Common metrics"
|
||||
|
@ -23989,9 +24004,6 @@ msgstr ""
|
|||
msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Contents of the credentials.json file of your service account, like: { \"type\": \"service_account\", \"project_id\": ... }"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Custom metrics"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24007,6 +24019,15 @@ msgstr ""
|
|||
msgid "PrometheusService|Finding custom metrics..."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|GitLab is managing Prometheus on your clusters."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|GitLab manages Prometheus on your clusters."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|IAP_CLIENT_ID.apps.googleusercontent.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Install Prometheus on clusters"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24022,6 +24043,12 @@ msgstr ""
|
|||
msgid "PrometheusService|Missing environment variable"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Monitor application health with Prometheus metrics and dashboards"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Monitor your project’s environments by deploying and configuring Prometheus on your clusters."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|More information"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24034,22 +24061,22 @@ msgstr ""
|
|||
msgid "PrometheusService|No custom metrics have been created. Create one using the button above"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
|
||||
msgid "PrometheusService|PrometheusService|The ID of the IAP-secured resource."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
|
||||
msgid "PrometheusService|Select this checkbox to override the auto configuration settings with your own settings."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
|
||||
msgid "PrometheusService|The Prometheus API base URL."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|The contents of the credentials.json file of your service account."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Time-series monitoring service"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
|
||||
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
|
||||
|
@ -24058,6 +24085,12 @@ msgstr ""
|
|||
msgid "PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page has been deprecated."
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|https://prometheus.example.com/"
|
||||
msgstr ""
|
||||
|
||||
msgid "PrometheusService|{ \"type\": \"service_account\", \"project_id\": ... }"
|
||||
msgstr ""
|
||||
|
||||
msgid "Promote"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33870,6 +33903,9 @@ msgstr ""
|
|||
msgid "You are not allowed to unlink your primary login account"
|
||||
msgstr ""
|
||||
|
||||
msgid "You are not authorized to delete this site profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "You are not authorized to perform this action"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(new_pipelines_table: false)
|
||||
stub_application_setting(auto_devops_enabled: false)
|
||||
stub_ci_pipeline_yaml_file(YAML.dump(config))
|
||||
project.add_maintainer(user)
|
||||
|
|
|
@ -14,6 +14,7 @@ RSpec.describe 'Pipelines', :js do
|
|||
sign_in(user)
|
||||
stub_feature_flags(graphql_pipeline_details: false)
|
||||
stub_feature_flags(graphql_pipeline_details_users: false)
|
||||
stub_feature_flags(new_pipelines_table: false)
|
||||
|
||||
project.add_developer(user)
|
||||
project.update!(auto_devops_attributes: { enabled: false })
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import { GlEmptyState, GlLoadingIcon, GlSearchBoxByClick, GlSprintf } from '@gitlab/ui';
|
||||
import {
|
||||
GlEmptyState,
|
||||
GlLoadingIcon,
|
||||
GlSearchBoxByClick,
|
||||
GlSprintf,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
} from '@gitlab/ui';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { STATUSES } from '~/import_entities/constants';
|
||||
import ImportTable from '~/import_entities/import_groups/components/import_table.vue';
|
||||
|
@ -16,10 +24,15 @@ import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtur
|
|||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
const GlDropdownStub = stubComponent(GlDropdown, {
|
||||
template: '<div><h1 ref="text"><slot name="button-content"></slot></h1><slot></slot></div>',
|
||||
});
|
||||
|
||||
describe('import table', () => {
|
||||
let wrapper;
|
||||
let apolloProvider;
|
||||
|
||||
const SOURCE_URL = 'https://demo.host';
|
||||
const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
|
||||
const FAKE_GROUPS = [
|
||||
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
|
||||
|
@ -27,6 +40,9 @@ describe('import table', () => {
|
|||
];
|
||||
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
|
||||
|
||||
const findPaginationDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findPaginationDropdownText = () => findPaginationDropdown().find({ ref: 'text' }).text();
|
||||
|
||||
const createComponent = ({ bulkImportSourceGroups }) => {
|
||||
apolloProvider = createMockApollo([], {
|
||||
Query: {
|
||||
|
@ -42,11 +58,12 @@ describe('import table', () => {
|
|||
|
||||
wrapper = shallowMount(ImportTable, {
|
||||
propsData: {
|
||||
sourceUrl: 'https://demo.host',
|
||||
groupPathRegex: /.*/,
|
||||
sourceUrl: SOURCE_URL,
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
GlDropdown: GlDropdownStub,
|
||||
},
|
||||
localVue,
|
||||
apolloProvider,
|
||||
|
@ -152,6 +169,20 @@ describe('import table', () => {
|
|||
expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
|
||||
});
|
||||
|
||||
it('renders pagination dropdown', () => {
|
||||
expect(findPaginationDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('updates page size when selected in Dropdown', async () => {
|
||||
const otherOption = wrapper.findAllComponents(GlDropdownItem).at(1);
|
||||
expect(otherOption.text()).toMatchInterpolatedText('50 items per page');
|
||||
|
||||
otherOption.vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findPaginationDropdownText()).toMatchInterpolatedText('50 items per page');
|
||||
});
|
||||
|
||||
it('updates page when page change is requested', async () => {
|
||||
const REQUESTED_PAGE = 2;
|
||||
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
|
||||
|
@ -179,7 +210,7 @@ describe('import table', () => {
|
|||
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toContain('Showing 21-21 of 38');
|
||||
expect(wrapper.text()).toContain('Showing 21-21 of 38 groups from');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -225,7 +256,7 @@ describe('import table', () => {
|
|||
findFilterInput().vm.$emit('submit', FILTER_VALUE);
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo"');
|
||||
expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo" from');
|
||||
});
|
||||
|
||||
it('properly resets filter in graphql query when search box is cleared', async () => {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { GlTable } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
|
||||
|
||||
describe('Pipelines Table', () => {
|
||||
|
@ -12,20 +14,28 @@ describe('Pipelines Table', () => {
|
|||
viewType: 'root',
|
||||
};
|
||||
|
||||
const createComponent = (props = defaultProps) => {
|
||||
wrapper = mount(PipelinesTable, {
|
||||
propsData: props,
|
||||
});
|
||||
const createComponent = (props = defaultProps, flagState = false) => {
|
||||
wrapper = extendedWrapper(
|
||||
mount(PipelinesTable, {
|
||||
propsData: props,
|
||||
provide: {
|
||||
glFeatures: {
|
||||
newPipelinesTable: flagState,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const findRows = () => wrapper.findAll('.commit.gl-responsive-table-row');
|
||||
const findGlTable = () => wrapper.findComponent(GlTable);
|
||||
const findLegacyTable = () => wrapper.findByTestId('ci-table');
|
||||
|
||||
preloadFixtures(jsonFixtureName);
|
||||
|
||||
beforeEach(() => {
|
||||
const { pipelines } = getJSONFixture(jsonFixtureName);
|
||||
pipeline = pipelines.find((p) => p.user !== null && p.commit !== null);
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -33,33 +43,50 @@ describe('Pipelines Table', () => {
|
|||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('table', () => {
|
||||
it('should render a table', () => {
|
||||
expect(wrapper.classes()).toContain('ci-table');
|
||||
describe('table with feature flag off', () => {
|
||||
describe('renders the table correctly', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('should render a table', () => {
|
||||
expect(wrapper.classes()).toContain('ci-table');
|
||||
});
|
||||
|
||||
it('should render table head with correct columns', () => {
|
||||
expect(wrapper.find('.table-section.js-pipeline-status').text()).toEqual('Status');
|
||||
|
||||
expect(wrapper.find('.table-section.js-pipeline-info').text()).toEqual('Pipeline');
|
||||
|
||||
expect(wrapper.find('.table-section.js-pipeline-commit').text()).toEqual('Commit');
|
||||
|
||||
expect(wrapper.find('.table-section.js-pipeline-stages').text()).toEqual('Stages');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render table head with correct columns', () => {
|
||||
expect(wrapper.find('.table-section.js-pipeline-status').text()).toEqual('Status');
|
||||
describe('without data', () => {
|
||||
it('should render an empty table', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.find('.table-section.js-pipeline-info').text()).toEqual('Pipeline');
|
||||
expect(findRows()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
expect(wrapper.find('.table-section.js-pipeline-commit').text()).toEqual('Commit');
|
||||
describe('with data', () => {
|
||||
it('should render rows', () => {
|
||||
createComponent({ pipelines: [pipeline], viewType: 'root' });
|
||||
|
||||
expect(wrapper.find('.table-section.js-pipeline-stages').text()).toEqual('Stages');
|
||||
expect(findRows()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without data', () => {
|
||||
it('should render an empty table', () => {
|
||||
expect(findRows()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
describe('table with feature flag on', () => {
|
||||
it('displays new table', () => {
|
||||
createComponent(defaultProps, true);
|
||||
|
||||
describe('with data', () => {
|
||||
it('should render rows', () => {
|
||||
createComponent({ pipelines: [pipeline], viewType: 'root' });
|
||||
|
||||
expect(findRows()).toHaveLength(1);
|
||||
expect(findGlTable().exists()).toBe(true);
|
||||
expect(findLegacyTable().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,11 @@ describe('Timeago component', () => {
|
|||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(TimeAgo, {
|
||||
propsData: {
|
||||
...props,
|
||||
pipeline: {
|
||||
details: {
|
||||
...props,
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -28,7 +32,7 @@ describe('Timeago component', () => {
|
|||
|
||||
describe('with duration', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ duration: 10, finishedTime: '' });
|
||||
createComponent({ duration: 10, finished_at: '' });
|
||||
});
|
||||
|
||||
it('should render duration and timer svg', () => {
|
||||
|
@ -41,7 +45,7 @@ describe('Timeago component', () => {
|
|||
|
||||
describe('without duration', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ duration: 0, finishedTime: '' });
|
||||
createComponent({ duration: 0, finished_at: '' });
|
||||
});
|
||||
|
||||
it('should not render duration and timer svg', () => {
|
||||
|
@ -51,7 +55,7 @@ describe('Timeago component', () => {
|
|||
|
||||
describe('with finishedTime', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ duration: 0, finishedTime: '2017-04-26T12:40:23.277Z' });
|
||||
createComponent({ duration: 0, finished_at: '2017-04-26T12:40:23.277Z' });
|
||||
});
|
||||
|
||||
it('should render time and calendar icon', () => {
|
||||
|
@ -66,7 +70,7 @@ describe('Timeago component', () => {
|
|||
|
||||
describe('without finishedTime', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ duration: 0, finishedTime: '' });
|
||||
createComponent({ duration: 0, finished_at: '' });
|
||||
});
|
||||
|
||||
it('should not render time and calendar icon', () => {
|
||||
|
|
|
@ -7,6 +7,8 @@ import userDataMock from '../../user_data_mock';
|
|||
describe('UncollapsedReviewerList component', () => {
|
||||
let wrapper;
|
||||
|
||||
const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
|
||||
|
||||
function createComponent(props = {}) {
|
||||
const propsData = {
|
||||
users: [],
|
||||
|
@ -58,19 +60,29 @@ describe('UncollapsedReviewerList component', () => {
|
|||
const user = userDataMock();
|
||||
|
||||
createComponent({
|
||||
users: [user, { ...user, id: 2, username: 'hello-world' }],
|
||||
users: [user, { ...user, id: 2, username: 'hello-world', approved: true }],
|
||||
});
|
||||
});
|
||||
|
||||
it('only has one user', () => {
|
||||
it('has both users', () => {
|
||||
expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(2);
|
||||
});
|
||||
|
||||
it('shows one user with avatar, username and author name', () => {
|
||||
it('shows both users with avatar, username and author name', () => {
|
||||
expect(wrapper.text()).toContain(`@root`);
|
||||
expect(wrapper.text()).toContain(`@hello-world`);
|
||||
});
|
||||
|
||||
it('renders approval icon', () => {
|
||||
expect(reviewerApprovalIcons().length).toBe(1);
|
||||
});
|
||||
|
||||
it('shows that hello-world approved', () => {
|
||||
const icon = reviewerApprovalIcons().at(0);
|
||||
|
||||
expect(icon.attributes('title')).toEqual('Approved by @hello-world');
|
||||
});
|
||||
|
||||
it('renders re-request loading icon', async () => {
|
||||
await wrapper.setData({ loadingStates: { 2: 'loading' } });
|
||||
|
||||
|
|
|
@ -10,4 +10,5 @@ export default () => ({
|
|||
can_merge: true,
|
||||
can_update_merge_request: true,
|
||||
reviewed: true,
|
||||
approved: false,
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { GlDropdown } from '@gitlab/ui';
|
||||
import { getByText } from '@testing-library/dom';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
|
||||
|
@ -25,6 +26,9 @@ describe('MultiSelectDropdown Component', () => {
|
|||
slots: {
|
||||
search: '<p>Search</p>',
|
||||
},
|
||||
stubs: {
|
||||
GlDropdown,
|
||||
},
|
||||
});
|
||||
expect(getByText(wrapper.element, 'Search')).toBeDefined();
|
||||
});
|
||||
|
|
|
@ -511,20 +511,23 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
|
|||
type: 'checkbox',
|
||||
name: 'manual_configuration',
|
||||
title: s_('PrometheusService|Active'),
|
||||
help: s_('PrometheusService|Select this checkbox to override the auto configuration settings with your own settings.'),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'api_url',
|
||||
title: 'API URL',
|
||||
placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
|
||||
placeholder: s_('PrometheusService|https://prometheus.example.com/'),
|
||||
help: s_('PrometheusService|The Prometheus API base URL.'),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'google_iap_audience_client_id',
|
||||
title: 'Google IAP Audience Client ID',
|
||||
placeholder: s_('PrometheusService|Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com)'),
|
||||
placeholder: s_('PrometheusService|IAP_CLIENT_ID.apps.googleusercontent.com'),
|
||||
help: s_('PrometheusService|PrometheusService|The ID of the IAP-secured resource.'),
|
||||
autocomplete: 'off',
|
||||
required: false
|
||||
},
|
||||
|
@ -532,7 +535,8 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
|
|||
type: 'textarea',
|
||||
name: 'google_iap_service_account_json',
|
||||
title: 'Google IAP Service Account JSON',
|
||||
placeholder: s_('PrometheusService|Contents of the credentials.json file of your service account, like: { "type": "service_account", "project_id": ... }'),
|
||||
placeholder: s_('PrometheusService|{ "type": "service_account", "project_id": ... }'),
|
||||
help: s_('PrometheusService|The contents of the credentials.json file of your service account.'),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
|
|
|
@ -3,19 +3,22 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe MergeRequestUserEntity do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:request) { EntityRequest.new(project: project, current_user: user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:merge_request) { create(:merge_request) }
|
||||
let(:request) { EntityRequest.new(project: merge_request.target_project, current_user: user) }
|
||||
|
||||
let(:entity) do
|
||||
described_class.new(user, request: request)
|
||||
described_class.new(user, request: request, merge_request: merge_request)
|
||||
end
|
||||
|
||||
context 'as json' do
|
||||
describe '#as_json' do
|
||||
subject { entity.as_json }
|
||||
|
||||
it 'exposes needed attributes' do
|
||||
expect(subject).to include(:id, :name, :username, :state, :avatar_url, :web_url, :can_merge)
|
||||
is_expected.to include(
|
||||
:id, :name, :username, :state, :avatar_url, :web_url,
|
||||
:can_merge, :can_update_merge_request, :reviewed, :approved
|
||||
)
|
||||
end
|
||||
|
||||
context 'when `status` is not preloaded' do
|
||||
|
@ -24,6 +27,22 @@ RSpec.describe MergeRequestUserEntity do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the user has not approved the merge-request' do
|
||||
it 'exposes that the user has not approved the MR' do
|
||||
expect(subject).to include(approved: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user has approved the merge-request' do
|
||||
before do
|
||||
merge_request.approvals.create!(user: user)
|
||||
end
|
||||
|
||||
it 'exposes that the user has approved the MR' do
|
||||
expect(subject).to include(approved: true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `status` is preloaded' do
|
||||
before do
|
||||
user.create_status!(availability: :busy)
|
||||
|
@ -35,5 +54,27 @@ RSpec.describe MergeRequestUserEntity do
|
|||
expect(subject[:availability]).to eq('busy')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'performance' do
|
||||
let_it_be(:user_a) { create(:user) }
|
||||
let_it_be(:user_b) { create(:user) }
|
||||
let_it_be(:merge_request_b) { create(:merge_request) }
|
||||
|
||||
it 'is linear in the number of merge requests' do
|
||||
pending "See: https://gitlab.com/gitlab-org/gitlab/-/issues/322549"
|
||||
baseline = ActiveRecord::QueryRecorder.new do
|
||||
ent = described_class.new(user_a, request: request, merge_request: merge_request)
|
||||
ent.as_json
|
||||
end
|
||||
|
||||
expect do
|
||||
a = described_class.new(user_a, request: request, merge_request: merge_request_b)
|
||||
b = described_class.new(user_b, request: request, merge_request: merge_request_b)
|
||||
|
||||
a.as_json
|
||||
b.as_json
|
||||
end.not_to exceed_query_limit(baseline)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,8 +19,21 @@ RSpec.describe MergeRequests::BuildService do
|
|||
let(:label_ids) { [] }
|
||||
let(:merge_request) { service.execute }
|
||||
let(:compare) { double(:compare, commits: commits) }
|
||||
let(:commit_1) { double(:commit_1, sha: 'f00ba7', safe_message: "Initial commit\n\nCreate the app") }
|
||||
let(:commit_2) { double(:commit_2, sha: 'f00ba7', safe_message: 'This is a bad commit message!') }
|
||||
let(:commit_1) do
|
||||
double(:commit_1, sha: 'f00ba6', safe_message: 'Initial commit',
|
||||
gitaly_commit?: false, id: 'f00ba6', parent_ids: ['f00ba5'])
|
||||
end
|
||||
|
||||
let(:commit_2) do
|
||||
double(:commit_2, sha: 'f00ba7', safe_message: "Closes #1234 Second commit\n\nCreate the app",
|
||||
gitaly_commit?: false, id: 'f00ba7', parent_ids: ['f00ba6'])
|
||||
end
|
||||
|
||||
let(:commit_3) do
|
||||
double(:commit_3, sha: 'f00ba8', safe_message: 'This is a bad commit message!',
|
||||
gitaly_commit?: false, id: 'f00ba8', parent_ids: ['f00ba7'])
|
||||
end
|
||||
|
||||
let(:commits) { nil }
|
||||
|
||||
let(:params) do
|
||||
|
@ -47,6 +60,7 @@ RSpec.describe MergeRequests::BuildService do
|
|||
allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare)
|
||||
allow(project).to receive(:commit).and_return(commit_1)
|
||||
allow(project).to receive(:commit).and_return(commit_2)
|
||||
allow(project).to receive(:commit).and_return(commit_3)
|
||||
end
|
||||
|
||||
shared_examples 'allows the merge request to be created' do
|
||||
|
@ -137,7 +151,7 @@ RSpec.describe MergeRequests::BuildService do
|
|||
|
||||
context 'when target branch is missing' do
|
||||
let(:target_branch) { nil }
|
||||
let(:commits) { Commit.decorate([commit_1], project) }
|
||||
let(:commits) { Commit.decorate([commit_2], project) }
|
||||
|
||||
before do
|
||||
stub_compare
|
||||
|
@ -199,8 +213,8 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
|
||||
context 'one commit in the diff' do
|
||||
let(:commits) { Commit.decorate([commit_1], project) }
|
||||
let(:commit_description) { commit_1.safe_message.split(/\n+/, 2).last }
|
||||
let(:commits) { Commit.decorate([commit_2], project) }
|
||||
let(:commit_description) { commit_2.safe_message.split(/\n+/, 2).last }
|
||||
|
||||
before do
|
||||
stub_compare
|
||||
|
@ -209,7 +223,7 @@ RSpec.describe MergeRequests::BuildService do
|
|||
it_behaves_like 'allows the merge request to be created'
|
||||
|
||||
it 'uses the title of the commit as the title of the merge request' do
|
||||
expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first)
|
||||
expect(merge_request.title).to eq(commit_2.safe_message.split("\n").first)
|
||||
end
|
||||
|
||||
it 'uses the description of the commit as the description of the merge request' do
|
||||
|
@ -225,10 +239,10 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
|
||||
context 'commit has no description' do
|
||||
let(:commits) { Commit.decorate([commit_2], project) }
|
||||
let(:commits) { Commit.decorate([commit_3], project) }
|
||||
|
||||
it 'uses the title of the commit as the title of the merge request' do
|
||||
expect(merge_request.title).to eq(commit_2.safe_message)
|
||||
expect(merge_request.title).to eq(commit_3.safe_message)
|
||||
end
|
||||
|
||||
it 'sets the description to nil' do
|
||||
|
@ -257,7 +271,7 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
|
||||
it 'uses the title of the commit as the title of the merge request' do
|
||||
expect(merge_request.title).to eq('Initial commit')
|
||||
expect(merge_request.title).to eq('Closes #1234 Second commit')
|
||||
end
|
||||
|
||||
it 'appends the closing description' do
|
||||
|
@ -310,8 +324,8 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'more than one commit in the diff' do
|
||||
let(:commits) { Commit.decorate([commit_1, commit_2], project) }
|
||||
context 'no multi-line commit messages in the diff' do
|
||||
let(:commits) { Commit.decorate([commit_1, commit_3], project) }
|
||||
|
||||
before do
|
||||
stub_compare
|
||||
|
@ -365,6 +379,55 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'a multi-line commit message in the diff' do
|
||||
let(:commits) { Commit.decorate([commit_1, commit_2, commit_3], project) }
|
||||
|
||||
before do
|
||||
stub_compare
|
||||
end
|
||||
|
||||
it_behaves_like 'allows the merge request to be created'
|
||||
|
||||
it 'uses the first line of the first multi-line commit message as the title' do
|
||||
expect(merge_request.title).to eq('Closes #1234 Second commit')
|
||||
end
|
||||
|
||||
it 'adds the remaining lines of the first multi-line commit message as the description' do
|
||||
expect(merge_request.description).to eq('Create the app')
|
||||
end
|
||||
|
||||
context 'when the source branch matches an issue' do
|
||||
where(:issue_tracker, :source_branch, :title, :closing_message) do
|
||||
:jira | 'FOO-123-fix-issue' | 'Resolve FOO-123 "Fix issue"' | 'Closes FOO-123'
|
||||
:jira | 'fix-issue' | 'Fix issue' | nil
|
||||
:custom_issue_tracker | '123-fix-issue' | 'Resolve #123 "Fix issue"' | 'Closes #123'
|
||||
:custom_issue_tracker | 'fix-issue' | 'Fix issue' | nil
|
||||
:internal | '123-fix-issue' | 'Resolve "A bug"' | 'Closes #123'
|
||||
:internal | 'fix-issue' | 'Fix issue' | nil
|
||||
:internal | '124-fix-issue' | '124 fix issue' | nil
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
if issue_tracker == :internal
|
||||
issue.update!(iid: 123)
|
||||
else
|
||||
create(:"#{issue_tracker}_service", project: project)
|
||||
project.reload
|
||||
end
|
||||
end
|
||||
|
||||
it 'sets the correct title' do
|
||||
expect(merge_request.title).to eq('Closes #1234 Second commit')
|
||||
end
|
||||
|
||||
it 'sets the closing description' do
|
||||
expect(merge_request.description).to eq("Create the app#{closing_message ? "\n\n" + closing_message : ''}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the issue is not accessible to user' do
|
||||
let(:source_branch) { "#{issue.iid}-fix-issue" }
|
||||
|
@ -373,12 +436,12 @@ RSpec.describe MergeRequests::BuildService do
|
|||
project.team.truncate
|
||||
end
|
||||
|
||||
it 'uses branch title as the merge request title' do
|
||||
expect(merge_request.title).to eq("#{issue.iid} fix issue")
|
||||
it 'uses the first line of the first multi-line commit message as the title' do
|
||||
expect(merge_request.title).to eq('Closes #1234 Second commit')
|
||||
end
|
||||
|
||||
it 'does not set a description' do
|
||||
expect(merge_request.description).to be_nil
|
||||
it 'adds the remaining lines of the first multi-line commit message as the description' do
|
||||
expect(merge_request.description).to eq('Create the app')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -386,12 +449,12 @@ RSpec.describe MergeRequests::BuildService do
|
|||
let(:source_branch) { "#{issue.iid}-fix-issue" }
|
||||
let(:issue_confidential) { true }
|
||||
|
||||
it 'uses the title of the branch as the merge request title' do
|
||||
expect(merge_request.title).to eq("#{issue.iid} fix issue")
|
||||
it 'uses the first line of the first multi-line commit message as the title' do
|
||||
expect(merge_request.title).to eq('Closes #1234 Second commit')
|
||||
end
|
||||
|
||||
it 'does not set a description' do
|
||||
expect(merge_request.description).to be_nil
|
||||
it 'adds the remaining lines of the first multi-line commit message as the description' do
|
||||
expect(merge_request.description).to eq('Create the app')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -399,7 +462,7 @@ RSpec.describe MergeRequests::BuildService do
|
|||
context 'source branch does not exist' do
|
||||
before do
|
||||
allow(project).to receive(:commit).with(source_branch).and_return(nil)
|
||||
allow(project).to receive(:commit).with(target_branch).and_return(commit_1)
|
||||
allow(project).to receive(:commit).with(target_branch).and_return(commit_2)
|
||||
end
|
||||
|
||||
it_behaves_like 'forbids the merge request from being created' do
|
||||
|
@ -409,7 +472,7 @@ RSpec.describe MergeRequests::BuildService do
|
|||
|
||||
context 'target branch does not exist' do
|
||||
before do
|
||||
allow(project).to receive(:commit).with(source_branch).and_return(commit_1)
|
||||
allow(project).to receive(:commit).with(source_branch).and_return(commit_2)
|
||||
allow(project).to receive(:commit).with(target_branch).and_return(nil)
|
||||
end
|
||||
|
||||
|
@ -433,7 +496,7 @@ RSpec.describe MergeRequests::BuildService do
|
|||
context 'upstream project has disabled merge requests' do
|
||||
let(:upstream_project) { create(:project, :merge_requests_disabled) }
|
||||
let(:project) { create(:project, forked_from_project: upstream_project) }
|
||||
let(:commits) { Commit.decorate([commit_1], project) }
|
||||
let(:commits) { Commit.decorate([commit_2], project) }
|
||||
|
||||
it 'sets target project correctly' do
|
||||
expect(merge_request.target_project).to eq(project)
|
||||
|
@ -441,8 +504,8 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
|
||||
context 'target_project is set and accessible by current_user' do
|
||||
let(:target_project) { create(:project, :public, :repository)}
|
||||
let(:commits) { Commit.decorate([commit_1], project) }
|
||||
let(:target_project) { create(:project, :public, :repository) }
|
||||
let(:commits) { Commit.decorate([commit_2], project) }
|
||||
|
||||
it 'sets target project correctly' do
|
||||
expect(merge_request.target_project).to eq(target_project)
|
||||
|
@ -450,8 +513,8 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
|
||||
context 'target_project is set but not accessible by current_user' do
|
||||
let(:target_project) { create(:project, :private, :repository)}
|
||||
let(:commits) { Commit.decorate([commit_1], project) }
|
||||
let(:target_project) { create(:project, :private, :repository) }
|
||||
let(:commits) { Commit.decorate([commit_2], project) }
|
||||
|
||||
it 'sets target project correctly' do
|
||||
expect(merge_request.target_project).to eq(project)
|
||||
|
@ -469,8 +532,8 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
|
||||
context 'source_project is set and accessible by current_user' do
|
||||
let(:source_project) { create(:project, :public, :repository)}
|
||||
let(:commits) { Commit.decorate([commit_1], project) }
|
||||
let(:source_project) { create(:project, :public, :repository) }
|
||||
let(:commits) { Commit.decorate([commit_2], project) }
|
||||
|
||||
before do
|
||||
# To create merge requests _from_ a project the user needs at least
|
||||
|
@ -484,8 +547,8 @@ RSpec.describe MergeRequests::BuildService do
|
|||
end
|
||||
|
||||
context 'source_project is set but not accessible by current_user' do
|
||||
let(:source_project) { create(:project, :private, :repository)}
|
||||
let(:commits) { Commit.decorate([commit_1], project) }
|
||||
let(:source_project) { create(:project, :private, :repository) }
|
||||
let(:commits) { Commit.decorate([commit_2], project) }
|
||||
|
||||
it 'sets source project correctly' do
|
||||
expect(merge_request.source_project).to eq(project)
|
||||
|
|
32
spec/tasks/admin_mode_spec.rb
Normal file
32
spec/tasks/admin_mode_spec.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rake_helper'
|
||||
|
||||
RSpec.describe 'admin mode on tasks' do
|
||||
before do
|
||||
allow(::Gitlab::Runtime).to receive(:test_suite?).and_return(false)
|
||||
allow(::Gitlab::Runtime).to receive(:rake?).and_return(true)
|
||||
end
|
||||
|
||||
shared_examples 'verify admin mode' do |state|
|
||||
it 'matches the expected admin mode' do
|
||||
Rake::Task.define_task :verify_admin_mode do
|
||||
expect(Gitlab::Auth::CurrentUserMode.new(user).admin_mode?).to be(state)
|
||||
end
|
||||
|
||||
run_rake_task('verify_admin_mode')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a regular user' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
include_examples 'verify admin mode', false
|
||||
end
|
||||
|
||||
describe 'with an admin' do
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
include_examples 'verify admin mode', true
|
||||
end
|
||||
end
|
|
@ -59,7 +59,7 @@ RSpec.describe 'projects/settings/operations/show' do
|
|||
|
||||
expect(rendered).to have_content _('Prometheus')
|
||||
expect(rendered).to have_content _('Link Prometheus monitoring to GitLab.')
|
||||
expect(rendered).to have_content _('To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
|
||||
expect(rendered).to have_content _('To enable the installation of Prometheus on your clusters, deactivate the manual configuration.')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -71,7 +71,7 @@ RSpec.describe 'projects/settings/operations/show' do
|
|||
it 'renders the Operations Settings page' do
|
||||
render
|
||||
|
||||
expect(rendered).not_to have_content _('Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used.')
|
||||
expect(rendered).not_to have_content _('Auto configuration settings are used unless you override their values here.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue