Commit c38626b1 authored by Jose Vargas's avatar Jose Vargas

Merge branch 'master' into ce-to-ee-2018-08-07

parents fad1fe7c 9b649a36
......@@ -33,19 +33,24 @@ const categoryLabelMap = {
const IS_VISIBLE = 'is-visible';
const IS_RENDERED = 'is-rendered';
class AwardsHandler {
export class AwardsHandler {
constructor(emoji) {
this.emoji = emoji;
this.eventListeners = [];
this.toggleButtonSelector = '.js-add-award';
this.menuClass = 'js-award-emoji-menu';
}
bindEvents() {
// If the user shows intent let's pre-build the menu
this.registerEventListener(
'one',
$(document),
'mouseenter focus',
'.js-add-award',
this.toggleButtonSelector,
'mouseenter focus',
() => {
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
if ($menu.length === 0) {
requestAnimationFrame(() => {
this.createEmojiMenu();
......@@ -53,7 +58,7 @@ class AwardsHandler {
}
},
);
this.registerEventListener('on', $(document), 'click', '.js-add-award', e => {
this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
......@@ -61,15 +66,17 @@ class AwardsHandler {
this.registerEventListener('on', $('html'), 'click', e => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu').length) {
if (!$target.closest(`.${this.menuClass}`).length) {
$('.js-awards-block.current').removeClass('current');
if ($('.emoji-menu').is(':visible')) {
$('.js-add-award.is-active').removeClass('is-active');
this.hideMenuElement($('.emoji-menu'));
if ($(`.${this.menuClass}`).is(':visible')) {
$(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
this.hideMenuElement($(`.${this.menuClass}`));
}
}
});
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', e => {
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
......@@ -101,7 +108,7 @@ class AwardsHandler {
$addBtn.closest('.js-awards-block').addClass('current');
}
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
const $userAuthored = this.isUserAuthored($addBtn);
if ($menu.length) {
......@@ -118,7 +125,7 @@ class AwardsHandler {
} else {
$addBtn.addClass('is-loading is-active');
this.createEmojiMenu(() => {
const $createdMenu = $('.emoji-menu');
const $createdMenu = $(`.${this.menuClass}`);
$addBtn.removeClass('is-loading');
this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => {
......@@ -156,7 +163,7 @@ class AwardsHandler {
}
const emojiMenuMarkup = `
<div class="emoji-menu">
<div class="emoji-menu ${this.menuClass}">
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
<div class="emoji-menu-content">
......@@ -185,7 +192,7 @@ class AwardsHandler {
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
const menu = document.querySelector('.emoji-menu');
const menu = document.querySelector(`.${this.menuClass}`);
const emojiContentElement = menu.querySelector('.emoji-menu-content');
const remainingCategories = Object.keys(categoryMap).slice(1);
const allCategoriesAddedPromise = remainingCategories.reduce(
......@@ -270,9 +277,9 @@ class AwardsHandler {
if (isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu'));
this.hideMenuElement($(`.${this.menuClass}`));
$('.js-add-award.is-active').removeClass('is-active');
$(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
awardName: emoji,
......@@ -291,9 +298,9 @@ class AwardsHandler {
return typeof callback === 'function' ? callback() : undefined;
});
this.hideMenuElement($('.emoji-menu'));
this.hideMenuElement($(`.${this.menuClass}`));
return $('.js-add-award.is-active').removeClass('is-active');
return $(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
}
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
......@@ -321,7 +328,7 @@ class AwardsHandler {
getVotesBlock() {
if (isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
const $el = $(`${this.toggleButtonSelector}.is-active`).closest('.note.timeline-entry');
if ($el.length) {
return $el;
......@@ -458,7 +465,7 @@ class AwardsHandler {
}
createEmoji(votesBlock, emoji) {
if ($('.emoji-menu').length) {
if ($(`.${this.menuClass}`).length) {
this.createAwardButtonForVotesBlock(votesBlock, emoji);
}
this.createEmojiMenu(() => {
......@@ -538,7 +545,7 @@ class AwardsHandler {
this.searchEmojis(term);
});
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
this.registerEventListener('on', $menu, transitionEndEventString, e => {
if (e.target === e.currentTarget) {
// Clear the search
......@@ -608,7 +615,7 @@ class AwardsHandler {
this.eventListeners.forEach(entry => {
entry.element.off.call(entry.element, ...entry.args);
});
$('.emoji-menu').remove();
$(`.${this.menuClass}`).remove();
}
}
......@@ -616,7 +623,11 @@ let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
Emoji => new AwardsHandler(Emoji),
Emoji => {
const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents();
return awardsHandler;
},
);
}
return awardsHandlerPromise;
......
import { AwardsHandler } from '~/awards_handler';
class EmojiMenu extends AwardsHandler {
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback) {
super(emoji);
this.selectEmojiCallback = selectEmojiCallback;
this.toggleButtonSelector = toggleButtonSelector;
this.menuClass = menuClass;
}
postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
callback();
}
}
export default EmojiMenu;
import $ from 'jquery';
import createFlash from '~/flash';
import GfmAutoComplete from '~/gfm_auto_complete';
import EmojiMenu from './emoji_menu';
document.addEventListener('DOMContentLoaded', () => {
const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field');
const findNoEmojiPlaceholder = () => document.getElementById('js-no-emoji-placeholder');
const removeStatusEmoji = () => {
const statusEmoji = toggleEmojiMenuButton.querySelector('gl-emoji');
if (statusEmoji) {
statusEmoji.remove();
}
};
const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji;
findNoEmojiPlaceholder().classList.add('hidden');
removeStatusEmoji();
toggleEmojiMenuButton.innerHTML += emojiTag;
};
const clearEmojiButton = document.getElementById('js-clear-user-status-button');
clearEmojiButton.addEventListener('click', () => {
statusEmojiField.value = '';
statusMessageField.value = '';
removeStatusEmoji();
findNoEmojiPlaceholder().classList.remove('hidden');
});
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(statusMessageField), { emojis: true });
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
const emojiMenu = new EmojiMenu(
Emoji,
toggleEmojiMenuButtonSelector,
'js-status-emoji-menu',
selectEmojiCallback,
);
emojiMenu.bindEvents();
})
.catch(() => createFlash('Failed to load emoji list!'));
});
import initForm from '../form';
import MirrorRepos from './mirror_repos';
document.addEventListener('DOMContentLoaded', initForm);
document.addEventListener('DOMContentLoaded', () => {
initForm();
const mirrorReposContainer = document.querySelector('.js-mirror-settings');
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
});
import $ from 'jquery';
import _ from 'underscore';
import { __ } from '~/locale';
import Flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
export default class MirrorRepos {
constructor(container) {
this.$container = $(container);
this.$form = $('.js-mirror-form', this.$container);
this.$urlInput = $('.js-mirror-url', this.$form);
this.$protectedBranchesInput = $('.js-mirror-protected', this.$form);
this.$table = $('.js-mirrors-table-body', this.$container);
this.mirrorEndpoint = this.$form.data('projectMirrorEndpoint');
}
init() {
this.initMirrorPush();
this.registerUpdateListeners();
}
initMirrorPush() {
this.$passwordGroup = $('.js-password-group', this.$container);
this.$password = $('.js-password', this.$passwordGroup);
this.$authMethod = $('.js-auth-method', this.$form);
this.$authMethod.on('change', () => this.togglePassword());
this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl());
}
updateUrl() {
let val = this.$urlInput.val();
if (this.$password) {
const password = this.$password.val();
if (password) val = val.replace('@', `:${password}@`);
}
$('.js-mirror-url-hidden', this.$form).val(val);
}
updateProtectedBranches() {
const val = this.$protectedBranchesInput.get(0).checked
? this.$protectedBranchesInput.val()
: '0';
$('.js-mirror-protected-hidden', this.$form).val(val);
}
registerUpdateListeners() {
this.debouncedUpdateUrl = _.debounce(() => this.updateUrl(), 200);
this.$urlInput.on('input', () => this.debouncedUpdateUrl());
this.$protectedBranchesInput.on('change', () => this.updateProtectedBranches());
this.$table.on('click', '.js-delete-mirror', event => this.deleteMirror(event));
}
togglePassword() {
const isPassword = this.$authMethod.val() === 'password';
if (!isPassword) {
this.$password.val('');
this.updateUrl();
}
this.$passwordGroup.collapse(isPassword ? 'show' : 'hide');
}
deleteMirror(event, existingPayload) {
const $target = $(event.currentTarget);
let payload = existingPayload;
if (!payload) {
payload = {
project: {
remote_mirrors_attributes: {
id: $target.data('mirrorId'),
enabled: 0,
},
},
};
}
return axios
.put(this.mirrorEndpoint, payload)
.then(() => this.removeRow($target))
.catch(() => Flash(__('Failed to remove mirror.')));
}
/* eslint-disable class-methods-use-this */
removeRow($target) {
const row = $target.closest('tr');
$('.js-delete-mirror', row).tooltip('hide');
row.remove();
}
/* eslint-enable class-methods-use-this */
}
......@@ -339,3 +339,13 @@ input[type=color].form-control {
vertical-align: unset;
}
}
// Bootstrap 3 compatibility because bootstrap_form Gem is not updated yet
.input-group-btn:first-child {
@extend .input-group-prepend;
}
// Bootstrap 3 compatibility because bootstrap_form Gem is not updated yet
.input-group-btn:last-child {
@extend .input-group-append;
}
......@@ -201,7 +201,7 @@ label {
}
.gl-show-field-errors {
.form-control {
.form-control:not(textarea) {
height: 34px;
}
......
......@@ -546,6 +546,7 @@ ul.notes {
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
......
......@@ -416,6 +416,26 @@ table.u2f-registrations {
}
}
.edit-user {
.clear-user-status {
svg {
fill: $gl-text-color-secondary;
}
}
.emoji-menu-toggle-button {
@extend .note-action-button;
.no-emoji-placeholder {
position: relative;
svg {
fill: $gl-text-color-secondary;
}
}
}
}
.gitlab-slack-gif {
width: 100%;
max-width: $add-to-slack-gif-max-width;
......
......@@ -342,3 +342,17 @@
margin-bottom: 0;
}
}
.mirror-error-badge {
background-color: $error-bg;
border-radius: $border-radius-default;
color: $white-light;
}
.push-pull-table {
margin-top: 1em;
.mirror-action-buttons {
padding-right: 0;
}
}
......@@ -9,8 +9,4 @@ module ProfilesHelper
end
end
end
def show_user_status_field?
Feature.enabled?(:user_status_form) || cookies[:feature_user_status_form] == 'true'
end
end
......@@ -31,17 +31,37 @@
%hr
= link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted'
- if show_user_status_field?
%hr
.row
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0= s_("User|Current Status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface. The message can contain emoji codes, too.")
.col-lg-8
.row
= f.fields_for :status, @user.status do |status_form|
= status_form.text_field :emoji
= status_form.text_field :message, maxlength: 100
%hr
.row
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0= s_("User|Current status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.col-lg-8
= f.fields_for :status, @user.status do |status_form|
- emoji_button = button_tag type: :button,
class: 'js-toggle-emoji-menu emoji-menu-toggle-button btn has-tooltip',
title: s_("Profiles|Add status emoji") do
- if @user.status
= emoji_icon @user.status.emoji
%span#js-no-emoji-placeholder.no-emoji-placeholder{ class: ('hidden' if @user.status) }
= sprite_icon('emoji_slightly_smiling_face', css_class: 'award-control-icon-neutral')
= sprite_icon('emoji_smiley', css_class: 'award-control-icon-positive')
= sprite_icon('emoji_smile', css_class: 'award-control-icon-super-positive')
- reset_message_button = button_tag type: :button,
id: 'js-clear-user-status-button',
class: 'clear-user-status btn has-tooltip',
title: s_("Profiles|Clear status") do
= sprite_icon("close")
= status_form.hidden_field :emoji, id: 'js-status-emoji-field'
= status_form.text_field :message,
id: 'js-status-message-field',
class: 'form-control input-lg',
label: s_("Profiles|Your status"),
prepend: emoji_button,
append: reset_message_button,
placeholder: s_("Profiles|What's your status?")
%hr
.row
.col-lg-4.profile-settings-sidebar
......
.account-well.prepend-top-default.append-bottom-default
%ul
%li
The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>.
%li
Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.
%li
The update action will time out after 10 minutes. For big repositories, use a clone/push combination.
%li
The Git LFS objects will <strong>not</strong> be synced.
= _('The repository must be accessible over <code>http://</code>,
<code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe
%li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe
%li= _("The update action will time out after #{import_will_timeout_message(Gitlab.config.gitlab_shell.git_timeout)} minutes. For big repositories, use a clone/push combination.")
%li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
%li
= _('This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.')
- expanded = Rails.env.test?
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
%section.settings.project-mirror-settings.js-mirror-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Mirroring repositories')
%button.btn.js-settings-toggle
= expanded ? _('Collapse') : _('Expand')
%p
= _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.')
= link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank'
.settings-content
= form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'false', data: mirrors_form_data_attributes } do |f|
.card
.card-header
%h3.card-title= _('Mirror a repository')
.card-body
%div= form_errors(@project)
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
= text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", title: 'A valid repository URL is required'
= render 'projects/mirrors/instructions'
= render 'projects/mirrors/mirror_repos_form', f: f
.form-check.append-bottom-10
= check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input'
= label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches'), target: '_blank'
.card-footer
= f.submit _('Mirror repository'), class: 'btn btn-create', name: :update_remote_mirror
.card
.table-responsive
%table.table.push-pull-table
%thead
%tr
%th
= _('Mirrored repositories')
= render_if_exists 'projects/mirrors/mirrored_repositories_count'
%th= _('Direction')
%th= _('Last update')
%th
%th
%tbody.js-mirrors-table-body
= render_if_exists 'projects/mirrors/table_pull_row'
- @project.remote_mirrors.each_with_index do |mirror, index|
- if mirror.enabled
%tr
%td= mirror.safe_url
%td= _('Push')
%td= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
%td
- if mirror.last_error.present?
.badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
%td.mirror-action-buttons
.btn-group.mirror-actions-group.pull-right{ role: 'group' }
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
%button.js-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
.form-group
= label_tag :mirror_direction, _('Mirror direction'), class: 'label-light'
= select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction', disabled: true
= f.fields_for :remote_mirrors, @remote_mirror do |rm_f|
= rm_f.hidden_field :enabled, value: '1'
= rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+"
= rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden'
.form-group
= label_tag :auth_method, _('Authentication method'), class: 'label-bold'
= select_tag :auth_method, options_for_select([[[_('None'), 'none'], [_('Password'), 'password']], 'none'), { class: "form-control js-auth-method", disabled: true }
.form-group.js-password-group.collapse
= label_tag :password, _('Password'), class: 'label-bold'
= text_field_tag :password, '', class: 'form-control js-password'
- expanded = Rails.env.test?
%section.settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
Push to a remote repository
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Set up the remote repository that you want to update with the content of the current repository
every time someone pushes to it.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
.settings-content
= form_for @project, url: project_mirror_path(@project) do |f|
%div
= form_errors(@project)
= render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
- if @remote_mirror.last_error.present?
.card.bg-danger
.card-header
- if @remote_mirror.last_update_at
The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}.
- else
The remote repository failed to update.
- if @remote_mirror.last_successful_update_at
Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
.card-body
%pre
:preserve
#{h(@remote_mirror.last_error.strip)}
= f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
.form-group
= rm_form.check_box :enabled, class: "float-left"
.prepend-left-20
= rm_form.label :enabled, "Remote mirror repository", class: "label-bold append-bottom-0"
%p.light.append-bottom-0
Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
.form-group.has-feedback
= rm_form.label :url, "Git repository URL", class: "label-bold"
= rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions"
.form-group
= rm_form.check_box :only_protected_branches, class: 'float-left'
.prepend-left-20
= rm_form.label :only_protected_branches, class: 'label-bold'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
- if can?(current_user, :admin_mirror, @project)
= render 'projects/mirrors/pull'
- if can?(current_user, :admin_remote_mirror, @project)
= render 'projects/mirrors/push'
= render 'projects/mirrors/mirror_repos'
- if @project.has_remote_mirror?
.append-bottom-default
- if remote_mirror.update_in_progress?
%span.btn.disabled
= icon("refresh spin")
Updating&hellip;
- else
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do
= icon("refresh")
Update Now
- if @remote_mirror.last_successful_update_at
%p.inline.prepend-left-10
Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
- if remote_mirror.update_in_progress?
%button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') }
= icon("refresh spin")
- else
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= icon("refresh")
---
title: Restyle status message input on profile settings
merge_request: 20903
author:
type: changed
import $ from 'jquery';
import { __ } from '~/locale';
import Flash from '~/flash';
import MirrorRepos from '~/pages/projects/settings/repository/show/mirror_repos';
import MirrorPull from 'ee/mirrors/mirror_pull';
export default class EEMirrorRepos extends MirrorRepos {
constructor(...args) {
super(...args);
this.$password = undefined;
this.$mirrorDirectionSelect = $('.js-mirror-direction', this.$form);
this.$insertionPoint = $('.js-form-insertion-point', this.$form);
this.$repoCount = $('.js-mirrored-repo-count', this.$container);
this.directionFormMap = {
push: $('.js-push-mirrors-form', this.$form).html(),
pull: $('.js-pull-mirrors-form', this.$form).html(),
};
}
init() {
this.$insertionPoint.collapse({
toggle: false,
});
this.handleUpdate();
super.init();
}
handleUpdate() {
return this.hideForm()
.then(() => {
this.updateForm();
this.showForm();
})
.catch(() => {
Flash(__('Something went wrong on our end.'));
});
}
hideForm() {
return new Promise(resolve => {
if (!this.$insertionPoint.html()) return resolve();
this.$insertionPoint.one('hidden.bs.collapse', () => {
resolve();
});
return this.$insertionPoint.collapse('hide');
});
}
showForm() {
return new Promise(resolve => {
this.$insertionPoint.one('shown.bs.collapse', () => {
resolve();
});
this.$insertionPoint.collapse('show');
});
}
updateForm() {
const direction = this.$mirrorDirectionSelect.val();
this.$insertionPoint.collapse('hide');
this.$insertionPoint.html(this.directionFormMap[direction]);
this.$insertionPoint.collapse('show');
this.updateUrl();
this.updateProtectedBranches();
if (direction === 'pull') return this.initMirrorPull();
if (this.mirrorPull) this.mirrorPull.destroy();
return this.initMirrorPush();
}
initMirrorPull() {
this.$password.off('input.updateUrl');
this.$password = undefined;
this.mirrorPull = new MirrorPull('.js-mirror-form');
this.mirrorPull.init();
this.initSelect2();
}
initSelect2() {
$('.js-mirror-user', this.$form).select2({
width: 'resolve',
dropdownAutoWidth: true,
});
}
registerUpdateListeners() {
super.registerUpdateListeners();
this.$mirrorDirectionSelect.on('change', () => this.handleUpdate());
}
deleteMirror(event) {
const $target = $(event.currentTarget);
const isPullMirror = $target.hasClass('js-delete-pull-mirror');
let payload;
if (isPullMirror) {
payload = {
project: {
mirror: false,
},
};
}
return super.deleteMirror(event, payload)
.then(() => {
if (isPullMirror) this.$mirrorDirectionSelect.removeAttr('disabled');
});
}
removeRow($target) {
super.removeRow($target);
const currentCount = parseInt(this.$repoCount.text().replace(/(\(|\))/, ''), 10);
this.$repoCount.text(`(${currentCount - 1})`);
}
}
/* eslint-disable no-new */
import ProtectedBranchCreate from 'ee/protected_branches/protected_branch_create';
import ProtectedBranchEditList from 'ee/protected_branches/protected_branch_edit_list';
import ProtectedTagCreate from 'ee/protected_tags/protected_tag_create';
import ProtectedTagEditList from 'ee/protected_tags/protected_tag_edit_list';
import UsersSelect from '~/users_select';
import UserCallout from '~/user_callout';
import initSettingsPanels from '~/settings_panels';
......@@ -7,13 +12,8 @@ import CEProtectedBranchCreate from '~/protected_branches/protected_branch_creat
import CEProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
import CEProtectedTagCreate from '~/protected_tags/protected_tag_create';
import CEProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import MirrorPull from 'ee/mirrors/mirror_pull';
import DueDateSelectors from '~/due_date_select';
import ProtectedBranchCreate from 'ee/protected_branches/protected_branch_create';
import ProtectedBranchEditList from 'ee/protected_branches/protected_branch_edit_list';
import ProtectedTagCreate from 'ee/protected_tags/protected_tag_create';
import ProtectedTagEditList from 'ee/protected_tags/protected_tag_edit_list';
import EEMirrorRepos from './ee_mirror_repos';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect();
......@@ -36,11 +36,8 @@ document.addEventListener('DOMContentLoaded', () => {
new CEProtectedTagEditList();
}
const mirrorPull = new MirrorPull('.js-project-mirror-push-form');
if (mirrorPull) {
mirrorPull.init();
}
const pushPullContainer = document.querySelector('.js-mirror-settings');
if (pushPullContainer) new EEMirrorRepos(pushPullContainer).init();
new DueDateSelectors();
});
......@@ -16,7 +16,7 @@
}
.ssh-public-key {
width: 95%;
width: 94%;
word-wrap: break-word;
word-break: break-all;
}
......
......@@ -28,4 +28,14 @@ module MirrorHelper
def options_for_mirror_user
options_from_collection_for_select(default_mirror_users, :id, :name, @project.mirror_user_id || current_user.id)
end
def mirrored_repositories_count(project = @project)
count = project.mirror == true ? 1 : 0
count + @project.remote_mirrors.to_a.count { |mirror| mirror.enabled }
end
def mirrors_form_data_attributes
{ project_mirror_ssh_endpoint: ssh_host_keys_project_mirror_path(@project, :json),
project_mirror_endpoint: project_mirror_path(@project) }
end
end
- import_data = @project.import_data || @project.build_import_data
- is_one_user_option = default_mirror_users.count == 1
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
- can_push = can?(current_user, :admin_remote_mirror, @project)
- can_pull = can?(current_user, :admin_mirror, @project)
- options = []
- options.push([_('Push'), 'push']) if can_push
- if can_pull
- has_existing_pull_mirror = can_pull && @project.mirror
- pull_addition_method = has_existing_pull_mirror ? options.method(:push) : options.method(:unshift)
- pull_addition_method.call([_('Pull'), 'pull']) if can_pull
.form-group
= label_tag :mirror_direction, _('Mirror direction'), class: 'label-light'
= select_tag :mirror_direction, options_for_select(options), class: 'form-control js-mirror-direction', disabled: (options.count == 1) || has_existing_pull_mirror
.js-form-insertion-point
- if can_push
%template.js-push-mirrors-form
= f.fields_for :remote_mirrors, @project.remote_mirrors.build do |rm_f|
= rm_f.hidden_field :enabled, value: '1'
= rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+"
= rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden'
.form-group
= label_tag :auth_method, _('Authentication method'), class: 'label-bold'
= select_tag :auth_method, options_for_select([[_('None'), 'none'], [_('Password'), 'password']], 'none'), { class: "form-control js-auth-method" }
.form-group.js-password-group.collapse
= label_tag :password, _('Password'), class: 'label-bold'
= text_field_tag :password, '', type: 'password', class: 'form-control js-password'
- if can_pull
%template.js-pull-mirrors-form
= f.hidden_field :mirror, value: '1'
= f.hidden_field :username_only_import_url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+"
= f.hidden_field :only_mirror_protected_branches, class: 'js-mirror-protected-hidden'
= f.fields_for :import_data, import_data do |import_form|
= render partial: 'projects/mirrors/pull/ssh_host_keys', locals: { f: import_form }
= render partial: 'projects/mirrors/pull/authentication_method', locals: { f: import_form }
.form-group
= f.label :mirror_user_id, _('Mirror user'), class: 'label-light'
- if is_one_user_option
= select_tag(:mirror_user_id_select, options_for_mirror_user, class: 'js-mirror-user select2 lg append-bottom-5', required: true, disabled: true)
= f.hidden_field :mirror_user_id, value: default_mirror_users.first.id if is_one_user_option
- else
= f.select(:mirror_user_id, options_for_mirror_user, {}, class: 'js-mirror-user select2 lg append-bottom-5', required: true)
.help-block
= _('This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user.')
.form-check.append-bottom-10
= f.check_box :mirror_overwrites_diverged_branches, class: 'form-check-input', checked: false
= f.label :mirror_overwrites_diverged_branches, _('Overwrite diverged branches'), class: 'form-check-label'
.form-text.text-muted
= _("If disabled, a diverged local branch will not be automatically updated with commits from its remote counterpart, to prevent local data loss. If the default branch (%{default_branch}) has diverged and cannot be updated, mirroring will fail. Other diverged branches are silently ignored.") % { default_branch: @project.default_branch }
- if @project.builds_enabled?
= render 'shared/mirror_trigger_builds_setting', f: f, checked: false
%span.js-mirrored-repo-count (#{mirrored_repositories_count})
- expanded = Rails.env.test?
- import_data = @project.import_data || @project.build_import_data
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
%section.settings.project-mirror-settings.no-animate#js-pull-remote-repository{ class: ('expanded' if expanded) }
.settings-header
%h4
Pull from a remote repository
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Set up your project to automatically have its branches, tags, and commits
updated from an upstream repository.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pulling-from-a-remote-repository'), target: '_blank'
.settings-content
= form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors project-mirror-push-form js-project-mirror-push-form', autocomplete: 'false', data: { project_mirror_endpoint: ssh_host_keys_project_mirror_path(@project, :json) } } do |f|
%div
= form_errors(@project)
= render "shared/mirror_update_button"
= render "projects/mirrors/pull/mirror_update_fail"
.form-group
= f.check_box :mirror, class: "float-left"
.prepend-left-20
= f.label :mirror, "Mirror repository", class: "label-bold append-bottom-0"
.form-group
= f.label :username_only_import_url, "Git repository URL", class: "label-bold"
= f.text_field :username_only_import_url, class: 'form-control js-repo-url', placeholder: 'https://username@gitlab.company.com/group/project.git', required: 'required', pattern: "(#{protocols}):\/\/.+", title: 'URL must have protocol present (eg; ssh://...)'
= render "projects/mirrors/instructions"
= f.fields_for :import_data, import_data do |import_form|
= render partial: "projects/mirrors/pull/ssh_host_keys", locals: { f: import_form }
= render partial: "projects/mirrors/pull/authentication_method", locals: { f: import_form }
.form-group
= f.label :mirror_user_id, "Mirror user", class: "label-bold"
= select_tag('project[mirror_user_id]', options_for_mirror_user, class: "select2 lg", required: true)
.form-text.text-muted
This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.
You can only assign yourself to be the mirror user.
.form-group
= f.check_box :only_mirror_protected_branches, class: 'float-left'
.prepend-left-20
= f.label :only_mirror_protected_branches, class: 'label-bold'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
.form-group
= f.check_box :mirror_overwrites_diverged_branches, class: 'float-left'
.prepend-left-20
= f.label :mirror_overwrites_diverged_branches, "Overwrite diverged branches", class: 'label-bold'
.form-text.text-muted
If disabled, a diverged local branch will not be automatically updated with commits from its remote counterpart,
to prevent local data loss. If the default branch (#{@project.default_branch}) has diverged and cannot be updated,
mirroring will fail. Other diverged branches are silently ignored.
- if @project.builds_enabled?
= render "shared/mirror_trigger_builds_setting", f: f
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'
- if @project.mirror
%tr
%td= @project.username_only_import_url
%td= _('Pull')
%td= @project.mirror_last_update_at.present? ? time_ago_with_tooltip(@project.mirror_last_update_at) : _('Never')
%td
- if @project.import_error.present?
.badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(@project.import_error.try(:strip)) }= _('Error')
%td.mirror-action-buttons
.btn-group.mirror-actions-group.pull-right{ role: 'group' }
- if @project.mirror?
- if @project.mirror_about_to_update? || @project.updating_mirror?
%button.btn.disabled{ type: 'button', data: { container: 'body', toggle: 'tooltip' }, title: _('Updating') }= icon("refresh spin")
- else
= link_to update_now_project_mirror_path(@project), method: :post, class: 'btn js-force-update-mirror', data: { container: 'body', toggle: 'tooltip' }, title: _('Update now') do
= icon("refresh")
%button.js-delete-mirror.js-delete-pull-mirror.btn.btn-danger{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
......@@ -4,31 +4,31 @@
- ssh_public_key_present = import_data.ssh_public_key.present?
.form-group
= f.label :auth_method, 'Authentication method', class: 'label-bold'
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
= f.select :auth_method,
options_for_select([['Password authentication', 'password'], ['SSH public key authentication', 'ssh_public_key']], import_data.auth_method),
{}, { class: "form-control js-pull-mirror-auth-type #{'hidden' unless import_data.ssh_import?}" }
options_for_select([[_('Password'), 'password'], [_('SSH public key'), 'ssh_public_key']], import_data.auth_method),
{}, { class: "form-control js-pull-mirror-auth-type" }
.form-group
.account-well.changing-auth-method.hidden.js-well-changing-auth
= icon('spinner spin lg')
.account-well.well-password-auth.hidden.js-well-password-auth
= f.label :password, "Password", class: "label-bold"
.collapse.js-well-changing-auth
.changing-auth-method= icon('spinner spin lg')
.well-password-auth.collapse.js-well-password-auth
= f.label :password, _("Password"), class: "label-bold"
= f.password_field :password, value: import_data.password, class: 'form-control'
.account-well.well-ssh-auth.hidden.js-well-ssh-auth
%p.js-ssh-public-key-present{ class: ('hidden' unless ssh_public_key_present) }
Here is the public SSH key that needs to be added to the remote
server. For more information, please refer to the documentation.
%p.js-ssh-public-key-pending{ class: ('hidden' if ssh_public_key_present) }
An SSH key will be automatically generated when the form is
submitted. For more information, please refer to the documentation.
.well-ssh-auth.collapse.js-well-ssh-auth
%p.js-ssh-public-key-present{ class: ('collapse' unless ssh_public_key_present) }
= _('Here is the public SSH key that needs to be added to the remote server. For more information, please refer to the documentation.')
%p.js-ssh-public-key-pending{ class: ('collapse' if ssh_public_key_present) }
= _('An SSH key will be automatically generated when the form is submitted. For more information, please refer to the documentation.')
.clearfix.js-ssh-public-key-wrap{ class: ('hidden' unless ssh_public_key_present) }
.clearfix.js-ssh-public-key-wrap{ class: ('collapse' unless ssh_public_key_present) }
%code.prepend-top-10.ssh-public-key
= import_data.ssh_public_key
= clipboard_button(text: import_data.ssh_public_key, title: _("Copy SSH public key to clipboard"), class: 'prepend-top-10 btn-copy-ssh-public-key')
= link_to 'Regenerate key', project_mirror_path(@project, project: { import_data_attributes: regen_data }),
method: :patch,
data: { confirm: 'Are you sure you want to regenerate public key? You will have to update the public key on the remote server before mirroring will work again.' },
class: "btn btn-inverted btn-warning prepend-top-10 js-btn-regenerate-ssh-key #{ 'hidden' unless ssh_public_key_present }"
= button_tag type: 'button',
data: { endpoint: project_mirror_path(@project, project: { import_data_attributes: regen_data }) },
class: "btn btn-inverted btn-warning prepend-top-10 js-btn-regenerate-ssh-key#{ ' collapse' unless ssh_public_key_present }" do
%i.fa.fa-spinner.fa-spin.js-spinner
= _('Regenerate key')
= render 'projects/mirrors/pull/regenerate_public_ssh_key_confirm_modal'
.modal.js-regenerate-public-ssh-key-confirm-modal{ tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h3.modal-title.page-title
Regenerate public SSH key?
%button.close.js-cancel{ type: 'button', 'data-dismiss': 'modal', 'aria-label' => _('Close') }
%span{ 'aria-hidden': true } &times;
.modal-body
%p= _('Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again.')
.form-actions.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn js-cancel'
= button_tag _('Regenerate key'), type: 'button', class: 'btn btn-inverted btn-warning js-confirm'
......@@ -2,13 +2,13 @@
- verified_by = import_data.ssh_known_hosts_verified_by
- verified_at = import_data.ssh_known_hosts_verified_at
.form-group.js-ssh-host-keys-section{ class: ('hidden' unless import_data.ssh_import?) }
%button.btn.btn-inverted.btn-success.append-bottom-15.js-detect-host-keys{ type: 'button' }
.form-group.js-ssh-host-keys-section{ class: ('collapse' unless import_data.ssh_import?) }
%button.btn.btn-inverted.btn-success.inline.js-detect-host-keys.append-right-10{ type: 'button' }
= icon('spinner spin', class: 'detect-host-keys-load-spinner hidden')
Detect host keys
.fingerprint-ssh-info.js-fingerprint-ssh-info{ class: ('hidden' unless import_data.ssh_import?) }
= _('Detect host keys')
.fingerprint-ssh-info.js-fingerprint-ssh-info.prepend-top-10.append-bottom-10{ class: ('collapse' unless import_data.ssh_import?) }
%label.label-bold
Fingerprints
= _('Fingerprints')
.fingerprints-list.js-fingerprints-list
- import_data.ssh_known_hosts_fingerprints.each do |fp|
%code= fp.fingerprint
......@@ -19,16 +19,15 @@
- if verified_by
= link_to verified_by.name, user_path(verified_by)
- else
a deleted user
= _('a deleted user')
#{time_ago_in_words(verified_at)} ago
.js-ssh-hosts-advanced
%button.btn.btn-sm.btn-default.prepend-top-10.append-bottom-15.btn-show-advanced.show-advanced{ type: 'button' }
.js-ssh-hosts-advanced.inline
%button.btn.btn-default.btn-show-advanced.show-advanced{ type: 'button' }
%span.label-show
Show advanced
= _('Input host keys manually')
%span.label-hide
Hide advanced
= icon('chevron')
.js-ssh-known-hosts.hidden
= f.label :ssh_known_hosts, 'SSH host keys', class: 'label-bold'
= f.text_area :ssh_known_hosts, class: 'form-control known-hosts js-known-hosts', rows: '10'
= _('Hide host keys manual input')
.js-ssh-known-hosts.collapse.prepend-top-default
= f.label :ssh_known_hosts, _('SSH host keys'), class: 'label-bold'
= f.text_area :ssh_known_hosts, class: 'form-control known-hosts js-known-hosts', rows: '10'
.form-group
= f.check_box :mirror_trigger_builds, class: "float-left"
.prepend-left-20
= f.label :mirror_trigger_builds, "Trigger pipelines for mirror updates", class: "label-bold"
%p.light.append-bottom-0
Trigger pipelines when branches or tags are updated from the upstream repository.
Depending on the activity of the upstream repository, this may greatly increase the load on your CI runners.
Only enable this if you know they can handle the load.
<strong>CI will run using the credentials assigned above.</strong>
- checked = local_assigns.fetch(:checked, nil)
- check_box_options = {}
- check_box_options[:checked] = checked unless checked.nil?
.form-check.append-bottom-10
= f.check_box :mirror_trigger_builds, check_box_options.merge(class: "form-check-input")
= f.label :mirror_trigger_builds, _("Trigger pipelines for mirror updates"), class: "form-check-label"
.form-text.text-muted
= _('Trigger pipelines when branches or tags are updated from the upstream repository. Depending on the activity of the upstream repository, this may greatly increase the load on your CI runners. Only enable this if you know they can handle the load.')
%strong= _('CI will run using the credentials assigned above.')
......@@ -30,7 +30,7 @@ describe 'Project mirror', :js do
visit project_mirror_path(project)
end
Sidekiq::Testing.fake! { click_link('Update Now') }
Sidekiq::Testing.fake! { find('.js-force-update-mirror').click }
end
end
......@@ -44,7 +44,7 @@ describe 'Project mirror', :js do
visit project_mirror_path(project)
end
expect(page).to have_content('Update Now')
expect(page).to have_selector('.js-force-update-mirror')
expect(page).to have_selector('.btn.disabled')
end
end
......@@ -66,10 +66,10 @@ describe 'Project mirror', :js do
visit project_settings_repository_path(project)
page.within('.project-mirror-settings') do
check 'Mirror repository'
fill_in 'Git repository URL', with: 'http://user@example.com'
select 'Pull', from: 'Mirror direction'
fill_in 'Password', with: 'foo'
click_without_sidekiq 'Save changes'
click_without_sidekiq 'Mirror repository'
end
expect(page).to have_content('Mirroring settings were successfully updated')
......@@ -87,8 +87,9 @@ describe 'Project mirror', :js do
page.within('.project-mirror-settings') do
fill_in 'Git repository URL', with: 'http://2.example.com'
select('Pull', from: 'Mirror direction')
fill_in 'Password', with: ''
click_without_sidekiq 'Save changes'
click_without_sidekiq 'Mirror repository'
end
expect(page).to have_content('Mirroring settings were successfully updated')
......@@ -104,9 +105,9 @@ describe 'Project mirror', :js do
visit project_settings_repository_path(project)
page.within('.project-mirror-settings') do
check 'Mirror repository'
fill_in 'Git repository URL', with: 'ssh://user@example.com'
select 'SSH public key authentication', from: 'Authentication method'
select('Pull', from: 'Mirror direction')
select 'SSH public key', from: 'Authentication method'
# Generates an SSH public key with an asynchronous PUT and displays it
wait_for_requests
......@@ -114,7 +115,7 @@ describe 'Project mirror', :js do
expect(import_data.ssh_public_key).not_to be_nil
expect(page).to have_content(import_data.ssh_public_key)
click_without_sidekiq 'Save changes'
click_without_sidekiq 'Mirror repository'
end
# We didn't set any host keys
......@@ -128,11 +129,16 @@ describe 'Project mirror', :js do
expect(import_data.auth_method).to eq('ssh_public_key')
expect(import_data.password).to be_blank
find('.js-delete-mirror').click
fill_in 'Git repository URL', with: 'ssh://user@example.com'
select('Pull', from: 'Mirror direction')
first_key = import_data.ssh_public_key
expect(page).to have_content(first_key)
# Check regenerating the public key works
accept_confirm { click_without_sidekiq 'Regenerate key' }
click_without_sidekiq 'Regenerate key'
find('.js-regenerate-public-ssh-key-confirm-modal .js-confirm').click
wait_for_requests
expect(page).not_to have_content(first_key)
......@@ -151,12 +157,13 @@ describe 'Project mirror', :js do
page.within('.project-mirror-settings') do
fill_in 'Git repository URL', with: 'ssh://example.com'
select('Pull', from: 'Mirror direction')
click_on 'Detect host keys'
wait_for_requests
expect(page).to have_content(key.fingerprint)
click_on 'Show advanced'
click_on 'Input host keys manually'
expect(page).to have_field('SSH host keys', with: key.key_text)
end
......@@ -169,6 +176,7 @@ describe 'Project mirror', :js do
page.within('.project-mirror-settings') do
fill_in 'Git repository URL', with: 'ssh://example.com'
select('Pull', from: 'Mirror direction')
click_on 'Detect host keys'
wait_for_requests
end
......@@ -182,9 +190,14 @@ describe 'Project mirror', :js do
page.within('.project-mirror-settings') do
fill_in 'Git repository URL', with: 'ssh://example.com'
click_on 'Show advanced'
select('Pull', from: 'Mirror direction')
click_on 'Input host keys manually'
fill_in 'SSH host keys', with: "example.com #{key.key_text}"
click_without_sidekiq 'Save changes'
click_without_sidekiq 'Mirror repository'
find('.js-delete-mirror').click
fill_in 'Git repository URL', with: 'ssh://example.com'
select('Pull', from: 'Mirror direction')
expect(page).to have_content(key.fingerprint)
expect(page).to have_content("Verified by #{h(user.name)} less than a minute ago")
......@@ -198,20 +211,22 @@ describe 'Project mirror', :js do
page.within('.project-mirror-settings') do
fill_in 'Git repository URL', with: 'ssh://example.com'
select('Pull', from: 'Mirror direction')
execute_script 'document.querySelector("html").scrollTop = 1000;'
expect(page).to have_select('Authentication method')
# SSH can use password authentication but needs host keys
select 'Password authentication', from: 'Authentication method'
select 'Password', from: 'Authentication method'
expect(page).to have_field('Password')
expect(page).to have_button('Detect host keys')
expect(page).to have_button('Show advanced')
expect(page).to have_button('Input host keys manually')
# SSH public key authentication also needs host keys but no password
select 'SSH public key authentication', from: 'Authentication method'
select 'SSH public key', from: 'Authentication method'
expect(page).not_to have_field('Password')
expect(page).to have_button('Detect host keys')
expect(page).to have_button('Show advanced')
expect(page).to have_button('Input host keys manually')
end
end
......@@ -220,12 +235,13 @@ describe 'Project mirror', :js do
page.within('.project-mirror-settings') do
fill_in 'Git repository URL', with: 'https://example.com'
select('Pull', from: 'Mirror direction')
# HTTPS can't use public key authentication and doesn't need host keys
expect(page).to have_field('Password')
expect(page).not_to have_select('Authentication method')
expect(page).not_to have_button('Detect host keys')
expect(page).not_to have_button('Show advanced')
expect(page).not_to have_button('Input host keys manually')
end
end
end
......
......@@ -18,7 +18,7 @@ describe 'Project settings > [EE] repository' do
visit project_settings_repository_path(project)
end
it 'does not show pull mirror settings' do
it 'does not show pull mirror settings', :js do
expect(page).to have_no_selector('#project_mirror')
expect(page).to have_no_selector('#project_import_url')
expect(page).to have_no_selector('#project_mirror_user_id', visible: false)
......
......@@ -495,6 +495,9 @@ msgstr ""
msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
msgid "An SSH key will be automatically generated when the form is submitted. For more information, please refer to the documentation."
msgstr ""
msgid "An application called %{link_to_client} is requesting access to your GitLab account."
msgstr ""
......@@ -666,6 +669,9 @@ msgstr ""
msgid "Are you sure you want to lose unsaved changes?"
msgstr ""
msgid "Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again."
msgstr ""
msgid "Are you sure you want to remove %{group_name}?"
msgstr ""
......@@ -750,6 +756,9 @@ msgstr ""
msgid "Authentication log"
msgstr ""
msgid "Authentication method"
msgstr ""
msgid "Author"
msgstr ""
......@@ -1178,6 +1187,9 @@ msgstr ""
msgid "CI / CD Settings"
msgstr ""
msgid "CI will run using the credentials assigned above."
msgstr ""
msgid "CI/CD"
msgstr ""
......@@ -2480,12 +2492,18 @@ msgstr ""
msgid "Details"
msgstr ""
msgid "Detect host keys"
msgstr ""
msgid "Diffs|No file name available"
msgstr ""
msgid "Diffs|Something went wrong while fetching diff lines."
msgstr ""
msgid "Direction"
msgstr ""
msgid "Directory name"
msgstr ""
......@@ -2762,6 +2780,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
msgid "Error"
msgstr ""
msgid "Error Reporting and Logging"
msgstr ""
......@@ -2912,6 +2933,9 @@ msgstr ""
msgid "Failed to remove issue from board, please try again."
msgstr ""
msgid "Failed to remove mirror."
msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
......@@ -2960,6 +2984,9 @@ msgstr ""
msgid "Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file."
msgstr ""
msgid "Fingerprints"
msgstr ""
msgid "Finished"
msgstr ""
......@@ -3616,6 +3643,12 @@ msgstr ""
msgid "Help page text and support page url."
msgstr ""
msgid "Here is the public SSH key that needs to be added to the remote server. For more information, please refer to the documentation."
msgstr ""
msgid "Hide host keys manual input"
msgstr ""
msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] ""
......@@ -3684,6 +3717,9 @@ msgstr ""
msgid "Identity provider single sign on URL"
msgstr ""
msgid "If disabled, a diverged local branch will not be automatically updated with commits from its remote counterpart, to prevent local data loss. If the default branch (%{default_branch}) has diverged and cannot be updated, mirroring will fail. Other diverged branches are silently ignored."
msgstr ""
msgid "If disabled, the access level will depend on the user's permissions in the project."
msgstr ""
......@@ -3780,12 +3816,21 @@ msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
msgid "Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>."
msgstr ""
msgid "Incompatible Project"
msgstr ""
msgid "Inline"
msgstr ""
msgid "Input host keys manually"
msgstr ""
msgid "Input your repository URL"
msgstr ""
msgid "Install GitLab Runner"
msgstr ""
......@@ -4393,6 +4438,24 @@ msgstr ""
msgid "Milestones|This action cannot be reversed."
msgstr ""
msgid "Mirror a repository"
msgstr ""
msgid "Mirror direction"
msgstr ""
msgid "Mirror repository"
msgstr ""
msgid "Mirror user"
msgstr ""
msgid "Mirrored repositories"
msgstr ""
msgid "Mirroring repositories"
msgstr ""
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
......@@ -4462,6 +4525,9 @@ msgstr ""
msgid "Network"
msgstr ""
msgid "Never"
msgstr ""
msgid "New"
msgstr ""
......@@ -4755,6 +4821,9 @@ msgstr ""
msgid "Only comments from the following commit are shown below"
msgstr ""
msgid "Only mirror protected branches"
msgstr ""
msgid "Only project members can comment."
msgstr ""
......@@ -4818,6 +4887,9 @@ msgstr ""
msgid "Overview"
msgstr ""
msgid "Overwrite diverged branches"
msgstr ""
msgid "Owner"
msgstr ""
......@@ -5112,9 +5184,15 @@ msgstr ""
msgid "Profiles|Add key"
msgstr ""
msgid "Profiles|Add status emoji"
msgstr ""
msgid "Profiles|Change username"
msgstr ""
msgid "Profiles|Clear status"
msgstr ""
msgid "Profiles|Current path: %{path}"
msgstr ""
......@@ -5142,7 +5220,7 @@ msgstr ""
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
msgstr ""
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface. The message can contain emoji codes, too."
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
msgstr ""
msgid "Profiles|Type your %{confirmationValue} to confirm:"
......@@ -5160,6 +5238,9 @@ msgstr ""
msgid "Profiles|Username successfully changed"
msgstr ""
msgid "Profiles|What's your status?"
msgstr ""
msgid "Profiles|You don't have access to delete this user."
msgstr ""
......@@ -5169,6 +5250,9 @@ msgstr ""
msgid "Profiles|Your account is currently an owner in these groups:"
msgstr ""
msgid "Profiles|Your status"
msgstr ""
msgid "Profiles|e.g. My MacBook key"
msgstr ""
......@@ -5457,6 +5541,12 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
msgid "Pull"
msgstr ""
msgid "Push"
msgstr ""
msgid "Push Rules"
msgstr ""
......@@ -5502,6 +5592,9 @@ msgstr ""
msgid "Refresh"
msgstr ""
msgid "Regenerate key"
msgstr ""
msgid "Register / Sign In"
msgstr ""
......@@ -5729,6 +5822,12 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
msgid "SSH host keys"
msgstr ""
msgid "SSH public key"
msgstr ""
msgid "SSL Verification"
msgstr ""
......@@ -5945,6 +6044,9 @@ msgstr ""
msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
msgstr ""
msgid "Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically."
msgstr ""
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
......@@ -6423,6 +6525,9 @@ msgstr ""
msgid "The Advanced Global Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
msgid "The Git LFS objects will <strong>not</strong> be synced."
msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
......@@ -6495,6 +6600,9 @@ msgstr ""
msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
msgstr ""
msgid "The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> and <code>git://</code>."
msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
......@@ -6672,6 +6780,12 @@ msgstr ""
msgid "This user has no identities"
msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches."
msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
msgid "This will delete the custom metric, Are you sure?"
msgstr ""
......@@ -6964,6 +7078,12 @@ msgstr ""
msgid "Trending"
msgstr ""
msgid "Trigger pipelines for mirror updates"
msgstr ""
msgid "Trigger pipelines when branches or tags are updated from the upstream repository. Depending on the activity of the upstream repository, this may greatly increase the load on your CI runners. Only enable this if you know they can handle the load."
msgstr ""
msgid "Trigger this manual action"
msgstr ""
......@@ -7033,9 +7153,15 @@ msgstr ""
msgid "Update"
msgstr ""
msgid "Update now"
msgstr ""
msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
msgid "Updating"
msgstr ""
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
......@@ -7108,7 +7234,7 @@ msgstr ""
msgid "Users"
msgstr ""
msgid "User|Current Status"
msgid "User|Current status"
msgstr ""
msgid "Variables"
......@@ -7543,6 +7669,9 @@ msgstr ""
msgid "Your projects"
msgstr ""
msgid "a deleted user"
msgstr ""
msgid "ago"
msgstr ""
......
......@@ -8,6 +8,10 @@ describe 'User edit profile' do
visit(profile_path)
end
def submit_settings
click_button 'Update profile settings'
end
it 'changes user profile' do
fill_in 'user_skype', with: 'testskype'
fill_in 'user_linkedin', with: 'testlinkedin'
......@@ -16,7 +20,7 @@ describe 'User edit profile' do
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
fill_in 'user_organization', with: 'GitLab'
click_button 'Update profile settings'
submit_settings
expect(user.reload).to have_attributes(
skype: 'testskype',
......@@ -34,7 +38,7 @@ describe 'User edit profile' do
context 'user avatar' do
before do
attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
click_button 'Update profile settings'
submit_settings
end
it 'changes user avatar' do
......@@ -56,30 +60,75 @@ describe 'User edit profile' do
end
end
context 'user status' do
it 'hides user status when the feature is disabled' do
stub_feature_flags(user_status_form: false)
context 'user status', :js do
def select_emoji(emoji_name)
toggle_button = find('.js-toggle-emoji-menu')
toggle_button.click
emoji_button = find(%Q{.js-status-emoji-menu .js-emoji-btn gl-emoji[data-name="#{emoji_name}"]})
emoji_button.click
end
it 'shows the user status form' do
visit(profile_path)
expect(page).not_to have_content('Current Status')
expect(page).to have_content('Current status')
end
it 'shows the status form when the feature is enabled' do
stub_feature_flags(user_status_form: true)
it 'adds emoji to user status' do
emoji = 'biohazard'
visit(profile_path)
select_emoji(emoji)
submit_settings
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji(emoji)
end
end
it 'adds message to user status' do
message = 'I have something to say'
visit(profile_path)
fill_in 'js-status-message-field', with: message
submit_settings
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji('speech_balloon')
expect(page).to have_content message
end
end
expect(page).to have_content('Current Status')
it 'adds message and emoji to user status' do
emoji = 'tanabata_tree'
message = 'Playing outside'
visit(profile_path)
select_emoji(emoji)
fill_in 'js-status-message-field', with: message
submit_settings
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji(emoji)
expect(page).to have_content message
end
end
it 'shows the status form when the feature is enabled by setting a cookie', :js do
stub_feature_flags(user_status_form: false)
set_cookie('feature_user_status_form', 'true')
it 'clears the user status' do
user_status = create(:user_status, user: user, message: 'Eating bread', emoji: 'stuffed_flatbread')
visit user_path(user)
within('.cover-status') do
expect(page).to have_emoji(user_status.emoji)
expect(page).to have_content user_status.message
end
visit(profile_path)
click_button 'js-clear-user-status-button'
submit_settings
expect(page).to have_content('Current Status')
visit user_path(user)
expect(page).not_to have_selector '.cover-status'
end
end
end
......@@ -17,7 +17,7 @@ describe 'Project remote mirror', :feature do
visit project_mirror_path(project)
expect(page).to have_content('The remote repository failed to update.')
expect_mirror_to_have_error_and_timeago('Never')
end
end
......@@ -27,8 +27,14 @@ describe 'Project remote mirror', :feature do
visit project_mirror_path(project)
expect(page).to have_content('The remote repository failed to update 5 minutes ago.')
expect_mirror_to_have_error_and_timeago('5 minutes ago')
end
end
def expect_mirror_to_have_error_and_timeago(timeago)
row = first('.js-mirrors-table-body tr')
expect(row).to have_content('Error')
expect(row).to have_content(timeago)
end
end
end
......@@ -129,9 +129,8 @@ describe 'Projects > Settings > Repository settings' do
visit project_settings_repository_path(project)
end
it 'shows push mirror settings' do
expect(page).to have_selector('#project_remote_mirrors_attributes_0_enabled')
expect(page).to have_selector('#project_remote_mirrors_attributes_0_url')
it 'shows push mirror settings', :js do
expect(page).to have_selector('#mirror_direction')
end
end
end
......
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import EmojiMenu from '~/pages/profiles/show/emoji_menu';
import { TEST_HOST } from 'spec/test_constants';
describe('EmojiMenu', () => {
const dummyEmojiTag = '<dummy></tag>';
const dummyToggleButtonSelector = '.toggle-button-selector';
const dummyMenuClass = 'dummy-menu-class';
let emojiMenu;
let dummySelectEmojiCallback;
let dummyEmojiList;
beforeEach(() => {
dummySelectEmojiCallback = jasmine.createSpy('dummySelectEmojiCallback');
dummyEmojiList = {
glEmojiTag() {
return dummyEmojiTag;
},
normalizeEmojiName(emoji) {
return emoji;
},
isEmojiNameValid() {
return true;
},
getEmojiCategoryMap() {
return { dummyCategory: [] };
},
};
emojiMenu = new EmojiMenu(
dummyEmojiList,
dummyToggleButtonSelector,
dummyMenuClass,
dummySelectEmojiCallback,
);
});
afterEach(() => {
emojiMenu.destroy();
});
describe('addAward', () => {
const dummyAwardUrl = `${TEST_HOST}/award/url`;
const dummyEmoji = 'tropical_fish';
const dummyVotesBlock = () => $('<div />');
it('calls selectEmojiCallback', done => {
expect(dummySelectEmojiCallback).not.toHaveBeenCalled();
emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => {
expect(dummySelectEmojiCallback).toHaveBeenCalledWith(dummyEmoji, dummyEmojiTag);
done();
});
});
it('does not make an axios requst', done => {
spyOn(axios, 'request').and.stub();
emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => {
expect(axios.request).not.toHaveBeenCalled();
done();
});
});
});
describe('bindEvents', () => {
beforeEach(() => {
spyOn(emojiMenu, 'registerEventListener').and.stub();
});
it('binds event listeners to custom toggle button', () => {
emojiMenu.bindEvents();
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'one',
jasmine.anything(),
'mouseenter focus',
dummyToggleButtonSelector,
'mouseenter focus',
jasmine.anything(),
);
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
jasmine.anything(),
'click',
dummyToggleButtonSelector,
jasmine.anything(),
);
});
it('binds event listeners to custom menu class', () => {
emojiMenu.bindEvents();
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
jasmine.anything(),
'click',
`.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`,
jasmine.anything(),
);
});
});
describe('createEmojiMenu', () => {
it('renders the menu with custom menu class', () => {
const menuElement = () =>
document.body.querySelector(`.emoji-menu.${dummyMenuClass} .emoji-menu-content`);
expect(menuElement()).toBe(null);
emojiMenu.createEmojiMenu();
expect(menuElement()).not.toBe(null);
});
});
});
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