Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-01 18:11:21 +00:00
parent dff63567c3
commit 69f0d90aad
66 changed files with 1193 additions and 638 deletions

View 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>

View File

@ -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,
});
},
},
});
};

View 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),
},
}),
});
};

View File

@ -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>

View File

@ -1,3 +1,3 @@
import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle';
import initCycleAnalytics from '~/cycle_analytics';
document.addEventListener('DOMContentLoaded', initCycleAnalytics);

View File

@ -1,3 +1,3 @@
import initForm from '../../../../shared/milestones/form';
import initForm from '~/shared/milestones/form';
document.addEventListener('DOMContentLoaded', () => initForm());
initForm();

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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"

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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">

View File

@ -0,0 +1,7 @@
query mergeRequestSidebarDetails($fullPath: ID!, $iid: String!) {
project(fullPath: $fullPath) {
mergeRequest(iid: $iid) {
iid # currently unused.
}
}
}

View File

@ -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 });
}

View File

@ -22,6 +22,7 @@ export default class SidebarMediator {
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
fullPath: options.fullPath,
iid: options.iid,
issuableType: options.issuableType,
});
SidebarMediator.singleton = this;
}

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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
}
]

View File

@ -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')

View File

@ -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

View File

@ -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}}

View File

@ -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.') }}

View File

@ -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 }

View File

@ -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 projects environments')
= s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your projects environments.')
= link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn gl-button btn-success'
%hr

View File

@ -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.')

View File

@ -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 projects environments')
= s_('PrometheusService|Monitor your projects 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'

View File

@ -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.')

View File

@ -0,0 +1,5 @@
---
title: Prefill first multiline commit message for new MRs
merge_request: 52984
author: Max Coplan @vegerot
type: changed

View File

@ -0,0 +1,5 @@
---
title: Updated UI text to match style guidelines
merge_request: 53179
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Show icon next to reviewers who have approved
merge_request: 54365
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Auto-enable admin mode on privileged environments
merge_request: 53015
author: Diego Louzán
type: changed

View 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

View File

@ -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. |

View File

@ -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

View File

@ -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:

View File

@ -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"'
```

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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.

View File

@ -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,

View File

@ -7,6 +7,8 @@ module BulkImports
include Gitlab::ClassAttributes
include Runner
NotAllowedError = Class.new(StandardError)
def initialize(context)
@context = context
end

View File

@ -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

View File

@ -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 projects 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 projects 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 projects 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 ""

View File

@ -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)

View File

@ -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 })

View File

@ -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 () => {

View File

@ -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);
});
});
});

View File

@ -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', () => {

View File

@ -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' } });

View File

@ -10,4 +10,5 @@ export default () => ({
can_merge: true,
can_update_merge_request: true,
reviewed: true,
approved: false,
});

View File

@ -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();
});

View File

@ -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
}
]

View File

@ -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

View File

@ -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)

View 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

View File

@ -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