Commit 570bf1bb authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Merge branch 'master' into fl-vue-mr-widget

* master: (21 commits)
  normalize headers correctly i18n flash message
  fixed dashboard projects not being filterable
  Converted filterable_list to axios
  Converted due_date_select to axios
  Converted dropzone_input to axios
  Converted create_merge_request_dropdown to axios
  converted compare_autocomplete to axios
  Convered compare.js to axios
  Set alternate object directories in run_git
  Digital Ocean Spaces now supports AWS v4 streaming API
  Fix spec failures in issues_spec.rb
  Fix #42486.
  Generalize toggle_buttons.js
  update code based on feedback
  add changelog
  fix spec
  add spec
  disable retry attempts for Import/Export until that is fixed
  add an extra spec
  fix validation error on services
  ...
parents 5ba03e58 f8dd398a
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue'; import applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/** /**
* Cluster page has 2 separate parts: * Cluster page has 2 separate parts:
...@@ -48,12 +49,9 @@ export default class Clusters { ...@@ -48,12 +49,9 @@ export default class Clusters {
installPrometheusEndpoint: installPrometheusPath, installPrometheusEndpoint: installPrometheusPath,
}); });
this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
this.showToken = this.showToken.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.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success'); this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating'); this.creatingContainer = document.querySelector('.js-cluster-creating');
...@@ -63,6 +61,7 @@ export default class Clusters { ...@@ -63,6 +61,7 @@ export default class Clusters {
this.tokenField = document.querySelector('.js-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token');
initSettingsPanels(); initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(); this.initApplications();
if (this.store.state.status !== 'created') { if (this.store.state.status !== 'created') {
...@@ -101,13 +100,11 @@ export default class Clusters { ...@@ -101,13 +100,11 @@ export default class Clusters {
} }
addListeners() { addListeners() {
this.toggleButton.addEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication); eventHub.$on('installApplication', this.installApplication);
} }
removeListeners() { removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication); eventHub.$off('installApplication', this.installApplication);
} }
...@@ -151,11 +148,6 @@ export default class Clusters { ...@@ -151,11 +148,6 @@ export default class Clusters {
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); 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() { showToken() {
const type = this.tokenField.getAttribute('type'); const type = this.tokenField.getAttribute('type');
......
import Flash from '../flash'; import Flash from '../flash';
import { s__ } from '../locale'; import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
import ClustersService from './services/clusters_service'; 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');
};
/** export default () => {
* Toggles checked class for the given button const clusterList = document.querySelector('.js-clusters-list');
* @param {HTMLElement} button // The empty state won't have a clusterList
*/ if (clusterList) {
const toggleValue = (button) => { setupToggleButtons(
button.classList.toggle('is-checked'); 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.'));
});
}));
}
/* 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 */ /* 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 { localTimeAgo } from './lib/utils/datetime_utility';
import axios from './lib/utils/axios_utils';
export default class Compare { export default class Compare {
constructor(opts) { constructor(opts) {
...@@ -41,17 +42,14 @@ export default class Compare { ...@@ -41,17 +42,14 @@ export default class Compare {
} }
getTargetProject() { getTargetProject() {
return $.ajax({ $('.mr_target_commit').empty();
url: this.opts.targetProjectUrl,
data: { return axios.get(this.opts.targetProjectUrl, {
target_project_id: $("input[name='merge_request[target_project_id]']").val() params: {
}, target_project_id: $("input[name='merge_request[target_project_id]']").val(),
beforeSend: function() {
return $('.mr_target_commit').empty();
}, },
success: function(html) { }).then(({ data }) => {
return $('.js-target-branch-dropdown .dropdown-content').html(html); $('.js-target-branch-dropdown .dropdown-content').html(data);
}
}); });
} }
...@@ -68,22 +66,19 @@ export default class Compare { ...@@ -68,22 +66,19 @@ export default class Compare {
}); });
} }
static sendAjax(url, loading, target, data) { static sendAjax(url, loading, target, params) {
var $target; const $target = $(target);
$target = $(target);
return $.ajax({ loading.show();
url: url, $target.empty();
data: data,
beforeSend: function() { return axios.get(url, {
loading.show(); params,
return $target.empty(); }).then(({ data }) => {
}, loading.hide();
success: function(html) { $target.html(data);
loading.hide(); const className = '.' + $target[0].className.replace(' ', '.');
$target.html(html); localTimeAgo($('.js-timeago', className));
var className = '.' + $target[0].className.replace(' ', '.');
localTimeAgo($('.js-timeago', className));
}
}); });
} }
} }
/* 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 */ /* 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() { export default function initCompareAutocomplete() {
$('.js-compare-dropdown').each(function() { $('.js-compare-dropdown').each(function() {
...@@ -10,15 +13,14 @@ export default function initCompareAutocomplete() { ...@@ -10,15 +13,14 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({ $dropdown.glDropdown({
data: function(term, callback) { data: function(term, callback) {
return $.ajax({ axios.get($dropdown.data('refsUrl'), {
url: $dropdown.data('refs-url'), params: {
data: {
ref: $dropdown.data('ref'), ref: $dropdown.data('ref'),
search: term, search: term,
} },
}).done(function(refs) { }).then(({ data }) => {
return callback(refs); callback(data);
}); }).catch(() => flash(__('Error fetching refs')));
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
......
/* eslint-disable no-new */ /* eslint-disable no-new */
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import Flash from './flash'; import Flash from './flash';
import DropLab from './droplab/drop_lab'; import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter'; import ISetter from './droplab/plugins/input_setter';
...@@ -74,60 +75,52 @@ export default class CreateMergeRequestDropdown { ...@@ -74,60 +75,52 @@ export default class CreateMergeRequestDropdown {
} }
checkAbilityToCreateBranch() { checkAbilityToCreateBranch() {
return $.ajax({ this.setUnavailableButtonState();
type: 'GET',
dataType: 'json', axios.get(this.canCreatePath)
url: this.canCreatePath, .then(({ data }) => {
beforeSend: () => this.setUnavailableButtonState(), this.setUnavailableButtonState(false);
})
.done((data) => { if (data.can_create_branch) {
this.setUnavailableButtonState(false); this.available();
this.enable();
if (data.can_create_branch) {
this.available(); if (!this.droplabInitialized) {
this.enable(); this.droplabInitialized = true;
this.initDroplab();
if (!this.droplabInitialized) { this.bindEvents();
this.droplabInitialized = true; }
this.initDroplab(); } else if (data.has_related_branch) {
this.bindEvents(); this.hide();
} }
} else if (data.has_related_branch) { })
this.hide(); .catch(() => {
} this.unavailable();
}).fail(() => { this.disable();
this.unavailable(); Flash('Failed to check if a new branch can be created.');
this.disable(); });
new Flash('Failed to check if a new branch can be created.');
});
} }
createBranch() { createBranch() {
return $.ajax({ this.isCreatingBranch = true;
method: 'POST',
dataType: 'json', return axios.post(this.createBranchPath)
url: this.createBranchPath, .then(({ data }) => {
beforeSend: () => (this.isCreatingBranch = true), this.branchCreated = true;
}) window.location.href = data.url;
.done((data) => { })
this.branchCreated = true; .catch(() => Flash('Failed to create a branch for this issue. Please try again.'));
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
} }
createMergeRequest() { createMergeRequest() {
return $.ajax({ this.isCreatingMergeRequest = true;
method: 'POST',
dataType: 'json', return axios.post(this.createMrPath)
url: this.createMrPath, .then(({ data }) => {
beforeSend: () => (this.isCreatingMergeRequest = true), this.mergeRequestCreated = true;
}) window.location.href = data.url;
.done((data) => { })
this.mergeRequestCreated = true; .catch(() => Flash('Failed to create Merge Request. Please try again.'));
window.location.href = data.url;
})
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
} }
disable() { disable() {
...@@ -200,39 +193,33 @@ export default class CreateMergeRequestDropdown { ...@@ -200,39 +193,33 @@ export default class CreateMergeRequestDropdown {
getRef(ref, target = 'all') { getRef(ref, target = 'all') {
if (!ref) return false; if (!ref) return false;
return $.ajax({ return axios.get(this.refsPath + ref)
method: 'GET', .then(({ data }) => {
dataType: 'json', const branches = data[Object.keys(data)[0]];
url: this.refsPath + ref, const tags = data[Object.keys(data)[1]];
beforeSend: () => { let result;
this.isGettingRef = true;
}, if (target === 'branch') {
}) result = CreateMergeRequestDropdown.findByValue(branches, ref);
.always(() => { } else {
this.isGettingRef = false; result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
}) CreateMergeRequestDropdown.findByValue(tags, ref, true);
.done((data) => { this.suggestedRef = result;
const branches = data[Object.keys(data)[0]]; }
const tags = data[Object.keys(data)[1]];
let result;
if (target === 'branch') { this.isGettingRef = false;
result = CreateMergeRequestDropdown.findByValue(branches, ref);
} else {
result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
CreateMergeRequestDropdown.findByValue(tags, ref, true);
this.suggestedRef = result;
}
return this.updateInputState(target, ref, result); return this.updateInputState(target, ref, result);
}) })
.fail(() => { .catch(() => {
this.unavailable(); this.unavailable();
this.disable(); this.disable();
new Flash('Failed to get ref.'); new Flash('Failed to get ref.');
return false; this.isGettingRef = false;
});
return false;
});
} }
getTargetData(target) { getTargetData(target) {
...@@ -332,12 +319,12 @@ export default class CreateMergeRequestDropdown { ...@@ -332,12 +319,12 @@ export default class CreateMergeRequestDropdown {
xhr = this.createBranch(); xhr = this.createBranch();
} }
xhr.fail(() => { xhr.catch(() => {
this.isCreatingMergeRequest = false; this.isCreatingMergeRequest = false;
this.isCreatingBranch = false; this.isCreatingBranch = false;
});
xhr.always(() => this.enable()); this.enable();
});
this.disable(); this.disable();
} }
......
...@@ -2,6 +2,7 @@ import Dropzone from 'dropzone'; ...@@ -2,6 +2,7 @@ import Dropzone from 'dropzone';
import _ from 'underscore'; import _ from 'underscore';
import './preview_markdown'; import './preview_markdown';
import csrf from './lib/utils/csrf'; import csrf from './lib/utils/csrf';
import axios from './lib/utils/axios_utils';
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
...@@ -235,25 +236,21 @@ export default function dropzoneInput(form) { ...@@ -235,25 +236,21 @@ export default function dropzoneInput(form) {
uploadFile = (item, filename) => { uploadFile = (item, filename) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', item, filename); formData.append('file', item, filename);
return $.ajax({
url: uploadsPath, showSpinner();
type: 'POST', closeAlertMessage();
data: formData,
dataType: 'json', axios.post(uploadsPath, formData)
processData: false, .then(({ data }) => {
contentType: false, const md = data.link.markdown;
headers: csrf.headers,
beforeSend: () => {
showSpinner();
return closeAlertMessage();
},
success: (e, text, response) => {
const md = response.responseJSON.link.markdown;
insertToTextArea(filename, md); insertToTextArea(filename, md);
}, closeSpinner();
error: response => showError(response.responseJSON.message), })
complete: () => closeSpinner(), .catch((e) => {
}); showError(e.response.data.message);
closeSpinner();
});
}; };
updateAttachingMessage = (files, messageContainer) => { updateAttachingMessage = (files, messageContainer) => {
......
/* global dateFormat */ /* global dateFormat */
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import axios from './lib/utils/axios_utils';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect { class DueDateSelect {
...@@ -125,37 +126,30 @@ class DueDateSelect { ...@@ -125,37 +126,30 @@ class DueDateSelect {
} }
submitSelectedDate(isDropdown) { submitSelectedDate(isDropdown) {
return $.ajax({ const selectedDateValue = this.datePayload[this.abilityName].due_date;
type: 'PUT', const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
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';
this.$loading.removeClass('hidden').fadeIn(); this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) { if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown'); this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide(); this.$selectbox.hide();
} }
this.$value.css('display', ''); this.$value.css('display', '');
this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`); this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
this.$sidebarValue.html(this.displayedDate); this.$sidebarValue.html(this.displayedDate);
return selectedDateValue.length ? $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length);
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden'); return axios.put(this.issueUpdateURL, this.datePayload)
}, .then(() => {
}).done(() => { if (isDropdown) {
if (isDropdown) { this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.trigger('loaded.gl.dropdown'); this.$dropdown.dropdown('toggle');
this.$dropdown.dropdown('toggle'); }
} return this.$loading.fadeOut();
return this.$loading.fadeOut(); });
});
} }
} }
......
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
/** /**
* Makes search request for content when user types a value in the search input. * Makes search request for content when user types a value in the search input.
...@@ -54,32 +55,26 @@ export default class FilterableList { ...@@ -54,32 +55,26 @@ export default class FilterableList {
this.listFilterElement.removeEventListener('input', this.debounceFilter); this.listFilterElement.removeEventListener('input', this.debounceFilter);
} }
filterResults(queryData) { filterResults(params) {
if (this.isBusy) { if (this.isBusy) {
return false; return false;
} }
$(this.listHolderElement).fadeTo(250, 0.5); $(this.listHolderElement).fadeTo(250, 0.5);
return $.ajax({ this.isBusy = true;
url: this.getFilterEndpoint(),
data: queryData, return axios.get(this.getFilterEndpoint(), {
type: 'GET', params,
dataType: 'json', }).then((res) => {
context: this, this.onFilterSuccess(res, params);
complete: this.onFilterComplete, this.onFilterComplete();
beforeSend: () => { }).catch(() => this.onFilterComplete());
this.isBusy = true;
},
success: (response, textStatus, xhr) => {
this.onFilterSuccess(response, xhr, queryData);
},
});
} }
onFilterSuccess(response, xhr, queryData) { onFilterSuccess(response, queryData) {
if (response.html) { if (response.data.html) {
this.listHolderElement.innerHTML = response.html; this.listHolderElement.innerHTML = response.data.html;
} }
// Change url so if user reload a page - search results are saved // Change url so if user reload a page - search results are saved
......
import FilterableList from '~/filterable_list'; import FilterableList from '~/filterable_list';
import eventHub from './event_hub'; 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 { export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
...@@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList { ...@@ -94,23 +94,14 @@ export default class GroupFilterableList extends FilterableList {
this.form.querySelector(`[name="${this.filterInputField}"]`).value = ''; this.form.querySelector(`[name="${this.filterInputField}"]`).value = '';
} }
onFilterSuccess(data, xhr, queryData) { onFilterSuccess(res, queryData) {
const currentPath = this.getPagePath(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({ window.history.replaceState({
page: currentPath, page: currentPath,
}, document.title, currentPath); }, document.title, currentPath);
eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
eventHub.$emit('updatePagination', paginationData); eventHub.$emit('updatePagination', normalizeHeaders(res.headers));
} }
} }
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));
});
}
...@@ -568,6 +568,9 @@ class Project < ActiveRecord::Base ...@@ -568,6 +568,9 @@ class Project < ActiveRecord::Base
RepositoryForkWorker.perform_async(id, RepositoryForkWorker.perform_async(id,
forked_from_project.repository_storage_path, forked_from_project.repository_storage_path,
forked_from_project.disk_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 else
RepositoryImportWorker.perform_async(self.id) RepositoryImportWorker.perform_async(self.id)
end end
......
...@@ -2,7 +2,7 @@ class EmailsOnPushService < Service ...@@ -2,7 +2,7 @@ class EmailsOnPushService < Service
boolean_accessor :send_from_committer_email boolean_accessor :send_from_committer_email
boolean_accessor :disable_diffs boolean_accessor :disable_diffs
prop_accessor :recipients prop_accessor :recipients
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :valid_recipients?
def title def title
'Emails on push' 'Emails on push'
......
...@@ -4,7 +4,7 @@ class IrkerService < Service ...@@ -4,7 +4,7 @@ class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :recipients, :channels prop_accessor :recipients, :channels
boolean_accessor :colorize_messages boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :valid_recipients?
before_validation :get_channels before_validation :get_channels
......
class PipelinesEmailService < Service class PipelinesEmailService < Service
prop_accessor :recipients prop_accessor :recipients
boolean_accessor :notify_only_broken_pipelines boolean_accessor :notify_only_broken_pipelines
validates :recipients, presence: true, if: :activated? validates :recipients, presence: true, if: :valid_recipients?
def initialize_properties def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true } self.properties ||= { notify_only_broken_pipelines: true }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
# and implement a set of methods # and implement a set of methods
class Service < ActiveRecord::Base class Service < ActiveRecord::Base
include Sortable include Sortable
include Importable
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
default_value_for :active, false default_value_for :active, false
...@@ -295,4 +297,8 @@ class Service < ActiveRecord::Base ...@@ -295,4 +297,8 @@ class Service < ActiveRecord::Base
project.cache_has_external_wiki project.cache_has_external_wiki
end end
end end
def valid_recipients?
activated? && !importing?
end
end end
...@@ -5,15 +5,16 @@ ...@@ -5,15 +5,16 @@
= markdown_field(current_application_settings, :help_page_text) = markdown_field(current_application_settings, :help_page_text)
%hr %hr
- unless current_application_settings.help_page_hide_commercial_content? %h1
%h1 GitLab
GitLab Community Edition
Community Edition - if user_signed_in?
- if user_signed_in? %span= Gitlab::VERSION
%span= Gitlab::VERSION %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) = version_status_badge
= version_status_badge %hr
- unless current_application_settings.help_page_hide_commercial_content?
%p.slead %p.slead
GitLab is open source software to collaborate on code. GitLab is open source software to collaborate on code.
%br %br
......
...@@ -12,11 +12,12 @@ ...@@ -12,11 +12,12 @@
.table-section.section-10 .table-section.section-10
.table-mobile-header{ role: "rowheader" } .table-mobile-header{ role: "rowheader" }
.table-mobile-content .table-mobile-content
%button{ type: "button", %button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?, disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } 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") = icon("spinner spin", class: "loading-icon")
%span.toggle-icon %span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
......
...@@ -10,13 +10,12 @@ ...@@ -10,13 +10,12 @@
= s_('ClusterIntegration|Cluster integration is enabled for this project.') = s_('ClusterIntegration|Cluster integration is enabled for this project.')
- else - else
= s_('ClusterIntegration|Cluster integration is disabled for this project.') = s_('ClusterIntegration|Cluster integration is disabled for this project.')
%label.append-bottom-10 %label.append-bottom-10.js-cluster-enable-toggle-area
= field.hidden_field :enabled, { class: 'js-toggle-input'}
%button{ type: 'button', %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"), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @cluster) } disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon %span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = 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') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
......
...@@ -20,7 +20,11 @@ class RepositoryImportWorker ...@@ -20,7 +20,11 @@ class RepositoryImportWorker
# to those importers to mark the import process as complete. # to those importers to mark the import process as complete.
return if service.async? 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 project.after_import
end end
...@@ -33,4 +37,8 @@ class RepositoryImportWorker ...@@ -33,4 +37,8 @@ class RepositoryImportWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false false
end end
def fail_import(project, message)
project.mark_import_as_failed(message)
end
end end
---
title: Fixes destination already exists, and some particular service errors on Import/Export
error
merge_request: 16714
author:
type: fixed
---
title: Fix version information not showing on help page if commercial content display
was disabled.
merge_request: 16743
author:
type: fixed
...@@ -169,15 +169,11 @@ For Omnibus GitLab packages: ...@@ -169,15 +169,11 @@ For Omnibus GitLab packages:
1. [Reconfigure GitLab] for the changes to take effect 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, This example can be used for a bucket in Amsterdam (AMS3).
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).
1. For example, with [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/), 1. Add the following to `/etc/gitlab/gitlab.rb`:
this example configuration can be used for a bucket in Amsterdam (AMS3):
```ruby ```ruby
gitlab_rails['backup_upload_connection'] = { gitlab_rails['backup_upload_connection'] = {
...@@ -185,7 +181,6 @@ this example configuration can be used for a bucket in Amsterdam (AMS3): ...@@ -185,7 +181,6 @@ this example configuration can be used for a bucket in Amsterdam (AMS3):
'region' => 'ams3', 'region' => 'ams3',
'aws_access_key_id' => 'AKIAKIAKI', 'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123', 'aws_secret_access_key' => 'secret123',
'aws_signature_version' => 2,
'endpoint' => 'https://ams3.digitaloceanspaces.com' 'endpoint' => 'https://ams3.digitaloceanspaces.com'
} }
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket' 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): ...@@ -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 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: For installations from source:
......
...@@ -42,9 +42,7 @@ module Gitlab ...@@ -42,9 +42,7 @@ module Gitlab
end end
def load_blame_by_shelling_out def load_blame_by_shelling_out
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{@repo.path} blame -p #{@sha} -- #{@path}) @repo.shell_blame(@sha, @path)
# Read in binary mode to ensure ASCII-8BIT
IO.popen(cmd, 'rb') {|io| io.read }
end end
def process_raw_blame(output) def process_raw_blame(output)
......
...@@ -19,6 +19,8 @@ module Gitlab ...@@ -19,6 +19,8 @@ module Gitlab
cmd_output = "" cmd_output = ""
cmd_status = 0 cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
stdout.set_encoding(Encoding::ASCII_8BIT)
yield(stdin) if block_given? yield(stdin) if block_given?
stdin.close stdin.close
......
...@@ -614,11 +614,11 @@ module Gitlab ...@@ -614,11 +614,11 @@ module Gitlab
if is_enabled if is_enabled
gitaly_ref_client.find_ref_name(sha, ref_path) gitaly_ref_client.find_ref_name(sha, ref_path)
else 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] # Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 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 end
end end
...@@ -887,8 +887,7 @@ module Gitlab ...@@ -887,8 +887,7 @@ module Gitlab
"delete #{ref}\x00\x00" "delete #{ref}\x00\x00"
end end
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] message, status = run_git(%w[update-ref --stdin -z]) do |stdin|
message, status = popen(command, path) do |stdin|
stdin.write(instructions.join) stdin.write(instructions.join)
end end
...@@ -1409,6 +1408,11 @@ module Gitlab ...@@ -1409,6 +1408,11 @@ module Gitlab
end end
end end
def shell_blame(sha, path)
output, _status = run_git(%W(blame -p #{sha} -- #{path}))
output
end
private private
def shell_write_ref(ref_path, ref, old_ref) def shell_write_ref(ref_path, ref, old_ref)
...@@ -1433,6 +1437,12 @@ module Gitlab ...@@ -1433,6 +1437,12 @@ module Gitlab
def run_git(args, chdir: path, env: {}, nice: false, &block) def run_git(args, chdir: path, env: {}, nice: false, &block)
cmd = [Gitlab.config.git.bin_path, *args] cmd = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice 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 circuit_breaker.perform do
popen(cmd, chdir, env, &block) popen(cmd, chdir, env, &block)
end end
...@@ -1624,7 +1634,7 @@ module Gitlab ...@@ -1624,7 +1634,7 @@ module Gitlab
offset_in_ruby = use_follow_flag && options[:offset].present? offset_in_ruby = use_follow_flag && options[:offset].present?
limit += offset if offset_in_ruby 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 << "--max-count=#{limit}"
cmd << '--format=%H' cmd << '--format=%H'
cmd << "--skip=#{offset}" unless offset_in_ruby cmd << "--skip=#{offset}" unless offset_in_ruby
...@@ -1640,7 +1650,7 @@ module Gitlab ...@@ -1640,7 +1650,7 @@ module Gitlab
cmd += Array(options[:path]) cmd += Array(options[:path])
end 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 = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
lines.map! { |c| Rugged::Commit.new(rugged, c.strip) } lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
...@@ -1678,18 +1688,23 @@ module Gitlab ...@@ -1678,18 +1688,23 @@ module Gitlab
end end
def alternate_object_directories 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? if relative_paths.any?
relative_paths.map { |d| File.join(path, d) } relative_paths.map { |d| File.join(path, d) }
else else
Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES) absolute_object_directories.flat_map { |d| d.split(File::PATH_SEPARATOR) }
.flatten
.compact
.flat_map { |d| d.split(File::PATH_SEPARATOR) }
end end
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 # Get the content of a blob for a given commit. If the blob is a commit
# (for submodules) then return the blob's OID. # (for submodules) then return the blob's OID.
def blob_content(commit, blob_name) def blob_content(commit, blob_name)
...@@ -1833,13 +1848,13 @@ module Gitlab ...@@ -1833,13 +1848,13 @@ module Gitlab
def count_commits_by_shelling_out(options) def count_commits_by_shelling_out(options)
cmd = count_commits_shelling_command(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) process_count_commits_raw_output(raw_output, options)
end end
def count_commits_shelling_command(options) 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 << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before] cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count] cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
...@@ -1884,20 +1899,17 @@ module Gitlab ...@@ -1884,20 +1899,17 @@ module Gitlab
return [] return []
end end
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree) cmd = %W(ls-tree -r --full-tree --full-name -- #{actual_ref})
cmd += %w(-r) raw_output, _status = run_git(cmd)
cmd += %w(--full-tree)
cmd += %w(--full-name)
cmd += %W(-- #{actual_ref})
raw_output = IO.popen(cmd, &:read).split("\n").map do |f| lines = raw_output.split("\n").map do |f|
stuff, path = f.split("\t") stuff, path = f.split("\t")
_mode, type, _sha = stuff.split(" ") _mode, type, _sha = stuff.split(" ")
path if type == "blob" path if type == "blob"
# Contain only blob type # Contain only blob type
end end
raw_output.compact lines.compact
end end
# Returns true if the given ref name exists # Returns true if the given ref name exists
......
...@@ -19,8 +19,13 @@ module Gitlab ...@@ -19,8 +19,13 @@ module Gitlab
def error(error) def error(error)
error_out(error.message, caller[0].dup) error_out(error.message, caller[0].dup)
@errors << error.message @errors << error.message
# Debug: # 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 end
private private
......
...@@ -21,7 +21,11 @@ namespace :gitlab do ...@@ -21,7 +21,11 @@ namespace :gitlab do
_, status = Gitlab::Popen.popen(%w[which gmake]) _, status = Gitlab::Popen.popen(%w[which gmake])
command << (status.zero? ? 'gmake' : 'make') 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) Gitlab::SetupHelper.create_gitaly_configuration(args.dir)
Dir.chdir(args.dir) do Dir.chdir(args.dir) do
......
...@@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do ...@@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do
context 'when user disables the cluster' do context 'when user disables the cluster' do
before 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' } page.within('#cluster-integration') { click_button 'Save changes' }
end end
......
...@@ -62,7 +62,7 @@ feature 'User Cluster', :js do ...@@ -62,7 +62,7 @@ feature 'User Cluster', :js do
context 'when user disables the cluster' do context 'when user disables the cluster' do
before 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' fill_in 'cluster_name', with: 'dev-cluster'
page.within('#cluster-integration') { click_button 'Save changes' } page.within('#cluster-integration') { click_button 'Save changes' }
end end
......
...@@ -37,13 +37,13 @@ feature 'Clusters', :js do ...@@ -37,13 +37,13 @@ feature 'Clusters', :js do
context 'inline update of cluster' do context 'inline update of cluster' do
it 'user can update 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 end
context 'with sucessfull request' do context 'with sucessfull request' do
it 'user sees updated cluster' do it 'user sees updated cluster' do
expect do expect do
page.find('.js-toggle-cluster-list').click page.find('.js-project-feature-toggle').click
wait_for_requests wait_for_requests
end.to change { cluster.reload.enabled } end.to change { cluster.reload.enabled }
...@@ -57,7 +57,7 @@ feature 'Clusters', :js do ...@@ -57,7 +57,7 @@ feature 'Clusters', :js do
expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false } 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_content('Something went wrong on our end.')
expect(page).to have_selector('.is-checked') expect(page).to have_selector('.is-checked')
......
...@@ -23,16 +23,24 @@ describe('Clusters', () => { ...@@ -23,16 +23,24 @@ describe('Clusters', () => {
}); });
describe('toggle', () => { describe('toggle', () => {
it('should update the button and the input field on click', () => { it('should update the button and the input field on click', (done) => {
cluster.toggleButton.click(); 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( toggleButton.click();
cluster.toggleButton.classList,
).not.toContain('is-checked');
expect( getSetTimeoutPromise()
cluster.toggleInput.getAttribute('value'), .then(() => {
).toEqual('false'); expect(
toggleButton.classList,
).not.toContain('is-checked');
expect(
toggleInput.getAttribute('value'),
).toEqual('false');
})
.then(done)
.catch(done.fail);
}); });
}); });
......
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);
});
});
});
...@@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle ...@@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect(response).to be_success expect(response).to be_success
store_frontend_fixture(response, example.description) store_frontend_fixture(response, example.description)
end 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 end
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();
});
});
});
...@@ -7096,7 +7096,7 @@ ...@@ -7096,7 +7096,7 @@
"project_id": 5, "project_id": 5,
"created_at": "2016-06-14T15:01:51.232Z", "created_at": "2016-06-14T15:01:51.232Z",
"updated_at": "2016-06-14T15:01:51.232Z", "updated_at": "2016-06-14T15:01:51.232Z",
"active": false, "active": true,
"properties": { "properties": {
}, },
......
...@@ -164,6 +164,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do ...@@ -164,6 +164,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService') expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
end 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 it 'has project feature' do
project_feature = saved_project_json['project_feature'] project_feature = saved_project_json['project_feature']
expect(project_feature).not_to be_empty expect(project_feature).not_to be_empty
...@@ -279,7 +283,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do ...@@ -279,7 +283,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
commit_id: ci_build.pipeline.sha) commit_id: ci_build.pipeline.sha)
create(:event, :created, target: milestone, project: project, author: user) 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)
create(:project_custom_attribute, project: project) create(:project_custom_attribute, project: project)
......
...@@ -1441,7 +1441,7 @@ describe API::Issues, :mailer do ...@@ -1441,7 +1441,7 @@ describe API::Issues, :mailer do
context 'when source project does not exist' do context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' 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 to_project_id: target_project.id
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
...@@ -1452,7 +1452,7 @@ describe API::Issues, :mailer do ...@@ -1452,7 +1452,7 @@ describe API::Issues, :mailer do
context 'when target project does not exist' do context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), 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) expect(response).to have_gitlab_http_status(404)
end end
......
...@@ -76,7 +76,11 @@ describe 'gitlab:gitaly namespace rake task' do ...@@ -76,7 +76,11 @@ describe 'gitlab:gitaly namespace rake task' do
end end
context 'when Rails.env is test' do 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 before do
allow(Rails.env).to receive(:test?).and_return(true) allow(Rails.env).to receive(:test?).and_return(true)
......
...@@ -49,9 +49,22 @@ describe RepositoryImportWorker do ...@@ -49,9 +49,22 @@ describe RepositoryImportWorker do
expect do expect do
subject.perform(project.id) 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 expect(project.reload.import_jid).not_to be_nil
end 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 end
context 'when using an asynchronous importer' do context 'when using an asynchronous importer' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment