Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml8
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue36
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_button.vue8
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/constants.js7
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/state.js2
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js689
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js135
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js44
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js42
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/index.js891
-rw-r--r--app/controllers/clusters/clusters_controller.rb1
-rw-r--r--app/controllers/projects/pages_controller.rb2
-rw-r--r--app/finders/merge_requests_finder.rb4
-rw-r--r--app/models/clusters/providers/aws.rb2
-rw-r--r--app/services/clusters/aws/provision_service.rb1
-rw-r--r--app/services/pages/delete_service.rb7
-rw-r--r--app/workers/all_queues.yml12
-rw-r--r--app/workers/pages_remove_worker.rb17
-rw-r--r--changelogs/unreleased/231463-gitlab-ci-aws-eks-integration-kubernetes-version-update.yml5
-rw-r--r--changelogs/unreleased/djensen-finish-renaming-analytics-workspace.yml5
-rw-r--r--changelogs/unreleased/id-enable-merge-request-draft-filter.yml5
-rw-r--r--config/feature_flags/development/async_pages_removal.yml7
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20200818052219_add_kubernetes_version_to_cluster_providers_aws.rb28
-rw-r--r--db/schema_migrations/202008180522191
-rw-r--r--db/structure.sql4
-rw-r--r--doc/user/analytics/index.md6
-rw-r--r--lib/tasks/gitlab/sidekiq.rake2
-rw-r--r--locale/gitlab.pot10
-rw-r--r--package.json2
-rw-r--r--rubocop/cop/rspec/timecop_freeze.rb41
-rw-r--r--spec/factories/clusters/providers/aws.rb1
-rw-r--r--spec/features/projects/pages_spec.rb4
-rw-r--r--spec/features/projects_spec.rb2
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js10
-rw-r--r--spec/frontend/clusters/components/uninstall_application_button_spec.js17
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js1
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js3
-rw-r--r--spec/lib/backup/repository_spec.rb2
-rw-r--r--spec/requests/api/pages/pages_spec.rb4
-rw-r--r--spec/rubocop/cop/rspec/timecop_freeze_spec.rb52
-rw-r--r--spec/services/clusters/aws/provision_service_spec.rb1
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb2
-rw-r--r--spec/services/merge_requests/build_service_spec.rb4
-rw-r--r--spec/services/pages/delete_services_spec.rb46
-rw-r--r--spec/services/projects/fork_service_spec.rb38
-rw-r--r--spec/support/helpers/project_forks_helper.rb16
-rw-r--r--spec/workers/pages_remove_worker_spec.rb26
-rw-r--r--vendor/aws/cloudformation/eks_cluster.yaml7
-rw-r--r--yarn.lock8
51 files changed, 1285 insertions, 986 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index a00ca6199c2..3fce90ee723 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -282,6 +282,14 @@ Cop/ActiveRecordAssociationReload:
Gitlab/AvoidFeatureGet:
Enabled: true
+RSpec/TimecopFreeze:
+ Enabled: false
+ AutoCorrect: true
+ Include:
+ - 'spec/**/*.rb'
+ - 'ee/spec/**/*.rb'
+ - 'qa/spec/**/*.rb'
+
Naming/PredicateName:
Enabled: true
Exclude:
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index c86db28515f..3b285e3bc31 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -1,9 +1,8 @@
<script>
-import { GlLink, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlModalDirective, GlSprintf, GlButton } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
-import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue';
import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
@@ -12,7 +11,7 @@ import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
export default {
components: {
- loadingButton,
+ GlButton,
identicon,
GlLink,
GlSprintf,
@@ -390,16 +389,18 @@ export default {
</div>
<template v-if="updateAvailable || updateFailed || isUpdating">
<template v-if="updatingNeedsConfirmation">
- <loading-button
+ <gl-button
v-gl-modal-directive="updateModalId"
- class="btn btn-primary js-cluster-application-update-button mt-2"
+ class="js-cluster-application-update-button mt-2"
+ variant="info"
+ category="primary"
:loading="isUpdating"
:disabled="isUpdating"
- :label="updateButtonLabel"
data-qa-selector="update_button_with_confirmation"
:data-qa-application="id"
- />
-
+ >
+ {{ updateButtonLabel }}
+ </gl-button>
<update-application-confirmation-modal
:application="id"
:application-title="title"
@@ -407,16 +408,19 @@ export default {
/>
</template>
- <loading-button
+ <gl-button
v-else
- class="btn btn-primary js-cluster-application-update-button mt-2"
+ class="js-cluster-application-update-button mt-2"
+ variant="info"
+ category="primary"
:loading="isUpdating"
:disabled="isUpdating"
- :label="updateButtonLabel"
data-qa-selector="update_button"
:data-qa-application="id"
@click="updateConfirmed"
- />
+ >
+ {{ updateButtonLabel }}
+ </gl-button>
</template>
</div>
</div>
@@ -431,16 +435,18 @@ export default {
}}</a>
</div>
<div class="btn-group table-action-buttons">
- <loading-button
+ <gl-button
v-if="displayInstallButton"
:loading="installButtonLoading"
:disabled="disabled || installButtonDisabled"
- :label="installButtonLabel"
class="js-cluster-application-install-button"
+ variant="default"
data-qa-selector="install_button"
:data-qa-application="id"
@click="installClicked"
- />
+ >
+ {{ installButtonLabel }}
+ </gl-button>
<uninstall-application-button
v-if="displayUninstallButton"
v-gl-modal-directive="uninstallModalId"
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
index 8465312d84d..73191d6d84d 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_button.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
@@ -1,5 +1,5 @@
<script>
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { GlButton } from '@gitlab/ui';
import { APPLICATION_STATUS } from '~/clusters/constants';
import { __ } from '~/locale';
@@ -7,7 +7,7 @@ const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
- LoadingButton,
+ GlButton,
},
props: {
status: {
@@ -30,5 +30,7 @@ export default {
</script>
<template>
- <loading-button :label="label" :disabled="disabled" :loading="loading" />
+ <gl-button :disabled="disabled" variant="default" :loading="loading">
+ {{ label }}
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/constants.js b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
index 27547ed8449..471d6e1f0aa 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/constants.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
@@ -1 +1,6 @@
-export const KUBERNETES_VERSIONS = [{ name: '1.14', value: '1.14' }];
+export const KUBERNETES_VERSIONS = [
+ { name: '1.14', value: '1.14' },
+ { name: '1.15', value: '1.15' },
+ { name: '1.16', value: '1.16', default: true },
+ { name: '1.17', value: '1.17' },
+];
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index caf2729a4c7..5abff3c7831 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -56,6 +56,7 @@ export const createCluster = ({ dispatch, state }) => {
environment_scope: state.environmentScope,
managed: state.gitlabManagedCluster,
provider_aws_attributes: {
+ kubernetes_version: state.kubernetesVersion,
region: state.selectedRegion,
vpc_id: state.selectedVpc,
subnet_ids: state.selectedSubnet,
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
index d1337e7ea4a..ed51e95e434 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
@@ -1,6 +1,6 @@
import { KUBERNETES_VERSIONS } from '../constants';
-const [{ value: kubernetesVersion }] = KUBERNETES_VERSIONS;
+const kubernetesVersion = KUBERNETES_VERSIONS.find(version => version.default).value;
export default () => ({
createRolePath: null,
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
new file mode 100644
index 00000000000..c17f2d2efe4
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
@@ -0,0 +1,689 @@
+/* eslint-disable consistent-return */
+import $ from 'jquery';
+import { escape } from 'lodash';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { isObject } from '~/lib/utils/type_utility';
+import renderItem from './render';
+import { GitLabDropdownRemote } from './gl_dropdown_remote';
+import { GitLabDropdownInput } from './gl_dropdown_input';
+import { GitLabDropdownFilter } from './gl_dropdown_filter';
+
+const LOADING_CLASS = 'is-loading';
+
+const PAGE_TWO_CLASS = 'is-page-two';
+
+const ACTIVE_CLASS = 'is-active';
+
+const INDETERMINATE_CLASS = 'is-indeterminate';
+
+let currentIndex = -1;
+
+const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
+
+const SELECTABLE_CLASSES = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES}, .option-hidden)`;
+
+const CURSOR_SELECT_SCROLL_PADDING = 5;
+
+const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)';
+
+const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
+
+export class GitLabDropdown {
+ constructor(el1, options) {
+ let selector;
+ let self;
+ this.el = el1;
+ this.options = options;
+ this.updateLabel = this.updateLabel.bind(this);
+ this.hidden = this.hidden.bind(this);
+ this.opened = this.opened.bind(this);
+ this.shouldPropagate = this.shouldPropagate.bind(this);
+ self = this;
+ selector = $(this.el).data('target');
+ this.dropdown = selector != null ? $(selector) : $(this.el).parent();
+ // Set Defaults
+ this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+ this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
+ this.highlight = Boolean(this.options.highlight);
+ this.icon = Boolean(this.options.icon);
+ this.filterInputBlur =
+ this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
+ // If no input is passed create a default one
+ self = this;
+ // If selector was passed
+ if (typeof this.filterInput === 'string') {
+ this.filterInput = this.getElement(this.filterInput);
+ }
+ const searchFields = this.options.search ? this.options.search.fields : [];
+ if (this.options.data) {
+ // If we provided data
+ // data could be an array of objects or a group of arrays
+ if (typeof this.options.data === 'object' && !(this.options.data instanceof Function)) {
+ this.fullData = this.options.data;
+ currentIndex = -1;
+ this.parseData(this.options.data);
+ this.focusTextInput();
+ } else {
+ this.remote = new GitLabDropdownRemote(this.options.data, {
+ dataType: this.options.dataType,
+ beforeSend: this.toggleLoading.bind(this),
+ success: data => {
+ this.fullData = data;
+ this.parseData(this.fullData);
+ this.focusTextInput();
+
+ // Update dropdown position since remote data may have changed dropdown size
+ this.dropdown.find('.dropdown-menu-toggle').dropdown('update');
+
+ if (
+ this.options.filterable &&
+ this.filter &&
+ this.filter.input &&
+ this.filter.input.val() &&
+ this.filter.input.val().trim() !== ''
+ ) {
+ return this.filter.input.trigger('input');
+ }
+ },
+ instance: this,
+ });
+ }
+ }
+ if (this.noFilterInput.length) {
+ this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
+ this.plainInput.onInput(this.addInput.bind(this));
+ }
+ // Init filterable
+ if (this.options.filterable) {
+ this.filter = new GitLabDropdownFilter(this.filterInput, {
+ elIsInput: $(this.el).is('input'),
+ filterInputBlur: this.filterInputBlur,
+ filterByText: this.options.filterByText,
+ onFilter: this.options.onFilter,
+ remote: this.options.filterRemote,
+ query: this.options.data,
+ keys: searchFields,
+ instance: this,
+ elements: () => {
+ selector = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = `.dropdown-page-one ${selector}`;
+ }
+ return $(selector, this.dropdown);
+ },
+ data: () => this.fullData,
+ callback: data => {
+ this.parseData(data);
+ if (this.filterInput.val() !== '') {
+ selector = SELECTABLE_CLASSES;
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = `.dropdown-page-one ${selector}`;
+ }
+ if ($(this.el).is('input')) {
+ currentIndex = -1;
+ } else {
+ $(selector, this.dropdown)
+ .first()
+ .find('a')
+ .addClass('is-focused');
+ currentIndex = 0;
+ }
+ }
+ },
+ });
+ }
+ // Event listeners
+ this.dropdown.on('shown.bs.dropdown', this.opened);
+ this.dropdown.on('hidden.bs.dropdown', this.hidden);
+ $(this.el).on('update.label', this.updateLabel);
+ this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
+ this.dropdown.on('keyup', e => {
+ // Escape key
+ if (e.which === 27) {
+ return $('.dropdown-menu-close', this.dropdown).trigger('click');
+ }
+ });
+ this.dropdown.on('blur', 'a', e => {
+ let $dropdownMenu;
+ let $relatedTarget;
+ if (e.relatedTarget != null) {
+ $relatedTarget = $(e.relatedTarget);
+ $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
+ if ($dropdownMenu.length === 0) {
+ return this.dropdown.removeClass('show');
+ }
+ }
+ });
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on('click', e => {
+ e.preventDefault();
+ e.stopPropagation();
+ return this.togglePage();
+ });
+ }
+ if (this.options.selectable) {
+ selector = '.dropdown-content a';
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = '.dropdown-page-one .dropdown-content a';
+ }
+ this.dropdown.on('click', selector, e => {
+ const $el = $(e.currentTarget);
+ const selected = self.rowClicked($el);
+ const selectedObj = selected ? selected[0] : null;
+ const isMarking = selected ? selected[1] : null;
+ if (this.options.clicked) {
+ this.options.clicked.call(this, {
+ selectedObj,
+ $el,
+ e,
+ isMarking,
+ });
+ }
+
+ // Update label right after all modifications in dropdown has been done
+ if (this.options.toggleLabel) {
+ this.updateLabel(selectedObj, $el, this);
+ }
+
+ $el.trigger('blur');
+ });
+ }
+ }
+
+ // Finds an element inside wrapper element
+ getElement(selector) {
+ return this.dropdown.find(selector);
+ }
+
+ toggleLoading() {
+ return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
+ }
+
+ togglePage() {
+ const menu = $('.dropdown-menu', this.dropdown);
+ if (menu.hasClass(PAGE_TWO_CLASS)) {
+ if (this.remote) {
+ this.remote.execute();
+ }
+ }
+ menu.toggleClass(PAGE_TWO_CLASS);
+ // Focus first visible input on active page
+ return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
+ }
+
+ parseData(data) {
+ let groupData;
+ let html;
+ this.renderedData = data;
+ if (this.options.filterable && data.length === 0) {
+ // render no matching results
+ html = [this.noResults()];
+ }
+ // Handle array groups
+ else if (isObject(data)) {
+ html = [];
+
+ Object.keys(data).forEach(name => {
+ groupData = data[name];
+ html.push(
+ this.renderItem(
+ {
+ content: name,
+ type: 'header',
+ },
+ name,
+ ),
+ );
+ this.renderData(groupData, name).map(item => html.push(item));
+ });
+ } else {
+ // Render each row
+ html = this.renderData(data);
+ }
+ // Render the full menu
+ const fullHtml = this.renderMenu(html);
+ return this.appendMenu(fullHtml);
+ }
+
+ renderData(data, group) {
+ return data.map((obj, index) => this.renderItem(obj, group || false, index));
+ }
+
+ shouldPropagate(e) {
+ let $target;
+ if (this.options.multiSelect || this.options.shouldPropagate === false) {
+ $target = $(e.target);
+ if (
+ $target &&
+ !$target.hasClass('dropdown-menu-close') &&
+ !$target.hasClass('dropdown-menu-close-icon') &&
+ !$target.data('isLink')
+ ) {
+ e.stopPropagation();
+
+ // This prevents automatic scrolling to the top
+ if ($target.closest('a').length) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ filteredFullData() {
+ return this.fullData.filter(
+ r =>
+ typeof r === 'object' &&
+ !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
+ !Object.prototype.hasOwnProperty.call(r, 'header'),
+ );
+ }
+
+ opened(e) {
+ this.resetRows();
+ this.addArrowKeyEvent();
+
+ const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
+ const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
+ const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
+ const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
+
+ // Makes indeterminate items effective
+ if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
+ this.parseData(this.fullData);
+ }
+
+ // Process the data to make sure rendered data
+ // matches the correct layout
+ const inputValue = this.filterInput.val();
+ if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
+ this.options.processData.call(
+ this.options,
+ inputValue,
+ this.filteredFullData(),
+ this.parseData.bind(this),
+ );
+ }
+
+ const contentHtml = $('.dropdown-content', this.dropdown).html();
+ if (this.remote && contentHtml === '') {
+ this.remote.execute();
+ } else {
+ this.focusTextInput();
+ }
+
+ if (this.options.showMenuAbove) {
+ this.positionMenuAbove();
+ }
+
+ if (this.options.opened) {
+ if (this.options.preserveContext) {
+ this.options.opened(e);
+ } else {
+ this.options.opened.call(this, e);
+ }
+ }
+
+ return this.dropdown.trigger('shown.gl.dropdown');
+ }
+
+ positionMenuAbove() {
+ const $menu = this.dropdown.find('.dropdown-menu');
+
+ $menu.addClass('dropdown-open-top');
+ $menu.css('top', 'initial');
+ $menu.css('bottom', '100%');
+ }
+
+ hidden(e) {
+ this.resetRows();
+ this.removeArrowKeyEvent();
+ const $input = this.dropdown.find('.dropdown-input-field');
+ if (this.options.filterable) {
+ $input.blur();
+ }
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
+ }
+ if (this.options.hidden) {
+ this.options.hidden.call(this, e);
+ }
+ return this.dropdown.trigger('hidden.gl.dropdown');
+ }
+
+ // Render the full menu
+ renderMenu(html) {
+ if (this.options.renderMenu) {
+ return this.options.renderMenu(html);
+ }
+ return $('<ul>').append(html);
+ }
+
+ // Append the menu into the dropdown
+ appendMenu(html) {
+ return this.clearMenu().append(html);
+ }
+
+ clearMenu() {
+ let selector = '.dropdown-content';
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ if (this.options.containerSelector) {
+ selector = this.options.containerSelector;
+ } else {
+ selector = '.dropdown-page-one .dropdown-content';
+ }
+ }
+
+ return $(selector, this.dropdown).empty();
+ }
+
+ renderItem(data, group, index) {
+ let parent;
+
+ if (this.dropdown && this.dropdown[0]) {
+ parent = this.dropdown[0].parentNode;
+ }
+
+ return renderItem({
+ instance: this,
+ options: {
+ ...this.options,
+ icon: this.icon,
+ highlight: this.highlight,
+ highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
+ highlightTemplate: this.highlightTemplate.bind(this),
+ parent,
+ },
+ data,
+ group,
+ index,
+ });
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ highlightTemplate(text, template) {
+ return `"<b>${escape(text)}</b>" ${template}`;
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ highlightTextMatches(text, term) {
+ const occurrences = fuzzaldrinPlus.match(text, term);
+ const { indexOf } = [];
+
+ return text
+ .split('')
+ .map((character, i) => {
+ if (indexOf.call(occurrences, i) !== -1) {
+ return `<b>${character}</b>`;
+ }
+ return character;
+ })
+ .join('');
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ noResults() {
+ return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>';
+ }
+
+ rowClicked(el) {
+ let field;
+ let groupName;
+ let selectedIndex;
+ let selectedObject;
+ let isMarking;
+ const { fieldName } = this.options;
+ const isInput = $(this.el).is('input');
+ if (this.renderedData) {
+ groupName = el.data('group');
+ if (groupName) {
+ selectedIndex = el.data('index');
+ selectedObject = this.renderedData[groupName][selectedIndex];
+ } else {
+ selectedIndex = el.closest('li').index();
+ this.selectedIndex = selectedIndex;
+ selectedObject = this.renderedData[selectedIndex];
+ }
+ }
+
+ if (this.options.vue) {
+ if (el.hasClass(ACTIVE_CLASS)) {
+ el.removeClass(ACTIVE_CLASS);
+ } else {
+ el.addClass(ACTIVE_CLASS);
+ }
+
+ return [selectedObject];
+ }
+
+ field = [];
+ const value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
+ if (isInput) {
+ field = $(this.el);
+ } else if (value != null) {
+ field = this.dropdown
+ .parent()
+ .find(`input[name='${fieldName}'][value='${value.toString().replace(/'/g, "\\'")}']`);
+ }
+
+ if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
+ return [selectedObject];
+ }
+
+ if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
+ isMarking = false;
+ el.removeClass(ACTIVE_CLASS);
+ if (field && field.length) {
+ this.clearField(field, isInput);
+ }
+ } else if (el.hasClass(INDETERMINATE_CLASS)) {
+ isMarking = true;
+ el.addClass(ACTIVE_CLASS);
+ el.removeClass(INDETERMINATE_CLASS);
+ if (field && field.length && value == null) {
+ this.clearField(field, isInput);
+ }
+ if ((!field || !field.length) && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ }
+ } else {
+ isMarking = true;
+ if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
+ this.dropdown.find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS);
+ if (!isInput) {
+ this.dropdown
+ .parent()
+ .find(`input[name='${fieldName}']`)
+ .remove();
+ }
+ }
+ if (field && field.length && value == null) {
+ this.clearField(field, isInput);
+ }
+ // Toggle active class for the tick mark
+ el.addClass(ACTIVE_CLASS);
+ if (value != null) {
+ if ((!field || !field.length) && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ } else if (field && field.length) {
+ field.val(value).trigger('change');
+ }
+ }
+ }
+
+ return [selectedObject, isMarking];
+ }
+
+ focusTextInput() {
+ if (this.options.filterable) {
+ const initialScrollTop = $(window).scrollTop();
+
+ if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) {
+ this.filterInput.focus();
+ }
+
+ if ($(window).scrollTop() < initialScrollTop) {
+ $(window).scrollTop(initialScrollTop);
+ }
+ }
+ }
+
+ addInput(fieldName, value, selectedObject, single) {
+ // Create hidden input for form
+ if (single) {
+ $(`input[name="${fieldName}"]`).remove();
+ }
+
+ const $input = $('<input>')
+ .attr('type', 'hidden')
+ .attr('name', fieldName)
+ .val(value);
+ if (this.options.inputId != null) {
+ $input.attr('id', this.options.inputId);
+ }
+
+ if (this.options.multiSelect) {
+ Object.keys(selectedObject).forEach(attribute => {
+ $input.attr(`data-${attribute}`, selectedObject[attribute]);
+ });
+ }
+
+ if (this.options.inputMeta) {
+ $input.attr('data-meta', selectedObject[this.options.inputMeta]);
+ }
+
+ this.dropdown.before($input).trigger('change');
+ }
+
+ selectRowAtIndex(index) {
+ // If we pass an option index
+ let selector;
+ if (typeof index !== 'undefined') {
+ selector = `${SELECTABLE_CLASSES}:eq(${index}) a`;
+ } else {
+ selector = '.dropdown-content .is-focused';
+ }
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = `.dropdown-page-one ${selector}`;
+ }
+ // simulate a click on the first link
+ const $el = $(selector, this.dropdown);
+ if ($el.length) {
+ const href = $el.attr('href');
+ if (href && href !== '#') {
+ visitUrl(href);
+ } else {
+ $el.trigger('click');
+ }
+ }
+ }
+
+ addArrowKeyEvent() {
+ const ARROW_KEY_CODES = [38, 40];
+ let selector = SELECTABLE_CLASSES;
+ if (this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = `.dropdown-page-one ${selector}`;
+ }
+ return $('body').on('keydown', e => {
+ let $listItems;
+ let PREV_INDEX;
+ const currentKeyCode = e.which;
+ if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ PREV_INDEX = currentIndex;
+ $listItems = $(selector, this.dropdown);
+ // if @options.filterable
+ // $input.blur()
+ if (currentKeyCode === 40) {
+ // Move down
+ if (currentIndex < $listItems.length - 1) {
+ currentIndex += 1;
+ }
+ } else if (currentKeyCode === 38) {
+ // Move up
+ if (currentIndex > 0) {
+ currentIndex -= 1;
+ }
+ }
+ if (currentIndex !== PREV_INDEX) {
+ this.highlightRowAtIndex($listItems, currentIndex);
+ }
+ return false;
+ }
+ if (currentKeyCode === 13 && currentIndex !== -1) {
+ e.preventDefault();
+ this.selectRowAtIndex();
+ }
+ });
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ removeArrowKeyEvent() {
+ return $('body').off('keydown');
+ }
+
+ resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ }
+
+ highlightRowAtIndex($listItems, index) {
+ if (!$listItems) {
+ // eslint-disable-next-line no-param-reassign
+ $listItems = $(SELECTABLE_CLASSES, this.dropdown);
+ }
+
+ // Remove the class for the previously focused row
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ // Update the class for the row at the specific index
+ const $listItem = $listItems.eq(index);
+ $listItem.find('a:first-child').addClass('is-focused');
+ // Dropdown content scroll area
+ const $dropdownContent = $listItem.closest('.dropdown-content');
+ const dropdownScrollTop = $dropdownContent.scrollTop();
+ const dropdownContentHeight = $dropdownContent.outerHeight();
+ const dropdownContentTop = $dropdownContent.prop('offsetTop');
+ const dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
+ // Get the offset bottom of the list item
+ const listItemHeight = $listItem.outerHeight();
+ const listItemTop = $listItem.prop('offsetTop');
+ const listItemBottom = listItemTop + listItemHeight;
+ if (!index) {
+ // Scroll the dropdown content to the top
+ $dropdownContent.scrollTop(0);
+ } else if (index === $listItems.length - 1) {
+ // Scroll the dropdown content to the bottom
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
+ // Scroll the dropdown content down
+ $dropdownContent.scrollTop(
+ listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
+ );
+ } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
+ // Scroll the dropdown content up
+ return $dropdownContent.scrollTop(
+ listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
+ );
+ }
+ }
+
+ updateLabel(selected = null, el = null, instance = null) {
+ let toggleText = this.options.toggleLabel(selected, el, instance);
+ if (this.options.updateLabel) {
+ // Option to override the dropdown label text
+ toggleText = this.options.updateLabel;
+ }
+
+ return $(this.el)
+ .find('.dropdown-toggle-text')
+ .text(toggleText);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ clearField(field, isInput) {
+ return isInput ? field.val('') : field.remove();
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
new file mode 100644
index 00000000000..89ffb5f5f79
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
@@ -0,0 +1,135 @@
+/* eslint-disable consistent-return */
+
+import $ from 'jquery';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { isObject } from '~/lib/utils/type_utility';
+
+const BLUR_KEYCODES = [27, 40];
+
+const HAS_VALUE_CLASS = 'has-value';
+
+export class GitLabDropdownFilter {
+ constructor(input, options) {
+ let ref;
+ let timeout;
+ this.input = input;
+ this.options = options;
+ // eslint-disable-next-line no-cond-assign
+ this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
+ const $inputContainer = this.input.parent();
+ const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', e => {
+ // Clear click
+ e.preventDefault();
+ e.stopPropagation();
+ return this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ });
+ // Key events
+ timeout = '';
+ this.input
+ .on('keydown', e => {
+ const keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', () => {
+ if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.addClass(HAS_VALUE_CLASS);
+ } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.removeClass(HAS_VALUE_CLASS);
+ }
+ // Only filter asynchronously only if option remote is set
+ if (this.options.remote) {
+ clearTimeout(timeout);
+ // eslint-disable-next-line no-return-assign
+ return (timeout = setTimeout(() => {
+ $inputContainer.parent().addClass('is-loading');
+
+ return this.options.query(this.input.val(), data => {
+ $inputContainer.parent().removeClass('is-loading');
+ return this.options.callback(data);
+ });
+ }, 250));
+ }
+ return this.filter(this.input.val());
+ });
+ }
+
+ static shouldBlur(keyCode) {
+ return BLUR_KEYCODES.indexOf(keyCode) !== -1;
+ }
+
+ filter(searchText) {
+ let group;
+ let results;
+ let tmp;
+ if (this.options.onFilter) {
+ this.options.onFilter(searchText);
+ }
+ const data = this.options.data();
+ if (data != null && !this.options.filterByText) {
+ results = data;
+ if (searchText !== '') {
+ // When data is an array of objects therefore [object Array] e.g.
+ // [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ]
+ if (Array.isArray(data)) {
+ results = fuzzaldrinPlus.filter(data, searchText, {
+ key: this.options.keys,
+ });
+ }
+ // If data is grouped therefore an [object Object]. e.g.
+ // {
+ // groupName1: [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ],
+ // groupName2: [
+ // { prop: 'abc' },
+ // { prop: 'def' }
+ // ]
+ // }
+ else if (isObject(data)) {
+ results = {};
+ Object.keys(data).forEach(key => {
+ group = data[key];
+ tmp = fuzzaldrinPlus.filter(group, searchText, {
+ key: this.options.keys,
+ });
+ if (tmp.length) {
+ results[key] = tmp.map(item => item);
+ }
+ });
+ }
+ }
+ return this.options.callback(results);
+ }
+ const elements = this.options.elements();
+ if (searchText) {
+ // eslint-disable-next-line func-names
+ elements.each(function() {
+ const $el = $(this);
+ const matches = fuzzaldrinPlus.match($el.text().trim(), searchText);
+ if (!$el.is('.dropdown-header')) {
+ if (matches.length) {
+ return $el.show().removeClass('option-hidden');
+ }
+ return $el.hide().addClass('option-hidden');
+ }
+ });
+ } else {
+ elements.show().removeClass('option-hidden');
+ }
+
+ elements
+ .parent()
+ .find('.dropdown-menu-empty-item')
+ .toggleClass('hidden', elements.is(':visible'));
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js
new file mode 100644
index 00000000000..d857071d05f
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js
@@ -0,0 +1,44 @@
+export class GitLabDropdownInput {
+ constructor(input, options) {
+ this.input = input;
+ this.options = options;
+ this.fieldName = this.options.fieldName || 'field-name';
+ const $inputContainer = this.input.parent();
+ const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', e => {
+ // Clear click
+ e.preventDefault();
+ e.stopPropagation();
+ return this.input
+ .val('')
+ .trigger('input')
+ .focus();
+ });
+
+ this.input
+ .on('keydown', e => {
+ const keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', e => {
+ let val = e.currentTarget.value || this.options.inputFieldName;
+ val = val
+ .split(' ')
+ .join('-') // replaces space with dash
+ .replace(/[^a-zA-Z0-9 -]/g, '')
+ .toLowerCase() // replace non alphanumeric
+ .replace(/(-)\1+/g, '-'); // replace repeated dashes
+ this.cb(this.options.fieldName, val, {}, true);
+ this.input
+ .closest('.dropdown')
+ .find('.dropdown-toggle-text')
+ .text(val);
+ });
+ }
+
+ onInput(cb) {
+ this.cb = cb;
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
new file mode 100644
index 00000000000..1f6a2e1f646
--- /dev/null
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
@@ -0,0 +1,42 @@
+/* eslint-disable consistent-return */
+
+import axios from '../lib/utils/axios_utils';
+
+export class GitLabDropdownRemote {
+ constructor(dataEndpoint, options) {
+ this.dataEndpoint = dataEndpoint;
+ this.options = options;
+ }
+
+ execute() {
+ if (typeof this.dataEndpoint === 'string') {
+ return this.fetchData();
+ } else if (typeof this.dataEndpoint === 'function') {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+ return this.dataEndpoint('', data => {
+ // Fetch the data by calling the data function
+ if (this.options.success) {
+ this.options.success(data);
+ }
+ if (this.options.beforeSend) {
+ return this.options.beforeSend();
+ }
+ });
+ }
+ }
+
+ fetchData() {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+
+ // Fetch the data through ajax if the data is a string
+ return axios.get(this.dataEndpoint).then(({ data }) => {
+ if (this.options.success) {
+ return this.options.success(data);
+ }
+ });
+ }
+}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/index.js b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
index 3f2474b5c8c..90e7f15b5b7 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/index.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
@@ -1,894 +1,5 @@
-/* eslint-disable max-classes-per-file, one-var, consistent-return */
-
import $ from 'jquery';
-import { escape } from 'lodash';
-import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import axios from '../lib/utils/axios_utils';
-import { visitUrl } from '~/lib/utils/url_utility';
-import { isObject } from '~/lib/utils/type_utility';
-import renderItem from './render';
-
-const BLUR_KEYCODES = [27, 40];
-
-const HAS_VALUE_CLASS = 'has-value';
-
-const LOADING_CLASS = 'is-loading';
-
-const PAGE_TWO_CLASS = 'is-page-two';
-
-const ACTIVE_CLASS = 'is-active';
-
-const INDETERMINATE_CLASS = 'is-indeterminate';
-
-let currentIndex = -1;
-
-const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
-
-const SELECTABLE_CLASSES = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES}, .option-hidden)`;
-
-const CURSOR_SELECT_SCROLL_PADDING = 5;
-
-const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)';
-
-const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
-
-class GitLabDropdownInput {
- constructor(input, options) {
- this.input = input;
- this.options = options;
- this.fieldName = this.options.fieldName || 'field-name';
- const $inputContainer = this.input.parent();
- const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', e => {
- // Clear click
- e.preventDefault();
- e.stopPropagation();
- return this.input
- .val('')
- .trigger('input')
- .focus();
- });
-
- this.input
- .on('keydown', e => {
- const keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', e => {
- let val = e.currentTarget.value || this.options.inputFieldName;
- val = val
- .split(' ')
- .join('-') // replaces space with dash
- .replace(/[^a-zA-Z0-9 -]/g, '')
- .toLowerCase() // replace non alphanumeric
- .replace(/(-)\1+/g, '-'); // replace repeated dashes
- this.cb(this.options.fieldName, val, {}, true);
- this.input
- .closest('.dropdown')
- .find('.dropdown-toggle-text')
- .text(val);
- });
- }
-
- onInput(cb) {
- this.cb = cb;
- }
-}
-
-class GitLabDropdownFilter {
- constructor(input, options) {
- let ref, timeout;
- this.input = input;
- this.options = options;
- // eslint-disable-next-line no-cond-assign
- this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
- const $inputContainer = this.input.parent();
- const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
- $clearButton.on('click', e => {
- // Clear click
- e.preventDefault();
- e.stopPropagation();
- return this.input
- .val('')
- .trigger('input')
- .focus();
- });
- // Key events
- timeout = '';
- this.input
- .on('keydown', e => {
- const keyCode = e.which;
- if (keyCode === 13 && !options.elIsInput) {
- e.preventDefault();
- }
- })
- .on('input', () => {
- if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.addClass(HAS_VALUE_CLASS);
- } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
- $inputContainer.removeClass(HAS_VALUE_CLASS);
- }
- // Only filter asynchronously only if option remote is set
- if (this.options.remote) {
- clearTimeout(timeout);
- // eslint-disable-next-line no-return-assign
- return (timeout = setTimeout(() => {
- $inputContainer.parent().addClass('is-loading');
-
- return this.options.query(this.input.val(), data => {
- $inputContainer.parent().removeClass('is-loading');
- return this.options.callback(data);
- });
- }, 250));
- }
- return this.filter(this.input.val());
- });
- }
-
- static shouldBlur(keyCode) {
- return BLUR_KEYCODES.indexOf(keyCode) !== -1;
- }
-
- filter(searchText) {
- let group, results, tmp;
- if (this.options.onFilter) {
- this.options.onFilter(searchText);
- }
- const data = this.options.data();
- if (data != null && !this.options.filterByText) {
- results = data;
- if (searchText !== '') {
- // When data is an array of objects therefore [object Array] e.g.
- // [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ]
- if (Array.isArray(data)) {
- results = fuzzaldrinPlus.filter(data, searchText, {
- key: this.options.keys,
- });
- }
- // If data is grouped therefore an [object Object]. e.g.
- // {
- // groupName1: [
- // { prop: 'foo' },
- // { prop: 'baz' }
- // ],
- // groupName2: [
- // { prop: 'abc' },
- // { prop: 'def' }
- // ]
- // }
- else if (isObject(data)) {
- results = {};
- Object.keys(data).forEach(key => {
- group = data[key];
- tmp = fuzzaldrinPlus.filter(group, searchText, {
- key: this.options.keys,
- });
- if (tmp.length) {
- results[key] = tmp.map(item => item);
- }
- });
- }
- }
- return this.options.callback(results);
- }
- const elements = this.options.elements();
- if (searchText) {
- // eslint-disable-next-line func-names
- elements.each(function() {
- const $el = $(this);
- const matches = fuzzaldrinPlus.match($el.text().trim(), searchText);
- if (!$el.is('.dropdown-header')) {
- if (matches.length) {
- return $el.show().removeClass('option-hidden');
- }
- return $el.hide().addClass('option-hidden');
- }
- });
- } else {
- elements.show().removeClass('option-hidden');
- }
-
- elements
- .parent()
- .find('.dropdown-menu-empty-item')
- .toggleClass('hidden', elements.is(':visible'));
- }
-}
-
-class GitLabDropdownRemote {
- constructor(dataEndpoint, options) {
- this.dataEndpoint = dataEndpoint;
- this.options = options;
- }
-
- execute() {
- if (typeof this.dataEndpoint === 'string') {
- return this.fetchData();
- } else if (typeof this.dataEndpoint === 'function') {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
- return this.dataEndpoint('', data => {
- // Fetch the data by calling the data function
- if (this.options.success) {
- this.options.success(data);
- }
- if (this.options.beforeSend) {
- return this.options.beforeSend();
- }
- });
- }
- }
-
- fetchData() {
- if (this.options.beforeSend) {
- this.options.beforeSend();
- }
-
- // Fetch the data through ajax if the data is a string
- return axios.get(this.dataEndpoint).then(({ data }) => {
- if (this.options.success) {
- return this.options.success(data);
- }
- });
- }
-}
-
-class GitLabDropdown {
- constructor(el1, options) {
- let selector, self;
- this.el = el1;
- this.options = options;
- this.updateLabel = this.updateLabel.bind(this);
- this.hidden = this.hidden.bind(this);
- this.opened = this.opened.bind(this);
- this.shouldPropagate = this.shouldPropagate.bind(this);
- self = this;
- selector = $(this.el).data('target');
- this.dropdown = selector != null ? $(selector) : $(this.el).parent();
- // Set Defaults
- this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
- this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
- this.highlight = Boolean(this.options.highlight);
- this.icon = Boolean(this.options.icon);
- this.filterInputBlur =
- this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
- // If no input is passed create a default one
- self = this;
- // If selector was passed
- if (typeof this.filterInput === 'string') {
- this.filterInput = this.getElement(this.filterInput);
- }
- const searchFields = this.options.search ? this.options.search.fields : [];
- if (this.options.data) {
- // If we provided data
- // data could be an array of objects or a group of arrays
- if (typeof this.options.data === 'object' && !(this.options.data instanceof Function)) {
- this.fullData = this.options.data;
- currentIndex = -1;
- this.parseData(this.options.data);
- this.focusTextInput();
- } else {
- this.remote = new GitLabDropdownRemote(this.options.data, {
- dataType: this.options.dataType,
- beforeSend: this.toggleLoading.bind(this),
- success: data => {
- this.fullData = data;
- this.parseData(this.fullData);
- this.focusTextInput();
-
- // Update dropdown position since remote data may have changed dropdown size
- this.dropdown.find('.dropdown-menu-toggle').dropdown('update');
-
- if (
- this.options.filterable &&
- this.filter &&
- this.filter.input &&
- this.filter.input.val() &&
- this.filter.input.val().trim() !== ''
- ) {
- return this.filter.input.trigger('input');
- }
- },
- instance: this,
- });
- }
- }
- if (this.noFilterInput.length) {
- this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
- this.plainInput.onInput(this.addInput.bind(this));
- }
- // Init filterable
- if (this.options.filterable) {
- this.filter = new GitLabDropdownFilter(this.filterInput, {
- elIsInput: $(this.el).is('input'),
- filterInputBlur: this.filterInputBlur,
- filterByText: this.options.filterByText,
- onFilter: this.options.onFilter,
- remote: this.options.filterRemote,
- query: this.options.data,
- keys: searchFields,
- instance: this,
- elements: () => {
- selector = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- selector = `.dropdown-page-one ${selector}`;
- }
- return $(selector, this.dropdown);
- },
- data: () => this.fullData,
- callback: data => {
- this.parseData(data);
- if (this.filterInput.val() !== '') {
- selector = SELECTABLE_CLASSES;
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- selector = `.dropdown-page-one ${selector}`;
- }
- if ($(this.el).is('input')) {
- currentIndex = -1;
- } else {
- $(selector, this.dropdown)
- .first()
- .find('a')
- .addClass('is-focused');
- currentIndex = 0;
- }
- }
- },
- });
- }
- // Event listeners
- this.dropdown.on('shown.bs.dropdown', this.opened);
- this.dropdown.on('hidden.bs.dropdown', this.hidden);
- $(this.el).on('update.label', this.updateLabel);
- this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
- this.dropdown.on('keyup', e => {
- // Escape key
- if (e.which === 27) {
- return $('.dropdown-menu-close', this.dropdown).trigger('click');
- }
- });
- this.dropdown.on('blur', 'a', e => {
- let $dropdownMenu, $relatedTarget;
- if (e.relatedTarget != null) {
- $relatedTarget = $(e.relatedTarget);
- $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
- if ($dropdownMenu.length === 0) {
- return this.dropdown.removeClass('show');
- }
- }
- });
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on('click', e => {
- e.preventDefault();
- e.stopPropagation();
- return this.togglePage();
- });
- }
- if (this.options.selectable) {
- selector = '.dropdown-content a';
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- selector = '.dropdown-page-one .dropdown-content a';
- }
- this.dropdown.on('click', selector, e => {
- const $el = $(e.currentTarget);
- const selected = self.rowClicked($el);
- const selectedObj = selected ? selected[0] : null;
- const isMarking = selected ? selected[1] : null;
- if (this.options.clicked) {
- this.options.clicked.call(this, {
- selectedObj,
- $el,
- e,
- isMarking,
- });
- }
-
- // Update label right after all modifications in dropdown has been done
- if (this.options.toggleLabel) {
- this.updateLabel(selectedObj, $el, this);
- }
-
- $el.trigger('blur');
- });
- }
- }
-
- // Finds an element inside wrapper element
- getElement(selector) {
- return this.dropdown.find(selector);
- }
-
- toggleLoading() {
- return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
- }
-
- togglePage() {
- const menu = $('.dropdown-menu', this.dropdown);
- if (menu.hasClass(PAGE_TWO_CLASS)) {
- if (this.remote) {
- this.remote.execute();
- }
- }
- menu.toggleClass(PAGE_TWO_CLASS);
- // Focus first visible input on active page
- return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
- }
-
- parseData(data) {
- let groupData, html;
- this.renderedData = data;
- if (this.options.filterable && data.length === 0) {
- // render no matching results
- html = [this.noResults()];
- }
- // Handle array groups
- else if (isObject(data)) {
- html = [];
-
- Object.keys(data).forEach(name => {
- groupData = data[name];
- html.push(
- this.renderItem(
- {
- content: name,
- type: 'header',
- },
- name,
- ),
- );
- this.renderData(groupData, name).map(item => html.push(item));
- });
- } else {
- // Render each row
- html = this.renderData(data);
- }
- // Render the full menu
- const fullHtml = this.renderMenu(html);
- return this.appendMenu(fullHtml);
- }
-
- renderData(data, group) {
- return data.map((obj, index) => this.renderItem(obj, group || false, index));
- }
-
- shouldPropagate(e) {
- let $target;
- if (this.options.multiSelect || this.options.shouldPropagate === false) {
- $target = $(e.target);
- if (
- $target &&
- !$target.hasClass('dropdown-menu-close') &&
- !$target.hasClass('dropdown-menu-close-icon') &&
- !$target.data('isLink')
- ) {
- e.stopPropagation();
-
- // This prevents automatic scrolling to the top
- if ($target.closest('a').length) {
- return false;
- }
- }
-
- return true;
- }
- }
-
- filteredFullData() {
- return this.fullData.filter(
- r =>
- typeof r === 'object' &&
- !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
- !Object.prototype.hasOwnProperty.call(r, 'header'),
- );
- }
-
- opened(e) {
- this.resetRows();
- this.addArrowKeyEvent();
-
- const dropdownToggle = this.dropdown.find('.dropdown-menu-toggle');
- const hasFilterBulkUpdate = dropdownToggle.hasClass('js-filter-bulk-update');
- const shouldRefreshOnOpen = dropdownToggle.hasClass('js-gl-dropdown-refresh-on-open');
- const hasMultiSelect = dropdownToggle.hasClass('js-multiselect');
-
- // Makes indeterminate items effective
- if (this.fullData && (shouldRefreshOnOpen || hasFilterBulkUpdate)) {
- this.parseData(this.fullData);
- }
-
- // Process the data to make sure rendered data
- // matches the correct layout
- const inputValue = this.filterInput.val();
- if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
- this.options.processData.call(
- this.options,
- inputValue,
- this.filteredFullData(),
- this.parseData.bind(this),
- );
- }
-
- const contentHtml = $('.dropdown-content', this.dropdown).html();
- if (this.remote && contentHtml === '') {
- this.remote.execute();
- } else {
- this.focusTextInput();
- }
-
- if (this.options.showMenuAbove) {
- this.positionMenuAbove();
- }
-
- if (this.options.opened) {
- if (this.options.preserveContext) {
- this.options.opened(e);
- } else {
- this.options.opened.call(this, e);
- }
- }
-
- return this.dropdown.trigger('shown.gl.dropdown');
- }
-
- positionMenuAbove() {
- const $menu = this.dropdown.find('.dropdown-menu');
-
- $menu.addClass('dropdown-open-top');
- $menu.css('top', 'initial');
- $menu.css('bottom', '100%');
- }
-
- hidden(e) {
- this.resetRows();
- this.removeArrowKeyEvent();
- const $input = this.dropdown.find('.dropdown-input-field');
- if (this.options.filterable) {
- $input.blur();
- }
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
- }
- if (this.options.hidden) {
- this.options.hidden.call(this, e);
- }
- return this.dropdown.trigger('hidden.gl.dropdown');
- }
-
- // Render the full menu
- renderMenu(html) {
- if (this.options.renderMenu) {
- return this.options.renderMenu(html);
- }
- return $('<ul>').append(html);
- }
-
- // Append the menu into the dropdown
- appendMenu(html) {
- return this.clearMenu().append(html);
- }
-
- clearMenu() {
- let selector = '.dropdown-content';
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- if (this.options.containerSelector) {
- selector = this.options.containerSelector;
- } else {
- selector = '.dropdown-page-one .dropdown-content';
- }
- }
-
- return $(selector, this.dropdown).empty();
- }
-
- renderItem(data, group, index) {
- let parent;
-
- if (this.dropdown && this.dropdown[0]) {
- parent = this.dropdown[0].parentNode;
- }
-
- return renderItem({
- instance: this,
- options: {
- ...this.options,
- icon: this.icon,
- highlight: this.highlight,
- highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
- highlightTemplate: this.highlightTemplate.bind(this),
- parent,
- },
- data,
- group,
- index,
- });
- }
-
- // eslint-disable-next-line class-methods-use-this
- highlightTemplate(text, template) {
- return `"<b>${escape(text)}</b>" ${template}`;
- }
-
- // eslint-disable-next-line class-methods-use-this
- highlightTextMatches(text, term) {
- const occurrences = fuzzaldrinPlus.match(text, term);
- const { indexOf } = [];
-
- return text
- .split('')
- .map((character, i) => {
- if (indexOf.call(occurrences, i) !== -1) {
- return `<b>${character}</b>`;
- }
- return character;
- })
- .join('');
- }
-
- // eslint-disable-next-line class-methods-use-this
- noResults() {
- return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>';
- }
-
- rowClicked(el) {
- let field, groupName, selectedIndex, selectedObject, isMarking;
- const { fieldName } = this.options;
- const isInput = $(this.el).is('input');
- if (this.renderedData) {
- groupName = el.data('group');
- if (groupName) {
- selectedIndex = el.data('index');
- selectedObject = this.renderedData[groupName][selectedIndex];
- } else {
- selectedIndex = el.closest('li').index();
- this.selectedIndex = selectedIndex;
- selectedObject = this.renderedData[selectedIndex];
- }
- }
-
- if (this.options.vue) {
- if (el.hasClass(ACTIVE_CLASS)) {
- el.removeClass(ACTIVE_CLASS);
- } else {
- el.addClass(ACTIVE_CLASS);
- }
-
- return [selectedObject];
- }
-
- field = [];
- const value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
- if (isInput) {
- field = $(this.el);
- } else if (value != null) {
- field = this.dropdown
- .parent()
- .find(`input[name='${fieldName}'][value='${value.toString().replace(/'/g, "\\'")}']`);
- }
-
- if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
- return [selectedObject];
- }
-
- if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
- isMarking = false;
- el.removeClass(ACTIVE_CLASS);
- if (field && field.length) {
- this.clearField(field, isInput);
- }
- } else if (el.hasClass(INDETERMINATE_CLASS)) {
- isMarking = true;
- el.addClass(ACTIVE_CLASS);
- el.removeClass(INDETERMINATE_CLASS);
- if (field && field.length && value == null) {
- this.clearField(field, isInput);
- }
- if ((!field || !field.length) && fieldName) {
- this.addInput(fieldName, value, selectedObject);
- }
- } else {
- isMarking = true;
- if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
- this.dropdown.find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS);
- if (!isInput) {
- this.dropdown
- .parent()
- .find(`input[name='${fieldName}']`)
- .remove();
- }
- }
- if (field && field.length && value == null) {
- this.clearField(field, isInput);
- }
- // Toggle active class for the tick mark
- el.addClass(ACTIVE_CLASS);
- if (value != null) {
- if ((!field || !field.length) && fieldName) {
- this.addInput(fieldName, value, selectedObject);
- } else if (field && field.length) {
- field.val(value).trigger('change');
- }
- }
- }
-
- return [selectedObject, isMarking];
- }
-
- focusTextInput() {
- if (this.options.filterable) {
- const initialScrollTop = $(window).scrollTop();
-
- if (this.dropdown.is('.show') && !this.filterInput.is(':focus')) {
- this.filterInput.focus();
- }
-
- if ($(window).scrollTop() < initialScrollTop) {
- $(window).scrollTop(initialScrollTop);
- }
- }
- }
-
- addInput(fieldName, value, selectedObject, single) {
- // Create hidden input for form
- if (single) {
- $(`input[name="${fieldName}"]`).remove();
- }
-
- const $input = $('<input>')
- .attr('type', 'hidden')
- .attr('name', fieldName)
- .val(value);
- if (this.options.inputId != null) {
- $input.attr('id', this.options.inputId);
- }
-
- if (this.options.multiSelect) {
- Object.keys(selectedObject).forEach(attribute => {
- $input.attr(`data-${attribute}`, selectedObject[attribute]);
- });
- }
-
- if (this.options.inputMeta) {
- $input.attr('data-meta', selectedObject[this.options.inputMeta]);
- }
-
- this.dropdown.before($input).trigger('change');
- }
-
- selectRowAtIndex(index) {
- // If we pass an option index
- let selector;
- if (typeof index !== 'undefined') {
- selector = `${SELECTABLE_CLASSES}:eq(${index}) a`;
- } else {
- selector = '.dropdown-content .is-focused';
- }
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- selector = `.dropdown-page-one ${selector}`;
- }
- // simulate a click on the first link
- const $el = $(selector, this.dropdown);
- if ($el.length) {
- const href = $el.attr('href');
- if (href && href !== '#') {
- visitUrl(href);
- } else {
- $el.trigger('click');
- }
- }
- }
-
- addArrowKeyEvent() {
- const ARROW_KEY_CODES = [38, 40];
- let selector = SELECTABLE_CLASSES;
- if (this.dropdown.find('.dropdown-toggle-page').length) {
- selector = `.dropdown-page-one ${selector}`;
- }
- return $('body').on('keydown', e => {
- let $listItems, PREV_INDEX;
- const currentKeyCode = e.which;
- if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
- e.preventDefault();
- e.stopImmediatePropagation();
- PREV_INDEX = currentIndex;
- $listItems = $(selector, this.dropdown);
- // if @options.filterable
- // $input.blur()
- if (currentKeyCode === 40) {
- // Move down
- if (currentIndex < $listItems.length - 1) {
- currentIndex += 1;
- }
- } else if (currentKeyCode === 38) {
- // Move up
- if (currentIndex > 0) {
- currentIndex -= 1;
- }
- }
- if (currentIndex !== PREV_INDEX) {
- this.highlightRowAtIndex($listItems, currentIndex);
- }
- return false;
- }
- if (currentKeyCode === 13 && currentIndex !== -1) {
- e.preventDefault();
- this.selectRowAtIndex();
- }
- });
- }
-
- // eslint-disable-next-line class-methods-use-this
- removeArrowKeyEvent() {
- return $('body').off('keydown');
- }
-
- resetRows() {
- currentIndex = -1;
- $('.is-focused', this.dropdown).removeClass('is-focused');
- }
-
- highlightRowAtIndex($listItems, index) {
- if (!$listItems) {
- // eslint-disable-next-line no-param-reassign
- $listItems = $(SELECTABLE_CLASSES, this.dropdown);
- }
-
- // Remove the class for the previously focused row
- $('.is-focused', this.dropdown).removeClass('is-focused');
- // Update the class for the row at the specific index
- const $listItem = $listItems.eq(index);
- $listItem.find('a:first-child').addClass('is-focused');
- // Dropdown content scroll area
- const $dropdownContent = $listItem.closest('.dropdown-content');
- const dropdownScrollTop = $dropdownContent.scrollTop();
- const dropdownContentHeight = $dropdownContent.outerHeight();
- const dropdownContentTop = $dropdownContent.prop('offsetTop');
- const dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
- // Get the offset bottom of the list item
- const listItemHeight = $listItem.outerHeight();
- const listItemTop = $listItem.prop('offsetTop');
- const listItemBottom = listItemTop + listItemHeight;
- if (!index) {
- // Scroll the dropdown content to the top
- $dropdownContent.scrollTop(0);
- } else if (index === $listItems.length - 1) {
- // Scroll the dropdown content to the bottom
- $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
- } else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
- // Scroll the dropdown content down
- $dropdownContent.scrollTop(
- listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
- );
- } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
- // Scroll the dropdown content up
- return $dropdownContent.scrollTop(
- listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
- );
- }
- }
-
- updateLabel(selected = null, el = null, instance = null) {
- let toggleText = this.options.toggleLabel(selected, el, instance);
- if (this.options.updateLabel) {
- // Option to override the dropdown label text
- toggleText = this.options.updateLabel;
- }
-
- return $(this.el)
- .find('.dropdown-toggle-text')
- .text(toggleText);
- }
-
- // eslint-disable-next-line class-methods-use-this
- clearField(field, isInput) {
- return isInput ? field.val('') : field.remove();
- }
-}
+import { GitLabDropdown } from './gl_dropdown';
export default function initDeprecatedJQueryDropdown($el, opts) {
// eslint-disable-next-line func-names
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index 2e8b3d764ca..b6d5eb7e5c0 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -238,6 +238,7 @@ class Clusters::ClustersController < Clusters::BaseController
:environment_scope,
:managed,
provider_aws_attributes: [
+ :kubernetes_version,
:key_name,
:role_arn,
:region,
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index 2a8bc823931..9ad6bf4095a 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -21,7 +21,7 @@ class Projects::PagesController < Projects::ApplicationController
format.html do
redirect_to project_pages_path(@project),
status: :found,
- notice: 'Pages were removed'
+ notice: 'Pages were scheduled for removal'
end
end
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index b70d0b7a06a..37da29b32ff 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -110,7 +110,9 @@ class MergeRequestsFinder < IssuableFinder
.or(table[:title].matches('WIP %'))
.or(table[:title].matches('[WIP]%'))
- return items unless Feature.enabled?(:merge_request_draft_filter)
+ # Let's keep this FF around until https://gitlab.com/gitlab-org/gitlab/-/issues/232999
+ # is implemented
+ return items unless Feature.enabled?(:merge_request_draft_filter, default_enabled: true)
items
.or(table[:title].matches('Draft - %'))
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index 86869361ed8..35e8b751b3d 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -37,7 +37,7 @@ module Clusters
greater_than: 0
}
- validates :key_name, :region, :instance_type, :security_group_id, length: { in: 1..255 }
+ validates :kubernetes_version, :key_name, :region, :instance_type, :security_group_id, length: { in: 1..255 }
validates :subnet_ids, presence: true
def nullify_credentials
diff --git a/app/services/clusters/aws/provision_service.rb b/app/services/clusters/aws/provision_service.rb
index 109e4c04a9c..b454a7a5f59 100644
--- a/app/services/clusters/aws/provision_service.rb
+++ b/app/services/clusters/aws/provision_service.rb
@@ -63,6 +63,7 @@ module Clusters
[
parameter('ClusterName', provider.cluster.name),
parameter('ClusterRole', provider.role_arn),
+ parameter('KubernetesVersion', provider.kubernetes_version),
parameter('ClusterControlPlaneSecurityGroup', provider.security_group_id),
parameter('VpcId', provider.vpc_id),
parameter('Subnets', provider.subnet_ids.join(',')),
diff --git a/app/services/pages/delete_service.rb b/app/services/pages/delete_service.rb
index 7408943e78c..1ac12af9a2a 100644
--- a/app/services/pages/delete_service.rb
+++ b/app/services/pages/delete_service.rb
@@ -3,8 +3,11 @@
module Pages
class DeleteService < BaseService
def execute
- project.remove_pages
- project.pages_domains.destroy_all # rubocop: disable Cop/DestroyAll
+ if Feature.enabled?(:async_pages_removal, project)
+ PagesRemoveWorker.perform_async(project.id)
+ else
+ PagesRemoveWorker.new.perform(project.id)
+ end
end
end
end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e16032cdc50..57f09fbfc2f 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1358,7 +1358,7 @@
:tags: []
- :name: flush_counter_increments
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1450,7 +1450,7 @@
:urgency: :high
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent:
:tags: []
- :name: merge_request_mergeability_check
:feature_category: :source_code_management
@@ -1532,6 +1532,14 @@
:weight: 1
:idempotent:
:tags: []
+- :name: pages_remove
+ :feature_category: :pages
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: pages_update_configuration
:feature_category: :pages
:has_external_dependencies:
diff --git a/app/workers/pages_remove_worker.rb b/app/workers/pages_remove_worker.rb
new file mode 100644
index 00000000000..b83168fd7bd
--- /dev/null
+++ b/app/workers/pages_remove_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class PagesRemoveWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: 3
+ feature_category :pages
+ loggable_arguments 0
+
+ def perform(project_id)
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ project.remove_pages
+ project.pages_domains.delete_all
+ end
+end
diff --git a/changelogs/unreleased/231463-gitlab-ci-aws-eks-integration-kubernetes-version-update.yml b/changelogs/unreleased/231463-gitlab-ci-aws-eks-integration-kubernetes-version-update.yml
new file mode 100644
index 00000000000..054eea85a6d
--- /dev/null
+++ b/changelogs/unreleased/231463-gitlab-ci-aws-eks-integration-kubernetes-version-update.yml
@@ -0,0 +1,5 @@
+---
+title: Update EKS Kubernetes versions
+merge_request: 38644
+author:
+type: fixed
diff --git a/changelogs/unreleased/djensen-finish-renaming-analytics-workspace.yml b/changelogs/unreleased/djensen-finish-renaming-analytics-workspace.yml
new file mode 100644
index 00000000000..ad84a3ed4c5
--- /dev/null
+++ b/changelogs/unreleased/djensen-finish-renaming-analytics-workspace.yml
@@ -0,0 +1,5 @@
+---
+title: Re-name Analytics Workspace as instance-level analytics
+merge_request: 40436
+author:
+type: changed
diff --git a/changelogs/unreleased/id-enable-merge-request-draft-filter.yml b/changelogs/unreleased/id-enable-merge-request-draft-filter.yml
new file mode 100644
index 00000000000..88d29ee7733
--- /dev/null
+++ b/changelogs/unreleased/id-enable-merge-request-draft-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Include draft merge request into filter response
+merge_request: 40376
+author:
+type: changed
diff --git a/config/feature_flags/development/async_pages_removal.yml b/config/feature_flags/development/async_pages_removal.yml
new file mode 100644
index 00000000000..2377f7764cc
--- /dev/null
+++ b/config/feature_flags/development/async_pages_removal.yml
@@ -0,0 +1,7 @@
+---
+name: async_pages_removal
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38313
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/239318
+group: team::Scalability
+type: development
+default_enabled: false \ No newline at end of file
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index cda830de8f6..2e04522c824 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -178,6 +178,8 @@
- 1
- - pages_domain_verification
- 1
+- - pages_remove
+ - 1
- - pages_update_configuration
- 1
- - personal_access_tokens
diff --git a/db/migrate/20200818052219_add_kubernetes_version_to_cluster_providers_aws.rb b/db/migrate/20200818052219_add_kubernetes_version_to_cluster_providers_aws.rb
new file mode 100644
index 00000000000..fa3d5210e91
--- /dev/null
+++ b/db/migrate/20200818052219_add_kubernetes_version_to_cluster_providers_aws.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddKubernetesVersionToClusterProvidersAws < ActiveRecord::Migration[6.0]
+ # Uncomment the following include if you require helper functions:
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless column_exists?(:cluster_providers_aws, :kubernetes_version)
+ add_column :cluster_providers_aws, :kubernetes_version, :text, null: false, default: '1.14'
+ end
+
+ add_text_limit :cluster_providers_aws, :kubernetes_version, 30
+ end
+
+ def down
+ if column_exists?(:cluster_providers_aws, :kubernetes_version)
+ remove_column :cluster_providers_aws, :kubernetes_version
+ end
+ end
+end
diff --git a/db/schema_migrations/20200818052219 b/db/schema_migrations/20200818052219
new file mode 100644
index 00000000000..1df09a53ca4
--- /dev/null
+++ b/db/schema_migrations/20200818052219
@@ -0,0 +1 @@
+94b494b5f8e351cf453699debf03aa28f8a9136c829cb7410c90590b5106cdd5 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index bec2f11356c..8d71b8d94fc 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10593,7 +10593,9 @@ CREATE TABLE public.cluster_providers_aws (
encrypted_secret_access_key_iv character varying(255),
encrypted_secret_access_key text,
session_token text,
- status_reason text
+ status_reason text,
+ kubernetes_version text DEFAULT '1.14'::text NOT NULL,
+ CONSTRAINT check_f1f42cd85e CHECK ((char_length(kubernetes_version) <= 30))
);
CREATE SEQUENCE public.cluster_providers_aws_id_seq
diff --git a/doc/user/analytics/index.md b/doc/user/analytics/index.md
index e12ec320543..044b9eb3e64 100644
--- a/doc/user/analytics/index.md
+++ b/doc/user/analytics/index.md
@@ -6,15 +6,15 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Analytics
-## Analytics workspace
+## Instance-level analytics
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12077) in GitLab 12.2.
-The Analytics workspace will make it possible to aggregate analytics across
+Instance-level analytics make it possible to aggregate analytics across
GitLab, so that users can view information across multiple projects and groups
in one place.
-To access the Analytics workspace, click on **More > Analytics** in the top navigation bar.
+[Learn more about instance-level analytics](../admin_area/analytics/index.md).
## Group-level analytics
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index eb3de195626..6f00db42d78 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -5,7 +5,7 @@ return if Rails.env.production?
namespace :gitlab do
namespace :sidekiq do
def write_yaml(path, banner, object)
- File.write(path, banner + YAML.dump(object))
+ File.write(path, banner + YAML.dump(object).gsub(/ *$/m, ''))
end
namespace :all_queues_yml do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 24b6ccc509b..f4181b68b0d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2834,9 +2834,6 @@ msgstr ""
msgid "An error occurred while saving assignees"
msgstr ""
-msgid "An error occurred while saving the approval settings"
-msgstr ""
-
msgid "An error occurred while saving the template. Please check if the template exists."
msgstr ""
@@ -21124,6 +21121,9 @@ msgstr ""
msgid "Rook"
msgstr ""
+msgid "Rule name is already taken."
+msgstr ""
+
msgid "Rules that define what git pushes are accepted for a project in this group. All newly created projects in this group will use these settings."
msgstr ""
@@ -24893,10 +24893,10 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
-msgid "There was an error while fetching the chart data."
+msgid "There was an error while fetching the chart data. Please refresh the page to try again."
msgstr ""
-msgid "There was an error while fetching the table data."
+msgid "There was an error while fetching the table data. Please refresh the page to try again."
msgstr ""
msgid "There was an error while fetching value stream analytics %{requestTypeName} data."
diff --git a/package.json b/package.json
index f5bcb056c7d..5ea9919883b 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.161.0",
- "@gitlab/ui": "20.8.0",
+ "@gitlab/ui": "20.9.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.10.2",
diff --git a/rubocop/cop/rspec/timecop_freeze.rb b/rubocop/cop/rspec/timecop_freeze.rb
new file mode 100644
index 00000000000..508b5df7c7f
--- /dev/null
+++ b/rubocop/cop/rspec/timecop_freeze.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module RSpec
+ # This cop checks for `Timecop.freeze` usage in specs.
+ #
+ # @example
+ #
+ # # bad
+ # Timecop.freeze(Time.current) { example.run }
+ #
+ # # good
+ # freeze_time(Time.current) { example.run }
+ #
+ class TimecopFreeze < RuboCop::Cop::Cop
+ include MatchRange
+ MESSAGE = 'Do not use `Timecop.freeze`, use `freeze_time` instead. ' \
+ 'See https://gitlab.com/gitlab-org/gitlab/-/issues/214432 for more info.'
+
+ def_node_matcher :timecop_freeze?, <<~PATTERN
+ (send (const nil? :Timecop) :freeze ?_)
+ PATTERN
+
+ def on_send(node)
+ return unless timecop_freeze?(node)
+
+ add_offense(node, location: :expression, message: MESSAGE)
+ end
+
+ def autocorrect(node)
+ -> (corrector) do
+ each_match_range(node.source_range, /^(Timecop\.freeze)/) do |match_range|
+ corrector.replace(match_range, 'freeze_time')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/providers/aws.rb b/spec/factories/clusters/providers/aws.rb
index 2c54300e606..497181de89a 100644
--- a/spec/factories/clusters/providers/aws.rb
+++ b/spec/factories/clusters/providers/aws.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
factory :cluster_provider_aws, class: 'Clusters::Providers::Aws' do
association :cluster, platform_type: :kubernetes, provider_type: :aws
+ kubernetes_version { '1.16' }
role_arn { 'arn:aws:iam::123456789012:role/role-name' }
vpc_id { 'vpc-00000000000000000' }
subnet_ids { %w(subnet-00000000000000000 subnet-11111111111111111) }
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index e1ace817c72..243579ee2f7 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -380,14 +380,14 @@ RSpec.shared_examples 'pages settings editing' do
expect(project).to be_pages_deployed
end
- it 'removes the pages' do
+ it 'removes the pages', :sidekiq_inline do
visit project_pages_path(project)
expect(page).to have_link('Remove pages')
accept_confirm { click_link 'Remove pages' }
- expect(page).to have_content('Pages were removed')
+ expect(page).to have_content('Pages were scheduled for removal')
expect(project.reload.pages_deployed?).to be_falsey
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 3577498c3b4..970500985ae 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -159,7 +159,7 @@ RSpec.describe 'Project' do
describe 'remove forked relationship', :js do
let(:user) { create(:user) }
- let(:project) { fork_project(create(:project, :public), user, namespace_id: user.namespace) }
+ let(:project) { fork_project(create(:project, :public), user, namespace: user.namespace) }
before do
sign_in user
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index b97d4dbf355..82fa7cf8d0a 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -48,7 +48,7 @@ describe('Application Row', () => {
describe('Install button', () => {
const button = () => wrapper.find('.js-cluster-application-install-button');
const checkButtonState = (label, loading, disabled) => {
- expect(button().props('label')).toEqual(label);
+ expect(button().text()).toEqual(label);
expect(button().props('loading')).toEqual(loading);
expect(button().props('disabled')).toEqual(disabled);
};
@@ -56,7 +56,7 @@ describe('Application Row', () => {
it('has indeterminate state on page load', () => {
mountComponent({ status: null });
- expect(button().props('label')).toBeUndefined();
+ expect(button().text()).toBe('');
});
it('has install button', () => {
@@ -225,7 +225,7 @@ describe('Application Row', () => {
mountComponent({ updateAvailable: true });
expect(button().exists()).toBe(true);
- expect(button().props('label')).toContain('Update');
+ expect(button().text()).toContain('Update');
});
it('has enabled "Retry update" when update process fails', () => {
@@ -235,14 +235,14 @@ describe('Application Row', () => {
});
expect(button().exists()).toBe(true);
- expect(button().props('label')).toContain('Retry update');
+ expect(button().text()).toContain('Retry update');
});
it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => {
mountComponent({ status: APPLICATION_STATUS.UPDATING });
expect(button().exists()).toBe(true);
- expect(button().props('label')).toContain('Updating');
+ expect(button().text()).toContain('Updating');
});
it('clicking update button emits event', () => {
diff --git a/spec/frontend/clusters/components/uninstall_application_button_spec.js b/spec/frontend/clusters/components/uninstall_application_button_spec.js
index 9f9397d4d41..387e2188572 100644
--- a/spec/frontend/clusters/components/uninstall_application_button_spec.js
+++ b/spec/frontend/clusters/components/uninstall_application_button_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS;
@@ -19,14 +19,21 @@ describe('UninstallApplicationButton', () => {
});
describe.each`
- status | loading | disabled | label
+ status | loading | disabled | text
${INSTALLED} | ${false} | ${false} | ${'Uninstall'}
${UPDATING} | ${false} | ${true} | ${'Uninstall'}
${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'}
- `('when app status is $status', ({ loading, disabled, status, label }) => {
- it(`renders a loading=${loading}, disabled=${disabled} button with label="${label}"`, () => {
+ `('when app status is $status', ({ loading, disabled, status, text }) => {
+ beforeAll(() => {
createComponent({ status });
- expect(wrapper.find(LoadingButton).props()).toMatchObject({ loading, disabled, label });
+ });
+
+ it(`renders a button with loading=${loading} and disabled=${disabled}`, () => {
+ expect(wrapper.find(GlButton).props()).toMatchObject({ loading, disabled });
+ });
+
+ it(`renders a button with text="${text}"`, () => {
+ expect(wrapper.find(GlButton).text()).toBe(text);
});
});
});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
index 34d9ee733c4..6d0f8503103 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
@@ -147,6 +147,7 @@ describe('EksClusterConfigurationForm', () => {
initialState: {
clusterName: 'cluster name',
environmentScope: '*',
+ kubernetesVersion: '1.16',
selectedRegion: 'region',
selectedRole: 'role',
selectedKeyPair: 'key pair',
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
index 882a4a002bd..ed753888790 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -47,7 +47,7 @@ describe('EKS Cluster Store Actions', () => {
beforeEach(() => {
clusterName = 'my cluster';
environmentScope = 'production';
- kubernetesVersion = '11.1';
+ kubernetesVersion = '1.16';
region = 'regions-1';
vpc = 'vpc-1';
subnet = 'subnet-1';
@@ -180,6 +180,7 @@ describe('EKS Cluster Store Actions', () => {
environment_scope: environmentScope,
managed: gitlabManagedCluster,
provider_aws_attributes: {
+ kubernetes_version: kubernetesVersion,
region,
vpc_id: vpc,
subnet_ids: subnet,
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index fef5e018231..5629036694c 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe Backup::Repository do
end
end
- describe '#dump' do
+ describe '#dump', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/241251' do
before do
allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(storage_keys)
end
diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb
index 53e732928ff..f4c6de00e40 100644
--- a/spec/requests/api/pages/pages_spec.rb
+++ b/spec/requests/api/pages/pages_spec.rb
@@ -39,7 +39,9 @@ RSpec.describe API::Pages do
expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
- delete api("/projects/#{project.id}/pages", admin )
+ Sidekiq::Testing.inline! do
+ delete api("/projects/#{project.id}/pages", admin )
+ end
expect(project.reload.pages_metadatum.deployed?).to be(false)
end
diff --git a/spec/rubocop/cop/rspec/timecop_freeze_spec.rb b/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
new file mode 100644
index 00000000000..3809431a2fc
--- /dev/null
+++ b/spec/rubocop/cop/rspec/timecop_freeze_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/rspec/timecop_freeze'
+
+RSpec.describe RuboCop::Cop::RSpec::TimecopFreeze, type: :rubocop do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when calling Timecop.freeze' do
+ let(:source) do
+ <<~SRC
+ Timecop.freeze(Time.current) { example.run }
+ SRC
+ end
+
+ let(:corrected_source) do
+ <<~SRC
+ freeze_time(Time.current) { example.run }
+ SRC
+ end
+
+ it 'registers an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'can autocorrect the source' do
+ expect(autocorrect_source(source)).to eq(corrected_source)
+ end
+ end
+
+ context 'when calling a different method on Timecop' do
+ let(:source) do
+ <<~SRC
+ Timecop.travel(Time.current)
+ SRC
+ end
+
+ it 'does not register an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+end
diff --git a/spec/services/clusters/aws/provision_service_spec.rb b/spec/services/clusters/aws/provision_service_spec.rb
index 529e1d26575..52612e5ac40 100644
--- a/spec/services/clusters/aws/provision_service_spec.rb
+++ b/spec/services/clusters/aws/provision_service_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe Clusters::Aws::ProvisionService do
[
{ parameter_key: 'ClusterName', parameter_value: provider.cluster.name },
{ parameter_key: 'ClusterRole', parameter_value: provider.role_arn },
+ { parameter_key: 'KubernetesVersion', parameter_value: provider.kubernetes_version },
{ parameter_key: 'ClusterControlPlaneSecurityGroup', parameter_value: provider.security_group_id },
{ parameter_key: 'VpcId', parameter_value: provider.vpc_id },
{ parameter_key: 'Subnets', parameter_value: provider.subnet_ids.join(',') },
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 7f22af8bfc6..db25bb766c9 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -348,7 +348,7 @@ RSpec.describe Git::BranchHooksService do
context 'when the project is forked', :sidekiq_might_not_need_inline do
let(:upstream_project) { project }
- let(:forked_project) { fork_project(upstream_project, user, repository: true) }
+ let(:forked_project) { fork_project(upstream_project, user, repository: true, using_service: true) }
let!(:forked_service) do
described_class.new(forked_project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref })
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index f99be26927d..20ed57c6e51 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -88,6 +88,10 @@ RSpec.describe MergeRequests::BuildService do
let(:source_project) { fork_project(project, user) }
let(:merge_request) { described_class.new(project, user, mr_params).execute }
+ before do
+ project.add_reporter(user)
+ end
+
it 'assigns force_remove_source_branch' do
expect(merge_request.force_remove_source_branch?).to be_truthy
end
diff --git a/spec/services/pages/delete_services_spec.rb b/spec/services/pages/delete_services_spec.rb
index f6d4694b4dd..661ccb36ba9 100644
--- a/spec/services/pages/delete_services_spec.rb
+++ b/spec/services/pages/delete_services_spec.rb
@@ -3,25 +3,45 @@
require 'spec_helper'
RSpec.describe Pages::DeleteService do
- let_it_be(:project) { create(:project, path: "my.project")}
- let_it_be(:admin) { create(:admin) }
- let_it_be(:domain) { create(:pages_domain, project: project) }
- let_it_be(:service) { described_class.new(project, admin)}
+ shared_examples 'remove pages' do
+ let_it_be(:project) { create(:project, path: "my.project")}
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:domain) { create(:pages_domain, project: project) }
+ let_it_be(:service) { described_class.new(project, admin)}
- it 'deletes published pages' do
- expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
- expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
+ it 'deletes published pages' do
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
+ expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
- service.execute
+ Sidekiq::Testing.inline! { service.execute }
- expect(project.reload.pages_metadatum.deployed?).to be(false)
+ expect(project.reload.pages_metadatum.deployed?).to be(false)
+ end
+
+ it 'deletes all domains' do
+ expect(project.pages_domains.count).to be 1
+
+ Sidekiq::Testing.inline! { service.execute }
+
+ expect(project.reload.pages_domains.count).to be 0
+ end
end
- it 'deletes all domains' do
- expect(project.pages_domains.count).to be 1
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(async_pages_removal: false)
+
+ expect(PagesRemoveWorker).not_to receive(:perform_async)
+ end
+
+ it_behaves_like 'remove pages'
+ end
- service.execute
+ context 'with feature flag enabled' do
+ before do
+ expect(PagesRemoveWorker).to receive(:perform_async).and_call_original
+ end
- expect(project.reload.pages_domains.count).to be 0
+ it_behaves_like 'remove pages'
end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 925c2ff5d88..166a2dae55b 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Projects::ForkService do
it 'flushes the forks count cache of the source project', :clean_gitlab_redis_cache do
expect(from_project.forks_count).to be_zero
- fork_project(from_project, to_user)
+ fork_project(from_project, to_user, using_service: true)
BatchLoader::Executor.clear_current
expect(from_project.forks_count).to eq(1)
@@ -40,7 +40,7 @@ RSpec.describe Projects::ForkService do
@guest = create(:user)
@from_project.add_user(@guest, :guest)
end
- subject { fork_project(@from_project, @guest) }
+ subject { fork_project(@from_project, @guest, using_service: true) }
it { is_expected.not_to be_persisted }
it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
@@ -56,7 +56,7 @@ RSpec.describe Projects::ForkService do
end
describe "successfully creates project in the user namespace" do
- let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
+ let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace, using_service: true) }
it { expect(to_project).to be_persisted }
it { expect(to_project.errors).to be_empty }
@@ -92,21 +92,21 @@ RSpec.describe Projects::ForkService do
end
it 'imports the repository of the forked project', :sidekiq_might_not_need_inline do
- to_project = fork_project(@from_project, @to_user, repository: true)
+ to_project = fork_project(@from_project, @to_user, repository: true, using_service: true)
expect(to_project.empty_repo?).to be_falsy
end
end
context 'creating a fork of a fork' do
- let(:from_forked_project) { fork_project(@from_project, @to_user) }
+ let(:from_forked_project) { fork_project(@from_project, @to_user, using_service: true) }
let(:other_namespace) do
group = create(:group)
group.add_owner(@to_user)
group
end
- let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
+ let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace, using_service: true) }
it 'sets the root of the network to the root project' do
expect(to_project.fork_network.root_project).to eq(@from_project)
@@ -126,7 +126,7 @@ RSpec.describe Projects::ForkService do
context 'project already exists' do
it "fails due to validation, not transaction failure" do
@existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
- @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
+ @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace, using_service: true)
expect(@existing_project).to be_persisted
expect(@to_project).not_to be_persisted
@@ -137,7 +137,7 @@ RSpec.describe Projects::ForkService do
context 'repository in legacy storage already exists' do
let(:fake_repo_path) { File.join(TestEnv.repos_path, @to_user.namespace.full_path, "#{@from_project.path}.git") }
- let(:params) { { namespace: @to_user.namespace } }
+ let(:params) { { namespace: @to_user.namespace, using_service: true } }
before do
stub_application_setting(hashed_storage_enabled: false)
@@ -169,13 +169,13 @@ RSpec.describe Projects::ForkService do
context 'GitLab CI is enabled' do
it "forks and enables CI for fork" do
@from_project.enable_ci
- @to_project = fork_project(@from_project, @to_user)
+ @to_project = fork_project(@from_project, @to_user, using_service: true)
expect(@to_project.builds_enabled?).to be_truthy
end
end
context "CI/CD settings" do
- let(:to_project) { fork_project(@from_project, @to_user) }
+ let(:to_project) { fork_project(@from_project, @to_user, using_service: true) }
context "when origin has git depth specified" do
before do
@@ -206,7 +206,7 @@ RSpec.describe Projects::ForkService do
end
it "creates fork with lowest level" do
- forked_project = fork_project(@from_project, @to_user)
+ forked_project = fork_project(@from_project, @to_user, using_service: true)
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
@@ -218,7 +218,7 @@ RSpec.describe Projects::ForkService do
end
it "creates fork with private visibility levels" do
- forked_project = fork_project(@from_project, @to_user)
+ forked_project = fork_project(@from_project, @to_user, using_service: true)
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
@@ -232,7 +232,7 @@ RSpec.describe Projects::ForkService do
end
it 'fails' do
- to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
+ to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace, using_service: true)
expect(to_project.errors[:forked_from_project_id]).to eq(['is forbidden'])
end
@@ -253,7 +253,7 @@ RSpec.describe Projects::ForkService do
@group.add_user(@developer, GroupMember::DEVELOPER)
@project.add_user(@developer, :developer)
@project.add_user(@group_owner, :developer)
- @opts = { namespace: @group }
+ @opts = { namespace: @group, using_service: true }
end
context 'fork project for group' do
@@ -299,7 +299,7 @@ RSpec.describe Projects::ForkService do
group_owner = create(:user)
private_group.add_owner(group_owner)
- forked_project = fork_project(public_project, group_owner, namespace: private_group)
+ forked_project = fork_project(public_project, group_owner, namespace: private_group, using_service: true)
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
@@ -310,7 +310,7 @@ RSpec.describe Projects::ForkService do
context 'when a project is already forked' do
it 'creates a new poolresository after the project is moved to a new shard' do
project = create(:project, :public, :repository)
- fork_before_move = fork_project(project)
+ fork_before_move = fork_project(project, nil, using_service: true)
# Stub everything required to move a project to a Gitaly shard that does not exist
allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original
@@ -329,7 +329,7 @@ RSpec.describe Projects::ForkService do
destination_storage_name: 'test_second_storage'
)
Projects::UpdateRepositoryStorageService.new(storage_move).execute
- fork_after_move = fork_project(project.reload)
+ fork_after_move = fork_project(project.reload, nil, using_service: true)
pool_repository_before_move = PoolRepository.joins(:shard)
.find_by(source_project: project, shards: { name: 'default' })
pool_repository_after_move = PoolRepository.joins(:shard)
@@ -350,7 +350,7 @@ RSpec.describe Projects::ForkService do
context 'when no pool exists' do
it 'creates a new object pool' do
- forked_project = fork_project(fork_from_project, forker)
+ forked_project = fork_project(fork_from_project, forker, using_service: true)
expect(forked_project.pool_repository).to eq(fork_from_project.pool_repository)
end
@@ -360,7 +360,7 @@ RSpec.describe Projects::ForkService do
let!(:pool_repository) { create(:pool_repository, source_project: fork_from_project) }
it 'joins the object pool' do
- forked_project = fork_project(fork_from_project, forker)
+ forked_project = fork_project(fork_from_project, forker, using_service: true)
expect(forked_project.pool_repository).to eq(fork_from_project.pool_repository)
end
diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
index a32e39e52c8..4b4285f251e 100644
--- a/spec/support/helpers/project_forks_helper.rb
+++ b/spec/support/helpers/project_forks_helper.rb
@@ -17,14 +17,26 @@ module ProjectForksHelper
project.add_developer(user)
end
- unless params[:namespace] || params[:namespace_id]
+ unless params[:namespace]
params[:namespace] = create(:group)
params[:namespace].add_owner(user)
end
+ namespace = params[:namespace]
+ create_repository = params.delete(:repository)
+
+ unless params[:target_project] || params[:using_service]
+ target_level = [project.visibility_level, namespace.visibility_level].min
+ visibility_level = Gitlab::VisibilityLevel.closest_allowed_level(target_level)
+
+ params[:target_project] =
+ create(:project,
+ (:repository if create_repository),
+ visibility_level: visibility_level, creator: user, namespace: namespace)
+ end
+
service = Projects::ForkService.new(project, user, params)
- create_repository = params.delete(:repository)
# Avoid creating a repository
unless create_repository
allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
diff --git a/spec/workers/pages_remove_worker_spec.rb b/spec/workers/pages_remove_worker_spec.rb
new file mode 100644
index 00000000000..638e87043f2
--- /dev/null
+++ b/spec/workers/pages_remove_worker_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PagesRemoveWorker do
+ let_it_be(:project) { create(:project, path: "my.project")}
+ let_it_be(:domain) { create(:pages_domain, project: project) }
+ subject { described_class.new.perform(project.id) }
+
+ it 'deletes published pages' do
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
+ expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
+
+ subject
+
+ expect(project.reload.pages_metadatum.deployed?).to be(false)
+ end
+
+ it 'deletes all domains' do
+ expect(project.pages_domains.count).to be 1
+
+ subject
+
+ expect(project.reload.pages_domains.count).to be 0
+ end
+end
diff --git a/vendor/aws/cloudformation/eks_cluster.yaml b/vendor/aws/cloudformation/eks_cluster.yaml
index c32f54d66dc..c3f9c73cd1f 100644
--- a/vendor/aws/cloudformation/eks_cluster.yaml
+++ b/vendor/aws/cloudformation/eks_cluster.yaml
@@ -7,11 +7,12 @@ Parameters:
KubernetesVersion:
Description: The Kubernetes version to install
Type: String
- Default: 1.14
+ Default: 1.16
AllowedValues:
- - 1.12
- - 1.13
- 1.14
+ - 1.15
+ - 1.16
+ - 1.17
KeyName:
Description: The EC2 Key Pair to allow SSH access to the node instances
diff --git a/yarn.lock b/yarn.lock
index 4c1ae0b398a..85a2ab6257a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -848,10 +848,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.161.0.tgz#661e8d19862dfba0e4c558e2eb6d64b402c1453e"
integrity sha512-qsbboEICn08ZoEoAX/TuYygsFaXlzsCY+CfmdOzqvJbOdfHhVXmrJBxd2hP2qqjTZm2PkbRRmn+03+ce1jvatQ==
-"@gitlab/ui@20.8.0":
- version "20.8.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.8.0.tgz#00ecc698d2ed003f87fd59ea408f999c804d7dda"
- integrity sha512-BHXDFtY1Zw6rIkUopAJQMoDpWlAKyDZiHR13U6Tv+Ot0G6p6tW6PVVHb12m+fycNxBDKdYisdLJwH8MKNjWRrA==
+"@gitlab/ui@20.9.1":
+ version "20.9.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-20.9.1.tgz#1e5960102a0c41925a531d6b8d00072b4717da54"
+ integrity sha512-6C03BakNqharsDVChnIYzMbp0vj4syfoo4YDxJ1Ktfev8wq4gy5HrcoZt5dgNNXEfmwbyFnJlXaKBNDlLpbmVA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"