summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-16 15:08:46 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-16 15:08:46 +0000
commit3940f59a61a749824aa4425ebdcaed6f3ed601f2 (patch)
treeebe2ffc65d0d7e7c6cd742e10c243d0cfbbb9e55
parent3775eba7c1d41443461e3abcdab2867bbc4636ae (diff)
downloadgitlab-ce-3940f59a61a749824aa4425ebdcaed6f3ed601f2.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue5
-rw-r--r--app/assets/javascripts/behaviors/collapse_sidebar_on_window_resize.js11
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js21
-rw-r--r--app/assets/javascripts/merge_request_tabs.js14
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue203
-rw-r--r--app/assets/javascripts/pipeline_new/index.js2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue36
-rw-r--r--app/assets/javascripts/vue_shared/components/members/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue69
-rw-r--r--app/assets/javascripts/vue_shared/components/members/table/members_table.vue119
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue41
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/actions.js8
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/mutation_types.js3
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/mutations.js7
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/state.js2
-rw-r--r--app/assets/stylesheets/fontawesome_custom.scss8
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/drafts_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/graphql/mutations/issues/common_mutation_arguments.rb2
-rw-r--r--app/graphql/resolvers/ci/runner_platforms_resolver.rb30
-rw-r--r--app/graphql/types/ci/runner_architecture_type.rb15
-rw-r--r--app/graphql/types/ci/runner_platform_type.rb17
-rw-r--r--app/graphql/types/query_type.rb4
-rw-r--r--app/helpers/groups/group_members_helper.rb1
-rw-r--r--app/models/ci/pipeline.rb13
-rw-r--r--app/services/ci/create_downstream_pipeline_service.rb13
-rw-r--r--app/views/projects/pipelines/new.html.haml10
-rw-r--r--app/workers/build_finished_worker.rb17
-rw-r--r--changelogs/unreleased/267973-replace-fa-angle-double-left-with-gitlab-svg-chevron-double-lg-lef.yml5
-rw-r--r--changelogs/unreleased/issue_237977-2.yml5
-rw-r--r--changelogs/unreleased/migrate_blocked_by.yml5
-rw-r--r--changelogs/unreleased/sh-update-rack-2-1-4.yml5
-rw-r--r--config/feature_flags/development/new_pipeline_form_prefilled_vars.yml (renamed from config/feature_flags/development/ci_child_of_child_pipeline.yml)10
-rw-r--r--db/post_migrate/20201014142521_schedule_sync_blocking_issues_count.rb46
-rw-r--r--db/post_migrate/20201015073808_schedule_blocked_by_links_replacement.rb30
-rw-r--r--db/schema_migrations/202010141425211
-rw-r--r--db/schema_migrations/202010150738081
-rw-r--r--doc/administration/reference_architectures/1k_users.md25
-rw-r--r--doc/api/api_resources.md3
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql164
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json468
-rw-r--r--doc/api/graphql/reference/index.md14
-rw-r--r--doc/api/group_wikis.md4
-rw-r--r--doc/api/wikis.md2
-rw-r--r--doc/ci/parent_child_pipelines.md25
-rw-r--r--doc/development/documentation/site_architecture/release_process.md5
-rw-r--r--doc/development/migration_style_guide.md15
-rw-r--r--doc/development/sidekiq_style_guide.md79
-rw-r--r--doc/operations/incident_management/status_page.md4
-rw-r--r--doc/user/group/index.md45
-rw-r--r--doc/user/permissions.md6
-rw-r--r--doc/user/project/merge_requests/allow_collaboration.md5
-rw-r--r--doc/user/project/milestones/burndown_and_burnup_charts.md2
-rw-r--r--doc/user/project/static_site_editor/index.md172
-rw-r--r--doc/user/project/wiki/index.md3
-rw-r--r--lib/api/api_guard.rb39
-rw-r--r--lib/api/ci/runner.rb3
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/internal/lfs.rb2
-rw-r--r--lib/gitlab/background_migration/replace_blocked_by_links.rb29
-rw-r--r--lib/gitlab/ci/features.rb4
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/controllers/admin/users_controller_spec.rb21
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb4
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js15
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js165
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_author_time_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/members/action_buttons/remove_group_link_button_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/members/modals/remove_group_link_modal_spec.js106
-rw-r--r--spec/frontend/vue_shared/components/members/table/members_table_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js11
-rw-r--r--spec/frontend/vuex_shared/modules/members/actions_spec.js38
-rw-r--r--spec/frontend/vuex_shared/modules/members/mutations_spec.js30
-rw-r--r--spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb19
-rw-r--r--spec/graphql/types/ci/runner_architecture_type_spec.rb16
-rw-r--r--spec/graphql/types/ci/runner_platform_type_spec.rb17
-rw-r--r--spec/graphql/types/query_type_spec.rb11
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb5
-rw-r--r--spec/lib/gitlab/background_migration/replace_blocked_by_links_spec.rb36
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb9
-rw-r--r--spec/lib/gitlab/middleware/same_site_cookies_spec.rb4
-rw-r--r--spec/migrations/schedule_blocked_by_links_replacement_spec.rb37
-rw-r--r--spec/requests/api/api_guard/response_coercer_middleware_spec.rb55
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb14
-rw-r--r--spec/workers/build_finished_worker_spec.rb2
95 files changed, 2281 insertions, 401 deletions
diff --git a/Gemfile b/Gemfile
index 4f4cf0753c3..51f9d36cef9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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