Commit bade1d7b authored by Nick Thomas's avatar Nick Thomas

Merge branch 'master' into update-feature-categories-2020-10-01

parents 977be51c fb98fd37
...@@ -9,13 +9,13 @@ export default { ...@@ -9,13 +9,13 @@ export default {
}, },
inject: { inject: {
svgPath: { svgPath: {
type: String, default: '',
}, },
docsLink: { docsLink: {
type: String, default: '',
}, },
primaryButtonPath: { primaryButtonPath: {
type: String, default: '',
}, },
}, },
}; };
......
...@@ -10,16 +10,16 @@ export default { ...@@ -10,16 +10,16 @@ export default {
}, },
inject: { inject: {
isAdmin: { isAdmin: {
type: Boolean, default: false,
}, },
svgPath: { svgPath: {
type: String, default: '',
}, },
docsLink: { docsLink: {
type: String, default: '',
}, },
primaryButtonPath: { primaryButtonPath: {
type: String, default: '',
}, },
}, },
}; };
......
<script> <script>
import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui'; import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import PipelinesService from '~/pipelines/services/pipelines_service'; import PipelinesService from '~/pipelines/services/pipelines_service';
import PipelineStore from '~/pipelines/stores/pipelines_store'; import PipelineStore from '~/pipelines/stores/pipelines_store';
import pipelinesMixin from '~/pipelines/mixins/pipelines'; import pipelinesMixin from '~/pipelines/mixins/pipelines';
...@@ -126,16 +125,6 @@ export default { ...@@ -126,16 +125,6 @@ export default {
(latest.flags.detached_merge_request_pipeline || latest.flags.merge_request_pipeline) (latest.flags.detached_merge_request_pipeline || latest.flags.merge_request_pipeline)
); );
}, },
/**
* When we are on Desktop and the button is visible
* we need to add a negative margin to the table
* to make it inline with the button
*
* @returns {Boolean}
*/
shouldAddNegativeMargin() {
return this.canRenderPipelineButton && bp.isDesktop();
},
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
...@@ -205,65 +194,76 @@ export default { ...@@ -205,65 +194,76 @@ export default {
/> />
<div v-else-if="shouldRenderTable" class="table-holder"> <div v-else-if="shouldRenderTable" class="table-holder">
<div v-if="canRenderPipelineButton" class="nav justify-content-end"> <gl-button
<gl-button v-if="canRenderPipelineButton"
variant="success" block
class="js-run-mr-pipeline gl-mt-3 btn-wide-on-xs" class="gl-mt-3 gl-mb-0 gl-display-md-none"
:disabled="state.isRunningMergeRequestPipeline" variant="success"
@click="tryRunPipeline" data-testid="run_pipeline_button_mobile"
> :loading="state.isRunningMergeRequestPipeline"
<gl-loading-icon v-if="state.isRunningMergeRequestPipeline" inline /> @click="tryRunPipeline"
{{ s__('Pipelines|Run Pipeline') }} >
</gl-button> {{ s__('Pipelines|Run Pipeline') }}
</gl-button>
<gl-modal
:id="modalId"
ref="modal"
:modal-id="modalId"
:title="s__('Pipelines|Are you sure you want to run this pipeline?')"
:ok-title="s__('Pipelines|Run Pipeline')"
ok-variant="danger"
@ok="onClickRunPipeline"
>
<p>
{{
s__(
'Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables.',
)
}}
</p>
<p>
{{
s__(
"Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource.",
)
}}
</p>
<p>
{{
s__(
'Pipelines|If you are unsure, please ask a project maintainer to review it for you.',
)
}}
</p>
<gl-link
href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
target="_blank"
>
{{ s__('Pipelines|More Information') }}
</gl-link>
</gl-modal>
</div>
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown" :update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType" :view-type="viewType"
:class="{ 'negative-margin-top': shouldAddNegativeMargin }" >
/> <template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
<gl-button
variant="success"
data-testid="run_pipeline_button"
:loading="state.isRunningMergeRequestPipeline"
@click="tryRunPipeline"
>
{{ s__('Pipelines|Run Pipeline') }}
</gl-button>
</div>
</template>
</pipelines-table-component>
</div> </div>
<gl-modal
v-if="canRenderPipelineButton"
:id="modalId"
ref="modal"
:modal-id="modalId"
:title="s__('Pipelines|Are you sure you want to run this pipeline?')"
:ok-title="s__('Pipelines|Run Pipeline')"
ok-variant="danger"
@ok="onClickRunPipeline"
>
<p>
{{
s__(
'Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables.',
)
}}
</p>
<p>
{{
s__(
"Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource.",
)
}}
</p>
<p>
{{
s__('Pipelines|If you are unsure, please ask a project maintainer to review it for you.')
}}
</p>
<gl-link
href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
target="_blank"
>
{{ s__('Pipelines|More Information') }}
</gl-link>
</gl-modal>
<table-pagination <table-pagination
v-if="shouldRenderPagination" v-if="shouldRenderPagination"
:change="onChangePage" :change="onChangePage"
......
...@@ -32,7 +32,7 @@ export default { ...@@ -32,7 +32,7 @@ export default {
<gl-icon :size="12" name="angle-down" class="position-absolute" /> <gl-icon :size="12" name="angle-down" class="position-absolute" />
</a> </a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable"> <div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content"> <div class="dropdown-content" data-qa-selector="dropdown_content">
<ul> <ul>
<li v-for="version in versions" :key="version.id"> <li v-for="version in versions" :key="version.id">
<a :class="{ 'is-active': version.selected }" :href="version.href"> <a :class="{ 'is-active': version.selected }" :href="version.href">
......
...@@ -100,6 +100,7 @@ export default { ...@@ -100,6 +100,7 @@ export default {
<compare-dropdown-layout <compare-dropdown-layout
:versions="diffCompareDropdownTargetVersions" :versions="diffCompareDropdownTargetVersions"
class="mr-version-compare-dropdown" class="mr-version-compare-dropdown"
data-qa-selector="target_version_dropdown"
/> />
</template> </template>
<template #source> <template #source>
......
...@@ -245,7 +245,14 @@ export default { ...@@ -245,7 +245,14 @@ export default {
></strong> ></strong>
</span> </span>
<strong v-else v-gl-tooltip :title="filePath" class="file-title-name" data-container="body"> <strong
v-else
v-gl-tooltip
:title="filePath"
class="file-title-name"
data-container="body"
data-qa-selector="file_name_content"
>
{{ filePath }} {{ filePath }}
</strong> </strong>
</a> </a>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default { export default {
components: { components: {
DropdownButton, DropdownButton,
GlIcon,
GlLoadingIcon, GlLoadingIcon,
}, },
props: { props: {
...@@ -85,7 +86,7 @@ export default { ...@@ -85,7 +86,7 @@ export default {
type="search" type="search"
class="dropdown-input-field qa-dropdown-filter-input" class="dropdown-input-field qa-dropdown-filter-input"
/> />
<i aria-hidden="true" class="fa fa-search dropdown-input-search"></i> <gl-icon name="search" class="dropdown-input-search" aria-hidden="true" />
</div> </div>
<div class="dropdown-content"> <div class="dropdown-content">
<gl-loading-icon v-if="showLoading" size="lg" /> <gl-loading-icon v-if="showLoading" size="lg" />
......
...@@ -743,3 +743,22 @@ export const differenceInMilliseconds = (startDate, endDate = Date.now()) => { ...@@ -743,3 +743,22 @@ export const differenceInMilliseconds = (startDate, endDate = Date.now()) => {
const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate; const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate;
return endDateInMS - startDateInMS; return endDateInMS - startDateInMS;
}; };
/**
* A utility which returns a new date at the first day of the month for any given date.
*
* @param {Date} date
*
* @return {Date} the date at the first day of the month
*/
export const dateAtFirstDayOfMonth = date => new Date(newDate(date).setDate(1));
/**
* A utility function which checks if two dates match.
*
* @param {Date|Int} date1 Can be either a date object or a unix timestamp.
* @param {Date|Int} date2 Can be either a date object or a unix timestamp.
*
* @return {Boolean} true if the dates match
*/
export const datesMatch = (date1, date2) => differenceInMilliseconds(date1, date2) === 0;
...@@ -91,6 +91,10 @@ export default { ...@@ -91,6 +91,10 @@ export default {
<div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader"> <div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader">
{{ s__('Pipeline|Stages') }} {{ s__('Pipeline|Stages') }}
</div> </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> </div>
<pipelines-table-row-component <pipelines-table-row-component
v-for="model in pipelines" v-for="model in pipelines"
......
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import ReviewerAvatar from './reviewer_avatar.vue';
export default {
components: {
ReviewerAvatar,
},
props: {
user: {
type: Object,
required: true,
},
},
};
</script>
<template>
<button type="button" class="btn-link">
<reviewer-avatar :user="user" :img-size="24" />
<span class="author"> {{ user.name }} </span>
</button>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import CollapsedReviewer from './collapsed_reviewer.vue';
const DEFAULT_MAX_COUNTER = 99;
const DEFAULT_RENDER_COUNT = 5;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
CollapsedReviewer,
GlIcon,
},
props: {
users: {
type: Array,
required: true,
},
},
computed: {
hasNoUsers() {
return !this.users.length;
},
hasMoreThanOneReviewer() {
return this.users.length > 1;
},
hasMoreThanTwoReviewers() {
return this.users.length > 2;
},
allReviewersCanMerge() {
return this.users.every(user => user.can_merge);
},
sidebarAvatarCounter() {
if (this.users.length > DEFAULT_MAX_COUNTER) {
return `${DEFAULT_MAX_COUNTER}+`;
}
return `+${this.users.length - 1}`;
},
collapsedUsers() {
const collapsedLength = this.hasMoreThanTwoReviewers ? 1 : this.users.length;
return this.users.slice(0, collapsedLength);
},
tooltipTitleMergeStatus() {
const mergeLength = this.users.filter(u => u.can_merge).length;
if (mergeLength === this.users.length) {
return '';
} else if (mergeLength > 0) {
return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
mergeLength,
usersLength: this.users.length,
});
}
return this.users.length === 1 ? __('cannot merge') : __('no one can merge');
},
tooltipTitle() {
const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
const renderUsers = this.users.slice(0, maxRender);
const names = renderUsers.map(u => u.name);
if (!this.users.length) {
return __('Reviewer(s)');
}
if (this.users.length > names.length) {
names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
}
const text = names.join(', ');
return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
},
tooltipOptions() {
return { container: 'body', placement: 'left', boundary: 'viewport' };
},
},
};
</script>
<template>
<div
v-gl-tooltip="tooltipOptions"
:class="{ 'multiple-users': hasMoreThanOneReviewer }"
:title="tooltipTitle"
class="sidebar-collapsed-icon sidebar-collapsed-user"
>
<gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" />
<collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" />
<button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button">
<span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
<i
v-if="!allReviewersCanMerge"
aria-hidden="true"
class="fa fa-exclamation-triangle merge-icon"
></i>
</button>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { __, sprintf } from '~/locale';
export default {
props: {
user: {
type: Object,
required: true,
},
imgSize: {
type: Number,
required: true,
},
},
computed: {
reviewerAlt() {
return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
},
avatarUrl() {
return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
},
hasMergeIcon() {
return !this.user.can_merge;
},
},
};
</script>
<template>
<span class="position-relative">
<img
:alt="reviewerAlt"
:src="avatarUrl"
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
data-qa-selector="avatar_image"
/>
<i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i>
</span>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ReviewerAvatar from './reviewer_avatar.vue';
export default {
components: {
ReviewerAvatar,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
user: {
type: Object,
required: true,
},
rootPath: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
default: 'bottom',
required: false,
},
tooltipHasName: {
type: Boolean,
default: true,
required: false,
},
issuableType: {
type: String,
default: 'issue',
required: false,
},
},
computed: {
cannotMerge() {
return this.issuableType === 'merge_request' && !this.user.can_merge;
},
tooltipTitle() {
if (this.cannotMerge && this.tooltipHasName) {
return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
} else if (this.cannotMerge) {
return __('Cannot merge');
} else if (this.tooltipHasName) {
return this.user.name;
}
return '';
},
tooltipOption() {
return {
container: 'body',
placement: this.tooltipPlacement,
boundary: 'viewport',
};
},
reviewerUrl() {
return this.user.web_url;
},
},
};
</script>
<template>
<!-- must be `d-inline-block` or parent flex-basis causes width issues -->
<gl-link
v-gl-tooltip="tooltipOption"
:href="reviewerUrl"
:title="tooltipTitle"
class="d-inline-block"
>
<!-- use d-flex so that slot can be appropriately styled -->
<span class="d-flex">
<reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<slot :user="user"></slot>
</span>
</gl-link>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlLoadingIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
export default {
name: 'ReviewerTitle',
components: {
GlLoadingIcon,
},
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
numberOfReviewers: {
type: Number,
required: true,
},
editable: {
type: Boolean,
required: true,
},
showToggle: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
reviewerTitle() {
const reviewers = this.numberOfReviewers;
return n__('Reviewer', `%d Reviewers`, reviewers);
},
},
};
</script>
<template>
<div class="title hide-collapsed">
{{ reviewerTitle }}
<gl-loading-icon v-if="loading" inline class="align-bottom" />
<a
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="reviewer"
>
{{ __('Edit') }}
</a>
<a
v-if="showToggle"
:aria-label="__('Toggle sidebar')"
class="gutter-toggle float-right js-sidebar-toggle"
href="#"
role="button"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
</a>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import CollapsedReviewerList from './collapsed_reviewer_list.vue';
import UncollapsedReviewerList from './uncollapsed_reviewer_list.vue';
export default {
// name: 'Reviewers' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Reviewers',
components: {
CollapsedReviewerList,
UncollapsedReviewerList,
},
props: {
rootPath: {
type: String,
required: true,
},
users: {
type: Array,
required: true,
},
editable: {
type: Boolean,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
},
computed: {
hasNoUsers() {
return !this.users.length;
},
sortedReviewers() {
const canMergeUsers = this.users.filter(user => user.can_merge);
const canNotMergeUsers = this.users.filter(user => !user.can_merge);
return [...canMergeUsers, ...canNotMergeUsers];
},
},
methods: {
assignSelf() {
this.$emit('assign-self');
},
},
};
</script>
<template>
<div>
<collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
<div class="value hide-collapsed">
<template v-if="hasNoUsers">
<span class="assign-yourself no-value qa-assign-yourself">
{{ __('None') }}
</span>
</template>
<uncollapsed-reviewer-list
v-else
:users="sortedReviewers"
:root-path="rootPath"
:issuable-type="issuableType"
/>
</div>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { deprecatedCreateFlash as Flash } from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
import { __ } from '~/locale';
export default {
name: 'SidebarReviewers',
components: {
ReviewerTitle,
Reviewers,
},
mixins: [glFeatureFlagsMixin()],
props: {
mediator: {
type: Object,
required: true,
},
field: {
type: String,
required: true,
},
signedIn: {
type: Boolean,
required: false,
default: false,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
issuableIid: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
},
data() {
return {
store: new Store(),
loading: false,
};
},
created() {
this.removeReviewer = this.store.removeReviewer.bind(this.store);
this.addReviewer = this.store.addReviewer.bind(this.store);
this.removeAllReviewers = this.store.removeAllReviewers.bind(this.store);
// Get events from deprecatedJQueryDropdown
eventHub.$on('sidebar.removeReviewer', this.removeReviewer);
eventHub.$on('sidebar.addReviewer', this.addReviewer);
eventHub.$on('sidebar.removeAllReviewers', this.removeAllReviewers);
eventHub.$on('sidebar.saveReviewers', this.saveReviewers);
},
beforeDestroy() {
eventHub.$off('sidebar.removeReviewer', this.removeReviewer);
eventHub.$off('sidebar.addReviewer', this.addReviewer);
eventHub.$off('sidebar.removeAllReviewers', this.removeAllReviewers);
eventHub.$off('sidebar.saveReviewers', this.saveReviewers);
},
methods: {
saveReviewers() {
this.loading = true;
this.mediator
.saveReviewers(this.field)
.then(() => {
this.loading = false;
// Uncomment once this issue has been addressed > https://gitlab.com/gitlab-org/gitlab/-/issues/237922
// refreshUserMergeRequestCounts();
})
.catch(() => {
this.loading = false;
return new Flash(__('Error occurred when saving reviewers'));
});
},
},
};
</script>
<template>
<div>
<reviewer-title
:number-of-reviewers="store.reviewers.length"
:loading="loading || store.isFetching.reviewers"
:editable="store.editable"
:show-toggle="!signedIn"
/>
<reviewers
v-if="!store.isFetching.reviewers"
:root-path="store.rootPath"
:users="store.reviewers"
:editable="store.editable"
:issuable-type="issuableType"
class="value"
/>
</div>
</template>
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { __, sprintf } from '~/locale';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
ReviewerAvatarLink,
},
props: {
users: {
type: Array,
required: true,
},
rootPath: {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: 'issue',
},
},
data() {
return {
showLess: true,
};
},
computed: {
firstUser() {
return this.users[0];
},
hasOneUser() {
return this.users.length === 1;
},
hiddenReviewersLabel() {
const { numberOfHiddenReviewers } = this;
return sprintf(__('+ %{numberOfHiddenReviewers} more'), { numberOfHiddenReviewers });
},
renderShowMoreSection() {
return this.users.length > DEFAULT_RENDER_COUNT;
},
numberOfHiddenReviewers() {
return this.users.length - DEFAULT_RENDER_COUNT;
},
uncollapsedUsers() {
const uncollapsedLength = this.showLess
? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
: this.users.length;
return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
},
username() {
return `@${this.firstUser.username}`;
},
},
methods: {
toggleShowLess() {
this.showLess = !this.showLess;
},
},
};
</script>
<template>
<reviewer-avatar-link
v-if="hasOneUser"
#default="{ user }"
tooltip-placement="left"
:tooltip-has-name="false"
:user="firstUser"
:root-path="rootPath"
:issuable-type="issuableType"
>
<div class="ml-2">
<span class="author"> {{ user.name }} </span>
<span class="username"> {{ username }} </span>
</div>
</reviewer-avatar-link>
<div v-else>
<div class="user-list">
<div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more">
<button
type="button"
class="btn-link"
data-qa-selector="more_reviewers_link"
@click="toggleShowLess"
>
<template v-if="showLess">
{{ hiddenReviewersLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
</button>
</div>
</div>
</template>
...@@ -5,6 +5,7 @@ import Vuex from 'vuex'; ...@@ -5,6 +5,7 @@ import Vuex from 'vuex';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarLabels from './components/labels/sidebar_labels.vue'; import SidebarLabels from './components/labels/sidebar_labels.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue'; import IssuableLockForm from './components/lock/issuable_lock_form.vue';
...@@ -56,6 +57,36 @@ function mountAssigneesComponent(mediator) { ...@@ -56,6 +57,36 @@ function mountAssigneesComponent(mediator) {
}); });
} }
function mountReviewersComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-reviewers');
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
if (!el) return;
const { iid, fullPath } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
components: {
SidebarReviewers,
},
render: createElement =>
createElement('sidebar-reviewers', {
props: {
mediator,
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
issuableType: isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});
}
export function mountSidebarLabels() { export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels'); const el = document.querySelector('.js-sidebar-labels');
...@@ -245,6 +276,7 @@ function mountSeverityComponent() { ...@@ -245,6 +276,7 @@ function mountSeverityComponent() {
export function mountSidebar(mediator) { export function mountSidebar(mediator) {
mountAssigneesComponent(mediator); mountAssigneesComponent(mediator);
mountReviewersComponent(mediator);
mountConfidentialComponent(mediator); mountConfidentialComponent(mediator);
mountLockComponent(); mountLockComponent();
mountParticipantsComponent(mediator); mountParticipantsComponent(mediator);
......
<script> <script>
import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default { export default {
components: {
GlIcon,
},
props: { props: {
placeholderText: { placeholderText: {
type: String, type: String,
...@@ -41,5 +45,6 @@ export default { ...@@ -41,5 +45,6 @@ export default {
autocomplete="off" autocomplete="off"
/> />
<i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"> </i> <i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"> </i>
<gl-icon name="search" class="dropdown-input-search" aria-hidden="true" data-hidden="true" />
</div> </div>
</template> </template>
...@@ -230,13 +230,12 @@ export default { ...@@ -230,13 +230,12 @@ export default {
@keydown="onKeydown($event)" @keydown="onKeydown($event)"
@keyup="onKeyup($event)" @keyup="onKeyup($event)"
/> />
<i <gl-icon
:class="{ name="search"
hidden: showClearInputButton, class="dropdown-input-search"
}" :class="{ hidden: showClearInputButton }"
aria-hidden="true" aria-hidden="true"
class="fa fa-search dropdown-input-search" />
></i>
<gl-icon <gl-icon
name="close" name="close"
class="dropdown-input-clear" class="dropdown-input-clear"
......
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui'; import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
...@@ -39,9 +40,9 @@ export default { ...@@ -39,9 +40,9 @@ export default {
...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']), ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
visibleLabels() { visibleLabels() {
if (this.searchKey) { if (this.searchKey) {
return this.labels.filter(label => return fuzzaldrinPlus.filter(this.labels, this.searchKey, {
label.title.toLowerCase().includes(this.searchKey.toLowerCase()), key: ['title'],
); });
} }
return this.labels; return this.labels;
}, },
......
...@@ -417,12 +417,6 @@ ...@@ -417,12 +417,6 @@
} }
} }
@include media-breakpoint-down(xs) {
.btn-wide-on-xs {
width: 100%;
}
}
.btn-blank { .btn-blank {
padding: 0; padding: 0;
background: transparent; background: transparent;
......
...@@ -819,7 +819,6 @@ $pipeline-dropdown-line-height: 20px; ...@@ -819,7 +819,6 @@ $pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px; $pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px; $ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px; $ci-action-dropdown-svg-size: 12px;
$pipelines-table-header-height: 40px;
/* /*
CI variable lists CI variable lists
......
...@@ -431,10 +431,6 @@ ...@@ -431,10 +431,6 @@
margin-left: 0; margin-left: 0;
border-left: 0; border-left: 0;
} }
.file-actions .dropdown {
height: 28px;
}
} }
table.code { table.code {
......
...@@ -117,7 +117,8 @@ ...@@ -117,7 +117,8 @@
} }
} }
.assignee { .assignee,
.reviewer {
.merge-icon { .merge-icon {
color: $orange-400; color: $orange-400;
position: absolute; position: absolute;
......
...@@ -26,10 +26,6 @@ ...@@ -26,10 +26,6 @@
} }
.pipelines { .pipelines {
.negative-margin-top {
margin-top: -$pipelines-table-header-height;
}
.stage { .stage {
max-width: 90px; max-width: 90px;
width: 90px; width: 90px;
......
...@@ -21,11 +21,13 @@ module MultipleBoardsActions ...@@ -21,11 +21,13 @@ module MultipleBoardsActions
end end
def create def create
board = Boards::CreateService.new(parent, current_user, board_params).execute response = Boards::CreateService.new(parent, current_user, board_params).execute
respond_to do |format| respond_to do |format|
format.json do format.json do
if board.persisted? board = response.payload
if response.success?
extra_json = { board_path: board_path(board) } extra_json = { board_path: board_path(board) }
render json: serialize_as_json(board).merge(extra_json) render json: serialize_as_json(board).merge(extra_json)
else else
......
...@@ -17,9 +17,8 @@ class GroupMembersFinder < UnionFinder ...@@ -17,9 +17,8 @@ class GroupMembersFinder < UnionFinder
@params = params @params = params
end end
# rubocop: disable CodeReuse/ActiveRecord
def execute(include_relations: [:inherited, :direct]) def execute(include_relations: [:inherited, :direct])
group_members = group.members group_members = group_members_list
relations = [] relations = []
return group_members if include_relations == [:direct] return group_members if include_relations == [:direct]
...@@ -27,17 +26,13 @@ class GroupMembersFinder < UnionFinder ...@@ -27,17 +26,13 @@ class GroupMembersFinder < UnionFinder
relations << group_members if include_relations.include?(:direct) relations << group_members if include_relations.include?(:direct)
if include_relations.include?(:inherited) && group.parent if include_relations.include?(:inherited) && group.parent
parents_members = GroupMember.non_request.non_minimal_access parents_members = relation_group_members(group.ancestors)
.where(source_id: group.ancestors.select(:id))
.where.not(user_id: group.users.select(:id))
relations << parents_members relations << parents_members
end end
if include_relations.include?(:descendants) if include_relations.include?(:descendants)
descendant_members = GroupMember.non_request.non_minimal_access descendant_members = relation_group_members(group.descendants)
.where(source_id: group.descendants.select(:id))
.where.not(user_id: group.users.select(:id))
relations << descendant_members relations << descendant_members
end end
...@@ -47,7 +42,6 @@ class GroupMembersFinder < UnionFinder ...@@ -47,7 +42,6 @@ class GroupMembersFinder < UnionFinder
members = find_union(relations, GroupMember) members = find_union(relations, GroupMember)
filter_members(members) filter_members(members)
end end
# rubocop: enable CodeReuse/ActiveRecord
private private
...@@ -67,6 +61,22 @@ class GroupMembersFinder < UnionFinder ...@@ -67,6 +61,22 @@ class GroupMembersFinder < UnionFinder
def can_manage_members def can_manage_members
Ability.allowed?(user, :admin_group_member, group) Ability.allowed?(user, :admin_group_member, group)
end end
def group_members_list
group.members
end
def relation_group_members(relation)
all_group_members(relation).non_minimal_access
end
# rubocop: disable CodeReuse/ActiveRecord
def all_group_members(relation)
GroupMember.non_request
.where(source_id: relation.select(:id))
.where.not(user_id: group.users.select(:id))
end
# rubocop: enable CodeReuse/ActiveRecord
end end
GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder') GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
...@@ -4,6 +4,7 @@ module Mutations ...@@ -4,6 +4,7 @@ module Mutations
class BaseMutation < GraphQL::Schema::RelayClassicMutation class BaseMutation < GraphQL::Schema::RelayClassicMutation
prepend Gitlab::Graphql::Authorize::AuthorizeResource prepend Gitlab::Graphql::Authorize::AuthorizeResource
prepend Gitlab::Graphql::CopyFieldDescription prepend Gitlab::Graphql::CopyFieldDescription
prepend ::Gitlab::Graphql::GlobalIDCompatibility
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance' ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
......
...@@ -3,13 +3,18 @@ ...@@ -3,13 +3,18 @@
module Mutations module Mutations
module Ci module Ci
class Base < BaseMutation class Base < BaseMutation
argument :id, ::Types::GlobalIDType[::Ci::Pipeline], PipelineID = ::Types::GlobalIDType[::Ci::Pipeline]
argument :id, PipelineID,
required: true, required: true,
description: 'The id of the pipeline to mutate' description: 'The id of the pipeline to mutate'
private private
def find_object(id:) def find_object(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = PipelineID.coerce_isolated_input(id)
GlobalID::Locator.locate(id) GlobalID::Locator.locate(id)
end end
end end
......
...@@ -29,11 +29,18 @@ module Mutations ...@@ -29,11 +29,18 @@ module Mutations
private private
def parameters(**args) def parameters(**args)
args.transform_values { |id| GitlabSchema.find_by_gid(id) }.transform_values(&:sync).tap do |hash| args.transform_values { |id| find_design(id) }.transform_values(&:sync).tap do |hash|
hash.each { |k, design| not_found(args[k]) unless current_user.can?(:read_design, design) } hash.each { |k, design| not_found(args[k]) unless current_user.can?(:read_design, design) }
end end
end end
def find_design(id)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = DesignID.coerce_isolated_input(id)
GitlabSchema.object_from_id(id)
end
def not_found(gid) def not_found(gid)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Resource not available: #{gid}" raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Resource not available: #{gid}"
end end
......
...@@ -4,6 +4,7 @@ module Resolvers ...@@ -4,6 +4,7 @@ module Resolvers
class BaseResolver < GraphQL::Schema::Resolver class BaseResolver < GraphQL::Schema::Resolver
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
include ::Gitlab::Graphql::GlobalIDCompatibility
def self.single def self.single
@single ||= Class.new(self) do @single ||= Class.new(self) do
......
# frozen_string_literal: true # frozen_string_literal: true
module GraphQLExtensions
module ScalarExtensions
# Allow ID to unify with GlobalID Types
def ==(other)
if name == 'ID' && other.is_a?(self.class) &&
other.type_class.ancestors.include?(::Types::GlobalIDType)
return true
end
super
end
end
end
::GraphQL::ScalarType.prepend(GraphQLExtensions::ScalarExtensions)
module Types module Types
class GlobalIDType < BaseScalar class GlobalIDType < BaseScalar
graphql_name 'GlobalID' graphql_name 'GlobalID'
......
...@@ -49,8 +49,7 @@ module Types ...@@ -49,8 +49,7 @@ module Types
field :milestone, ::Types::MilestoneType, field :milestone, ::Types::MilestoneType,
null: true, null: true,
description: 'Find a milestone', description: 'Find a milestone' do
resolve: -> (_obj, args, _ctx) { GitlabSchema.find_by_gid(args[:id]) } do
argument :id, ::Types::GlobalIDType[Milestone], argument :id, ::Types::GlobalIDType[Milestone],
required: true, required: true,
description: 'Find a milestone by its ID' description: 'Find a milestone by its ID'
...@@ -86,7 +85,17 @@ module Types ...@@ -86,7 +85,17 @@ module Types
end end
def issue(id:) def issue(id:)
GitlabSchema.object_from_id(id, expected_type: ::Issue) # TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::Issue].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def milestone(id:)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[Milestone].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end end
end end
end end
......
...@@ -10,7 +10,7 @@ module Groups::GroupMembersHelper ...@@ -10,7 +10,7 @@ module Groups::GroupMembersHelper
end end
def render_invite_member_for_group(group, default_access_level) def render_invite_member_for_group(group, default_access_level)
render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: GroupMember.access_level_roles, default_access_level: default_access_level render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: group.access_level_roles, default_access_level: default_access_level
end end
def linked_groups_data_json(group_links) def linked_groups_data_json(group_links)
......
...@@ -356,6 +356,7 @@ class Group < Namespace ...@@ -356,6 +356,7 @@ class Group < Namespace
end end
group_hierarchy_members = GroupMember.active_without_invites_and_requests group_hierarchy_members = GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(source_id: source_ids) .where(source_id: source_ids)
GroupMember.from_union([group_hierarchy_members, GroupMember.from_union([group_hierarchy_members,
...@@ -550,6 +551,14 @@ class Group < Namespace ...@@ -550,6 +551,14 @@ class Group < Namespace
owners.first || parent&.default_owner || owner owners.first || parent&.default_owner || owner
end end
def access_level_roles
GroupMember.access_level_roles
end
def access_level_values
access_level_roles.values
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
...@@ -132,6 +132,8 @@ class User < ApplicationRecord ...@@ -132,6 +132,8 @@ class User < ApplicationRecord
-> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) }, -> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members, through: :group_members,
source: :group source: :group
has_many :minimal_access_group_members, -> { where(access_level: [Gitlab::Access::MINIMAL_ACCESS]) }, source: 'GroupMember', class_name: 'GroupMember'
has_many :minimal_access_groups, through: :minimal_access_group_members, source: :group
# Projects # Projects
has_many :groups_projects, through: :groups, source: :projects has_many :groups_projects, through: :groups, source: :projects
......
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
module Boards module Boards
class CreateService < Boards::BaseService class CreateService < Boards::BaseService
def execute def execute
create_board! if can_create_board? unless can_create_board?
return ServiceResponse.error(message: "You don't have the permission to create a board for this resource.")
end
create_board!
end end
private private
...@@ -15,12 +19,16 @@ module Boards ...@@ -15,12 +19,16 @@ module Boards
def create_board! def create_board!
board = parent.boards.create(params) board = parent.boards.create(params)
if board.persisted? unless board.persisted?
board.lists.create(list_type: :backlog) return ServiceResponse.error(message: "There was an error when creating a board.", payload: board)
board.lists.create(list_type: :closed) end
board.tap do |created_board|
created_board.lists.create(list_type: :backlog)
created_board.lists.create(list_type: :closed)
end end
board ServiceResponse.success(payload: board)
end end
end end
end end
......
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
%div %div
= users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all) = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
.gl-mt-3 .gl-mt-3
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" = select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2"
%hr %hr
= button_tag _('Add users to group'), class: "btn btn-success" = button_tag _('Add users to group'), class: "btn btn-success"
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true = render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
- signed_in = !!issuable_sidebar.dig(:current_user, :id) - signed_in = !!issuable_sidebar.dig(:current_user, :id)
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras" - add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- reviewers = local_assigns.fetch(:reviewers, nil)
- if Feature.enabled?(:vue_issuable_sidebar, @project.group) - if Feature.enabled?(:vue_issuable_sidebar, @project.group)
%aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in, %aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in,
...@@ -28,6 +29,10 @@ ...@@ -28,6 +29,10 @@
.block.assignee.qa-assignee-block .block.assignee.qa-assignee-block
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees
- if Feature.enabled?(:merge_request_reviewers, @project) && reviewers
.block.reviewer.qa-reviewer-block
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar
- if issuable_sidebar[:supports_milestone] - if issuable_sidebar[:supports_milestone]
......
---
title: Add No Access Role for top group members
merge_request: 40942
author:
type: added
---
title: Don't expose http_request_duration_seconds metrics in sidekiq exporter
merge_request: 43941
author:
type: performance
---
title: Add fuzzy search support to labels dropdown
merge_request: 43969
author:
type: fixed
---
title: Add index for project_id and sha to deployments table
merge_request: 43836
author:
type: performance
---
title: Replace fa-search fontawesome icons with GitLab SVG in Vue components
merge_request: 43879
author:
type: changed
---
name: minimal_access_role
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40942
rollout_issue_url:
group: group::access
type: licensed
default_enabled: true
...@@ -69,7 +69,9 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled? ...@@ -69,7 +69,9 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Metrics.gauge(:deployments, 'GitLab Version', {}, :max).set({ version: Gitlab::VERSION }, 1) Gitlab::Metrics.gauge(:deployments, 'GitLab Version', {}, :max).set({ version: Gitlab::VERSION }, 1)
Gitlab::Metrics::RequestsRackMiddleware.initialize_http_request_duration_seconds unless Gitlab::Runtime.sidekiq?
Gitlab::Metrics::RequestsRackMiddleware.initialize_http_request_duration_seconds
end
rescue IOError => e rescue IOError => e
Gitlab::ErrorTracking.track_exception(e) Gitlab::ErrorTracking.track_exception(e)
Gitlab::Metrics.error_detected! Gitlab::Metrics.error_detected!
......
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexOnProjectIdAndShaToDeployments < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_deployments_on_project_id_sha'
disable_ddl_transaction!
def up
add_concurrent_index :deployments, [:project_id, :sha], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name(:deployments, INDEX_NAME)
end
end
45530bb3090d9e8df3a79f42a06b042e0c40f6e185078c6d79d7ec334175c7d5
\ No newline at end of file
...@@ -20007,6 +20007,8 @@ CREATE INDEX index_deployments_on_project_id_and_status_and_created_at ON deploy ...@@ -20007,6 +20007,8 @@ CREATE INDEX index_deployments_on_project_id_and_status_and_created_at ON deploy
CREATE INDEX index_deployments_on_project_id_and_updated_at_and_id ON deployments USING btree (project_id, updated_at DESC, id DESC); CREATE INDEX index_deployments_on_project_id_and_updated_at_and_id ON deployments USING btree (project_id, updated_at DESC, id DESC);
CREATE INDEX index_deployments_on_project_id_sha ON deployments USING btree (project_id, sha);
CREATE INDEX index_deployments_on_user_id_and_status_and_created_at ON deployments USING btree (user_id, status, created_at); CREATE INDEX index_deployments_on_user_id_and_status_and_created_at ON deployments USING btree (user_id, status, created_at);
CREATE INDEX index_description_versions_on_epic_id ON description_versions USING btree (epic_id) WHERE (epic_id IS NOT NULL); CREATE INDEX index_description_versions_on_epic_id ON description_versions USING btree (epic_id) WHERE (epic_id IS NOT NULL);
......
...@@ -118,7 +118,7 @@ DRIs: ...@@ -118,7 +118,7 @@ DRIs:
|------------------------------|------------------------| |------------------------------|------------------------|
| Product | Jackie Porter | | Product | Jackie Porter |
| Leadership | Daniel Croft | | Leadership | Daniel Croft |
| Engineering | TBD | | Engineering | Kamil Trzciński |
Domain Experts: Domain Experts:
......
...@@ -609,8 +609,8 @@ then available as environment variables on the running application ...@@ -609,8 +609,8 @@ then available as environment variables on the running application
container. container.
CAUTION: **Caution:** CAUTION: **Caution:**
Variables with multi-line values are not currently supported due to Variables with multi-line values are not supported due to
limitations with the current Auto DevOps scripting environment. limitations with the Auto DevOps scripting environment.
### Override a variable by manually running a pipeline ### Override a variable by manually running a pipeline
...@@ -785,7 +785,7 @@ Examples: ...@@ -785,7 +785,7 @@ Examples:
##### Enable or disable parenthesis support for variables **(CORE ONLY)** ##### Enable or disable parenthesis support for variables **(CORE ONLY)**
The feature is currently deployed behind a feature flag that is **enabled by default**. The feature is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it for your instance. can opt to disable it for your instance.
...@@ -820,8 +820,7 @@ NOTE: **Note:** ...@@ -820,8 +820,7 @@ NOTE: **Note:**
The available regular expression syntax is limited. See [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/35438) The available regular expression syntax is limited. See [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/35438)
for more details. for more details.
If needed, you can use a test pipeline to determine whether a regular expression will If needed, you can use a test pipeline to determine whether a regular expression works in a variable. The example below tests the `^mast.*` regular expression directly,
work in a variable. The example below tests the `^mast.*` regular expression directly,
as well as from within a variable: as well as from within a variable:
```yaml ```yaml
......
...@@ -49,6 +49,20 @@ See also: ...@@ -49,6 +49,20 @@ See also:
- [Exposing Global IDs](#exposing-global-ids). - [Exposing Global IDs](#exposing-global-ids).
- [Mutation arguments](#object-identifier-arguments). - [Mutation arguments](#object-identifier-arguments).
We have a custom scalar type (`Types::GlobalIDType`) which should be used as the
type of input and output arguments when the value is a `GlobalID`. The benefits
of using this type instead of `ID` are:
- it validates that the value is a `GlobalID`
- it parses it into a `GlobalID` before passing it to user code
- it can be parameterized on the type of the object (e.g.
`GlobalIDType[Project]`) which offers even better validation and security.
Consider using this type for all new arguments and result types. Remember that
it is perfectly possible to parameterize this type with a concern or a
supertype, if you want to accept a wider range of objects (e.g.
`GlobalIDType[Issuable]` vs `GlobalIDType[Issue]`).
## Types ## Types
We use a code-first schema, and we declare what type everything is in Ruby. We use a code-first schema, and we declare what type everything is in Ruby.
......
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_ce_to_ee.md
---
This document was moved to [another location](upgrading_from_ce_to_ee.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
---
redirect_to: upgrading_from_source.md
---
This document was moved to [another location](upgrading_from_source.md).
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment