diff options
39 files changed, 500 insertions, 395 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 637d0dbde23..4dddb6eb0d6 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -14,6 +14,7 @@ import { import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import applications from './components/applications.vue'; +import setupToggleButtons from '../toggle_buttons'; /** * Cluster page has 2 separate parts: @@ -48,12 +49,9 @@ export default class Clusters { installPrometheusEndpoint: installPrometheusPath, }); - this.toggle = this.toggle.bind(this); this.installApplication = this.installApplication.bind(this); this.showToken = this.showToken.bind(this); - this.toggleButton = document.querySelector('.js-toggle-cluster'); - this.toggleInput = document.querySelector('.js-toggle-input'); this.errorContainer = document.querySelector('.js-cluster-error'); this.successContainer = document.querySelector('.js-cluster-success'); this.creatingContainer = document.querySelector('.js-cluster-creating'); @@ -63,6 +61,7 @@ export default class Clusters { this.tokenField = document.querySelector('.js-cluster-token'); initSettingsPanels(); + setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(); if (this.store.state.status !== 'created') { @@ -101,13 +100,11 @@ export default class Clusters { } addListeners() { - this.toggleButton.addEventListener('click', this.toggle); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); } removeListeners() { - this.toggleButton.removeEventListener('click', this.toggle); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); eventHub.$off('installApplication', this.installApplication); } @@ -151,11 +148,6 @@ export default class Clusters { this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); } - toggle() { - this.toggleButton.classList.toggle('is-checked'); - this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString()); - } - showToken() { const type = this.tokenField.getAttribute('type'); diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index 6844d1dbd83..2e3ad244375 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,58 +1,20 @@ import Flash from '../flash'; import { s__ } from '../locale'; +import setupToggleButtons from '../toggle_buttons'; import ClustersService from './services/clusters_service'; -/** - * Toggles loading and disabled classes. - * @param {HTMLElement} button - */ -const toggleLoadingButton = (button) => { - if (button.getAttribute('disabled')) { - button.removeAttribute('disabled'); - } else { - button.setAttribute('disabled', true); - } - - button.classList.toggle('is-loading'); -}; -/** - * Toggles checked class for the given button - * @param {HTMLElement} button - */ -const toggleValue = (button) => { - button.classList.toggle('is-checked'); +export default () => { + const clusterList = document.querySelector('.js-clusters-list'); + // The empty state won't have a clusterList + if (clusterList) { + setupToggleButtons( + document.querySelector('.js-clusters-list'), + (value, toggle) => + ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } }) + .catch((err) => { + Flash(s__('ClusterIntegration|Something went wrong on our end.')); + throw err; + }), + ); + } }; - -/** - * Handles toggle buttons in the cluster's table. - * - * When the user clicks the toggle button for each cluster, it: - * - toggles the button - * - shows a loading and disables button - * - Makes a put request to the given endpoint - * Once we receive the response, either: - * 1) Show updated status in case of successfull response - * 2) Show initial status in case of failed response - */ -export default function setClusterTableToggles() { - document.querySelectorAll('.js-toggle-cluster-list') - .forEach(button => button.addEventListener('click', (e) => { - const toggleButton = e.currentTarget; - const endpoint = toggleButton.getAttribute('data-endpoint'); - - toggleValue(toggleButton); - toggleLoadingButton(toggleButton); - - const value = toggleButton.classList.contains('is-checked'); - - ClustersService.updateCluster(endpoint, { cluster: { enabled: value } }) - .then(() => { - toggleLoadingButton(toggleButton); - }) - .catch(() => { - toggleLoadingButton(toggleButton); - toggleValue(toggleButton); - Flash(s__('ClusterIntegration|Something went wrong on our end.')); - }); - })); -} diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 144caf1d278..e2a008e8904 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ import { localTimeAgo } from './lib/utils/datetime_utility'; +import axios from './lib/utils/axios_utils'; export default class Compare { constructor(opts) { @@ -41,17 +42,14 @@ export default class Compare { } getTargetProject() { - return $.ajax({ - url: this.opts.targetProjectUrl, - data: { - target_project_id: $("input[name='merge_request[target_project_id]']").val() - }, - beforeSend: function() { - return $('.mr_target_commit').empty(); + $('.mr_target_commit').empty(); + + return axios.get(this.opts.targetProjectUrl, { + params: { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), }, - success: function(html) { - return $('.js-target-branch-dropdown .dropdown-content').html(html); - } + }).then(({ data }) => { + $('.js-target-branch-dropdown .dropdown-content').html(data); }); } @@ -68,22 +66,19 @@ export default class Compare { }); } - static sendAjax(url, loading, target, data) { - var $target; - $target = $(target); - return $.ajax({ - url: url, - data: data, - beforeSend: function() { - loading.show(); - return $target.empty(); - }, - success: function(html) { - loading.hide(); - $target.html(html); - var className = '.' + $target[0].className.replace(' ', '.'); - localTimeAgo($('.js-timeago', className)); - } + static sendAjax(url, loading, target, params) { + const $target = $(target); + + loading.show(); + $target.empty(); + + return axios.get(url, { + params, + }).then(({ data }) => { + loading.hide(); + $target.html(data); + const className = '.' + $target[0].className.replace(' ', '.'); + localTimeAgo($('.js-timeago', className)); }); } } diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index e633ef8a29e..59899e97be1 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -1,4 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ +import { __ } from './locale'; +import axios from './lib/utils/axios_utils'; +import flash from './flash'; export default function initCompareAutocomplete() { $('.js-compare-dropdown').each(function() { @@ -10,15 +13,14 @@ export default function initCompareAutocomplete() { const $filterInput = $('input[type="search"]', $dropdownContainer); $dropdown.glDropdown({ data: function(term, callback) { - return $.ajax({ - url: $dropdown.data('refs-url'), - data: { + axios.get($dropdown.data('refsUrl'), { + params: { ref: $dropdown.data('ref'), search: term, - } - }).done(function(refs) { - return callback(refs); - }); + }, + }).then(({ data }) => { + callback(data); + }).catch(() => flash(__('Error fetching refs'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index bc23a72762f..482d83621e2 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -1,5 +1,6 @@ /* eslint-disable no-new */ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; @@ -74,60 +75,52 @@ export default class CreateMergeRequestDropdown { } checkAbilityToCreateBranch() { - return $.ajax({ - type: 'GET', - dataType: 'json', - url: this.canCreatePath, - beforeSend: () => this.setUnavailableButtonState(), - }) - .done((data) => { - this.setUnavailableButtonState(false); - - if (data.can_create_branch) { - this.available(); - this.enable(); - - if (!this.droplabInitialized) { - this.droplabInitialized = true; - this.initDroplab(); - this.bindEvents(); + this.setUnavailableButtonState(); + + axios.get(this.canCreatePath) + .then(({ data }) => { + this.setUnavailableButtonState(false); + + if (data.can_create_branch) { + this.available(); + this.enable(); + + if (!this.droplabInitialized) { + this.droplabInitialized = true; + this.initDroplab(); + this.bindEvents(); + } + } else if (data.has_related_branch) { + this.hide(); } - } else if (data.has_related_branch) { - this.hide(); - } - }).fail(() => { - this.unavailable(); - this.disable(); - new Flash('Failed to check if a new branch can be created.'); - }); + }) + .catch(() => { + this.unavailable(); + this.disable(); + Flash('Failed to check if a new branch can be created.'); + }); } createBranch() { - return $.ajax({ - method: 'POST', - dataType: 'json', - url: this.createBranchPath, - beforeSend: () => (this.isCreatingBranch = true), - }) - .done((data) => { - this.branchCreated = true; - window.location.href = data.url; - }) - .fail(() => new Flash('Failed to create a branch for this issue. Please try again.')); + this.isCreatingBranch = true; + + return axios.post(this.createBranchPath) + .then(({ data }) => { + this.branchCreated = true; + window.location.href = data.url; + }) + .catch(() => Flash('Failed to create a branch for this issue. Please try again.')); } createMergeRequest() { - return $.ajax({ - method: 'POST', - dataType: 'json', - url: this.createMrPath, - beforeSend: () => (this.isCreatingMergeRequest = true), - }) - .done((data) => { - this.mergeRequestCreated = true; - window.location.href = data.url; - }) - .fail(() => new Flash('Failed to create Merge Request. Please try again.')); + this.isCreatingMergeRequest = true; + + return axios.post(this.createMrPath) + .then(({ data }) => { + this.mergeRequestCreated = true; + window.location.href = data.url; + }) + .catch(() => Flash('Failed to create Merge Request. Please try again.')); } disable() { @@ -200,39 +193,33 @@ export default class CreateMergeRequestDropdown { getRef(ref, target = 'all') { if (!ref) return false; - return $.ajax({ - method: 'GET', - dataType: 'json', - url: this.refsPath + ref, - beforeSend: () => { - this.isGettingRef = true; - }, - }) - .always(() => { - this.isGettingRef = false; - }) - .done((data) => { - const branches = data[Object.keys(data)[0]]; - const tags = data[Object.keys(data)[1]]; - let result; + return axios.get(this.refsPath + ref) + .then(({ data }) => { + const branches = data[Object.keys(data)[0]]; + const tags = data[Object.keys(data)[1]]; + let result; + + if (target === 'branch') { + result = CreateMergeRequestDropdown.findByValue(branches, ref); + } else { + result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || + CreateMergeRequestDropdown.findByValue(tags, ref, true); + this.suggestedRef = result; + } - if (target === 'branch') { - result = CreateMergeRequestDropdown.findByValue(branches, ref); - } else { - result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || - CreateMergeRequestDropdown.findByValue(tags, ref, true); - this.suggestedRef = result; - } + this.isGettingRef = false; - return this.updateInputState(target, ref, result); - }) - .fail(() => { - this.unavailable(); - this.disable(); - new Flash('Failed to get ref.'); + return this.updateInputState(target, ref, result); + }) + .catch(() => { + this.unavailable(); + this.disable(); + new Flash('Failed to get ref.'); - return false; - }); + this.isGettingRef = false; + + return false; + }); } getTargetData(target) { @@ -332,12 +319,12 @@ export default class CreateMergeRequestDropdown { xhr = this.createBranch(); } - xhr.fail(() => { + xhr.catch(() => { this.isCreatingMergeRequest = false; this.isCreatingBranch = false; - }); - xhr.always(() => this.enable()); + this.enable(); + }); this.disable(); } diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 550dbdda922..ba89e5726fa 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -2,6 +2,7 @@ import Dropzone from 'dropzone'; import _ from 'underscore'; import './preview_markdown'; import csrf from './lib/utils/csrf'; +import axios from './lib/utils/axios_utils'; Dropzone.autoDiscover = false; @@ -235,25 +236,21 @@ export default function dropzoneInput(form) { uploadFile = (item, filename) => { const formData = new FormData(); formData.append('file', item, filename); - return $.ajax({ - url: uploadsPath, - type: 'POST', - data: formData, - dataType: 'json', - processData: false, - contentType: false, - headers: csrf.headers, - beforeSend: () => { - showSpinner(); - return closeAlertMessage(); - }, - success: (e, text, response) => { - const md = response.responseJSON.link.markdown; + + showSpinner(); + closeAlertMessage(); + + axios.post(uploadsPath, formData) + .then(({ data }) => { + const md = data.link.markdown; + insertToTextArea(filename, md); - }, - error: response => showError(response.responseJSON.message), - complete: () => closeSpinner(), - }); + closeSpinner(); + }) + .catch((e) => { + showError(e.response.data.message); + closeSpinner(); + }); }; updateAttachingMessage = (files, messageContainer) => { diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index ada985913bb..bd4c58b7cb1 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -1,6 +1,7 @@ /* global dateFormat */ import Pikaday from 'pikaday'; +import axios from './lib/utils/axios_utils'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; class DueDateSelect { @@ -125,37 +126,30 @@ class DueDateSelect { } submitSelectedDate(isDropdown) { - return $.ajax({ - type: 'PUT', - url: this.issueUpdateURL, - data: this.datePayload, - dataType: 'json', - beforeSend: () => { - const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + const selectedDateValue = this.datePayload[this.abilityName].due_date; + const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; - this.$loading.removeClass('hidden').fadeIn(); + this.$loading.removeClass('hidden').fadeIn(); - if (isDropdown) { - this.$dropdown.trigger('loading.gl.dropdown'); - this.$selectbox.hide(); - } + if (isDropdown) { + this.$dropdown.trigger('loading.gl.dropdown'); + this.$selectbox.hide(); + } - this.$value.css('display', ''); - this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); - this.$sidebarValue.html(this.displayedDate); + this.$value.css('display', ''); + this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); + this.$sidebarValue.html(this.displayedDate); - return selectedDateValue.length ? - $('.js-remove-due-date-holder').removeClass('hidden') : - $('.js-remove-due-date-holder').addClass('hidden'); - }, - }).done(() => { - if (isDropdown) { - this.$dropdown.trigger('loaded.gl.dropdown'); - this.$dropdown.dropdown('toggle'); - } - return this.$loading.fadeOut(); - }); + $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length); + + return axios.put(this.issueUpdateURL, this.datePayload) + .then(() => { + if (isDropdown) { + this.$dropdown.trigger('loaded.gl.dropdown'); + this.$dropdown.dropdown('toggle'); + } + return this.$loading.fadeOut(); + }); } } diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 9e91f72b2ea..a10f027de53 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import axios from './lib/utils/axios_utils'; /** * Makes search request for content when user types a value in the search input. @@ -54,32 +55,26 @@ export default class FilterableList { this.listFilterElement.removeEventListener('input', this.debounceFilter); } - filterResults(queryData) { + filterResults(params) { if (this.isBusy) { return false; } $(this.listHolderElement).fadeTo(250, 0.5); - return $.ajax({ - url: this.getFilterEndpoint(), - data: queryData, - type: 'GET', - dataType: 'json', - context: this, - complete: this.onFilterComplete, - beforeSend: () => { - this.isBusy = true; - }, - success: (response, textStatus, xhr) => { - this.onFilterSuccess(response, xhr, queryData); - }, - }); + this.isBusy = true; + + return axios.get(this.getFilterEndpoint(), { + params, + }).then((res) => { + this.onFilterSuccess(res, params); + this.onFilterComplete(); + }).catch(() => this.onFilterComplete()); } - onFilterSuccess(response, xhr, queryData) { - if (response.html) { - this.listHolderElement.innerHTML = response.html; + onFilterSuccess(response, queryData) { + if (response.data.html) { + this.listHolderElement.innerHTML = response.data.html; } // Change url so if user reload a page - search results are saved diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 2db233b09da..31d56d15c23 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,6 +1,6 @@ import FilterableList from '~/filterable_list'; import eventHub from './event_hub'; -import { getParameterByName } from '../lib/utils/common_utils'; +import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils'; export default class GroupFilterableList extends FilterableList { constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { @@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList { this.form.querySelector(`[name="${this.filterInputField}"]`).value = ''; } - onFilterSuccess(data, xhr, queryData) { + onFilterSuccess(res, queryData) { const currentPath = this.getPagePath(queryData); - const paginationData = { - 'X-Per-Page': xhr.getResponseHeader('X-Per-Page'), - 'X-Page': xhr.getResponseHeader('X-Page'), - 'X-Total': xhr.getResponseHeader('X-Total'), - 'X-Total-Pages': xhr.getResponseHeader('X-Total-Pages'), - 'X-Next-Page': xhr.getResponseHeader('X-Next-Page'), - 'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'), - }; - window.history.replaceState({ page: currentPath, }, document.title, currentPath); - eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); - eventHub.$emit('updatePagination', paginationData); + eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); + eventHub.$emit('updatePagination', normalizeHeaders(res.headers)); } } diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js new file mode 100644 index 00000000000..974dc3ee052 --- /dev/null +++ b/app/assets/javascripts/toggle_buttons.js @@ -0,0 +1,61 @@ +import $ from 'jquery'; +import Flash from './flash'; +import { __ } from './locale'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; + +/* + example HAML: + ``` + %button.js-project-feature-toggle.project-feature-toggle{ type: "button", + class: "#{'is-checked' if enabled?}", + 'aria-label': _('Toggle Cluster') } + %input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? } + ``` +*/ + +function updatetoggle(toggle, isOn) { + toggle.classList.toggle('is-checked', isOn); +} + +function onToggleClicked(toggle, input, clickCallback) { + const previousIsOn = convertPermissionToBoolean(input.value); + + // Visually change the toggle and start loading + updatetoggle(toggle, !previousIsOn); + toggle.setAttribute('disabled', true); + toggle.classList.toggle('is-loading', true); + + Promise.resolve(clickCallback(!previousIsOn, toggle)) + .then(() => { + // Actually change the input value + input.setAttribute('value', !previousIsOn); + }) + .catch(() => { + // Revert the visuals if something goes wrong + updatetoggle(toggle, previousIsOn); + }) + .then(() => { + // Remove the loading indicator in any case + toggle.removeAttribute('disabled'); + toggle.classList.toggle('is-loading', false); + + $(input).trigger('trigger-change'); + }) + .catch(() => { + Flash(__('Something went wrong when toggling the button')); + }); +} + +export default function setupToggleButtons(container, clickCallback = () => {}) { + const toggles = container.querySelectorAll('.js-project-feature-toggle'); + + toggles.forEach((toggle) => { + const input = toggle.querySelector('.js-project-feature-toggle-input'); + const isOn = convertPermissionToBoolean(input.value); + + // Get the visible toggle in sync with the hidden input + updatetoggle(toggle, isOn); + + toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback)); + }); +} diff --git a/app/models/project.rb b/app/models/project.rb index e19873f64ce..d0d0fd6e093 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -568,6 +568,9 @@ class Project < ActiveRecord::Base RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, forked_from_project.disk_path) + elsif gitlab_project_import? + # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved. + RepositoryImportWorker.set(retry: false).perform_async(self.id) else RepositoryImportWorker.perform_async(self.id) end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 1a236e232f9..b604d860a87 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -2,7 +2,7 @@ class EmailsOnPushService < Service boolean_accessor :send_from_committer_email boolean_accessor :disable_diffs prop_accessor :recipients - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: :valid_recipients? def title 'Emails on push' diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 19357f90810..27bdf708c80 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -4,7 +4,7 @@ class IrkerService < Service prop_accessor :server_host, :server_port, :default_irc_uri prop_accessor :recipients, :channels boolean_accessor :colorize_messages - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: :valid_recipients? before_validation :get_channels diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 6a3118a11b8..9c7b58dead5 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,7 +1,7 @@ class PipelinesEmailService < Service prop_accessor :recipients boolean_accessor :notify_only_broken_pipelines - validates :recipients, presence: true, if: :activated? + validates :recipients, presence: true, if: :valid_recipients? def initialize_properties self.properties ||= { notify_only_broken_pipelines: true } diff --git a/app/models/service.rb b/app/models/service.rb index 96a064697f0..369cae2e85f 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,6 +2,8 @@ # and implement a set of methods class Service < ActiveRecord::Base include Sortable + include Importable + serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize default_value_for :active, false @@ -295,4 +297,8 @@ class Service < ActiveRecord::Base project.cache_has_external_wiki end end + + def valid_recipients? + activated? && !importing? + end end diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index b8692009225..fdd72ead2cb 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -5,15 +5,16 @@ = markdown_field(current_application_settings, :help_page_text) %hr -- unless current_application_settings.help_page_hide_commercial_content? - %h1 - GitLab - Community Edition - - if user_signed_in? - %span= Gitlab::VERSION - %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) - = version_status_badge +%h1 + GitLab + Community Edition + - if user_signed_in? + %span= Gitlab::VERSION + %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) + = version_status_badge + %hr +- unless current_application_settings.help_page_hide_commercial_content? %p.slead GitLab is open source software to collaborate on code. %br diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml index 3943dfc0856..20ee8086f93 100644 --- a/app/views/projects/clusters/_cluster.html.haml +++ b/app/views/projects/clusters/_cluster.html.haml @@ -12,11 +12,12 @@ .table-section.section-10 .table-mobile-header{ role: "rowheader" } .table-mobile-content - %button{ type: "button", - class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", + %button.js-project-feature-toggle.project-feature-toggle{ type: "button", + class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", "aria-label": s_("ClusterIntegration|Toggle Cluster"), disabled: !cluster.can_toggle_cluster?, data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } + %input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? } = icon("spinner spin", class: "loading-icon") %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml index 9d593ffc021..0af6e6e0577 100644 --- a/app/views/projects/clusters/_integration_form.html.haml +++ b/app/views/projects/clusters/_integration_form.html.haml @@ -10,13 +10,12 @@ = s_('ClusterIntegration|Cluster integration is enabled for this project.') - else = s_('ClusterIntegration|Cluster integration is disabled for this project.') - %label.append-bottom-10 - = field.hidden_field :enabled, { class: 'js-toggle-input'} - + %label.append-bottom-10.js-cluster-enable-toggle-area %button{ type: 'button', - class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", + class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", "aria-label": s_("ClusterIntegration|Toggle Cluster"), disabled: !can?(current_user, :update_cluster, @cluster) } + = field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'} %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 31e2798c36b..d79b5ee5346 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -20,7 +20,11 @@ class RepositoryImportWorker # to those importers to mark the import process as complete. return if service.async? - raise result[:message] if result[:status] == :error + if result[:status] == :error + fail_import(project, result[:message]) if project.gitlab_project_import? + + raise result[:message] + end project.after_import end @@ -33,4 +37,8 @@ class RepositoryImportWorker Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") false end + + def fail_import(project, message) + project.mark_import_as_failed(message) + end end diff --git a/changelogs/unreleased/42327-import-from-gitlab-com-fails-destination-already-exists-and-is-not-an-empty-directory-error.yml b/changelogs/unreleased/42327-import-from-gitlab-com-fails-destination-already-exists-and-is-not-an-empty-directory-error.yml new file mode 100644 index 00000000000..660f4f5d42c --- /dev/null +++ b/changelogs/unreleased/42327-import-from-gitlab-com-fails-destination-already-exists-and-is-not-an-empty-directory-error.yml @@ -0,0 +1,6 @@ +--- +title: Fixes destination already exists, and some particular service errors on Import/Export + error +merge_request: 16714 +author: +type: fixed diff --git a/changelogs/unreleased/cs-fix-commercial-content-check.yml b/changelogs/unreleased/cs-fix-commercial-content-check.yml new file mode 100644 index 00000000000..fec80e3ecd2 --- /dev/null +++ b/changelogs/unreleased/cs-fix-commercial-content-check.yml @@ -0,0 +1,6 @@ +--- +title: Fix version information not showing on help page if commercial content display + was disabled. +merge_request: 16743 +author: +type: fixed diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 76f33b765d3..24d678d5cde 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -169,15 +169,11 @@ For Omnibus GitLab packages: 1. [Reconfigure GitLab] for the changes to take effect -#### Digital Ocean Spaces and other S3-compatible providers +#### Digital Ocean Spaces -Not all S3 providers are fully-compatible with the Fog library. For example, -if you see `411 Length Required` errors after attempting to upload, you may -need to downgrade the `aws_signature_version` value from the default value to -2 [due to this issue](https://github.com/fog/fog-aws/issues/428). +This example can be used for a bucket in Amsterdam (AMS3). -1. For example, with [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/), -this example configuration can be used for a bucket in Amsterdam (AMS3): +1. Add the following to `/etc/gitlab/gitlab.rb`: ```ruby gitlab_rails['backup_upload_connection'] = { @@ -185,7 +181,6 @@ this example configuration can be used for a bucket in Amsterdam (AMS3): 'region' => 'ams3', 'aws_access_key_id' => 'AKIAKIAKI', 'aws_secret_access_key' => 'secret123', - 'aws_signature_version' => 2, 'endpoint' => 'https://ams3.digitaloceanspaces.com' } gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket' @@ -193,6 +188,13 @@ this example configuration can be used for a bucket in Amsterdam (AMS3): 1. [Reconfigure GitLab] for the changes to take effect +#### Other S3 Providers + +Not all S3 providers are fully-compatible with the Fog library. For example, +if you see `411 Length Required` errors after attempting to upload, you may +need to downgrade the `aws_signature_version` value from the default value to +2 [due to this issue](https://github.com/fog/fog-aws/issues/428). + --- For installations from source: diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 31effdba292..6d6ed065f79 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -42,9 +42,7 @@ module Gitlab end def load_blame_by_shelling_out - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) - # Read in binary mode to ensure ASCII-8BIT - IO.popen(cmd, 'rb') {|io| io.read } + @repo.shell_blame(@sha, @path) end def process_raw_blame(output) diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb index 1ccca13ce2f..e0bd2bbe47b 100644 --- a/lib/gitlab/git/popen.rb +++ b/lib/gitlab/git/popen.rb @@ -19,6 +19,8 @@ module Gitlab cmd_output = "" cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + stdout.set_encoding(Encoding::ASCII_8BIT) + yield(stdin) if block_given? stdin.close diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 638d335b523..64b491517cb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -614,11 +614,11 @@ module Gitlab if is_enabled gitaly_ref_client.find_ref_name(sha, ref_path) else - args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha}) + args = %W(for-each-ref --count=1 #{ref_path} --contains #{sha}) # Not found -> ["", 0] # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] - popen(args, @path).first.split.last + run_git(args).first.split.last end end end @@ -887,8 +887,7 @@ module Gitlab "delete #{ref}\x00\x00" end - command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] - message, status = popen(command, path) do |stdin| + message, status = run_git(%w[update-ref --stdin -z]) do |stdin| stdin.write(instructions.join) end @@ -1409,6 +1408,11 @@ module Gitlab end end + def shell_blame(sha, path) + output, _status = run_git(%W(blame -p #{sha} -- #{path})) + output + end + private def shell_write_ref(ref_path, ref, old_ref) @@ -1433,6 +1437,12 @@ module Gitlab def run_git(args, chdir: path, env: {}, nice: false, &block) cmd = [Gitlab.config.git.bin_path, *args] cmd.unshift("nice") if nice + + object_directories = alternate_object_directories + if object_directories.any? + env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = object_directories.join(File::PATH_SEPARATOR) + end + circuit_breaker.perform do popen(cmd, chdir, env, &block) end @@ -1624,7 +1634,7 @@ module Gitlab offset_in_ruby = use_follow_flag && options[:offset].present? limit += offset if offset_in_ruby - cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log] + cmd = %w[log] cmd << "--max-count=#{limit}" cmd << '--format=%H' cmd << "--skip=#{offset}" unless offset_in_ruby @@ -1640,7 +1650,7 @@ module Gitlab cmd += Array(options[:path]) end - raw_output = IO.popen(cmd) { |io| io.read } + raw_output, _status = run_git(cmd) lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines lines.map! { |c| Rugged::Commit.new(rugged, c.strip) } @@ -1678,18 +1688,23 @@ module Gitlab end def alternate_object_directories - relative_paths = Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact + relative_paths = relative_object_directories if relative_paths.any? relative_paths.map { |d| File.join(path, d) } else - Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES) - .flatten - .compact - .flat_map { |d| d.split(File::PATH_SEPARATOR) } + absolute_object_directories.flat_map { |d| d.split(File::PATH_SEPARATOR) } end end + def relative_object_directories + Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact + end + + def absolute_object_directories + Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).flatten.compact + end + # Get the content of a blob for a given commit. If the blob is a commit # (for submodules) then return the blob's OID. def blob_content(commit, blob_name) @@ -1833,13 +1848,13 @@ module Gitlab def count_commits_by_shelling_out(options) cmd = count_commits_shelling_command(options) - raw_output = IO.popen(cmd) { |io| io.read } + raw_output, _status = run_git(cmd) process_count_commits_raw_output(raw_output, options) end def count_commits_shelling_command(options) - cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list] + cmd = %w[rev-list] cmd << "--after=#{options[:after].iso8601}" if options[:after] cmd << "--before=#{options[:before].iso8601}" if options[:before] cmd << "--max-count=#{options[:max_count]}" if options[:max_count] @@ -1884,20 +1899,17 @@ module Gitlab return [] end - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) - cmd += %w(-r) - cmd += %w(--full-tree) - cmd += %w(--full-name) - cmd += %W(-- #{actual_ref}) + cmd = %W(ls-tree -r --full-tree --full-name -- #{actual_ref}) + raw_output, _status = run_git(cmd) - raw_output = IO.popen(cmd, &:read).split("\n").map do |f| + lines = raw_output.split("\n").map do |f| stuff, path = f.split("\t") _mode, type, _sha = stuff.split(" ") path if type == "blob" # Contain only blob type end - raw_output.compact + lines.compact end # Returns true if the given ref name exists diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index d03cbc880fd..b34cafc6876 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -19,8 +19,13 @@ module Gitlab def error(error) error_out(error.message, caller[0].dup) @errors << error.message + # Debug: - Rails.logger.error(error.backtrace.join("\n")) + if error.backtrace + Rails.logger.error("Import/Export backtrace: #{error.backtrace.join("\n")}") + else + Rails.logger.error("No backtrace found") + end end private diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index a2e68c0471b..aece8893974 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -21,7 +21,11 @@ namespace :gitlab do _, status = Gitlab::Popen.popen(%w[which gmake]) command << (status.zero? ? 'gmake' : 'make') - command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test? + if Rails.env.test? + command.push( + 'BUNDLE_FLAGS=--no-deployment', + "BUNDLE_PATH=#{Bundler.bundle_path}") + end Gitlab::SetupHelper.create_gitaly_configuration(args.dir) Dir.chdir(args.dir) do diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 8953b30bebf..94bde723e2f 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do context 'when user disables the cluster' do before do - page.find(:css, '.js-toggle-cluster').click + page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click page.within('#cluster-integration') { click_button 'Save changes' } end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index a519b9f9c7e..b9ab434c259 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -62,7 +62,7 @@ feature 'User Cluster', :js do context 'when user disables the cluster' do before do - page.find(:css, '.js-toggle-cluster').click + page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click fill_in 'cluster_name', with: 'dev-cluster' page.within('#cluster-integration') { click_button 'Save changes' } end diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index eae2910a8f6..497a50bebe4 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -37,13 +37,13 @@ feature 'Clusters', :js do context 'inline update of cluster' do it 'user can update cluster' do - expect(page).to have_selector('.js-toggle-cluster-list') + expect(page).to have_selector('.js-project-feature-toggle') end context 'with sucessfull request' do it 'user sees updated cluster' do expect do - page.find('.js-toggle-cluster-list').click + page.find('.js-project-feature-toggle').click wait_for_requests end.to change { cluster.reload.enabled } @@ -57,7 +57,7 @@ feature 'Clusters', :js do expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false } - page.find('.js-toggle-cluster-list').click + page.find('.js-project-feature-toggle').click expect(page).to have_content('Something went wrong on our end.') expect(page).to have_selector('.is-checked') diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index f5be9ea0fb2..7b38f6b7855 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -23,16 +23,24 @@ describe('Clusters', () => { }); describe('toggle', () => { - it('should update the button and the input field on click', () => { - cluster.toggleButton.click(); + it('should update the button and the input field on click', (done) => { + const toggleButton = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle'); + const toggleInput = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle-input'); - expect( - cluster.toggleButton.classList, - ).not.toContain('is-checked'); + toggleButton.click(); - expect( - cluster.toggleInput.getAttribute('value'), - ).toEqual('false'); + getSetTimeoutPromise() + .then(() => { + expect( + toggleButton.classList, + ).not.toContain('is-checked'); + + expect( + toggleInput.getAttribute('value'), + ).toEqual('false'); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js deleted file mode 100644 index 0a8b63ed5b4..00000000000 --- a/spec/javascripts/clusters/clusters_index_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import setClusterTableToggles from '~/clusters/clusters_index'; -import { setTimeout } from 'core-js/library/web/timers'; - -describe('Clusters table', () => { - preloadFixtures('clusters/index_cluster.html.raw'); - let mock; - - beforeEach(() => { - loadFixtures('clusters/index_cluster.html.raw'); - mock = new MockAdapter(axios); - setClusterTableToggles(); - }); - - describe('update cluster', () => { - it('renders loading state while request is made', () => { - const button = document.querySelector('.js-toggle-cluster-list'); - - button.click(); - - expect(button.classList).toContain('is-loading'); - expect(button.getAttribute('disabled')).toEqual('true'); - }); - - afterEach(() => { - mock.restore(); - }); - - it('shows updated state after sucessfull request', (done) => { - mock.onPut().reply(200, {}, {}); - const button = document.querySelector('.js-toggle-cluster-list'); - button.click(); - - expect(button.classList).toContain('is-loading'); - - setTimeout(() => { - expect(button.classList).not.toContain('is-loading'); - expect(button.classList).not.toContain('is-checked'); - done(); - }, 0); - }); - - it('shows inital state after failed request', (done) => { - mock.onPut().reply(500, {}, {}); - const button = document.querySelector('.js-toggle-cluster-list'); - - button.click(); - expect(button.classList).toContain('is-loading'); - - setTimeout(() => { - expect(button.classList).not.toContain('is-loading'); - expect(button.classList).toContain('is-checked'); - done(); - }, 0); - }); - }); -}); diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb index d26ea3febe8..8e74c4f859c 100644 --- a/spec/javascripts/fixtures/clusters.rb +++ b/spec/javascripts/fixtures/clusters.rb @@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle expect(response).to be_success store_frontend_fixture(response, example.description) end - - context 'rendering non-empty state' do - before do - cluster - end - - it 'clusters/index_cluster.html.raw' do |example| - get :index, - namespace_id: namespace, - project_id: project - - expect(response).to be_success - store_frontend_fixture(response, example.description) - end - end end diff --git a/spec/javascripts/toggle_buttons_spec.js b/spec/javascripts/toggle_buttons_spec.js new file mode 100644 index 00000000000..205e396d682 --- /dev/null +++ b/spec/javascripts/toggle_buttons_spec.js @@ -0,0 +1,120 @@ +import setupToggleButtons from '~/toggle_buttons'; +import getSetTimeoutPromise from './helpers/set_timeout_promise_helper'; + +function generateMarkup(isChecked = true) { + return ` + <button type="button" class="${isChecked ? 'is-checked' : ''} js-project-feature-toggle"> + <input type="hidden" class="js-project-feature-toggle-input" value="${isChecked}" /> + </button> + `; +} + +function setupFixture(isChecked, clickCallback) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = generateMarkup(isChecked); + + setupToggleButtons(wrapper, clickCallback); + + return wrapper; +} + +describe('ToggleButtons', () => { + describe('when input value is true', () => { + it('should initialize as checked', () => { + const wrapper = setupFixture(true); + + expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(true); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true'); + }); + + it('should toggle to unchecked when clicked', (done) => { + const wrapper = setupFixture(true); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when input value is false', () => { + it('should initialize as unchecked', () => { + const wrapper = setupFixture(false); + + expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(false); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false'); + }); + + it('should toggle to checked when clicked', (done) => { + const wrapper = setupFixture(false); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(true); + expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true'); + }) + .then(done) + .catch(done.fail); + }); + }); + + it('should emit `trigger-change` event', (done) => { + const changeSpy = jasmine.createSpy('changeEventHandler'); + const wrapper = setupFixture(false); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + const input = wrapper.querySelector('.js-project-feature-toggle-input'); + + $(input).on('trigger-change', changeSpy); + + toggleButton.click(); + + getSetTimeoutPromise() + .then(() => { + expect(changeSpy).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + describe('clickCallback', () => { + it('should show loading indicator while waiting', (done) => { + const isChecked = true; + const clickCallback = (newValue, toggleButton) => { + const input = toggleButton.querySelector('.js-project-feature-toggle-input'); + + expect(newValue).toEqual(false); + + // Check for the loading state + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(toggleButton.classList.contains('is-loading')).toEqual(true); + expect(toggleButton.disabled).toEqual(true); + expect(input.value).toEqual('true'); + + // After the callback finishes, check that the loading state is gone + getSetTimeoutPromise() + .then(() => { + expect(toggleButton.classList.contains('is-checked')).toEqual(false); + expect(toggleButton.classList.contains('is-loading')).toEqual(false); + expect(toggleButton.disabled).toEqual(false); + expect(input.value).toEqual('false'); + }) + .then(done) + .catch(done.fail); + }; + + const wrapper = setupFixture(isChecked, clickCallback); + const toggleButton = wrapper.querySelector('.js-project-feature-toggle'); + + toggleButton.click(); + }); + }); +}); diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 4cf33778d15..b6c1f0c81cb 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -7096,7 +7096,7 @@ "project_id": 5, "created_at": "2016-06-14T15:01:51.232Z", "updated_at": "2016-06-14T15:01:51.232Z", - "active": false, + "active": true, "properties": { }, diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 08e5bbbd400..5804c45871e 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -164,6 +164,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService') end + it 'saves the properties for a service' do + expect(saved_project_json['services'].first['properties']).to eq('one' => 'value') + end + it 'has project feature' do project_feature = saved_project_json['project_feature'] expect(project_feature).not_to be_empty @@ -279,7 +283,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do commit_id: ci_build.pipeline.sha) create(:event, :created, target: milestone, project: project, author: user) - create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') + create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' }) create(:project_custom_attribute, project: project) create(:project_custom_attribute, project: project) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 43218755f4f..13db40d21a5 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1441,7 +1441,7 @@ describe API::Issues, :mailer do context 'when source project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/123/issues/#{issue.iid}/move", user), + post api("/projects/12345/issues/#{issue.iid}/move", user), to_project_id: target_project.id expect(response).to have_gitlab_http_status(404) @@ -1452,7 +1452,7 @@ describe API::Issues, :mailer do context 'when target project does not exist' do it 'returns 404 when trying to move an issue' do post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), - to_project_id: 123 + to_project_id: 12345 expect(response).to have_gitlab_http_status(404) end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 6aba86fdc3c..b37d6ac831f 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -76,7 +76,11 @@ describe 'gitlab:gitaly namespace rake task' do end context 'when Rails.env is test' do - let(:command) { %w[make BUNDLE_FLAGS=--no-deployment] } + let(:command) do + %W[make + BUNDLE_FLAGS=--no-deployment + BUNDLE_PATH=#{Bundler.bundle_path}] + end before do allow(Rails.env).to receive(:test?).and_return(true) diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 7274a9f00f9..2b1a617ee62 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -49,9 +49,22 @@ describe RepositoryImportWorker do expect do subject.perform(project.id) - end.to raise_error(StandardError, error) + end.to raise_error(RuntimeError, error) expect(project.reload.import_jid).not_to be_nil end + + it 'updates the error on Import/Export' do + error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } + + project.update_attributes(import_jid: '123', import_type: 'gitlab_project') + expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error }) + + expect do + subject.perform(project.id) + end.to raise_error(RuntimeError, error) + + expect(project.reload.import_error).not_to be_nil + end end context 'when using an asynchronous importer' do |