diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-16 15:08:46 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-16 15:08:46 +0000 |
commit | 3940f59a61a749824aa4425ebdcaed6f3ed601f2 (patch) | |
tree | ebe2ffc65d0d7e7c6cd742e10c243d0cfbbb9e55 | |
parent | 3775eba7c1d41443461e3abcdab2867bbc4636ae (diff) | |
download | gitlab-ce-3940f59a61a749824aa4425ebdcaed6f3ed601f2.tar.gz |
Add latest changes from gitlab-org/gitlab@master
95 files changed, 2281 insertions, 401 deletions
@@ -172,7 +172,7 @@ gem 'diffy', '~> 3.3' gem 'diff_match_patch', '~> 0.1.0' # Application server -gem 'rack', '~> 2.0.9' +gem 'rack', '~> 2.1.4' # https://github.com/sharpstone/rack-timeout/blob/master/README.md#rails-apps-manually gem 'rack-timeout', '~> 0.5.1', require: 'rack/timeout/base' diff --git a/Gemfile.lock b/Gemfile.lock index ac70d988c0f..93670d22920 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -855,7 +855,7 @@ GEM public_suffix (4.0.6) pyu-ruby-sasl (0.0.3.3) raabro (1.1.6) - rack (2.0.9) + rack (2.1.4) rack-accept (0.4.5) rack (>= 0.4) rack-attack (6.3.0) @@ -1429,7 +1429,7 @@ DEPENDENCIES prometheus-client-mmap (~> 0.12.0) pry-byebug (~> 3.9.0) pry-rails (~> 0.3.9) - rack (~> 2.0.9) + rack (~> 2.1.4) rack-attack (~> 6.3.0) rack-cors (~> 1.0.6) rack-oauth2 (~> 1.9.3) diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index 72dc56facd2..f7a5d31b835 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -285,10 +285,9 @@ export default { variant="default" class="d-sm-none gl-absolute toggle-sidebar-mobile-button" type="button" + icon="chevron-double-lg-left" @click="toggleSidebar" - > - <i class="fa fa-angle-double-left"></i> - </gl-button> + /> </div> <div v-if="alert" diff --git a/app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js b/app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js index 719d76fef8f..c4af34b848b 100644 --- a/app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js +++ b/app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js @@ -32,11 +32,12 @@ export default () => { // Sidebar has an icon which corresponds to collapsing the sidebar // only then trigger the click. - if (sidebarGutterVueToggleEl) { - const collapseIcon = sidebarGutterVueToggleEl.querySelector('i.fa-angle-double-right'); - - if (collapseIcon) { - collapseIcon.click(); + if ( + sidebarGutterVueToggleEl && + !sidebarGutterVueToggleEl.classList.contains('js-sidebar-collapsed') + ) { + if (sidebarGutterVueToggleEl) { + sidebarGutterVueToggleEl.click(); } } } diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index d1e5dad7971..2d426ee663a 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import NewCommitForm from '../new_commit_form'; -import EditBlob from './edit_blob'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; import BlobFileDropzone from '../blob/blob_file_dropzone'; import initPopover from '~/blob/suggest_gitlab_ci_yml'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; @@ -24,6 +24,18 @@ export default () => { const commitButton = $('.js-commit-button'); const cancelLink = $('.btn.btn-cancel'); + import('./edit_blob') + .then(({ default: EditBlob } = {}) => { + new EditBlob({ + assetsPath: `${urlRoot}${assetsPath}`, + filePath, + currentAction, + projectId, + isMarkdown, + }); + }) + .catch(e => createFlash(e)); + cancelLink.on('click', () => { window.onbeforeunload = null; }); @@ -32,13 +44,6 @@ export default () => { window.onbeforeunload = null; }); - new EditBlob({ - assetsPath: `${urlRoot}${assetsPath}`, - filePath, - currentAction, - projectId, - isMarkdown, - }); new NewCommitForm(editBlobForm); // returning here blocks page navigation diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 52fa0038fbb..bdcdabe8f78 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -478,13 +478,14 @@ export default class MergeRequestTabs { } shrinkView() { - const $gutterIcon = $('.js-sidebar-toggle i:visible'); + const $gutterBtn = $('.js-sidebar-toggle:visible'); + const $expandSvg = $gutterBtn.find('.js-sidebar-expand'); // Wait until listeners are set setTimeout(() => { // Only when sidebar is expanded - if ($gutterIcon.is('.fa-angle-double-right')) { - $gutterIcon.closest('a').trigger('click', [true]); + if ($expandSvg.length && $expandSvg.hasClass('hidden')) { + $gutterBtn.trigger('click', [true]); } }, 0); } @@ -494,13 +495,14 @@ export default class MergeRequestTabs { if (parseBoolean(Cookies.get('collapsed_gutter'))) { return; } - const $gutterIcon = $('.js-sidebar-toggle i:visible'); + const $gutterBtn = $('.js-sidebar-toggle'); + const $collapseSvg = $gutterBtn.find('.js-sidebar-collapse'); // Wait until listeners are set setTimeout(() => { // Only when sidebar is collapsed - if ($gutterIcon.is('.fa-angle-double-left')) { - $gutterIcon.closest('a').trigger('click', [true]); + if ($collapseSvg.length && !$collapseSvg.hasClass('hidden')) { + $gutterBtn.trigger('click', [true]); } }, 0); } diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue index b05cf080aea..5841716c8c5 100644 --- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue +++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue @@ -1,4 +1,5 @@ <script> +import Vue from 'vue'; import { uniqueId } from 'lodash'; import { GlAlert, @@ -50,6 +51,10 @@ export default { type: String, required: true, }, + configVariablesPath: { + type: String, + required: true, + }, projectId: { type: String, required: true, @@ -86,7 +91,7 @@ export default { return { searchTerm: '', refValue: this.refParam, - variables: [], + form: {}, error: null, warnings: [], totalWarnings: 0, @@ -110,60 +115,122 @@ export default { shouldShowWarning() { return this.warnings.length > 0 && !this.isWarningDismissed; }, + variables() { + return this.form[this.refValue]?.variables ?? []; + }, + descriptions() { + return this.form[this.refValue]?.descriptions ?? {}; + }, }, created() { - this.addEmptyVariable(); - - if (this.variableParams) { - this.setVariableParams(VARIABLE_TYPE, this.variableParams); - } - - if (this.fileParams) { - this.setVariableParams(FILE_TYPE, this.fileParams); - } + this.setRefSelected(this.refValue); }, methods: { - setVariable(type, key, value) { - const variable = this.variables.find(v => v.key === key); + addEmptyVariable(refValue) { + const { variables } = this.form[refValue]; + + const lastVar = variables[variables.length - 1]; + if (lastVar?.key === '' && lastVar?.value === '') { + return; + } + + variables.push({ + uniqueId: uniqueId(`var-${refValue}`), + variable_type: VARIABLE_TYPE, + key: '', + value: '', + }); + }, + setVariable(refValue, type, key, value) { + const { variables } = this.form[refValue]; + + const variable = variables.find(v => v.key === key); if (variable) { variable.type = type; variable.value = value; } else { - // insert before the empty variable - this.variables.splice(this.variables.length - 1, 0, { - uniqueId: uniqueId('var'), + variables.push({ + uniqueId: uniqueId(`var-${refValue}`), key, value, variable_type: type, }); } }, - setVariableParams(type, paramsObj) { + setVariableParams(refValue, type, paramsObj) { Object.entries(paramsObj).forEach(([key, value]) => { - this.setVariable(type, key, value); + this.setVariable(refValue, type, key, value); }); }, - setRefSelected(ref) { - this.refValue = ref; + setRefSelected(refValue) { + this.refValue = refValue; + + if (!this.form[refValue]) { + this.fetchConfigVariables(refValue) + .then(({ descriptions, params }) => { + Vue.set(this.form, refValue, { + variables: [], + descriptions, + }); + + // Add default variables from yml + this.setVariableParams(refValue, VARIABLE_TYPE, params); + }) + .catch(() => { + Vue.set(this.form, refValue, { + variables: [], + descriptions: {}, + }); + }) + .finally(() => { + // Add/update variables, e.g. from query string + if (this.variableParams) { + this.setVariableParams(refValue, VARIABLE_TYPE, this.variableParams); + } + if (this.fileParams) { + this.setVariableParams(refValue, FILE_TYPE, this.fileParams); + } + + // Adds empty var at the end of the form + this.addEmptyVariable(refValue); + }); + } }, + isSelected(ref) { return ref === this.refValue; }, - addEmptyVariable() { - this.variables.push({ - uniqueId: uniqueId('var'), - variable_type: VARIABLE_TYPE, - key: '', - value: '', - }); - }, removeVariable(index) { this.variables.splice(index, 1); }, - canRemove(index) { return index < this.variables.length - 1; }, + + fetchConfigVariables(refValue) { + if (gon?.features?.newPipelineFormPrefilledVars) { + return axios + .get(this.configVariablesPath, { + params: { + sha: refValue, + }, + }) + .then(({ data }) => { + const params = {}; + const descriptions = {}; + + Object.entries(data).forEach(([key, { value, description }]) => { + if (description !== null) { + params[key] = value; + descriptions[key] = description; + } + }); + + return { params, descriptions }; + }); + } + return Promise.resolve({ params: {}, descriptions: {} }); + }, createPipeline() { const filteredVariables = this.variables .filter(({ key, value }) => key !== '' && value !== '') @@ -261,45 +328,53 @@ export default { <div v-for="(variable, index) in variables" :key="variable.uniqueId" - class="gl-display-flex gl-align-items-stretch gl-align-items-center gl-mb-4 gl-ml-n3 gl-pb-2 gl-border-b-solid gl-border-gray-200 gl-border-b-1 gl-flex-direction-column gl-md-flex-direction-row" + class="gl-mb-3 gl-ml-n3 gl-pb-2" data-testid="ci-variable-row" > - <gl-form-select - v-model="variable.variable_type" - :class="$options.formElementClasses" - :options="$options.typeOptions" - /> - <gl-form-input - v-model="variable.key" - :placeholder="s__('CiVariables|Input variable key')" - :class="$options.formElementClasses" - data-testid="pipeline-form-ci-variable-key" - @change.once="addEmptyVariable()" - /> - <gl-form-input - v-model="variable.value" - :placeholder="s__('CiVariables|Input variable value')" - class="gl-mb-3" - /> - - <template v-if="variables.length > 1"> - <gl-button - v-if="canRemove(index)" - class="gl-md-ml-3 gl-mb-3" - data-testid="remove-ci-variable-row" - variant="danger" - category="secondary" - @click="removeVariable(index)" - > - <gl-icon class="gl-mr-0! gl-display-none gl-display-md-block" name="clear" /> - <span class="gl-display-md-none">{{ s__('CiVariables|Remove variable') }}</span> - </gl-button> - <gl-button - v-else - class="gl-md-ml-3 gl-mb-3 gl-display-none gl-display-md-block gl-visibility-hidden" - icon="clear" + <div + class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row" + > + <gl-form-select + v-model="variable.variable_type" + :class="$options.formElementClasses" + :options="$options.typeOptions" + /> + <gl-form-input + v-model="variable.key" + :placeholder="s__('CiVariables|Input variable key')" + :class="$options.formElementClasses" + data-testid="pipeline-form-ci-variable-key" + @change="addEmptyVariable(refValue)" + /> + <gl-form-input + v-model="variable.value" + :placeholder="s__('CiVariables|Input variable value')" + class="gl-mb-3" + data-testid="pipeline-form-ci-variable-value" /> - </template> + + <template v-if="variables.length > 1"> + <gl-button + v-if="canRemove(index)" + class="gl-md-ml-3 gl-mb-3" + data-testid="remove-ci-variable-row" + variant="danger" + category="secondary" + @click="removeVariable(index)" + > + <gl-icon class="gl-mr-0! gl-display-none gl-display-md-block" name="clear" /> + <span class="gl-display-md-none">{{ s__('CiVariables|Remove variable') }}</span> + </gl-button> + <gl-button + v-else + class="gl-md-ml-3 gl-mb-3 gl-display-none gl-display-md-block gl-visibility-hidden" + icon="clear" + /> + </template> + </div> + <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3"> + {{ descriptions[variable.key] }} + </div> </div> <template #description diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js index f1ea86f8c5f..ff4f677654e 100644 --- a/app/assets/javascripts/pipeline_new/index.js +++ b/app/assets/javascripts/pipeline_new/index.js @@ -6,6 +6,7 @@ export default () => { const { projectId, pipelinesPath, + configVariablesPath, refParam, varParam, fileParam, @@ -25,6 +26,7 @@ export default () => { props: { projectId, pipelinesPath, + configVariablesPath, refParam, variableParams, fileParams, diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue index 43a54090e18..1569b326b31 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue @@ -1,10 +1,9 @@ <script> /* eslint-disable vue/no-v-html */ import { isEmpty } from 'lodash'; -import { GlLink } from '@gitlab/ui'; -import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; +import { GlLink, GlModal } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; /** * Pipeline Stop Modal. @@ -13,7 +12,7 @@ import { s__, sprintf } from '~/locale'; */ export default { components: { - GlModal: DeprecatedModal2, + GlModal, GlLink, CiIcon, }, @@ -46,6 +45,17 @@ export default { hasRef() { return !isEmpty(this.pipeline.ref); }, + primaryProps() { + return { + text: s__('Pipeline|Stop pipeline'), + attributes: [{ variant: 'danger' }], + }; + }, + cancelProps() { + return { + text: __('Cancel'), + }; + }, }, methods: { emitSubmit(event) { @@ -56,11 +66,11 @@ export default { </script> <template> <gl-modal - id="confirmation-modal" - :header-title-text="modalTitle" - :footer-primary-button-text="s__('Pipeline|Stop pipeline')" - footer-primary-button-variant="danger" - @submit="emitSubmit($event)" + modal-id="confirmation-modal" + :title="modalTitle" + :action-primary="primaryProps" + :action-cancel="cancelProps" + @primary="emitSubmit($event)" > <p v-html="modalText"></p> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue index 898973e03e1..7224ec455f6 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; import eventHub from '../../event_hub'; import { __ } from '~/locale'; import PipelinesActionsComponent from './pipelines_actions.vue'; @@ -24,6 +24,7 @@ export default { }, directives: { GlTooltip: GlTooltipDirective, + GlModalDirective, }, components: { PipelinesActionsComponent, @@ -366,12 +367,11 @@ export default { <gl-button v-if="pipeline.flags.cancelable" v-gl-tooltip.hover + v-gl-modal-directive="'confirmation-modal'" :aria-label="$options.i18n.cancelTitle" :title="$options.i18n.cancelTitle" :loading="isCancelling" :disabled="isCancelling" - data-toggle="modal" - data-target="#confirmation-modal" icon="close" variant="danger" category="primary" diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 18c3fa9d121..20dc7cb07e7 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -1,11 +1,12 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { n__, __ } from '~/locale'; export default { name: 'AssigneeTitle', components: { GlLoadingIcon, + GlIcon, }, props: { loading: { @@ -64,7 +65,7 @@ export default { href="#" role="button" > - <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i> + <gl-icon aria-hidden="true" data-hidden="true" name="chevron-double-lg-right" :size="12" /> </a> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue index 437f28907fd..4f4f7002dc9 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue @@ -1,13 +1,14 @@ <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 { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; export default { name: 'ReviewerTitle', components: { GlLoadingIcon, + GlIcon, }, props: { loading: { @@ -58,7 +59,7 @@ export default { href="#" role="button" > - <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i> + <gl-icon aria-hidden="true" data-hidden="true" name="chevron-double-lg-right" :size="12" /> </a> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue index 6b3007fce51..c762922d890 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue @@ -1,5 +1,5 @@ <script> -import tooltip from '~/vue_shared/directives/tooltip'; +import { GlTooltipDirective } from '@gitlab/ui'; import MrWidgetAuthor from './mr_widget_author.vue'; export default { @@ -8,7 +8,7 @@ export default { MrWidgetAuthor, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { actionText: { @@ -34,6 +34,7 @@ export default { <h4 class="js-mr-widget-author"> {{ actionText }} <mr-widget-author :author="author" /> - <time v-tooltip :title="dateTitle" data-container="body"> {{ dateReadable }} </time> + <span class="sr-only">{{ dateReadable }} ({{ dateTitle }})</span> + <time v-gl-tooltip.hover aria-hidden :title="dateTitle"> {{ dateReadable }} </time> </h4> </template> diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue b/app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue index 289fe78f899..2aebfe80db5 100644 --- a/app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue +++ b/app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue @@ -1,11 +1,27 @@ <script> +import ActionButtonGroup from './action_button_group.vue'; +import RemoveGroupLinkButton from './remove_group_link_button.vue'; + export default { name: 'GroupActionButtons', + components: { ActionButtonGroup, RemoveGroupLinkButton }, + props: { + member: { + type: Object, + required: true, + }, + permissions: { + type: Object, + required: true, + }, + }, }; </script> <template> - <span> - <!-- Temporarily empty --> - </span> + <action-button-group> + <div v-if="permissions.canRemove" class="gl-px-1"> + <remove-group-link-button :group-link="member" /> + </div> + </action-button-group> </template> diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue new file mode 100644 index 00000000000..9d89cb40676 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue @@ -0,0 +1,36 @@ +<script> +import { mapActions } from 'vuex'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + name: 'RemoveGroupLinkButton', + i18n: { + buttonTitle: s__('Members|Remove group'), + }, + components: { GlButton }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + groupLink: { + type: Object, + required: true, + }, + }, + methods: { + ...mapActions(['showRemoveGroupLinkModal']), + }, +}; +</script> + +<template> + <gl-button + v-gl-tooltip.hover + variant="danger" + :title="$options.i18n.buttonTitle" + :aria-label="$options.i18n.buttonTitle" + icon="remove" + @click="showRemoveGroupLinkModal(groupLink)" + /> +</template> diff --git a/app/assets/javascripts/vue_shared/components/members/constants.js b/app/assets/javascripts/vue_shared/components/members/constants.js index 665ba1df547..6509779053e 100644 --- a/app/assets/javascripts/vue_shared/components/members/constants.js +++ b/app/assets/javascripts/vue_shared/components/members/constants.js @@ -66,3 +66,5 @@ export const MEMBER_TYPES = { export const DAYS_TO_EXPIRE_SOON = 7; export const LEAVE_MODAL_ID = 'member-leave-modal'; + +export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id'; diff --git a/app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue b/app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue new file mode 100644 index 00000000000..e8890717724 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue @@ -0,0 +1,69 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import { GlModal, GlSprintf, GlForm } from '@gitlab/ui'; +import csrf from '~/lib/utils/csrf'; +import { __, s__, sprintf } from '~/locale'; +import { REMOVE_GROUP_LINK_MODAL_ID } from '../constants'; + +export default { + name: 'RemoveGroupLinkModal', + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: s__('Members|Remove group'), + attributes: { + variant: 'danger', + }, + }, + csrf, + i18n: { + modalBody: s__('Members|Are you sure you want to remove "%{groupName}"?'), + }, + modalId: REMOVE_GROUP_LINK_MODAL_ID, + components: { GlModal, GlSprintf, GlForm }, + computed: { + ...mapState(['memberPath', 'groupLinkToRemove', 'removeGroupLinkModalVisible']), + groupLinkPath() { + return this.memberPath.replace(/:id$/, this.groupLinkToRemove?.id); + }, + groupName() { + return this.groupLinkToRemove?.sharedWithGroup.fullName; + }, + modalTitle() { + return sprintf(s__('Members|Remove "%{groupName}"'), { groupName: this.groupName }); + }, + }, + methods: { + ...mapActions(['hideRemoveGroupLinkModal']), + handlePrimary() { + this.$refs.form.$el.submit(); + }, + }, +}; +</script> + +<template> + <gl-modal + v-bind="$attrs" + :modal-id="$options.modalId" + :visible="removeGroupLinkModalVisible" + :title="modalTitle" + :action-primary="$options.actionPrimary" + :action-cancel="$options.actionCancel" + size="sm" + @primary="handlePrimary" + @hide="hideRemoveGroupLinkModal" + > + <gl-form ref="form" :action="groupLinkPath" method="post"> + <p> + <gl-sprintf :message="$options.i18n.modalBody"> + <template #groupName>{{ groupName }}</template> + </gl-sprintf> + </p> + + <input type="hidden" name="_method" value="delete" /> + <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> + </gl-form> + </gl-modal> +</template> diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue b/app/assets/javascripts/vue_shared/components/members/table/members_table.vue index 4580e4a9f19..c1a80a85dbe 100644 --- a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue +++ b/app/assets/javascripts/vue_shared/components/members/table/members_table.vue @@ -10,6 +10,7 @@ import ExpiresAt from './expires_at.vue'; import MemberActionButtons from './member_action_buttons.vue'; import MembersTableCell from './members_table_cell.vue'; import RoleDropdown from './role_dropdown.vue'; +import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue'; export default { name: 'MembersTable', @@ -23,6 +24,7 @@ export default { MemberSource, MemberActionButtons, RoleDropdown, + RemoveGroupLinkModal, }, computed: { ...mapState(['members', 'tableFields']), @@ -37,69 +39,72 @@ export default { </script> <template> - <gl-table - class="members-table" - head-variant="white" - stacked="lg" - :fields="filteredFields" - :items="members" - primary-key="id" - thead-class="border-bottom" - :empty-text="__('No members found')" - show-empty - > - <template #cell(account)="{ item: member }"> - <members-table-cell #default="{ memberType, isCurrentUser }" :member="member"> - <member-avatar - :member-type="memberType" - :is-current-user="isCurrentUser" - :member="member" - /> - </members-table-cell> - </template> + <div> + <gl-table + class="members-table" + head-variant="white" + stacked="lg" + :fields="filteredFields" + :items="members" + primary-key="id" + thead-class="border-bottom" + :empty-text="__('No members found')" + show-empty + > + <template #cell(account)="{ item: member }"> + <members-table-cell #default="{ memberType, isCurrentUser }" :member="member"> + <member-avatar + :member-type="memberType" + :is-current-user="isCurrentUser" + :member="member" + /> + </members-table-cell> + </template> - <template #cell(source)="{ item: member }"> - <members-table-cell #default="{ isDirectMember }" :member="member"> - <member-source :is-direct-member="isDirectMember" :member-source="member.source" /> - </members-table-cell> - </template> + <template #cell(source)="{ item: member }"> + <members-table-cell #default="{ isDirectMember }" :member="member"> + <member-source :is-direct-member="isDirectMember" :member-source="member.source" /> + </members-table-cell> + </template> - <template #cell(granted)="{ item: { createdAt, createdBy } }"> - <created-at :date="createdAt" :created-by="createdBy" /> - </template> + <template #cell(granted)="{ item: { createdAt, createdBy } }"> + <created-at :date="createdAt" :created-by="createdBy" /> + </template> - <template #cell(invited)="{ item: { createdAt, createdBy } }"> - <created-at :date="createdAt" :created-by="createdBy" /> - </template> + <template #cell(invited)="{ item: { createdAt, createdBy } }"> + <created-at :date="createdAt" :created-by="createdBy" /> + </template> - <template #cell(requested)="{ item: { createdAt } }"> - <created-at :date="createdAt" /> - </template> + <template #cell(requested)="{ item: { createdAt } }"> + <created-at :date="createdAt" /> + </template> - <template #cell(expires)="{ item: { expiresAt } }"> - <expires-at :date="expiresAt" /> - </template> + <template #cell(expires)="{ item: { expiresAt } }"> + <expires-at :date="expiresAt" /> + </template> - <template #cell(maxRole)="{ item: member }"> - <members-table-cell #default="{ permissions }" :member="member"> - <role-dropdown v-if="permissions.canUpdate" :member="member" /> - <gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge> - </members-table-cell> - </template> + <template #cell(maxRole)="{ item: member }"> + <members-table-cell #default="{ permissions }" :member="member"> + <role-dropdown v-if="permissions.canUpdate" :member="member" /> + <gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge> + </members-table-cell> + </template> - <template #cell(actions)="{ item: member }"> - <members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member"> - <member-action-buttons - :member-type="memberType" - :is-current-user="isCurrentUser" - :permissions="permissions" - :member="member" - /> - </members-table-cell> - </template> + <template #cell(actions)="{ item: member }"> + <members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member"> + <member-action-buttons + :member-type="memberType" + :is-current-user="isCurrentUser" + :permissions="permissions" + :member="member" + /> + </members-table-cell> + </template> - <template #head(actions)="{ label }"> - <span data-testid="col-actions" class="gl-sr-only">{{ label }}</span> - </template> - </gl-table> + <template #head(actions)="{ label }"> + <span data-testid="col-actions" class="gl-sr-only">{{ label }}</span> + </template> + </gl-table> + <remove-group-link-modal /> + </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index 040a15406e0..6dacf4e10d3 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -1,11 +1,14 @@ <script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; export default { name: 'ToggleSidebar', + components: { + GlButton, + }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { collapsed: { @@ -22,6 +25,12 @@ export default { tooltipLabel() { return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar'); }, + buttonIcon() { + return this.collapsed ? 'chevron-double-lg-left' : 'chevron-double-lg-right'; + }, + allCssClasses() { + return [this.cssClasses, { 'js-sidebar-collapsed': this.collapsed }]; + }, }, methods: { toggle() { @@ -32,25 +41,15 @@ export default { </script> <template> - <button - v-tooltip + <gl-button + v-gl-tooltip:body.viewport.left :title="tooltipLabel" - :class="cssClasses" - type="button" - class="btn btn-blank gutter-toggle btn-sidebar-action js-sidebar-vue-toggle" - data-container="body" - data-placement="left" - data-boundary="viewport" + :class="allCssClasses" + class="gutter-toggle btn-sidebar-action js-sidebar-vue-toggle" + :icon="buttonIcon" + category="tertiary" + size="small" + :aria-label="__('toggle collapse')" @click="toggle" - > - <i - :class="{ - 'fa-angle-double-right': !collapsed, - 'fa-angle-double-left': collapsed, - }" - :aria-label="__('toggle collapse')" - class="fa" - > - </i> - </button> + /> </template> diff --git a/app/assets/javascripts/vuex_shared/modules/members/actions.js b/app/assets/javascripts/vuex_shared/modules/members/actions.js index b3e6de48370..f7fdddfd070 100644 --- a/app/assets/javascripts/vuex_shared/modules/members/actions.js +++ b/app/assets/javascripts/vuex_shared/modules/members/actions.js @@ -15,3 +15,11 @@ export const updateMemberRole = async ({ state, commit }, { memberId, accessLeve throw error; } }; + +export const showRemoveGroupLinkModal = ({ commit }, groupLink) => { + commit(types.SHOW_REMOVE_GROUP_LINK_MODAL, groupLink); +}; + +export const hideRemoveGroupLinkModal = ({ commit }) => { + commit(types.HIDE_REMOVE_GROUP_LINK_MODAL); +}; diff --git a/app/assets/javascripts/vuex_shared/modules/members/mutation_types.js b/app/assets/javascripts/vuex_shared/modules/members/mutation_types.js index 2d526650eb6..00f4c910669 100644 --- a/app/assets/javascripts/vuex_shared/modules/members/mutation_types.js +++ b/app/assets/javascripts/vuex_shared/modules/members/mutation_types.js @@ -2,3 +2,6 @@ export const RECEIVE_MEMBER_ROLE_SUCCESS = 'RECEIVE_MEMBER_ROLE_SUCCESS'; export const RECEIVE_MEMBER_ROLE_ERROR = 'RECEIVE_MEMBER_ROLE_ERROR'; export const HIDE_ERROR = 'HIDE_ERROR'; + +export const SHOW_REMOVE_GROUP_LINK_MODAL = 'SHOW_REMOVE_GROUP_LINK_MODAL'; +export const HIDE_REMOVE_GROUP_LINK_MODAL = 'HIDE_REMOVE_GROUP_LINK_MODAL'; diff --git a/app/assets/javascripts/vuex_shared/modules/members/mutations.js b/app/assets/javascripts/vuex_shared/modules/members/mutations.js index 5ee1147f658..281c947e68f 100644 --- a/app/assets/javascripts/vuex_shared/modules/members/mutations.js +++ b/app/assets/javascripts/vuex_shared/modules/members/mutations.js @@ -23,4 +23,11 @@ export default { state.showError = false; state.errorMessage = ''; }, + [types.SHOW_REMOVE_GROUP_LINK_MODAL](state, groupLink) { + state.removeGroupLinkModalVisible = true; + state.groupLinkToRemove = groupLink; + }, + [types.HIDE_REMOVE_GROUP_LINK_MODAL](state) { + state.removeGroupLinkModalVisible = false; + }, }; diff --git a/app/assets/javascripts/vuex_shared/modules/members/state.js b/app/assets/javascripts/vuex_shared/modules/members/state.js index 6f454b56b1e..e4867819e17 100644 --- a/app/assets/javascripts/vuex_shared/modules/members/state.js +++ b/app/assets/javascripts/vuex_shared/modules/members/state.js @@ -14,4 +14,6 @@ export default ({ requestFormatter, showError: false, errorMessage: '', + removeGroupLinkModalVisible: false, + groupLinkToRemove: null, }); diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index c1d2cb53ed2..8e30ddf8ce7 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -109,14 +109,6 @@ content: '\f110'; } -.fa-angle-double-right::before { - content: '\f101'; -} - -.fa-angle-double-left::before { - content: '\f100'; -} - .fa-trash-o::before { content: '\f014'; } diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index b841a089e2b..bd7b69384b2 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -183,7 +183,7 @@ class Admin::UsersController < Admin::ApplicationController # restore username to keep form action url. user.username = params[:id] format.html { render "edit" } - format.json { render json: [result[:message]], status: result[:status] } + format.json { render json: [result[:message]], status: :internal_server_error } end end end diff --git a/app/controllers/projects/merge_requests/drafts_controller.rb b/app/controllers/projects/merge_requests/drafts_controller.rb index f4846b1aa81..ca3f36cafe1 100644 --- a/app/controllers/projects/merge_requests/drafts_controller.rb +++ b/app/controllers/projects/merge_requests/drafts_controller.rb @@ -45,7 +45,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli if result[:status] == :success head :ok else - render json: { message: result[:message] }, status: result[:status] + render json: { message: result[:message] }, status: :internal_server_error end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 39dd7a9899d..953dce4d63c 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -17,6 +17,7 @@ class Projects::PipelinesController < Projects::ApplicationController push_frontend_feature_flag(:pipelines_security_report_summary, project) push_frontend_feature_flag(:new_pipeline_form, project) push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false) + push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development) end before_action :ensure_pipeline, only: [:show] diff --git a/app/graphql/mutations/issues/common_mutation_arguments.rb b/app/graphql/mutations/issues/common_mutation_arguments.rb index 7a87d9c2393..4b5b246281f 100644 --- a/app/graphql/mutations/issues/common_mutation_arguments.rb +++ b/app/graphql/mutations/issues/common_mutation_arguments.rb @@ -26,5 +26,3 @@ module Mutations end end end - -Mutations::Issues::CommonMutationArguments.prepend_if_ee('::EE::Mutations::Issues::CommonMutationArguments') diff --git a/app/graphql/resolvers/ci/runner_platforms_resolver.rb b/app/graphql/resolvers/ci/runner_platforms_resolver.rb new file mode 100644 index 00000000000..9677c5139b4 --- /dev/null +++ b/app/graphql/resolvers/ci/runner_platforms_resolver.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Resolvers + module Ci + class RunnerPlatformsResolver < BaseResolver + type Types::Ci::RunnerPlatformType, null: false + + def resolve(**args) + runner_instructions.map do |platform, data| + { + name: platform, human_readable_name: data[:human_readable_name], + architectures: parse_architectures(data[:download_locations]) + } + end + end + + private + + def runner_instructions + Gitlab::Ci::RunnerInstructions::OS.merge(Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS) + end + + def parse_architectures(download_locations) + download_locations&.map do |architecture, download_location| + { name: architecture, download_location: download_location } + end + end + end + end +end diff --git a/app/graphql/types/ci/runner_architecture_type.rb b/app/graphql/types/ci/runner_architecture_type.rb new file mode 100644 index 00000000000..526348abd9d --- /dev/null +++ b/app/graphql/types/ci/runner_architecture_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class RunnerArchitectureType < BaseObject + graphql_name 'RunnerArchitecture' + + field :name, GraphQL::STRING_TYPE, null: false, + description: 'Name of the runner platform architecture' + field :download_location, GraphQL::STRING_TYPE, null: false, + description: 'Download location for the runner for the platform architecture' + end + end +end diff --git a/app/graphql/types/ci/runner_platform_type.rb b/app/graphql/types/ci/runner_platform_type.rb new file mode 100644 index 00000000000..64719bc4908 --- /dev/null +++ b/app/graphql/types/ci/runner_platform_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + class RunnerPlatformType < BaseObject + graphql_name 'RunnerPlatform' + + field :name, GraphQL::STRING_TYPE, null: false, + description: 'Name slug of the runner platform' + field :human_readable_name, GraphQL::STRING_TYPE, null: false, + description: 'Human readable name of the runner platform' + field :architectures, Types::Ci::RunnerArchitectureType.connection_type, null: true, + description: 'Runner architectures supported for the platform' + end + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 73dd7c57223..bd4b53bdaa7 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -80,6 +80,10 @@ module Types description: 'Get statistics on the instance', resolver: Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver + field :runner_platforms, Types::Ci::RunnerPlatformType.connection_type, + null: true, description: 'Supported runner platforms', + resolver: Resolvers::Ci::RunnerPlatformsResolver + def design_management DesignManagementObject.new(nil) end diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index 4fd3fc2155d..ee90585112b 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -33,6 +33,7 @@ module Groups::GroupMembersHelper def linked_groups_list_data_attributes(group) { members: linked_groups_data_json(group.shared_with_group_links), + member_path: group_group_link_path(group, ':id'), group_id: group.id } end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index b29451315e8..684b6387ab1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -829,16 +829,9 @@ module Ci end def same_family_pipeline_ids - if ::Gitlab::Ci::Features.child_of_child_pipeline_enabled?(project) - ::Gitlab::Ci::PipelineObjectHierarchy.new( - base_and_ancestors(same_project: true), options: { same_project: true } - ).base_and_descendants.select(:id) - else - # If pipeline is a child of another pipeline, include the parent - # and the siblings, otherwise return only itself and children. - parent = parent_pipeline || self - [parent.id] + parent.child_pipelines.pluck(:id) - end + ::Gitlab::Ci::PipelineObjectHierarchy.new( + base_and_ancestors(same_project: true), options: { same_project: true } + ).base_and_descendants.select(:id) end def build_with_artifacts_in_self_and_descendants(name) diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb index 809c478b8c7..86d0cf079fc 100644 --- a/app/services/ci/create_downstream_pipeline_service.rb +++ b/app/services/ci/create_downstream_pipeline_service.rb @@ -77,16 +77,9 @@ module Ci # TODO: Remove this condition if favour of model validation # https://gitlab.com/gitlab-org/gitlab/issues/38338 - if ::Gitlab::Ci::Features.child_of_child_pipeline_enabled?(project) - if has_max_descendants_depth? - @bridge.drop!(:reached_max_descendant_pipelines_depth) - return false - end - else - if @bridge.triggers_child_pipeline? && @bridge.pipeline.parent_pipeline.present? - @bridge.drop!(:bridge_pipeline_is_child_pipeline) - return false - end + if has_max_descendants_depth? + @bridge.drop!(:reached_max_descendant_pipelines_depth) + return false end unless can_create_downstream_pipeline?(target_ref) diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 29e5f2cf5b4..cb5401cd329 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -7,7 +7,15 @@ %hr - if Feature.enabled?(:new_pipeline_form, @project) - #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project), max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } } + #js-new-pipeline{ data: { project_id: @project.id, + pipelines_path: project_pipelines_path(@project), + config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project), + ref_param: params[:ref] || @project.default_branch, + var_param: params[:var].to_json, + file_param: params[:file_var].to_json, + ref_names: @project.repository.ref_names.to_json.html_safe, + settings_link: project_settings_ci_cd_path(@project), + max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } } - else = form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f| diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index d0f7d65aed6..d7a5fcf4f18 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -9,6 +9,8 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker worker_resource_boundary :cpu tags :requires_disk_io + ARCHIVE_TRACES_IN = 2.minutes.freeze + # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| @@ -33,9 +35,22 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker # We execute these async as these are independent operations. BuildHooksWorker.perform_async(build.id) - ArchiveTraceWorker.perform_async(build.id) ExpirePipelineCacheWorker.perform_async(build.pipeline_id) if build.pipeline.cacheable? ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat? + + ## + # We want to delay sending a build trace to object storage operation to + # validate that this fixes a race condition between this and flushing live + # trace chunks and chunks being removed after consolidation and putting + # them into object storage archive. + # + # TODO This is temporary fix we should improve later, after we validate + # that this is indeed the culprit. + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/267112 for more + # details. + # + ArchiveTraceWorker.perform_in(ARCHIVE_TRACES_IN, build.id) end end diff --git a/changelogs/unreleased/267973-replace-fa-angle-double-left-with-gitlab-svg-chevron-double-lg-lef.yml b/changelogs/unreleased/267973-replace-fa-angle-double-left-with-gitlab-svg-chevron-double-lg-lef.yml new file mode 100644 index 00000000000..e3b56c5dbd9 --- /dev/null +++ b/changelogs/unreleased/267973-replace-fa-angle-double-left-with-gitlab-svg-chevron-double-lg-lef.yml @@ -0,0 +1,5 @@ +--- +title: Replace fa-angle-double-left and fa-angle-double-right icons with GitLab SVG +merge_request: 45251 +author: +type: changed diff --git a/changelogs/unreleased/issue_237977-2.yml b/changelogs/unreleased/issue_237977-2.yml new file mode 100644 index 00000000000..6e61ea9c405 --- /dev/null +++ b/changelogs/unreleased/issue_237977-2.yml @@ -0,0 +1,5 @@ +--- +title: Populate blocking issues count +merge_request: 45176 +author: +type: other diff --git a/changelogs/unreleased/migrate_blocked_by.yml b/changelogs/unreleased/migrate_blocked_by.yml new file mode 100644 index 00000000000..04f40b17159 --- /dev/null +++ b/changelogs/unreleased/migrate_blocked_by.yml @@ -0,0 +1,5 @@ +--- +title: Migrate blocked_by issue links to blocks type by swapping source and target +merge_request: 45262 +author: +type: other diff --git a/changelogs/unreleased/sh-update-rack-2-1-4.yml b/changelogs/unreleased/sh-update-rack-2-1-4.yml new file mode 100644 index 00000000000..4be62c3b017 --- /dev/null +++ b/changelogs/unreleased/sh-update-rack-2-1-4.yml @@ -0,0 +1,5 @@ +--- +title: Update to Rack v2.1.4 +merge_request: 45340 +author: +type: fixed diff --git a/config/feature_flags/development/ci_child_of_child_pipeline.yml b/config/feature_flags/development/new_pipeline_form_prefilled_vars.yml index 7a90334619a..6b821e7fd9e 100644 --- a/config/feature_flags/development/ci_child_of_child_pipeline.yml +++ b/config/feature_flags/development/new_pipeline_form_prefilled_vars.yml @@ -1,7 +1,7 @@ --- -name: ci_child_of_child_pipeline -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41102 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/243747 -group: group::continuous integration +name: new_pipeline_form_prefilled_vars +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44120 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263276 type: development -default_enabled: true +group: group::continuous integration +default_enabled: false diff --git a/db/post_migrate/20201014142521_schedule_sync_blocking_issues_count.rb b/db/post_migrate/20201014142521_schedule_sync_blocking_issues_count.rb new file mode 100644 index 00000000000..61b2b2aaad5 --- /dev/null +++ b/db/post_migrate/20201014142521_schedule_sync_blocking_issues_count.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'set' + +class ScheduleSyncBlockingIssuesCount < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + BATCH_SIZE = 50 + DELAY_INTERVAL = 120.seconds.to_i + MIGRATION = 'SyncBlockingIssuesCount'.freeze + + disable_ddl_transaction! + + class TmpIssueLink < ActiveRecord::Base + self.table_name = 'issue_links' + + include EachBatch + end + + def up + return unless Gitlab.ee? + + issue_link_ids = SortedSet.new + + TmpIssueLink.distinct.select(:source_id).where(link_type: 1).each_batch(of: 1000, column: :source_id) do |query| + issue_link_ids.merge(query.pluck(:source_id)) + end + + TmpIssueLink.distinct.select(:target_id).where(link_type: 2).each_batch(of: 1000, column: :target_id) do |query| + issue_link_ids.merge(query.pluck(:target_id)) + end + + issue_link_ids.each_slice(BATCH_SIZE).with_index do |items, index| + start_id, *, end_id = items + + arguments = [start_id, end_id] + + final_delay = DELAY_INTERVAL * (index + 1) + migrate_in(final_delay, MIGRATION, arguments) + end + end + + def down + # NO OP + end +end diff --git a/db/post_migrate/20201015073808_schedule_blocked_by_links_replacement.rb b/db/post_migrate/20201015073808_schedule_blocked_by_links_replacement.rb new file mode 100644 index 00000000000..7833d7c4c04 --- /dev/null +++ b/db/post_migrate/20201015073808_schedule_blocked_by_links_replacement.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class ScheduleBlockedByLinksReplacement < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INTERVAL = 2.minutes + # at the time of writing there were 47600 blocked_by issues: + # estimated time is 48 batches * 2 minutes -> 100 minutes + BATCH_SIZE = 1000 + MIGRATION = 'ReplaceBlockedByLinks' + + disable_ddl_transaction! + + class IssueLink < ActiveRecord::Base + include EachBatch + + self.table_name = 'issue_links' + end + + def up + relation = IssueLink.where(link_type: 2) + + queue_background_migration_jobs_by_range_at_intervals( + relation, MIGRATION, INTERVAL, batch_size: BATCH_SIZE) + end + + def down + end +end diff --git a/db/schema_migrations/20201014142521 b/db/schema_migrations/20201014142521 new file mode 100644 index 00000000000..d87f01628c3 --- /dev/null +++ b/db/schema_migrations/20201014142521 @@ -0,0 +1 @@ +aed103bb25b70eb8f6387d84225a8e51672a83c4586ccc65da3011ef010da4b1
\ No newline at end of file diff --git a/db/schema_migrations/20201015073808 b/db/schema_migrations/20201015073808 new file mode 100644 index 00000000000..c14d0b7a528 --- /dev/null +++ b/db/schema_migrations/20201015073808 @@ -0,0 +1 @@ +e44ab2a7b3014b44d7d84de1f7e618d2fc89f98b8d59f5f6fa331544e206355f
\ No newline at end of file diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md index f54a25b7a9b..0cb7b8868c3 100644 --- a/doc/administration/reference_architectures/1k_users.md +++ b/doc/administration/reference_architectures/1k_users.md @@ -39,24 +39,19 @@ the swap available when needed. ## Setup instructions -For this default reference architecture, to install GitLab use the standard +To install GitLab for this default reference architecture, use the standard [installation instructions](../../install/README.md). -NOTE: **Note:** -You can also optionally configure GitLab to use an -[external PostgreSQL service](../postgresql/external.md) or an -[external object storage service](../object_storage.md) for -added performance and reliability at a reduced complexity cost. +You can also optionally configure GitLab to use an [external PostgreSQL service](../postgresql/external.md) +or an [external object storage service](../object_storage.md) for added +performance and reliability at a reduced complexity cost. ## Configure Advanced Search **(STARTER ONLY)** -NOTE: **Note:** -Elasticsearch cluster design and requirements are dependent on your specific data. -For recommended best practices on how to set up your Elasticsearch cluster -alongside your instance, read how to -[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration). - -You can leverage Elasticsearch and enable Advanced Search for faster, more -advanced code search across your entire GitLab instance. +You can leverage Elasticsearch and [enable Advanced Search](../../integration/elasticsearch.md) +for faster, more advanced code search across your entire GitLab instance. -[Learn how to set it up.](../../integration/elasticsearch.md) +Elasticsearch cluster design and requirements are dependent on your specific +data. For recommended best practices about how to set up your Elasticsearch +cluster alongside your instance, read how to +[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration). diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index 3d448b2a65f..a2384ce77a0 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -80,7 +80,7 @@ The following API resources are available in the project context: | [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE)** | `/projects/:id/vulnerability_exports` | | [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` | | [Vulnerability findings](vulnerability_findings.md) **(ULTIMATE)** | `/projects/:id/vulnerability_findings` | -| [Wikis](wikis.md) | `/projects/:id/wikis` | +| [Project wikis](wikis.md) | `/projects/:id/wikis` | ## Group resources @@ -108,6 +108,7 @@ The following API resources are available in the group context: | [Notification settings](notification_settings.md) | `/groups/:id/notification_settings` (also available for projects and standalone) | | [Resource label events](resource_label_events.md) | `/groups/:id/epics/.../resource_label_events` (also available for projects) | | [Search](search.md) | `/groups/:id/search` (also available for projects and standalone) | +| [Group wikis](group_wikis.md) **(PREMIUM)** | `/groups/:id/wikis` | ## Standalone resources diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 264a83763ff..effe16fc41a 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -3472,6 +3472,11 @@ input CreateIssueInput { epicId: EpicID """ + The desired health status + """ + healthStatus: HealthStatus + + """ The IID (internal ID) of a project issue. Only admins and project owners can modify """ iid: Int @@ -3510,6 +3515,11 @@ input CreateIssueInput { Title of the issue """ title: String! + + """ + The weight of the issue + """ + weight: Int } """ @@ -15539,6 +15549,31 @@ type Query { ): ProjectConnection """ + Supported runner platforms + """ + runnerPlatforms( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RunnerPlatformConnection + + """ Find Snippets visible to the current user """ snippets( @@ -16712,6 +16747,125 @@ type RunDASTScanPayload { pipelineUrl: String } +type RunnerArchitecture { + """ + Download location for the runner for the platform architecture + """ + downloadLocation: String! + + """ + Name of the runner platform architecture + """ + name: String! +} + +""" +The connection type for RunnerArchitecture. +""" +type RunnerArchitectureConnection { + """ + A list of edges. + """ + edges: [RunnerArchitectureEdge] + + """ + A list of nodes. + """ + nodes: [RunnerArchitecture] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type RunnerArchitectureEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RunnerArchitecture +} + +type RunnerPlatform { + """ + Runner architectures supported for the platform + """ + architectures( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): RunnerArchitectureConnection + + """ + Human readable name of the runner platform + """ + humanReadableName: String! + + """ + Name slug of the runner platform + """ + name: String! +} + +""" +The connection type for RunnerPlatform. +""" +type RunnerPlatformConnection { + """ + A list of edges. + """ + edges: [RunnerPlatformEdge] + + """ + A list of nodes. + """ + nodes: [RunnerPlatform] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type RunnerPlatformEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: RunnerPlatform +} + """ Represents a CI configuration of SAST """ @@ -19658,6 +19812,11 @@ input UpdateIssueInput { epicId: ID """ + The desired health status + """ + healthStatus: HealthStatus + + """ The IID of the issue to mutate """ iid: String! @@ -19691,6 +19850,11 @@ input UpdateIssueInput { Title of the issue """ title: String + + """ + The weight of the issue + """ + weight: Int } """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index f3af80e824c..408b7e13c15 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -9377,6 +9377,26 @@ "defaultValue": null }, { + "name": "healthStatus", + "description": "The desired health status", + "type": { + "kind": "ENUM", + "name": "HealthStatus", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "weight", + "description": "The weight of the issue", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { "name": "epicId", "description": "The ID of an epic to associate the issue with", "type": { @@ -44969,6 +44989,59 @@ "deprecationReason": null }, { + "name": "runnerPlatforms", + "description": "Supported runner platforms", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RunnerPlatformConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "snippets", "description": "Find Snippets visible to the current user", "args": [ @@ -48215,6 +48288,381 @@ }, { "kind": "OBJECT", + "name": "RunnerArchitecture", + "description": null, + "fields": [ + { + "name": "downloadLocation", + "description": "Download location for the runner for the platform architecture", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "Name of the runner platform architecture", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RunnerArchitectureConnection", + "description": "The connection type for RunnerArchitecture.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RunnerArchitectureEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RunnerArchitecture", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RunnerArchitectureEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "RunnerArchitecture", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RunnerPlatform", + "description": null, + "fields": [ + { + "name": "architectures", + "description": "Runner architectures supported for the platform", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "RunnerArchitectureConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "humanReadableName", + "description": "Human readable name of the runner platform", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "Name slug of the runner platform", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RunnerPlatformConnection", + "description": "The connection type for RunnerPlatform.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RunnerPlatformEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "RunnerPlatform", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "RunnerPlatformEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "RunnerPlatform", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "SastCiConfiguration", "description": "Represents a CI configuration of SAST", "fields": [ @@ -57106,6 +57554,26 @@ "defaultValue": null }, { + "name": "healthStatus", + "description": "The desired health status", + "type": { + "kind": "ENUM", + "name": "HealthStatus", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "weight", + "description": "The weight of the issue", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { "name": "epicId", "description": "The ID of the parent epic. NULL when removing the association", "type": { diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index c1cf34dff6c..c6eb8603da9 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2252,6 +2252,20 @@ Autogenerated return type of RunDASTScan. | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `pipelineUrl` | String | URL of the pipeline that was created. | +### RunnerArchitecture + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `downloadLocation` | String! | Download location for the runner for the platform architecture | +| `name` | String! | Name of the runner platform architecture | + +### RunnerPlatform + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `humanReadableName` | String! | Human readable name of the runner platform | +| `name` | String! | Name slug of the runner platform | + ### SastCiConfigurationAnalyzersEntity Represents an analyzer entity in SAST CI configuration. diff --git a/doc/api/group_wikis.md b/doc/api/group_wikis.md index 414c795e092..c61a557fcc6 100644 --- a/doc/api/group_wikis.md +++ b/doc/api/group_wikis.md @@ -5,9 +5,9 @@ info: "To determine the technical writer assigned to the Stage/Group associated type: reference, api --- -# Wikis API +# Group wikis API **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212199) in GitLab 13.2. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212199) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5. Available only in APIv4. diff --git a/doc/api/wikis.md b/doc/api/wikis.md index 7d16a5a38ee..a8c002d4fac 100644 --- a/doc/api/wikis.md +++ b/doc/api/wikis.md @@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated type: reference, api --- -# Wikis API +# Project wikis API > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13372) in GitLab 10.0. diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md index cb117a40bfe..a0965643970 100644 --- a/doc/ci/parent_child_pipelines.md +++ b/doc/ci/parent_child_pipelines.md @@ -164,36 +164,13 @@ This is [resolved in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues ## Nested child pipelines > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29651) in GitLab 13.4. -> - It's [deployed behind a feature flag](../user/feature_flags.md), enabled by default. -> - It's enabled on GitLab.com. -> - It's recommended for production use. -> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-nested-child-pipelines). **(CORE ONLY)** +> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243747) in GitLab 13.5. Parent and child pipelines were introduced with a maximum depth of one level of child pipelines, which was later increased to two. A parent pipeline can trigger many child pipelines, and these child pipelines can trigger their own child pipelines. It's not possible to trigger another level of child pipelines. -### Enable or disable nested child pipelines **(CORE ONLY)** - -Nested child pipelines with a depth of two are under development but ready for -production use. This 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) -can opt to disable it. - -To enable it: - -```ruby -Feature.enable(:ci_child_of_child_pipeline) -``` - -To disable it: - -```ruby -Feature.disable(:ci_child_of_child_pipeline) -``` - ## Pass variables to a child pipeline You can [pass variables to a downstream pipeline](multi_project_pipelines.md#passing-variables-to-a-downstream-pipeline). diff --git a/doc/development/documentation/site_architecture/release_process.md b/doc/development/documentation/site_architecture/release_process.md index 98bb116aba6..d04d34ff786 100644 --- a/doc/development/documentation/site_architecture/release_process.md +++ b/doc/development/documentation/site_architecture/release_process.md @@ -121,10 +121,11 @@ versions (stable branches `X.Y` of the `gitlab-docs` project): pipelines succeed: NOTE: **Note:** - The `release-X-Y` branch needs to be present locally, otherwise the Rake - task will abort. + The `release-X-Y` branch needs to be present locally, + and you need to have switched to it, otherwise the Rake task will fail. ```shell + git checkout release-X-Y ./bin/rake release:dropdowns ``` diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 50493e27ce9..71191d1d871 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -295,13 +295,16 @@ end Adding foreign key to `projects`: +We can use the `add_concurrenct_foreign_key` method in this case, as this helper method +has the lock retries built into it. + ```ruby include Gitlab::Database::MigrationHelpers +disable_ddl_transaction! + def up - with_lock_retries do - add_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade - end + add_concurrent_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade end def down @@ -316,10 +319,10 @@ Adding foreign key to `users`: ```ruby include Gitlab::Database::MigrationHelpers +disable_ddl_transaction! + def up - with_lock_retries do - add_foreign_key :imports, :users, column: :user_id, on_delete: :cascade - end + add_concurrent_foreign_key :imports, :users, column: :user_id, on_delete: :cascade end def down diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md index 9e1542faed8..24570cfc07b 100644 --- a/doc/development/sidekiq_style_guide.md +++ b/doc/development/sidekiq_style_guide.md @@ -215,6 +215,85 @@ From the rails console: Feature.enable!(:disable_authorized_projects_deduplication) ``` +## Limited capacity worker + +It is possible to limit the number of concurrent running jobs for a worker class +by using the `LimitedCapacity::Worker` concern. + +The worker must implement three methods: + +- `perform_work` - the concern implements the usual `perform` method and calls +`perform_work` if there is any capacity available. +- `remaining_work_count` - number of jobs that will have work to perform. +- `max_running_jobs` - maximum number of jobs allowed to run concurrently. + +```ruby +class MyDummyWorker + include ApplicationWorker + include LimitedCapacity::Worker + + def perform_work(*args) + end + + def remaining_work_count(*args) + 5 + end + + def max_running_jobs + 25 + end +end +``` + +Additional to the regular worker, a cron worker must be defined as well to +backfill the queue with jobs. the arguments passed to `perform_with_capacity` +will be passed along to the `perform_work` method. + +```ruby +class ScheduleMyDummyCronWorker + include ApplicationWorker + include CronjobQueue + + def perform(*args) + MyDummyWorker.perform_with_capacity(*args) + end +end +``` + +### How many jobs are running? + +It will be running `max_running_jobs` at almost all times. + +The cron worker will check the remaining capacity on each execution and it +will schedule at most `max_running_jobs` jobs. Those jobs on completion will +re-enqueue themselves immediately, but not on failure. The cron worker is in +charge of replacing those failed jobs. + +### Handling errors and idempotence + +This concern disables Sidekiq retries, logs the errors, and sends the job to the +dead queue. This is done to have only one source that produces jobs and because +the retry would occupy a slot with a job that will be performed in the distant future. + +We let the cron worker enqueue new jobs, this could be seen as our retry and +back off mechanism because the job might fail again if executed immediately. +This means that for every failed job, we will be running at a lower capacity +until the cron worker fills the capacity again. If it is important for the +worker not to get a backlog, exceptions must be handled in `#perform_work` and +the job should not raise. + +The jobs are deduplicated using the `:none` strategy, but the worker is not +marked as `idempotent!`. + +### Metrics + +This concern exposes three Prometheus metrics of gauge type with the worker class +name as label: + +- `limited_capacity_worker_running_jobs` +- `limited_capacity_worker_max_running_jobs` +- `limited_capacity_worker_remaining_work_count` + ## Job urgency Jobs can have an `urgency` attribute set, which can be `:high`, diff --git a/doc/operations/incident_management/status_page.md b/doc/operations/incident_management/status_page.md index 18d5c6e10ef..e5d0ae1ddbb 100644 --- a/doc/operations/incident_management/status_page.md +++ b/doc/operations/incident_management/status_page.md @@ -4,7 +4,7 @@ group: Health 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/#designated-technical-writers --- -# GitLab Status Page **(ULTIMATE)** +# Status Page > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2479) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. @@ -25,7 +25,7 @@ Clicking an incident displays a detail page with more information about a partic valid image extension. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205166) in GitLab 13.1. - A chronological ordered list of updates to the incident. -## Set up a GitLab Status Page +## Set up a Status Page To configure a GitLab Status Page you must: diff --git a/doc/user/group/index.md b/doc/user/group/index.md index e22b14e43b9..92ca7033dcb 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -391,6 +391,51 @@ milestones. [Learn more about Epics.](epics/index.md) +## Group wikis **(PREMIUM)** + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13195) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5. +> - It's [deployed behind a feature flag](../feature_flags.md), enabled by default. +> - It's enabled on GitLab.com. +> - It's recommended for production use. +> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-group-wikis). + +CAUTION: **Warning:** +This feature might not be available to you. Check the **version history** note above for details. + +Group wikis work the same way as [project wikis](../project/wiki/index.md), please refer to those docs for details on usage. + +Group wikis can be edited by members with [Developer permissions](../../user/permissions.md#group-members-permissions) +and above. + +### Group wikis limitations + +There are a few limitations compared to project wikis: + +- Local Git access is not supported yet. +- Group wikis are not included in global search, group exports, backups, and Geo replication. +- Changes to group wikis don't show up in the group's activity feed. + +You can follow [this epic](https://gitlab.com/groups/gitlab-org/-/epics/2782) for updates. + +### Enable or disable group wikis **(CORE ONLY)** + +Group wikis are under development but ready for production use. +It is deployed behind a feature flag that is **enabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) +can opt to disable it for your instance. + +To enable it: + +```ruby +Feature.enable(:group_wikis) +``` + +To disable it: + +```ruby +Feature.disable(:group_wikis) +``` + ## Group Security Dashboard **(ULTIMATE)** Get an overview of the vulnerabilities of all the projects in a group and its subgroups. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index bfa2d2af082..92d89a303d2 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -243,6 +243,7 @@ group. | Action | Guest | Reporter | Developer | Maintainer | Owner | |--------------------------------------------------------|-------|----------|-----------|------------|-------| | Browse group | ✓ | ✓ | ✓ | ✓ | ✓ | +| View group wiki pages **(PREMIUM)** | ✓ (6) | ✓ | ✓ | ✓ | ✓ | | View Insights charts **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ | | View group epic **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ | | Create/edit group epic **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ | @@ -256,10 +257,12 @@ group. | Create/edit/delete group milestones | | | ✓ | ✓ | ✓ | | Create/edit/delete iterations | | | ✓ | ✓ | ✓ | | Enable/disable a dependency proxy **(PREMIUM)** | | | ✓ | ✓ | ✓ | +| Create and edit group wiki pages **(PREMIUM)** | | | ✓ | ✓ | ✓ | | Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ | | View/manage group-level Kubernetes cluster | | | | ✓ | ✓ | | Create subgroup | | | | ✓ (1) | ✓ | +| Delete group wiki pages **(PREMIUM)** | | | | ✓ | ✓ | | Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) | | Edit group settings | | | | | ✓ | | Manage group level CI/CD variables | | | | | ✓ | @@ -273,7 +276,7 @@ group. | Disable notification emails | | | | | ✓ | | View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ | | View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ | -| View Issue analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ | +| View Issue analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ | | View Productivity analytics **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ | | View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ | | View Billing **(FREE ONLY)** | | | | | ✓ (4) | @@ -287,6 +290,7 @@ group. - The [group level](group/index.md#default-project-creation-level). 1. Does not apply to subgroups. 1. Developers can push commits to the default branch of a new project only if the [default branch protection](group/index.md#changing-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected". +1. In addition, if your group is public or internal, all users who can see the group can also see group wiki pages. ### Subgroup permissions diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md index 60d247ccc19..4ba0b50a3cf 100644 --- a/doc/user/project/merge_requests/allow_collaboration.md +++ b/doc/user/project/merge_requests/allow_collaboration.md @@ -28,11 +28,12 @@ source project and only lasts while the merge request is open. Once enabled, upstream members will also be able to retry the pipelines and jobs of the merge request: -1. Enable the contribution while creating or editing a merge request. +1. While creating or editing a merge request, select the checkbox **Allow + commits from members who can merge to the target branch**. ![Enable contribution](img/allow_collaboration.png) -1. Once the merge request is created, you'll see that commits from members who +1. Once the merge request is created, you can see that commits from members who can merge to the target branch are allowed. ![Check that contribution is enabled](img/allow_collaboration_after_save.png) diff --git a/doc/user/project/milestones/burndown_and_burnup_charts.md b/doc/user/project/milestones/burndown_and_burnup_charts.md index 190ba2a1e7d..327a52a05ab 100644 --- a/doc/user/project/milestones/burndown_and_burnup_charts.md +++ b/doc/user/project/milestones/burndown_and_burnup_charts.md @@ -103,7 +103,7 @@ Reopened issues are considered as having been opened on the day after they were ## Burnup charts -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6903) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6903) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.5. Burnup charts show the assigned and completed work for a milestone. diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md index 1147b595c8d..8a2f62ec7a2 100644 --- a/doc/user/project/static_site_editor/index.md +++ b/doc/user/project/static_site_editor/index.md @@ -6,51 +6,43 @@ type: reference, how-to description: "The static site editor enables users to edit content on static websites without prior knowledge of the underlying templating language, site architecture or Git commands." --- -# Static Site Editor +# Static Site Editor **(CORE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10. > - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0. -> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1. -> - Markdown front matter hidden on the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216834) in GitLab 13.1. -> - Support for `*.md.erb` files [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223171) in GitLab 13.2. -> - Non-Markdown content blocks not editable on the WYSIWYG mode [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216836) in GitLab 13.3. -> - Ability to edit page front matter [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235921) in GitLab 13.4. - -DANGER: **Danger:** -In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282) -to the URL structure of the Static Site Editor. Follow the instructions in this -[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539) -to update your project with the latest changes. +> - Non-Markdown content blocks uneditable on the WYSIWYG mode [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216836) in GitLab 13.3. -Static Site Editor enables users to edit content on static websites without +Static Site Editor (SSE) enables users to edit content on static websites without prior knowledge of the underlying templating language, site architecture, or Git commands. A contributor to your project can quickly edit a Markdown page and submit the changes for review. ## Use cases -The Static Site Editors allows collaborators to submit changes to static site +The Static Site Editor allows collaborators to submit changes to static site files seamlessly. For example: -- Non-technical collaborators can easily edit a page directly from the browser; they don't need to know Git and the details of your project to be able to contribute. +- Non-technical collaborators can easily edit a page directly from the browser; + they don't need to know Git and the details of your project to be able to contribute. - Recently hired team members can quickly edit content. -- Temporary collaborators can jump from project to project and quickly edit pages instead of having to clone or fork every single project they need to submit changes to. +- Temporary collaborators can jump from project to project and quickly edit pages instead + of having to clone or fork every single project they need to submit changes to. ## Requirements - In order use the Static Site Editor feature, your project needs to be -pre-configured with the [Static Site Editor Middleman template](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman). -- The editor needs to be logged into GitLab and needs to be a member of the -project (with Developer or higher permission levels). + pre-configured with the [Static Site Editor Middleman template](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman). +- You need to be logged into GitLab and be a member of the + project (with Developer or higher permission levels). ## How it works -The Static Site Editor is in an early stage of development and only works for +The Static Site Editor is in an early stage of development and only supports Middleman sites for now. You have to use a specific site template to start using it. The project template is configured to deploy a [Middleman](https://middlemanapp.com/) static website with [GitLab Pages](../pages/index.md). -Once your website is up and running, you'll see a button **Edit this page** on +Once your website is up and running, an **Edit this page** button displays on the bottom-left corner of its pages: ![Edit this page button](img/edit_this_page_button_v12_10.png) @@ -61,61 +53,66 @@ click of a button: ![Static Site Editor](img/wysiwyg_editor_v13_3.png) -You can also edit the page's front matter both in WYSIWYG mode via the side-drawer and in Markdown -mode. - -![Editing page front matter in the Static Site Editor](img/front_matter_ui_v13_4.png) - When an editor submits their changes, in the background, GitLab automatically creates a new branch, commits their changes, and opens a merge request. The editor lands directly on the merge request, and then they can assign it to a colleague for review. -## Getting started +## Set up your project First, set up the project. Once done, you can use the Static Site Editor to -easily edit your content. - -### Set up your project - -1. To get started, create a new project from the -[Static Site Editor - Middleman](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman) -template. You can either [fork it](../repository/forking_workflow.md#creating-a-fork) -or [create a new project from a template](../../../gitlab-basics/create-project.md#built-in-templates). -1. Edit the `data/config.yml` file adding your project's path. -1. Editing the file triggers a CI/CD pipeline to deploy your project with GitLab Pages. +easily [edit your content](#edit-content). + +1. To get started, create a new project from the [Static Site Editor - Middleman](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman) + template. You can either [fork it](../repository/forking_workflow.md#creating-a-fork) + or [create a new project from a template](../../../gitlab-basics/create-project.md#built-in-templates). +1. Edit the [`data/config.yml`](#configuration-files) configuration file + to replace `<username>` and `<project-name>` with the proper values for + your project's path. This triggers a CI/CD pipeline to deploy your project + with GitLab Pages. 1. When the pipeline finishes, from your project's left-side menu, go to **Settings > Pages** to find the URL of your new website. 1. Visit your website and look at the bottom-left corner of the screen to see the new **Edit this page** button. -Anyone satisfying the [requirements](#requirements) will be able to edit the +Anyone satisfying the [requirements](#requirements) can edit the content of the pages without prior knowledge of Git or of your site's codebase. -NOTE: **Note:** -From GitLab 13.1 onward, the YAML front matter of Markdown files is hidden on the -WYSIWYG editor to avoid unintended changes. To edit it, use the Markdown editing mode, the regular -GitLab file editor, or the Web IDE. +## Edit content -NOTE: **Note:** -A new configuration file for the Static Site Editor was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/4267) -in GitLab 13.4. Beginning in 13.5, the `.gitlab/static-site-editor.yml` file will store additional -configuration options for the editor. When the functionality of the existing `data/config.yml` file -is replicated in the new configuration file, `data/config.yml` will be formally deprecated. +> - Support for modifying the default merge request title and description [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216861) in GitLab 13.5. -### Use the Static Site Editor to edit your content +After setting up your project, you can start editing content directly from the Static Site Editor. -For instance, suppose you are a recently hired technical writer at a large -company and a new feature has been added to the company product. +To edit a file: -1. You are assigned the task of updating the documentation. -1. You visit a page and see content that needs to be edited. -1. Click the **Edit this page** button on the production site. -1. The file is opened in the Static Site Editor in **WYSIWYG** mode. If you wish to edit the raw Markdown - instead, you can toggle the **Markdown** mode in the bottom-right corner. -1. You edit the file right there and click **Submit changes**. -1. A new merge request is automatically created and you assign it to your colleague for review. +1. Visit the page you want to edit. +1. Click the **Edit this page** button. +1. The file is opened in the Static Site Editor in **WYSIWYG** mode. If you + wish to edit the raw Markdown instead, you can toggle the **Markdown** mode + in the bottom-right corner. +1. When you're done, click **Submit changes...**. +1. (Optional) Adjust the default title and description of the merge request that will be submitted with your changes. +1. Click **Submit changes**. +1. A new merge request is automatically created and you can assign a colleague for review. -## Videos +### Text + +> Support for `*.md.erb` files [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223171) in GitLab 13.2. + +The Static Site Editors supports Markdown files (`.md`, `.md.erb`) for editing text. + +### Images + +> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1. + +You can add image files on the WYSIWYG mode by clicking the image icon (**{doc-image}**). +From there, link to a URL, add optional [ALT text](https://moz.com/learn/seo/alt-text), +and you're done. The link can reference images already hosted in your project, an asset hosted +externally on a content delivery network, or any other external URL. The editor renders thumbnail previews +so you can verify the correct image is included and there aren't any references to missing images. +default directory (`source/images/`). + +### Videos > - Support for embedding YouTube videos through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216642) in GitLab 13.5. @@ -126,6 +123,63 @@ The following URL/ID formats are supported: - YouTube embed URL (e.g. `https://www.youtube.com/embed/0t1DgySidms`) - YouTube video ID (e.g. `0t1DgySidms`) +### Front matter + +> - Markdown front matter hidden on the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216834) in GitLab 13.1. +> - Ability to edit page front matter [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235921) in GitLab 13.5. + +Front matter is a flexible and convenient way to define page-specific variables in data files +intended to be parsed by a static site generator. It is commonly used for setting a page's +title, layout template, or author, but can be used to pass any kind of metadata to the +generator as the page renders out to HTML. Included at the very top of each data file, the +front matter is often formatted as YAML or JSON and requires consistent and accurate syntax. + +To edit the front matter from the Static Site Editor you can use the GitLab's regular file editor, +the Web IDE, or easily update the data directly from the WYSIWYG editor: + +1. Click the **Page settings** button on the bottom-right to reveal a web form with the data you + have on the page's front matter. The form is populated with the current data: + + ![Editing page front matter in the Static Site Editor](img/front_matter_ui_v13_4.png) + +1. Update the values as you wish and close the panel. +1. When you're done, click **Submit changes...**. +1. Describe your changes (add a commit message). +1. Click **Submit changes**. +1. Click **View merge request** and GitLab will take you there. + +Note that support for adding new attributes to the page's front matter from the form is not supported +yet. You can do so by editing the file locally, through the GitLab regular file editor, or through the Web IDE. Once added, the form will load the new fields. + +## Configuration files + +The Static Site Editor uses Middleman's configuration file, `data/config.yml` +to customize the behavior of the project itself and to control the **Edit this +page** button, rendered through the file [`layout.erb`](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/-/blob/master/source/layouts/layout.erb). + +To [configure the project template to your own project](#set-up-your-project), +you must replace the `<username>` and `<project-name>` in the `data/config.yml` +file with the proper values for your project's path. + +[Other Static Site Generators](#using-other-static-site-generators) used with +the Static Site Editor may use different configuration files or approaches. + +## Using Other Static Site Generators + +Although Middleman is the only Static Site Generator currently officially supported +by the Static Site Editor, you can configure your project's build and deployment +to use a different Static Site Generator. In this case, use the Middleman layout +as an example, and follow a similar approach to properly render an **Edit this page** +button in your Static Site Generator's layout. + +## Upgrade from GitLab 12.10 to 13.0 + +In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282) +to the URL structure of the Static Site Editor. Follow the instructions in this +[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539) +to update your project with the 13.0 changes. + ## Limitations -- The Static Site Editor still cannot be quickly added to existing Middleman sites. Follow this [epic](https://gitlab.com/groups/gitlab-org/-/epics/2784) for updates. +- The Static Site Editor still cannot be quickly added to existing Middleman sites. + Follow this [epic](https://gitlab.com/groups/gitlab-org/-/epics/2784) for updates. diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md index 86d7cfa5d16..66ecbfc8707 100644 --- a/doc/user/project/wiki/index.md +++ b/doc/user/project/wiki/index.md @@ -19,6 +19,9 @@ You can create Wiki pages in the web interface or [locally using Git](#adding-and-editing-wiki-pages-locally) since every Wiki is a separate Git repository. +[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13195) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5, +**group wikis** became available. Their usage is similar to project wikis, with a few [limitations](../../group/index.md#group-wikis). + ## First time creating the Home page The first time you visit a Wiki, you will be directed to create the Home page. diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index bf5044b4832..0a486307653 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -19,6 +19,7 @@ module API end use AdminModeMiddleware + use ResponseCoercerMiddleware helpers HelperMethods @@ -188,6 +189,44 @@ module API end end + # Prior to Rack v2.1.x, returning a body of [nil] or [201] worked + # because the body was coerced to a string. However, this no longer + # works in Rack v2.1.0+. The Rack spec + # (https://github.com/rack/rack/blob/master/SPEC.rdoc#the-body-) + # says: + # + # The Body must respond to `each` and must only yield String values + # + # Because it's easy to return the wrong body type, this middleware + # will: + # + # 1. Inspect each element of the body if it is an Array. + # 2. Coerce each value to a string if necessary. + # 3. Flag a test and development error. + class ResponseCoercerMiddleware < ::Grape::Middleware::Base + def call(env) + response = super(env) + + status = response[0] + body = response[2] + + return response if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY[status] + return response unless body.is_a?(Array) + + body.map! do |part| + if part.is_a?(String) + part + else + err = ArgumentError.new("The response body should be a String, but it is of type #{part.class}") + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err) + part.to_s + end + end + + response + end + end + class AdminModeMiddleware < ::Grape::Middleware::Base def after # Use a Grape middleware since the Grape `after` blocks might run diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index b4880b205a6..ef679147c9f 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -72,6 +72,7 @@ module API post '/verify' do authenticate_runner! status 200 + body "200" end end @@ -183,6 +184,7 @@ module API service.execute.then do |result| header 'X-GitLab-Trace-Update-Interval', result.backoff status result.status + body result.status.to_s end end @@ -293,6 +295,7 @@ module API if result[:status] == :success status :created + body "201" else render_api_error!(result[:message], result[:http_status]) end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 690160cd5ac..c8aee1f3479 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -522,7 +522,7 @@ module API else header(*Gitlab::Workhorse.send_url(file.url)) status :ok - body + body "" end end diff --git a/lib/api/internal/lfs.rb b/lib/api/internal/lfs.rb index 656c00b69e1..630f0ec77a8 100644 --- a/lib/api/internal/lfs.rb +++ b/lib/api/internal/lfs.rb @@ -44,7 +44,7 @@ module API workhorse_headers = Gitlab::Workhorse.send_url(file.url) header workhorse_headers[0], workhorse_headers[1] env['api.format'] = :binary - body nil + body "" end end end diff --git a/lib/gitlab/background_migration/replace_blocked_by_links.rb b/lib/gitlab/background_migration/replace_blocked_by_links.rb new file mode 100644 index 00000000000..26626aaef79 --- /dev/null +++ b/lib/gitlab/background_migration/replace_blocked_by_links.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class ReplaceBlockedByLinks + class IssueLink < ActiveRecord::Base + self.table_name = 'issue_links' + end + + def perform(start_id, stop_id) + blocked_by_links = IssueLink.where(id: start_id..stop_id).where(link_type: 2) + + ActiveRecord::Base.transaction do + # if there is duplicit bi-directional relation (issue2 is blocked by issue1 + # and issue1 already links issue2), then we can just delete 'blocked by'. + # This should be rare as we have a pre-create check which checks if issues are + # already linked + blocked_by_links + .joins('INNER JOIN issue_links as opposite_links ON issue_links.source_id = opposite_links.target_id AND issue_links.target_id = opposite_links.source_id') + .where('opposite_links.link_type': 1) + .delete_all + + blocked_by_links.update_all('source_id=target_id,target_id=source_id,link_type=1') + end + end + end + end +end diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 11edd4f8de7..1e859385176 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -46,10 +46,6 @@ module Gitlab Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false) end - def self.child_of_child_pipeline_enabled?(project) - ::Feature.enabled?(:ci_child_of_child_pipeline, project, default_enabled: true) - end - def self.trace_overwrite? ::Feature.enabled?(:ci_trace_overwrite, type: :ops, default_enabled: false) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c66b3bc59e2..6cfb05768a2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16193,6 +16193,9 @@ msgstr "" msgid "Members|Are you sure you want to leave \"%{source}\"?" msgstr "" +msgid "Members|Are you sure you want to remove \"%{groupName}\"?" +msgstr "" + msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\"" msgstr "" @@ -16214,6 +16217,12 @@ msgstr "" msgid "Members|No expiration set" msgstr "" +msgid "Members|Remove \"%{groupName}\"" +msgstr "" + +msgid "Members|Remove group" +msgstr "" + msgid "Members|Role updated successfully." msgstr "" diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 64a262c7847..5312a0db7f5 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -390,7 +390,7 @@ RSpec.describe Admin::UsersController do describe 'POST update' do context 'when the password has changed' do - def update_password(user, password = User.random_password, password_confirmation = password) + def update_password(user, password = User.random_password, password_confirmation = password, format = :html) params = { id: user.to_param, user: { @@ -399,7 +399,7 @@ RSpec.describe Admin::UsersController do } } - post :update, params: params + post :update, params: params, format: format end context 'when admin changes their own password' do @@ -498,6 +498,23 @@ RSpec.describe Admin::UsersController do .not_to change { user.reload.encrypted_password } end end + + context 'when the update fails' do + let(:password) { User.random_password } + + before do + expect_next_instance_of(Users::UpdateService) do |service| + allow(service).to receive(:execute).and_return({ message: 'failed', status: :error }) + end + end + + it 'returns a 500 error' do + expect { update_password(admin, password, password, :json) } + .not_to change { admin.reload.password_expired? } + + expect(response).to have_gitlab_http_status(:error) + end + end end context 'admin notes' do diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index a9c196bb84b..3e78dfc3bc7 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -118,7 +118,7 @@ RSpec.describe 'Pipelines', :js do context 'when canceling' do before do find('.js-pipelines-cancel-button').click - find('.js-modal-primary-action').click + click_button 'Stop pipeline' wait_for_requests end @@ -407,7 +407,7 @@ RSpec.describe 'Pipelines', :js do context 'when canceling' do before do find('.js-pipelines-cancel-button').click - find('.js-modal-primary-action').click + click_button 'Stop pipeline' end it 'indicates that pipeline was canceled', :sidekiq_might_not_need_inline do diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js index a105b62586b..eecc54be35b 100644 --- a/spec/frontend/blob_edit/blob_bundle_spec.js +++ b/spec/frontend/blob_edit/blob_bundle_spec.js @@ -1,10 +1,25 @@ import $ from 'jquery'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import blobBundle from '~/blob_edit/blob_bundle'; +import EditorLite from '~/blob_edit/edit_blob'; + jest.mock('~/blob_edit/edit_blob'); describe('BlobBundle', () => { + it('does not load EditorLite by default', () => { + blobBundle(); + expect(EditorLite).not.toHaveBeenCalled(); + }); + + it('loads EditorLite for the edit screen', async () => { + setFixtures(`<div class="js-edit-blob-form"></div>`); + blobBundle(); + await waitForPromises(); + expect(EditorLite).toHaveBeenCalled(); + }); + describe('No Suggest Popover', () => { beforeEach(() => { setFixtures(` diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js index 97a92778f1a..040c0fbecc5 100644 --- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js +++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js @@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils'; import { GlDropdown, GlDropdownItem, GlForm, GlSprintf } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; +import httpStatusCodes from '~/lib/utils/http_status'; import axios from '~/lib/utils/axios_utils'; import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue'; import { mockRefs, mockParams, mockPostParams, mockProjectId, mockError } from '../mock_data'; @@ -11,7 +12,8 @@ jest.mock('~/lib/utils/url_utility', () => ({ redirectTo: jest.fn(), })); -const pipelinesPath = '/root/project/-/pipleines'; +const pipelinesPath = '/root/project/-/pipelines'; +const configVariablesPath = '/root/project/-/pipelines/config_variables'; const postResponse = { id: 1 }; describe('Pipeline New Form', () => { @@ -28,6 +30,7 @@ describe('Pipeline New Form', () => { const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]'); const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]'); const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]'); + const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]'); const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]'); const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]'); const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf); @@ -39,6 +42,7 @@ describe('Pipeline New Form', () => { propsData: { projectId: mockProjectId, pipelinesPath, + configVariablesPath, refs: mockRefs, defaultBranch: 'master', settingsLink: '', @@ -55,6 +59,7 @@ describe('Pipeline New Form', () => { beforeEach(() => { mock = new MockAdapter(axios); + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {}); }); afterEach(() => { @@ -66,7 +71,7 @@ describe('Pipeline New Form', () => { describe('Dropdown with branches and tags', () => { beforeEach(() => { - mock.onPost(pipelinesPath).reply(200, postResponse); + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse); }); it('displays dropdown with all branches and tags', () => { @@ -87,17 +92,27 @@ describe('Pipeline New Form', () => { }); describe('Form', () => { - beforeEach(() => { + beforeEach(async () => { createComponent('', mockParams, mount); - mock.onPost(pipelinesPath).reply(200, postResponse); + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse); + + await waitForPromises(); }); + it('displays the correct values for the provided query params', async () => { expect(findDropdown().props('text')).toBe('tag-1'); + expect(findVariableRows()).toHaveLength(3); + }); - await wrapper.vm.$nextTick(); + it('displays a variable from provided query params', () => { + expect(findKeyInputs().at(0).element.value).toBe('test_var'); + expect(findValueInputs().at(0).element.value).toBe('test_var_val'); + }); - expect(findVariableRows()).toHaveLength(3); + it('displays an empty variable for the user to fill out', async () => { + expect(findKeyInputs().at(2).element.value).toBe(''); + expect(findValueInputs().at(2).element.value).toBe(''); }); it('does not display remove icon for last row', () => { @@ -124,13 +139,143 @@ describe('Pipeline New Form', () => { }); it('creates blank variable on input change event', async () => { - findKeyInputs() - .at(2) - .trigger('change'); + const input = findKeyInputs().at(2); + input.element.value = 'test_var_2'; + input.trigger('change'); await wrapper.vm.$nextTick(); expect(findVariableRows()).toHaveLength(4); + expect(findKeyInputs().at(3).element.value).toBe(''); + expect(findValueInputs().at(3).element.value).toBe(''); + }); + + describe('when the form has been modified', () => { + const selectRef = i => + findDropdownItems() + .at(i) + .vm.$emit('click'); + + beforeEach(async () => { + const input = findKeyInputs().at(0); + input.element.value = 'test_var_2'; + input.trigger('change'); + + findRemoveIcons() + .at(1) + .trigger('click'); + + await wrapper.vm.$nextTick(); + }); + + it('form values are restored when the ref changes', async () => { + expect(findVariableRows()).toHaveLength(2); + + selectRef(1); + await waitForPromises(); + + expect(findVariableRows()).toHaveLength(3); + expect(findKeyInputs().at(0).element.value).toBe('test_var'); + }); + + it('form values are restored again when the ref is reverted', async () => { + selectRef(1); + await waitForPromises(); + + selectRef(2); + await waitForPromises(); + + expect(findVariableRows()).toHaveLength(2); + expect(findKeyInputs().at(0).element.value).toBe('test_var_2'); + }); + }); + }); + + describe('when feature flag new_pipeline_form_prefilled_vars is enabled', () => { + let origGon; + + const mockYmlKey = 'yml_var'; + const mockYmlValue = 'yml_var_val'; + const mockYmlDesc = 'A var from yml.'; + + beforeAll(() => { + origGon = window.gon; + window.gon = { features: { newPipelineFormPrefilledVars: true } }; + }); + + afterAll(() => { + window.gon = origGon; + }); + + describe('when yml defines a variable with description', () => { + beforeEach(async () => { + createComponent('', mockParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlValue, + description: mockYmlDesc, + }, + }); + + await waitForPromises(); + }); + + it('displays all the variables', async () => { + expect(findVariableRows()).toHaveLength(4); + }); + + it('displays a variable from yml', () => { + expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey); + expect(findValueInputs().at(0).element.value).toBe(mockYmlValue); + }); + + it('displays a variable from provided query params', () => { + expect(findKeyInputs().at(1).element.value).toBe('test_var'); + expect(findValueInputs().at(1).element.value).toBe('test_var_val'); + }); + + it('adds a description to the first variable from yml', () => { + expect( + findVariableRows() + .at(0) + .text(), + ).toContain(mockYmlDesc); + }); + + it('removes the description when a variable key changes', async () => { + findKeyInputs().at(0).element.value = 'yml_var_modified'; + findKeyInputs() + .at(0) + .trigger('change'); + + await wrapper.vm.$nextTick(); + + expect( + findVariableRows() + .at(0) + .text(), + ).not.toContain(mockYmlDesc); + }); + }); + + describe('when yml defines a variable without description', () => { + beforeEach(async () => { + createComponent('', mockParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlValue, + description: null, + }, + }); + + await waitForPromises(); + }); + + it('displays all the variables', async () => { + expect(findVariableRows()).toHaveLength(3); + }); }); }); @@ -138,7 +283,7 @@ describe('Pipeline New Form', () => { beforeEach(() => { createComponent(); - mock.onPost(pipelinesPath).reply(400, mockError); + mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError); findForm().vm.$emit('submit', dummySubmitEvent); diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js index 58ed92298bf..78efcb6e695 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js @@ -35,9 +35,7 @@ describe('MrWidgetAuthorTime', () => { }); it('renders provided time', () => { - expect(vm.$el.querySelector('time').getAttribute('data-original-title')).toEqual( - '2017-03-23T23:02:00.807Z', - ); + expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z'); expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago'); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js index 1921599ae95..9b51e8583ba 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -212,8 +212,6 @@ describe('MRWidgetMerged', () => { }); it('should use mergedEvent mergedAt as tooltip title', () => { - expect(vm.$el.querySelector('time').getAttribute('data-original-title')).toBe( - 'Jan 24, 2018 1:02pm GMT+0000', - ); + expect(vm.$el.querySelector('time').getAttribute('title')).toBe('Jan 24, 2018 1:02pm GMT+0000'); }); }); diff --git a/spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js b/spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js new file mode 100644 index 00000000000..84fe1c51773 --- /dev/null +++ b/spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js @@ -0,0 +1,64 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { GlButton } from '@gitlab/ui'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import RemoveGroupLinkButton from '~/vue_shared/components/members/action_buttons/remove_group_link_button.vue'; +import { group } from '../mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('RemoveGroupLinkButton', () => { + let wrapper; + + const actions = { + showRemoveGroupLinkModal: jest.fn(), + }; + + const createStore = () => { + return new Vuex.Store({ + actions, + }); + }; + + const createComponent = () => { + wrapper = mount(RemoveGroupLinkButton, { + localVue, + store: createStore(), + propsData: { + groupLink: group, + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + const findButton = () => wrapper.find(GlButton); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('displays a tooltip', () => { + const button = findButton(); + + expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined(); + expect(button.attributes('title')).toBe('Remove group'); + }); + + it('sets `aria-label` attribute', () => { + expect(findButton().attributes('aria-label')).toBe('Remove group'); + }); + + it('calls Vuex action to open remove group link modal when clicked', () => { + findButton().trigger('click'); + + expect(actions.showRemoveGroupLinkModal).toHaveBeenCalledWith(expect.any(Object), group); + }); +}); diff --git a/spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js b/spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js new file mode 100644 index 00000000000..84da051792d --- /dev/null +++ b/spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js @@ -0,0 +1,106 @@ +import { mount, createLocalVue, createWrapper } from '@vue/test-utils'; +import { GlModal, GlForm } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { within } from '@testing-library/dom'; +import Vuex from 'vuex'; +import RemoveGroupLinkModal from '~/vue_shared/components/members/modals/remove_group_link_modal.vue'; +import { REMOVE_GROUP_LINK_MODAL_ID } from '~/vue_shared/components/members/constants'; +import { group } from '../mock_data'; + +jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('RemoveGroupLinkModal', () => { + let wrapper; + + const actions = { + hideRemoveGroupLinkModal: jest.fn(), + }; + + const createStore = (state = {}) => { + return new Vuex.Store({ + state: { + memberPath: '/groups/foo-bar/-/group_links/:id', + groupLinkToRemove: group, + removeGroupLinkModalVisible: true, + ...state, + }, + actions, + }); + }; + + const createComponent = state => { + wrapper = mount(RemoveGroupLinkModal, { + localVue, + store: createStore(state), + attrs: { + static: true, + }, + }); + }; + + const findModal = () => wrapper.find(GlModal); + const findForm = () => findModal().find(GlForm); + const getByText = (text, options) => + createWrapper(within(findModal().element).getByText(text, options)); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when modal is open', () => { + beforeEach(async () => { + createComponent(); + await nextTick(); + }); + + it('sets modal ID', () => { + expect(findModal().props('modalId')).toBe(REMOVE_GROUP_LINK_MODAL_ID); + }); + + it('displays modal title', () => { + expect(getByText(`Remove "${group.sharedWithGroup.fullName}"`).exists()).toBe(true); + }); + + it('displays modal body', () => { + expect( + getByText(`Are you sure you want to remove "${group.sharedWithGroup.fullName}"?`).exists(), + ).toBe(true); + }); + + it('displays form with correct action and inputs', () => { + const form = findForm(); + + expect(form.attributes('action')).toBe(`/groups/foo-bar/-/group_links/${group.id}`); + expect(form.find('input[name="_method"]').attributes('value')).toBe('delete'); + expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe( + 'mock-csrf-token', + ); + }); + + it('submits the form when "Remove group" button is clicked', () => { + const submitSpy = jest.spyOn(findForm().element, 'submit'); + + getByText('Remove group').trigger('click'); + + expect(submitSpy).toHaveBeenCalled(); + + submitSpy.mockRestore(); + }); + + it('calls `hideRemoveGroupLinkModal` action when modal is closed', () => { + getByText('Cancel').trigger('click'); + + expect(actions.hideRemoveGroupLinkModal).toHaveBeenCalled(); + }); + }); + + it('modal does not show when `removeGroupLinkModalVisible` is `false`', () => { + createComponent({ removeGroupLinkModalVisible: false }); + + expect(findModal().vm.$attrs.visible).toBe(false); + }); +}); diff --git a/spec/frontend/vue_shared/components/members/table/members_table_spec.js b/spec/frontend/vue_shared/components/members/table/members_table_spec.js index ec3b75b82ea..20c1c26d2ee 100644 --- a/spec/frontend/vue_shared/components/members/table/members_table_spec.js +++ b/spec/frontend/vue_shared/components/members/table/members_table_spec.js @@ -43,6 +43,7 @@ describe('MemberList', () => { 'created-at', 'member-action-buttons', 'role-dropdown', + 'remove-group-link-modal', ], }); }; diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js index 4342f5e2105..f1c3e8a1ddc 100644 --- a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -11,15 +11,14 @@ describe('toggleSidebar', () => { }); }); - it('should render << when collapsed', () => { - expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true); + it('should render the "chevron-double-lg-left" icon when collapsed', () => { + expect(vm.$el.querySelector('[data-testid="chevron-double-lg-left-icon"]')).not.toBeNull(); }); - it('should render >> when collapsed', () => { + it('should render the "chevron-double-lg-right" icon when expanded', async () => { vm.collapsed = false; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true); - }); + await Vue.nextTick(); + expect(vm.$el.querySelector('[data-testid="chevron-double-lg-right-icon"]')).not.toBeNull(); }); it('should emit toggle event when button clicked', () => { diff --git a/spec/frontend/vuex_shared/modules/members/actions_spec.js b/spec/frontend/vuex_shared/modules/members/actions_spec.js index aa04114d3e3..833bd4cc175 100644 --- a/spec/frontend/vuex_shared/modules/members/actions_spec.js +++ b/spec/frontend/vuex_shared/modules/members/actions_spec.js @@ -1,11 +1,15 @@ import { noop } from 'lodash'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { members } from 'jest/vue_shared/components/members/mock_data'; +import { members, group } from 'jest/vue_shared/components/members/mock_data'; import testAction from 'helpers/vuex_action_helper'; import httpStatusCodes from '~/lib/utils/http_status'; import * as types from '~/vuex_shared/modules/members/mutation_types'; -import { updateMemberRole } from '~/vuex_shared/modules/members/actions'; +import { + updateMemberRole, + showRemoveGroupLinkModal, + hideRemoveGroupLinkModal, +} from '~/vuex_shared/modules/members/actions'; describe('Vuex members actions', () => { let mock; @@ -30,6 +34,8 @@ describe('Vuex members actions', () => { members, memberPath: '/groups/foo-bar/-/group_members/:id', requestFormatter: noop, + removeGroupLinkModalVisible: false, + groupLinkToRemove: null, }; describe('successful request', () => { @@ -73,4 +79,32 @@ describe('Vuex members actions', () => { }); }); }); + + describe('Group Link Modal', () => { + const state = { + removeGroupLinkModalVisible: false, + groupLinkToRemove: null, + }; + + describe('showRemoveGroupLinkModal', () => { + it(`commits ${types.SHOW_REMOVE_GROUP_LINK_MODAL} mutation`, () => { + testAction(showRemoveGroupLinkModal, group, state, [ + { + type: types.SHOW_REMOVE_GROUP_LINK_MODAL, + payload: group, + }, + ]); + }); + }); + + describe('hideRemoveGroupLinkModal', () => { + it(`commits ${types.HIDE_REMOVE_GROUP_LINK_MODAL} mutation`, () => { + testAction(hideRemoveGroupLinkModal, group, state, [ + { + type: types.HIDE_REMOVE_GROUP_LINK_MODAL, + }, + ]); + }); + }); + }); }); diff --git a/spec/frontend/vuex_shared/modules/members/mutations_spec.js b/spec/frontend/vuex_shared/modules/members/mutations_spec.js index 258af6a54ab..7338b19cfc9 100644 --- a/spec/frontend/vuex_shared/modules/members/mutations_spec.js +++ b/spec/frontend/vuex_shared/modules/members/mutations_spec.js @@ -1,4 +1,4 @@ -import { members } from 'jest/vue_shared/components/members/mock_data'; +import { members, group } from 'jest/vue_shared/components/members/mock_data'; import mutations from '~/vuex_shared/modules/members/mutations'; import * as types from '~/vuex_shared/modules/members/mutation_types'; @@ -59,4 +59,32 @@ describe('Vuex members mutations', () => { expect(state.errorMessage).toBe(''); }); }); + + describe(types.SHOW_REMOVE_GROUP_LINK_MODAL, () => { + it('sets `removeGroupLinkModalVisible` and `groupLinkToRemove`', () => { + const state = { + removeGroupLinkModalVisible: false, + groupLinkToRemove: null, + }; + + mutations[types.SHOW_REMOVE_GROUP_LINK_MODAL](state, group); + + expect(state).toEqual({ + removeGroupLinkModalVisible: true, + groupLinkToRemove: group, + }); + }); + }); + + describe(types.HIDE_REMOVE_GROUP_LINK_MODAL, () => { + it('sets `removeGroupLinkModalVisible` to `false`', () => { + const state = { + removeGroupLinkModalVisible: false, + }; + + mutations[types.HIDE_REMOVE_GROUP_LINK_MODAL](state); + + expect(state.removeGroupLinkModalVisible).toBe(false); + }); + }); }); diff --git a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb new file mode 100644 index 00000000000..1eb6f363d5b --- /dev/null +++ b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::RunnerPlatformsResolver do + include GraphqlHelpers + + describe '#resolve' do + subject(:resolve_subject) { resolve(described_class) } + + it 'returns all possible runner platforms' do + expect(resolve_subject).to include( + hash_including(name: :linux), hash_including(name: :osx), + hash_including(name: :windows), hash_including(name: :docker), + hash_including(name: :kubernetes) + ) + end + end +end diff --git a/spec/graphql/types/ci/runner_architecture_type_spec.rb b/spec/graphql/types/ci/runner_architecture_type_spec.rb new file mode 100644 index 00000000000..527adef8cf9 --- /dev/null +++ b/spec/graphql/types/ci/runner_architecture_type_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::RunnerArchitectureType do + specify { expect(described_class.graphql_name).to eq('RunnerArchitecture') } + + it 'exposes the expected fields' do + expected_fields = %i[ + name + download_location + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/ci/runner_platform_type_spec.rb b/spec/graphql/types/ci/runner_platform_type_spec.rb new file mode 100644 index 00000000000..66b83f607d0 --- /dev/null +++ b/spec/graphql/types/ci/runner_platform_type_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::RunnerPlatformType do + specify { expect(described_class.graphql_name).to eq('RunnerPlatform') } + + it 'exposes the expected fields' do + expected_fields = %i[ + name + human_readable_name + architectures + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 11f780a4f3f..1d9ca8323f8 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -22,6 +22,7 @@ RSpec.describe GitlabSchema.types['Query'] do users issue instance_statistics_measurements + runner_platforms ] expect(described_class).to have_graphql_fields(*expected_fields).at_least @@ -67,8 +68,16 @@ RSpec.describe GitlabSchema.types['Query'] do describe 'instance_statistics_measurements field' do subject { described_class.fields['instanceStatisticsMeasurements'] } - it 'returns issue' do + it 'returns instance statistics measurements' do is_expected.to have_graphql_type(Types::Admin::Analytics::InstanceStatistics::MeasurementType.connection_type) end end + + describe 'runner_platforms field' do + subject { described_class.fields['runnerPlatforms'] } + + it 'returns runner platforms' do + is_expected.to have_graphql_type(Types::Ci::RunnerPlatformType.connection_type) + end + end end diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb index abad6c9264a..bb92445cb19 100644 --- a/spec/helpers/groups/group_members_helper_spec.rb +++ b/spec/helpers/groups/group_members_helper_spec.rb @@ -88,9 +88,14 @@ RSpec.describe Groups::GroupMembersHelper do describe '#linked_groups_list_data_attributes' do include_context 'group_group_link' + before do + allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id') + end + it 'returns expected hash' do expect(helper.linked_groups_list_data_attributes(shared_group)).to include({ members: helper.linked_groups_data_json(shared_group.shared_with_group_links), + member_path: '/groups/foo-bar/-/group_links/:id', group_id: shared_group.id }) end diff --git a/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb b/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb new file mode 100644 index 00000000000..fa4f2d1fd88 --- /dev/null +++ b/spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::ReplaceBlockedByLinks, schema: 20201015073808 do + let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') } + let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') } + let(:issue2) { table(:issues).create!(project_id: project.id, title: 'b') } + let(:issue3) { table(:issues).create!(project_id: project.id, title: 'c') } + let(:issue_links) { table(:issue_links) } + let!(:blocks_link) { issue_links.create!(source_id: issue1.id, target_id: issue2.id, link_type: 1) } + let!(:bidirectional_link) { issue_links.create!(source_id: issue2.id, target_id: issue1.id, link_type: 2) } + let!(:blocked_link) { issue_links.create!(source_id: issue1.id, target_id: issue3.id, link_type: 2) } + + subject { described_class.new.perform(issue_links.minimum(:id), issue_links.maximum(:id)) } + + it 'deletes issue links where opposite relation already exists' do + expect { subject }.to change { issue_links.count }.by(-1) + end + + it 'ignores issue links other than blocked_by' do + subject + + expect(blocks_link.reload.link_type).to eq(1) + end + + it 'updates blocked_by issue links' do + subject + + link = blocked_link.reload + expect(link.link_type).to eq(1) + expect(link.source_id).to eq(issue3.id) + expect(link.target_id).to eq(issue1.id) + end +end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index af6146ea93c..7bac041cd65 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -142,8 +142,8 @@ RSpec.describe Gitlab::Middleware::Go do response = go expect(response[0]).to eq(403) - expect(response[1]['Content-Length']).to eq('0') - expect(response[2].body).to eq(['']) + expect(response[1]['Content-Length']).to be_nil + expect(response[2]).to eq(['']) end end end @@ -187,10 +187,11 @@ RSpec.describe Gitlab::Middleware::Go do it 'returns 404' do response = go + expect(response[0]).to eq(404) expect(response[1]['Content-Type']).to eq('text/html') expected_body = %{<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>} - expect(response[2].body).to eq([expected_body]) + expect(response[2]).to eq([expected_body]) end end @@ -262,7 +263,7 @@ RSpec.describe Gitlab::Middleware::Go do expect(response[0]).to eq(200) expect(response[1]['Content-Type']).to eq('text/html') expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}" /></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>} - expect(response[2].body).to eq([expected_body]) + expect(response[2]).to eq([expected_body]) end end end diff --git a/spec/lib/gitlab/middleware/same_site_cookies_spec.rb b/spec/lib/gitlab/middleware/same_site_cookies_spec.rb index 2d1a9b2eee2..18342fd78ac 100644 --- a/spec/lib/gitlab/middleware/same_site_cookies_spec.rb +++ b/spec/lib/gitlab/middleware/same_site_cookies_spec.rb @@ -60,12 +60,12 @@ RSpec.describe Gitlab::Middleware::SameSiteCookies do end context 'with no cookies' do - let(:cookies) { nil } + let(:cookies) { "" } it 'does not add headers' do response = do_request - expect(response['Set-Cookie']).to be_nil + expect(response['Set-Cookie']).to eq("") end end diff --git a/spec/migrations/schedule_blocked_by_links_replacement_spec.rb b/spec/migrations/schedule_blocked_by_links_replacement_spec.rb new file mode 100644 index 00000000000..36610507921 --- /dev/null +++ b/spec/migrations/schedule_blocked_by_links_replacement_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20201015073808_schedule_blocked_by_links_replacement') + +RSpec.describe ScheduleBlockedByLinksReplacement do + let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') } + let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') } + let(:issue2) { table(:issues).create!(project_id: project.id, title: 'b') } + let(:issue3) { table(:issues).create!(project_id: project.id, title: 'c') } + let!(:issue_links) do + [ + table(:issue_links).create!(source_id: issue1.id, target_id: issue2.id, link_type: 1), + table(:issue_links).create!(source_id: issue2.id, target_id: issue1.id, link_type: 2), + table(:issue_links).create!(source_id: issue1.id, target_id: issue3.id, link_type: 2) + ] + end + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + end + + it 'schedules jobs for blocked_by links' do + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration( + 2.minutes, issue_links[1].id, issue_links[1].id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration( + 4.minutes, issue_links[2].id, issue_links[2].id) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end +end diff --git a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb new file mode 100644 index 00000000000..6f3f97fe846 --- /dev/null +++ b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::APIGuard::ResponseCoercerMiddleware do + using RSpec::Parameterized::TableSyntax + + it 'is loaded' do + expect(API::API.middleware).to include([:use, described_class]) + end + + describe '#call' do + let(:app) do + Class.new(API::API) + end + + [ + nil, 201, 10.5, "test" + ].each do |val| + it 'returns a String body' do + app.get 'bodytest' do + status 200 + env['api.format'] = :binary + body val + end + + unless val.is_a?(String) + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(ArgumentError)) + end + + get api('/bodytest') + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(val.to_s) + end + end + + [100, 204, 304].each do |status| + it 'allows nil body' do + app.get 'statustest' do + status status + env['api.format'] = :binary + body nil + end + + expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception) + + get api('/statustest') + + expect(response.status).to eq(status) + expect(response.body).to eq('') + end + end + end +end diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb index 0f930c07373..03cea4074bf 100644 --- a/spec/services/ci/create_downstream_pipeline_service_spec.rb +++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb @@ -325,20 +325,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do expect(bridge.reload).to be_success end - - context 'when FF ci_child_of_child_pipeline is disabled' do - before do - stub_feature_flags(ci_child_of_child_pipeline: false) - end - - it 'does not create a further child pipeline' do - expect { service.execute(bridge) } - .not_to change { Ci::Pipeline.count } - - expect(bridge.reload).to be_failed - expect(bridge.failure_reason).to eq 'bridge_pipeline_is_child_pipeline' - end - end end context 'when upstream pipeline has a parent pipeline, which has a parent pipeline' do diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb index e7f7ae84621..11b50961e9e 100644 --- a/spec/workers/build_finished_worker_spec.rb +++ b/spec/workers/build_finished_worker_spec.rb @@ -20,10 +20,10 @@ RSpec.describe BuildFinishedWorker do expect_any_instance_of(BuildTraceSectionsWorker).to receive(:perform) expect_any_instance_of(BuildCoverageWorker).to receive(:perform) expect(BuildHooksWorker).to receive(:perform_async) - expect(ArchiveTraceWorker).to receive(:perform_async) expect(ExpirePipelineCacheWorker).to receive(:perform_async) expect(ChatNotificationWorker).not_to receive(:perform_async) expect(Ci::BuildReportResultWorker).not_to receive(:perform) + expect(ArchiveTraceWorker).to receive(:perform_in) subject end |