Commit cca92420 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge commit 'a8a4ca17' into 32815--Add-Custom-CI-Config-Path

* commit 'a8a4ca17':
  Remove IIFEs around several javascript classes
  Handles realtime with 2 states for environments table
  Revert "Merge branch '18000-remember-me-for-oauth-login' into 'master'"
  Disable Flipper memoizer in tests to avoid transient failures
  fix sidebar padding for full-width items (Time Tracking help)
  Replace 'snippets/snippets.feature' spinach with rspec
  32838 Add wells to admin dashboard overview to fix spacing problems
parents 1a581a6a a8a4ca17
...@@ -56,7 +56,6 @@ import GfmAutoComplete from './gfm_auto_complete'; ...@@ -56,7 +56,6 @@ import GfmAutoComplete from './gfm_auto_complete';
import ShortcutsBlob from './shortcuts_blob'; import ShortcutsBlob from './shortcuts_blob';
import initSettingsPanels from './settings_panels'; import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags'; import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -128,7 +127,6 @@ import OAuthRememberMe from './oauth_remember_me'; ...@@ -128,7 +127,6 @@ import OAuthRememberMe from './oauth_remember_me';
case 'sessions:new': case 'sessions:new':
new UsernameValidator(); new UsernameValidator();
new ActiveTabMemoizer(); new ActiveTabMemoizer();
new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents();
break; break;
case 'projects:boards:show': case 'projects:boards:show':
case 'projects:boards:index': case 'projects:boards:index':
......
...@@ -32,7 +32,6 @@ export default { ...@@ -32,7 +32,6 @@ export default {
state: store.state, state: store.state,
visibility: 'available', visibility: 'available',
isLoading: false, isLoading: false,
isLoadingFolderContent: false,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint, endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment, canCreateDeployment: environmentsData.canCreateDeployment,
...@@ -86,9 +85,6 @@ export default { ...@@ -86,9 +85,6 @@ export default {
errorCallback: this.errorCallback, errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => { notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
// We need to verify if any folder is open to also fecth it
this.openFolders = this.store.getOpenFolders();
}, },
}); });
...@@ -119,7 +115,7 @@ export default { ...@@ -119,7 +115,7 @@ export default {
this.store.toggleFolder(folder); this.store.toggleFolder(folder);
if (!folder.isOpen) { if (!folder.isOpen) {
this.fetchChildEnvironments(folder, folderUrl); this.fetchChildEnvironments(folder, folderUrl, true);
} }
}, },
...@@ -147,19 +143,17 @@ export default { ...@@ -147,19 +143,17 @@ export default {
.catch(this.errorCallback); .catch(this.errorCallback);
}, },
fetchChildEnvironments(folder, folderUrl) { fetchChildEnvironments(folder, folderUrl, showLoader = false) {
this.isLoadingFolderContent = true; this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
this.service.getFolderContent(folderUrl) this.service.getFolderContent(folderUrl)
.then(resp => resp.json()) .then(resp => resp.json())
.then((response) => { .then(response => this.store.setfolderContent(folder, response.environments))
this.store.setfolderContent(folder, response.environments); .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
this.isLoadingFolderContent = false;
})
.catch(() => { .catch(() => {
this.isLoadingFolderContent = false;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
}); });
}, },
...@@ -176,13 +170,13 @@ export default { ...@@ -176,13 +170,13 @@ export default {
successCallback(resp) { successCallback(resp) {
this.saveData(resp); this.saveData(resp);
// If folders are open while polling we need to open them again // We need to verify if any folder is open to also update it
if (this.openFolders.length) { const openFolders = this.store.getOpenFolders();
this.openFolders.map((folder) => { if (openFolders.length) {
openFolders.forEach((folder) => {
// TODO - Move this to the backend // TODO - Move this to the backend
const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`; const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
this.store.updateFolder(folder, 'isOpen', true);
return this.fetchChildEnvironments(folder, folderUrl); return this.fetchChildEnvironments(folder, folderUrl);
}); });
} }
...@@ -267,7 +261,7 @@ export default { ...@@ -267,7 +261,7 @@ export default {
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:is-loading-folder-content="isLoadingFolderContent" /> />
</div> </div>
<table-pagination <table-pagination
......
...@@ -29,12 +29,6 @@ export default { ...@@ -29,12 +29,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
isLoadingFolderContent: {
type: Boolean,
required: false,
default: false,
},
}, },
methods: { methods: {
...@@ -74,7 +68,7 @@ export default { ...@@ -74,7 +68,7 @@ export default {
/> />
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<div v-if="isLoadingFolderContent"> <div v-if="model.isLoadingFolderContent">
<loading-icon size="2" /> <loading-icon size="2" />
</div> </div>
......
...@@ -35,14 +35,18 @@ export default class EnvironmentsStore { ...@@ -35,14 +35,18 @@ export default class EnvironmentsStore {
*/ */
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => { const filteredEnvironments = environments.map((env) => {
const oldEnvironmentState = this.state.environments
.find(element => element.id === env.latest.id) || {};
let filtered = {}; let filtered = {};
if (env.size > 1) { if (env.size > 1) {
filtered = Object.assign({}, env, { filtered = Object.assign({}, env, {
isFolder: true, isFolder: true,
isLoadingFolderContent: oldEnvironmentState.isLoading || false,
folderName: env.name, folderName: env.name,
isOpen: false, isOpen: oldEnvironmentState.isOpen || false,
children: [], children: oldEnvironmentState.children || [],
}); });
} }
...@@ -98,7 +102,7 @@ export default class EnvironmentsStore { ...@@ -98,7 +102,7 @@ export default class EnvironmentsStore {
* @return {Array} * @return {Array}
*/ */
toggleFolder(folder) { toggleFolder(folder) {
return this.updateFolder(folder, 'isOpen', !folder.isOpen); return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen);
} }
/** /**
...@@ -125,23 +129,23 @@ export default class EnvironmentsStore { ...@@ -125,23 +129,23 @@ export default class EnvironmentsStore {
return updated; return updated;
}); });
return this.updateFolder(folder, 'children', updatedEnvironments); return this.updateEnvironmentProp(folder, 'children', updatedEnvironments);
} }
/** /**
* Given a folder a prop and a new value updates the correct folder. * Given a environment, a prop and a new value updates the correct environment.
* *
* @param {Object} folder * @param {Object} environment
* @param {String} prop * @param {String} prop
* @param {String|Boolean|Object|Array} newValue * @param {String|Boolean|Object|Array} newValue
* @return {Array} * @return {Array}
*/ */
updateFolder(folder, prop, newValue) { updateEnvironmentProp(environment, prop, newValue) {
const environments = this.state.environments; const environments = this.state.environments;
const updatedEnvironments = environments.map((env) => { const updatedEnvironments = environments.map((env) => {
const updateEnv = Object.assign({}, env); const updateEnv = Object.assign({}, env);
if (env.isFolder && env.id === folder.id) { if (env.id === environment.id) {
updateEnv[prop] = newValue; updateEnv[prop] = newValue;
} }
...@@ -149,8 +153,6 @@ export default class EnvironmentsStore { ...@@ -149,8 +153,6 @@ export default class EnvironmentsStore {
}); });
this.state.environments = updatedEnvironments; this.state.environments = updatedEnvironments;
return updatedEnvironments;
} }
getOpenFolders() { getOpenFolders() {
......
/**
* OAuth-based login buttons have a separate "remember me" checkbox.
*
* Toggling this checkbox adds/removes a `remember_me` parameter to the
* login buttons' href, which is passed on to the omniauth callback.
**/
export default class OAuthRememberMe {
constructor(opts = {}) {
this.container = opts.container || '';
this.loginLinkSelector = '.oauth-login';
}
bindEvents() {
$('#remember_me', this.container).on('click', this.toggleRememberMe);
}
// eslint-disable-next-line class-methods-use-this
toggleRememberMe(event) {
const rememberMe = $(event.target).is(':checked');
$('.oauth-login', this.container).each((i, element) => {
const href = $(element).attr('href');
if (rememberMe) {
$(element).attr('href', `${href}?remember_me=1`);
} else {
$(element).attr('href', href.replace('?remember_me=1', ''));
}
});
}
}
...@@ -2,56 +2,54 @@ ...@@ -2,56 +2,54 @@
/* eslint no-new: "off" */ /* eslint no-new: "off" */
import AccessorUtilities from './lib/utils/accessor'; import AccessorUtilities from './lib/utils/accessor';
((global) => { /**
/** * Memorize the last selected tab after reloading a page.
* Memorize the last selected tab after reloading a page. * Does that setting the current selected tab in the localStorage
* Does that setting the current selected tab in the localStorage */
*/ class ActiveTabMemoizer {
class ActiveTabMemoizer { constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { this.currentTabKey = currentTabKey;
this.currentTabKey = currentTabKey; this.tabSelector = tabSelector;
this.tabSelector = tabSelector; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.bootstrap();
this.bootstrap(); }
}
bootstrap() {
const tabs = document.querySelectorAll(this.tabSelector);
if (tabs.length > 0) {
tabs[0].addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'A') {
const anchorName = e.target.getAttribute('href');
this.saveData(anchorName);
}
});
}
this.showTab(); bootstrap() {
const tabs = document.querySelectorAll(this.tabSelector);
if (tabs.length > 0) {
tabs[0].addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'A') {
const anchorName = e.target.getAttribute('href');
this.saveData(anchorName);
}
});
} }
showTab() { this.showTab();
const anchorName = this.readData(); }
if (anchorName) {
const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`); showTab() {
if (tab) { const anchorName = this.readData();
tab.click(); if (anchorName) {
} const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
if (tab) {
tab.click();
} }
} }
}
saveData(val) { saveData(val) {
if (!this.isLocalStorageAvailable) return undefined; if (!this.isLocalStorageAvailable) return undefined;
return window.localStorage.setItem(this.currentTabKey, val); return window.localStorage.setItem(this.currentTabKey, val);
} }
readData() { readData() {
if (!this.isLocalStorageAvailable) return null; if (!this.isLocalStorageAvailable) return null;
return window.localStorage.getItem(this.currentTabKey); return window.localStorage.getItem(this.currentTabKey);
}
} }
}
global.ActiveTabMemoizer = ActiveTabMemoizer; window.ActiveTabMemoizer = ActiveTabMemoizer;
})(window);
...@@ -2,99 +2,97 @@ ...@@ -2,99 +2,97 @@
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
(function() { window.SingleFileDiff = (function() {
window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
WRAPPER = '<div class="diff-content"></div>'; WRAPPER = '<div class="diff-content"></div>';
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) { function SingleFileDiff(file) {
this.file = file; this.file = file;
this.toggleDiff = this.toggleDiff.bind(this); this.toggleDiff = this.toggleDiff.bind(this);
this.content = $('.diff-content', this.file); this.content = $('.diff-content', this.file);
this.$toggleIcon = $('.diff-toggle-caret', this.file); this.$toggleIcon = $('.diff-toggle-caret', this.file);
this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
this.isOpen = !this.diffForPath; this.isOpen = !this.diffForPath;
if (this.diffForPath) { if (this.diffForPath) {
this.collapsedContent = this.content; this.collapsedContent = this.content;
this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide(); this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
this.content = null; this.content = null;
this.collapsedContent.after(this.loadingContent); this.collapsedContent.after(this.loadingContent);
this.$toggleIcon.addClass('fa-caret-right'); this.$toggleIcon.addClass('fa-caret-right');
} else { } else {
this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide(); this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
this.content.after(this.collapsedContent); this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down'); this.$toggleIcon.addClass('fa-caret-down');
} }
$('.js-file-title, .click-to-expand', this.file).on('click', (function (e) {
this.toggleDiff($(e.target));
}).bind(this));
}
$('.js-file-title, .click-to-expand', this.file).on('click', (function (e) { SingleFileDiff.prototype.toggleDiff = function($target, cb) {
this.toggleDiff($(e.target)); if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
}).bind(this)); this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(cb);
} }
};
SingleFileDiff.prototype.toggleDiff = function($target, cb) { SingleFileDiff.prototype.getContentHTML = function(cb) {
if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; this.collapsedContent.hide();
this.isOpen = !this.isOpen; this.loadingContent.show();
if (!this.isOpen && !this.hasError) { $.get(this.diffForPath, (function(_this) {
this.content.hide(); return function(data) {
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down'); _this.loadingContent.hide();
this.collapsedContent.show(); if (data.html) {
if (typeof gl.diffNotesCompileComponents !== 'undefined') { _this.content = $(data.html);
gl.diffNotesCompileComponents(); _this.content.syntaxHighlight();
} else {
_this.hasError = true;
_this.content = $(ERROR_HTML);
} }
} else if (this.content) { _this.collapsedContent.after(_this.content);
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(cb);
}
};
SingleFileDiff.prototype.getContentHTML = function(cb) {
this.collapsedContent.hide();
this.loadingContent.show();
$.get(this.diffForPath, (function(_this) {
return function(data) {
_this.loadingContent.hide();
if (data.html) {
_this.content = $(data.html);
_this.content.syntaxHighlight();
} else {
_this.hasError = true;
_this.content = $(ERROR_HTML);
}
_this.collapsedContent.after(_this.content);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
FilesCommentButton.init($(_this.file)); FilesCommentButton.init($(_this.file));
if (cb) cb(); if (cb) cb();
}; };
})(this)); })(this));
}; };
return SingleFileDiff; return SingleFileDiff;
})(); })();
$.fn.singleFileDiff = function() { $.fn.singleFileDiff = function() {
return this.each(function() { return this.each(function() {
if (!$.data(this, 'singleFileDiff')) { if (!$.data(this, 'singleFileDiff')) {
return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this)); return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this));
} }
}); });
}; };
}).call(window);
/* /**
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable * Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API. * and controllable by a public API.
* */
* */
class SmartInterval {
(() => { /**
class SmartInterval { * @param { function } opts.callback Function to be called on each iteration (required)
/** * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { function } opts.callback Function to be called on each iteration (required) * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this * when the page is hidden
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
* when the page is hidden * @param { boolean } opts.lazyStart Configure if timer is initialized on
* @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor * instantiation or lazily
* @param { boolean } opts.lazyStart Configure if timer is initialized on * @param { boolean } opts.immediateExecution Configure if callback should
* instantiation or lazily * be executed before the first interval.
* @param { boolean } opts.immediateExecution Configure if callback should */
* be executed before the first interval. constructor(opts = {}) {
*/ this.cfg = {
constructor(opts = {}) { callback: opts.callback,
this.cfg = { startingInterval: opts.startingInterval,
callback: opts.callback, maxInterval: opts.maxInterval,
startingInterval: opts.startingInterval, hiddenInterval: opts.hiddenInterval,
maxInterval: opts.maxInterval, incrementByFactorOf: opts.incrementByFactorOf,
hiddenInterval: opts.hiddenInterval, lazyStart: opts.lazyStart,
incrementByFactorOf: opts.incrementByFactorOf, immediateExecution: opts.immediateExecution,
lazyStart: opts.lazyStart, };
immediateExecution: opts.immediateExecution,
}; this.state = {
intervalId: null,
this.state = { currentInterval: this.cfg.startingInterval,
intervalId: null, pageVisibility: 'visible',
currentInterval: this.cfg.startingInterval, };
pageVisibility: 'visible',
}; this.initInterval();
}
this.initInterval();
}
/* public */
start() {
const cfg = this.cfg;
const state = this.state;
if (cfg.immediateExecution) {
cfg.immediateExecution = false;
cfg.callback();
}
state.intervalId = window.setInterval(() => { /* public */
cfg.callback();
if (this.getCurrentInterval() === cfg.maxInterval) { start() {
return; const cfg = this.cfg;
} const state = this.state;
this.incrementInterval(); if (cfg.immediateExecution) {
this.resume(); cfg.immediateExecution = false;
}, this.getCurrentInterval()); cfg.callback();
} }
// cancel the existing timer, setting the currentInterval back to startingInterval state.intervalId = window.setInterval(() => {
cancel() { cfg.callback();
this.setCurrentInterval(this.cfg.startingInterval);
this.stopTimer();
}
onVisibilityHidden() { if (this.getCurrentInterval() === cfg.maxInterval) {
if (this.cfg.hiddenInterval) { return;
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
this.cancel();
} }
}
// start a timer, using the existing interval this.incrementInterval();
resume() { this.resume();
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped }, this.getCurrentInterval());
this.start(); }
}
onVisibilityVisible() { // cancel the existing timer, setting the currentInterval back to startingInterval
this.cancel(); cancel() {
this.start(); this.setCurrentInterval(this.cfg.startingInterval);
} this.stopTimer();
}
destroy() { onVisibilityHidden() {
if (this.cfg.hiddenInterval) {
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
this.cancel(); this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('beforeunload');
} }
}
/* private */ // start a timer, using the existing interval
resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start();
}
initInterval() { onVisibilityVisible() {
const cfg = this.cfg; this.cancel();
this.start();
}
if (!cfg.lazyStart) { destroy() {
this.start(); this.cancel();
} document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('beforeunload');
}
this.initVisibilityChangeHandling(); /* private */
this.initPageUnloadHandling();
}
initVisibilityChangeHandling() { initInterval() {
// cancel interval when tab no longer shown (prevents cached pages from polling) const cfg = this.cfg;
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
initPageUnloadHandling() { if (!cfg.lazyStart) {
// TODO: Consider refactoring in light of turbolinks removal. this.start();
// prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('beforeunload', () => this.cancel());
} }
handleVisibilityChange(e) { this.initVisibilityChangeHandling();
this.state.pageVisibility = e.target.visibilityState; this.initPageUnloadHandling();
const intervalAction = this.isPageVisible() ? }
this.onVisibilityVisible :
this.onVisibilityHidden;
intervalAction.apply(this); initVisibilityChangeHandling() {
} // cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
getCurrentInterval() { initPageUnloadHandling() {
return this.state.currentInterval; // TODO: Consider refactoring in light of turbolinks removal.
} // prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('beforeunload', () => this.cancel());
}
setCurrentInterval(newInterval) { handleVisibilityChange(e) {
this.state.currentInterval = newInterval; this.state.pageVisibility = e.target.visibilityState;
} const intervalAction = this.isPageVisible() ?
this.onVisibilityVisible :
this.onVisibilityHidden;
incrementInterval() { intervalAction.apply(this);
const cfg = this.cfg; }
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) { getCurrentInterval() {
nextInterval = cfg.maxInterval; return this.state.currentInterval;
} }
setCurrentInterval(newInterval) {
this.state.currentInterval = newInterval;
}
incrementInterval() {
const cfg = this.cfg;
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
this.setCurrentInterval(nextInterval); if (nextInterval > cfg.maxInterval) {
nextInterval = cfg.maxInterval;
} }
isPageVisible() { return this.state.pageVisibility === 'visible'; } this.setCurrentInterval(nextInterval);
}
stopTimer() { isPageVisible() { return this.state.pageVisibility === 'visible'; }
const state = this.state;
state.intervalId = window.clearInterval(state.intervalId); stopTimer() {
} const state = this.state;
state.intervalId = window.clearInterval(state.intervalId);
} }
gl.SmartInterval = SmartInterval; }
})(window.gl || (window.gl = {}));
window.gl.SmartInterval = SmartInterval;
/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */ /* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */
(global => { window.gl.SnippetsList = function() {
global.gl = global.gl || {}; var $holder = $('.snippets-list-holder');
gl.SnippetsList = function() { $holder.find('.pagination').on('ajax:success', (e, data) => {
var $holder = $('.snippets-list-holder'); $holder.replaceWith(data.html);
});
$holder.find('.pagination').on('ajax:success', (e, data) => { };
$holder.replaceWith(data.html);
});
};
})(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */ /* global Flash */
(function() { window.Star = (function() {
this.Star = (function() { function Star() {
function Star() { $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { var $starIcon, $starSpan, $this, toggleStar;
var $starIcon, $starSpan, $this, toggleStar; $this = $(this);
$this = $(this); $starSpan = $this.find('span');
$starSpan = $this.find('span'); $starIcon = $this.find('i');
$starIcon = $this.find('i'); toggleStar = function(isStarred) {
toggleStar = function(isStarred) { $this.parent().find('.star-count').text(data.star_count);
$this.parent().find('.star-count').text(data.star_count); if (isStarred) {
if (isStarred) { $starSpan.removeClass('starred').text('Star');
$starSpan.removeClass('starred').text('Star'); $starIcon.removeClass('fa-star').addClass('fa-star-o');
$starIcon.removeClass('fa-star').addClass('fa-star-o'); } else {
} else { $starSpan.addClass('starred').text('Unstar');
$starSpan.addClass('starred').text('Unstar'); $starIcon.removeClass('fa-star-o').addClass('fa-star');
$starIcon.removeClass('fa-star-o').addClass('fa-star'); }
} };
}; toggleStar($starSpan.hasClass('starred'));
toggleStar($starSpan.hasClass('starred')); }).on('ajax:error', function(e, xhr, status, error) {
}).on('ajax:error', function(e, xhr, status, error) { new Flash('Star toggle failed. Try again later.', 'alert');
new Flash('Star toggle failed. Try again later.', 'alert'); });
}); }
}
return Star; return Star;
})(); })();
}).call(window);
(() => { class Subscription {
class Subscription { constructor(containerElm) {
constructor(containerElm) { this.containerElm = containerElm;
this.containerElm = containerElm;
const subscribeButton = containerElm.querySelector('.js-subscribe-button'); const subscribeButton = containerElm.querySelector('.js-subscribe-button');
if (subscribeButton) { if (subscribeButton) {
// remove class so we don't bind twice // remove class so we don't bind twice
subscribeButton.classList.remove('js-subscribe-button'); subscribeButton.classList.remove('js-subscribe-button');
subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
}
} }
}
toggleSubscription(event) { toggleSubscription(event) {
const button = event.currentTarget; const button = event.currentTarget;
const buttonSpan = button.querySelector('span'); const buttonSpan = button.querySelector('span');
if (!buttonSpan || button.classList.contains('disabled')) { if (!buttonSpan || button.classList.contains('disabled')) {
return; return;
} }
button.classList.add('disabled'); button.classList.add('disabled');
const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
const toggleActionUrl = this.containerElm.dataset.url; const toggleActionUrl = this.containerElm.dataset.url;
$.post(toggleActionUrl, () => { $.post(toggleActionUrl, () => {
button.classList.remove('disabled'); button.classList.remove('disabled');
// hack to allow this to work with the issue boards Vue object // hack to allow this to work with the issue boards Vue object
if (document.querySelector('html').classList.contains('issue-boards-page')) { if (document.querySelector('html').classList.contains('issue-boards-page')) {
gl.issueBoards.boardStoreIssueSet( gl.issueBoards.boardStoreIssueSet(
'subscribed', 'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed, !gl.issueBoards.BoardsStore.detail.issue.subscribed,
); );
} else { } else {
buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
} }
}); });
} }
static bindAll(selector) { static bindAll(selector) {
[].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.Subscription = Subscription; window.gl.Subscription = Subscription;
})();
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.SubscriptionSelect = (function() { window.SubscriptionSelect = (function() {
function SubscriptionSelect() { function SubscriptionSelect() {
$('.js-subscription-event').each(function(i, el) { $('.js-subscription-event').each(function(i, el) {
var fieldName; var fieldName;
fieldName = $(el).data("field-name"); fieldName = $(el).data("field-name");
return $(el).glDropdown({ return $(el).glDropdown({
selectable: true, selectable: true,
fieldName: fieldName, fieldName: fieldName,
toggleLabel: (function(_this) { toggleLabel: (function(_this) {
return function(selected, el, instance) { return function(selected, el, instance) {
var $item, label; var $item, label;
label = 'Subscription'; label = 'Subscription';
$item = instance.dropdown.find('.is-active'); $item = instance.dropdown.find('.is-active');
if ($item.length) { if ($item.length) {
label = $item.text(); label = $item.text();
} }
return label; return label;
}; };
})(this), })(this),
clicked: function(options) { clicked: function(options) {
return options.e.preventDefault(); return options.e.preventDefault();
}, },
id: function(obj, el) { id: function(obj, el) {
return $(el).data("id"); return $(el).data("id");
} }
});
}); });
} });
}
return SubscriptionSelect; return SubscriptionSelect;
})(); })();
}).call(window);
...@@ -9,19 +9,18 @@ ...@@ -9,19 +9,18 @@
// //
// <div class="js-syntax-highlight"></div> // <div class="js-syntax-highlight"></div>
// //
(function() {
$.fn.syntaxHighlight = function() {
var $children;
if ($(this).hasClass('js-syntax-highlight')) { $.fn.syntaxHighlight = function() {
// Given the element itself, apply highlighting var $children;
return $(this).addClass(gon.user_color_scheme);
} else { if ($(this).hasClass('js-syntax-highlight')) {
// Given a parent element, recurse to any of its applicable children // Given the element itself, apply highlighting
$children = $(this).find('.js-syntax-highlight'); return $(this).addClass(gon.user_color_scheme);
if ($children.length) { } else {
return $children.syntaxHighlight(); // Given a parent element, recurse to any of its applicable children
} $children = $(this).find('.js-syntax-highlight');
if ($children.length) {
return $children.syntaxHighlight();
} }
}; }
}).call(window); };
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
(function() { window.TreeView = (function() {
this.TreeView = (function() { function TreeView() {
function TreeView() { this.initKeyNav();
this.initKeyNav(); // Code browser tree slider
// Code browser tree slider // Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
// Make the entire tree-item row clickable, but not if clicking another link (like a commit message) $(".tree-content-holder .tree-item").on('click', function(e) {
$(".tree-content-holder .tree-item").on('click', function(e) { var $clickedEl, path;
var $clickedEl, path; $clickedEl = $(e.target);
$clickedEl = $(e.target); path = $('.tree-item-file-name a', this).attr('href');
path = $('.tree-item-file-name a', this).attr('href'); if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) {
if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) { if (e.metaKey || e.which === 2) {
if (e.metaKey || e.which === 2) { e.preventDefault();
e.preventDefault(); return window.open(path, '_blank');
return window.open(path, '_blank'); } else {
} else { return gl.utils.visitUrl(path);
return gl.utils.visitUrl(path);
}
} }
}); }
// Show the "Loading commit data" for only the first element });
$('span.log_loading:first').removeClass('hide'); // Show the "Loading commit data" for only the first element
} $('span.log_loading:first').removeClass('hide');
}
TreeView.prototype.initKeyNav = function() { TreeView.prototype.initKeyNav = function() {
var li, liSelected; var li, liSelected;
li = $("tr.tree-item"); li = $("tr.tree-item");
liSelected = null; liSelected = null;
return $('body').keydown(function(e) { return $('body').keydown(function(e) {
var next, path; var next, path;
if ($("input:focus").length > 0 && (e.which === 38 || e.which === 40)) { if ($("input:focus").length > 0 && (e.which === 38 || e.which === 40)) {
return false; return false;
} }
if (e.which === 40) { if (e.which === 40) {
if (liSelected) { if (liSelected) {
next = liSelected.next(); next = liSelected.next();
if (next.length > 0) { if (next.length > 0) {
liSelected.removeClass("selected"); liSelected.removeClass("selected");
liSelected = next.addClass("selected"); liSelected = next.addClass("selected");
}
} else {
liSelected = li.eq(0).addClass("selected");
}
return $(liSelected).focus();
} else if (e.which === 38) {
if (liSelected) {
next = liSelected.prev();
if (next.length > 0) {
liSelected.removeClass("selected");
liSelected = next.addClass("selected");
}
} else {
liSelected = li.last().addClass("selected");
} }
return $(liSelected).focus(); } else {
} else if (e.which === 13) { liSelected = li.eq(0).addClass("selected");
path = $('.tree-item.selected .tree-item-file-name a').attr('href'); }
if (path) { return $(liSelected).focus();
return gl.utils.visitUrl(path); } else if (e.which === 38) {
if (liSelected) {
next = liSelected.prev();
if (next.length > 0) {
liSelected.removeClass("selected");
liSelected = next.addClass("selected");
} }
} else {
liSelected = li.last().addClass("selected");
}
return $(liSelected).focus();
} else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) {
return gl.utils.visitUrl(path);
} }
}); }
}; });
};
return TreeView; return TreeView;
})(); })();
}).call(window);
...@@ -2,34 +2,35 @@ ...@@ -2,34 +2,35 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
((global) => { class User {
global.User = class { constructor({ action }) {
constructor({ action }) { this.action = action;
this.action = action; this.placeProfileAvatarsToTop();
this.placeProfileAvatarsToTop(); this.initTabs();
this.initTabs(); this.hideProjectLimitMessage();
this.hideProjectLimitMessage(); }
}
placeProfileAvatarsToTop() { placeProfileAvatarsToTop() {
$('.profile-groups-avatars').tooltip({ $('.profile-groups-avatars').tooltip({
placement: 'top' placement: 'top'
}); });
} }
initTabs() { initTabs() {
return new global.UserTabs({ return new window.gl.UserTabs({
parentEl: '.user-profile', parentEl: '.user-profile',
action: this.action action: this.action
}); });
} }
hideProjectLimitMessage() { hideProjectLimitMessage() {
$('.hide-project-limit-message').on('click', e => { $('.hide-project-limit-message').on('click', e => {
e.preventDefault(); e.preventDefault();
Cookies.set('hide_project_limit_message', 'false'); Cookies.set('hide_project_limit_message', 'false');
$(this).parents('.project-limit-message').remove(); $(this).parents('.project-limit-message').remove();
}); });
} }
}; }
})(window.gl || (window.gl = {}));
window.gl = window.gl || {};
window.gl.User = User;
...@@ -59,117 +59,118 @@ content on the Users#show page. ...@@ -59,117 +59,118 @@ content on the Users#show page.
</div> </div>
</div> </div>
*/ */
((global) => {
class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.actions = Object.keys(this.loaded);
this.bindEvents();
if (this.action === 'show') {
this.action = this.defaultAction;
}
this.activateTab(this.action); class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.actions = Object.keys(this.loaded);
this.bindEvents();
if (this.action === 'show') {
this.action = this.defaultAction;
} }
bindEvents() { this.activateTab(this.action);
this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this); }
this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') bindEvents() {
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)); this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this);
this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper); this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
} .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
changeProjectsPage(e) { this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper);
e.preventDefault(); }
$('.tab-pane.active').empty(); changeProjectsPage(e) {
const endpoint = $(e.target).attr('href'); e.preventDefault();
this.loadTab(this.getCurrentAction(), endpoint);
}
tabShown(event) { $('.tab-pane.active').empty();
const $target = $(event.target); const endpoint = $(e.target).attr('href');
const action = $target.data('action'); this.loadTab(this.getCurrentAction(), endpoint);
const source = $target.attr('href'); }
const endpoint = $target.data('endpoint');
this.setTab(action, endpoint);
return this.setCurrentAction(source);
}
activateTab(action) { tabShown(event) {
return this.$parentEl.find(`.nav-links .js-${action}-tab a`) const $target = $(event.target);
.tab('show'); const action = $target.data('action');
} const source = $target.attr('href');
const endpoint = $target.data('endpoint');
this.setTab(action, endpoint);
return this.setCurrentAction(source);
}
setTab(action, endpoint) { activateTab(action) {
if (this.loaded[action]) { return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
return; .tab('show');
} }
if (action === 'activity') {
this.loadActivities();
}
const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; setTab(action, endpoint) {
if (loadableActions.indexOf(action) > -1) { if (this.loaded[action]) {
return this.loadTab(action, endpoint); return;
} }
if (action === 'activity') {
this.loadActivities();
} }
loadTab(action, endpoint) { const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
return $.ajax({ if (loadableActions.indexOf(action) > -1) {
beforeSend: () => this.toggleLoading(true), return this.loadTab(action, endpoint);
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
}
});
} }
}
loadActivities() { loadTab(action, endpoint) {
if (this.loaded['activity']) { return $.ajax({
return; beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
} }
const $calendarWrap = this.$parentEl.find('.user-calendar'); });
$calendarWrap.load($calendarWrap.data('href')); }
new gl.Activities();
return this.loaded['activity'] = true;
}
toggleLoading(status) { loadActivities() {
return this.$parentEl.find('.loading-status .loading') if (this.loaded['activity']) {
.toggle(status); return;
} }
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
new gl.Activities();
return this.loaded['activity'] = true;
}
setCurrentAction(source) { toggleLoading(status) {
let new_state = source; return this.$parentEl.find('.loading-status .loading')
new_state = new_state.replace(/\/+$/, ''); .toggle(status);
new_state += this._location.search + this._location.hash; }
history.replaceState({
url: new_state
}, document.title, new_state);
return new_state;
}
getCurrentAction() { setCurrentAction(source) {
return this.$parentEl.find('.nav-links .active a').data('action'); let new_state = source;
} new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
history.replaceState({
url: new_state
}, document.title, new_state);
return new_state;
} }
global.UserTabs = UserTabs;
})(window.gl || (window.gl = {})); getCurrentAction() {
return this.$parentEl.find('.nav-links .active a').data('action');
}
}
window.gl = window.gl || {};
window.gl.UserTabs = UserTabs;
/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ /* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
((global) => { const debounceTimeoutDuration = 1000;
const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline';
const invalidInputClass = 'gl-field-error-outline'; const successInputClass = 'gl-field-success-outline';
const successInputClass = 'gl-field-success-outline'; const unavailableMessageSelector = '.username .validation-error';
const unavailableMessageSelector = '.username .validation-error'; const successMessageSelector = '.username .validation-success';
const successMessageSelector = '.username .validation-success'; const pendingMessageSelector = '.username .validation-pending';
const pendingMessageSelector = '.username .validation-pending'; const invalidMessageSelector = '.username .gl-field-error';
const invalidMessageSelector = '.username .gl-field-error';
class UsernameValidator {
class UsernameValidator { constructor() {
constructor() { this.inputElement = $('#new_user_username');
this.inputElement = $('#new_user_username'); this.inputDomElement = this.inputElement.get(0);
this.inputDomElement = this.inputElement.get(0); this.state = {
this.state = { available: false,
available: false, valid: false,
valid: false, pending: false,
pending: false, empty: true
empty: true };
};
const debounceTimeout = _.debounce((username) => {
const debounceTimeout = _.debounce((username) => { this.validateUsername(username);
this.validateUsername(username); }, debounceTimeoutDuration);
}, debounceTimeoutDuration);
this.inputElement.on('keyup.username_check', () => {
this.inputElement.on('keyup.username_check', () => { const username = this.inputElement.val();
const username = this.inputElement.val();
this.state.valid = this.inputDomElement.validity.valid;
this.state.valid = this.inputDomElement.validity.valid; this.state.empty = !username.length;
this.state.empty = !username.length;
if (this.state.valid) {
return debounceTimeout(username);
}
this.renderState();
});
// Override generic field validation
this.inputElement.on('invalid', this.interceptInvalid.bind(this));
}
renderState() { if (this.state.valid) {
// Clear all state return debounceTimeout(username);
this.clearFieldValidationState();
if (this.state.valid && this.state.available) {
return this.setSuccessState();
} }
if (this.state.empty) { this.renderState();
return this.clearFieldValidationState(); });
}
if (this.state.pending) { // Override generic field validation
return this.setPendingState(); this.inputElement.on('invalid', this.interceptInvalid.bind(this));
} }
if (!this.state.available) { renderState() {
return this.setUnavailableState(); // Clear all state
} this.clearFieldValidationState();
if (!this.state.valid) { if (this.state.valid && this.state.available) {
return this.setInvalidState(); return this.setSuccessState();
}
} }
interceptInvalid(event) { if (this.state.empty) {
event.preventDefault(); return this.clearFieldValidationState();
event.stopPropagation();
} }
validateUsername(username) { if (this.state.pending) {
if (this.state.valid) { return this.setPendingState();
this.state.pending = true;
this.state.available = false;
this.renderState();
return $.ajax({
type: 'GET',
url: `${gon.relative_url_root}/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
}
} }
setAvailabilityState(usernameTaken) { if (!this.state.available) {
if (usernameTaken) { return this.setUnavailableState();
this.state.valid = false;
this.state.available = false;
} else {
this.state.available = true;
}
this.state.pending = false;
this.renderState();
} }
clearFieldValidationState() { if (!this.state.valid) {
this.inputElement.siblings('p').hide(); return this.setInvalidState();
this.inputElement.removeClass(invalidInputClass)
.removeClass(successInputClass);
} }
}
setUnavailableState() { interceptInvalid(event) {
const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector); event.preventDefault();
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); event.stopPropagation();
$usernameUnavailableMessage.show(); }
}
setSuccessState() { validateUsername(username) {
const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector); if (this.state.valid) {
this.inputElement.addClass(successInputClass).removeClass(invalidInputClass); this.state.pending = true;
$usernameSuccessMessage.show(); this.state.available = false;
this.renderState();
return $.ajax({
type: 'GET',
url: `${gon.relative_url_root}/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
} }
}
setPendingState() { setAvailabilityState(usernameTaken) {
const $usernamePendingMessage = $(pendingMessageSelector); if (usernameTaken) {
if (this.state.pending) { this.state.valid = false;
$usernamePendingMessage.show(); this.state.available = false;
} else { } else {
$usernamePendingMessage.hide(); this.state.available = true;
}
} }
this.state.pending = false;
this.renderState();
}
clearFieldValidationState() {
this.inputElement.siblings('p').hide();
setInvalidState() { this.inputElement.removeClass(invalidInputClass)
const $inputErrorMessage = $(invalidMessageSelector); .removeClass(successInputClass);
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); }
$inputErrorMessage.show();
setUnavailableState() {
const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector);
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
$usernameUnavailableMessage.show();
}
setSuccessState() {
const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector);
this.inputElement.addClass(successInputClass).removeClass(invalidInputClass);
$usernameSuccessMessage.show();
}
setPendingState() {
const $usernamePendingMessage = $(pendingMessageSelector);
if (this.state.pending) {
$usernamePendingMessage.show();
} else {
$usernamePendingMessage.hide();
} }
} }
global.UsernameValidator = UsernameValidator; setInvalidState() {
})(window); const $inputErrorMessage = $(invalidMessageSelector);
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
$inputErrorMessage.show();
}
}
window.UsernameValidator = UsernameValidator;
(() => { class VisibilitySelect {
const gl = window.gl || (window.gl = {}); constructor(container) {
if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
class VisibilitySelect { this.container = container;
constructor(container) { this.helpBlock = this.container.querySelector('.help-block');
if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); this.select = this.container.querySelector('select');
this.container = container; }
this.helpBlock = this.container.querySelector('.help-block');
this.select = this.container.querySelector('select');
}
init() { init() {
if (this.select) { if (this.select) {
this.updateHelpText(); this.updateHelpText();
this.select.addEventListener('change', this.updateHelpText.bind(this)); this.select.addEventListener('change', this.updateHelpText.bind(this));
} else { } else {
this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock; this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock;
}
} }
}
updateHelpText() { updateHelpText() {
this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
}
} }
}
gl.VisibilitySelect = VisibilitySelect; window.gl = window.gl || {};
})(); window.gl.VisibilitySelect = VisibilitySelect;
...@@ -4,66 +4,65 @@ ...@@ -4,66 +4,65 @@
import 'vendor/jquery.nicescroll'; import 'vendor/jquery.nicescroll';
import './breakpoints'; import './breakpoints';
((global) => { class Wikis {
class Wikis { constructor() {
constructor() { this.bp = Breakpoints.get();
this.bp = Breakpoints.get(); this.sidebarEl = document.querySelector('.js-wiki-sidebar');
this.sidebarEl = document.querySelector('.js-wiki-sidebar'); this.sidebarExpanded = false;
this.sidebarExpanded = false; $(this.sidebarEl).niceScroll();
$(this.sidebarEl).niceScroll();
const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
for (let i = 0; i < sidebarToggles.length; i += 1) { for (let i = 0; i < sidebarToggles.length; i += 1) {
sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
} }
this.newWikiForm = document.querySelector('form.new-wiki-page');
if (this.newWikiForm) {
this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
}
window.addEventListener('resize', () => this.renderSidebar()); this.newWikiForm = document.querySelector('form.new-wiki-page');
this.renderSidebar(); if (this.newWikiForm) {
this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
} }
handleNewWikiSubmit(e) { window.addEventListener('resize', () => this.renderSidebar());
if (!this.newWikiForm) return; this.renderSidebar();
}
const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); handleNewWikiSubmit(e) {
const slug = gl.text.slugify(slugInput.value); if (!this.newWikiForm) return;
if (slug.length > 0) { const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
const wikisPath = slugInput.getAttribute('data-wikis-path'); const slug = gl.text.slugify(slugInput.value);
window.location.href = `${wikisPath}/${slug}`;
e.preventDefault();
}
}
handleToggleSidebar(e) { if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
window.location.href = `${wikisPath}/${slug}`;
e.preventDefault(); e.preventDefault();
this.sidebarExpanded = !this.sidebarExpanded;
this.renderSidebar();
} }
}
sidebarCanCollapse() { handleToggleSidebar(e) {
const bootstrapBreakpoint = this.bp.getBreakpointSize(); e.preventDefault();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; this.sidebarExpanded = !this.sidebarExpanded;
} this.renderSidebar();
}
sidebarCanCollapse() {
const bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
}
renderSidebar() { renderSidebar() {
if (!this.sidebarEl) return; if (!this.sidebarEl) return;
const { classList } = this.sidebarEl; const { classList } = this.sidebarEl;
if (this.sidebarExpanded || !this.sidebarCanCollapse()) { if (this.sidebarExpanded || !this.sidebarCanCollapse()) {
if (!classList.contains('right-sidebar-expanded')) { if (!classList.contains('right-sidebar-expanded')) {
classList.remove('right-sidebar-collapsed'); classList.remove('right-sidebar-collapsed');
classList.add('right-sidebar-expanded'); classList.add('right-sidebar-expanded');
}
} else if (classList.contains('right-sidebar-expanded')) {
classList.add('right-sidebar-collapsed');
classList.remove('right-sidebar-expanded');
} }
} else if (classList.contains('right-sidebar-expanded')) {
classList.add('right-sidebar-collapsed');
classList.remove('right-sidebar-expanded');
} }
} }
}
global.Wikis = Wikis; window.gl = window.gl || {};
})(window.gl || (window.gl = {})); window.gl.Wikis = Wikis;
...@@ -34,65 +34,64 @@ window.Dropzone = Dropzone; ...@@ -34,65 +34,64 @@ window.Dropzone = Dropzone;
// **Cancelable** No // **Cancelable** No
// **Target** a.js-zen-leave // **Target** a.js-zen-leave
// //
(function() {
this.ZenMode = (function() { window.ZenMode = (function() {
function ZenMode() { function ZenMode() {
this.active_backdrop = null; this.active_backdrop = null;
this.active_textarea = null; this.active_textarea = null;
$(document).on('click', '.js-zen-enter', function(e) { $(document).on('click', '.js-zen-enter', function(e) {
e.preventDefault(); e.preventDefault();
return $(e.currentTarget).trigger('zen_mode:enter'); return $(e.currentTarget).trigger('zen_mode:enter');
}); });
$(document).on('click', '.js-zen-leave', function(e) { $(document).on('click', '.js-zen-leave', function(e) {
e.preventDefault();
return $(e.currentTarget).trigger('zen_mode:leave');
});
$(document).on('zen_mode:enter', (function(_this) {
return function(e) {
return _this.enter($(e.target).closest('.md-area').find('.zen-backdrop'));
};
})(this));
$(document).on('zen_mode:leave', (function(_this) {
return function(e) {
return _this.exit();
};
})(this));
$(document).on('keydown', function(e) {
// Esc
if (e.keyCode === 27) {
e.preventDefault(); e.preventDefault();
return $(e.currentTarget).trigger('zen_mode:leave'); return $(document).trigger('zen_mode:leave');
}); }
$(document).on('zen_mode:enter', (function(_this) { });
return function(e) { }
return _this.enter($(e.target).closest('.md-area').find('.zen-backdrop'));
};
})(this));
$(document).on('zen_mode:leave', (function(_this) {
return function(e) {
return _this.exit();
};
})(this));
$(document).on('keydown', function(e) {
// Esc
if (e.keyCode === 27) {
e.preventDefault();
return $(document).trigger('zen_mode:leave');
}
});
}
ZenMode.prototype.enter = function(backdrop) { ZenMode.prototype.enter = function(backdrop) {
Mousetrap.pause(); Mousetrap.pause();
this.active_backdrop = $(backdrop); this.active_backdrop = $(backdrop);
this.active_backdrop.addClass('fullscreen'); this.active_backdrop.addClass('fullscreen');
this.active_textarea = this.active_backdrop.find('textarea'); this.active_textarea = this.active_backdrop.find('textarea');
// Prevent a user-resized textarea from persisting to fullscreen // Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style'); this.active_textarea.removeAttr('style');
return this.active_textarea.focus(); return this.active_textarea.focus();
}; };
ZenMode.prototype.exit = function() { ZenMode.prototype.exit = function() {
if (this.active_textarea) { if (this.active_textarea) {
Mousetrap.unpause(); Mousetrap.unpause();
this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen');
this.scrollTo(this.active_textarea); this.scrollTo(this.active_textarea);
this.active_textarea = null; this.active_textarea = null;
this.active_backdrop = null; this.active_backdrop = null;
return Dropzone.forElement('.div-dropzone').enable(); return Dropzone.forElement('.div-dropzone').enable();
} }
}; };
ZenMode.prototype.scrollTo = function(zen_area) { ZenMode.prototype.scrollTo = function(zen_area) {
return $.scrollTo(zen_area, 0, { return $.scrollTo(zen_area, 0, {
offset: -150 offset: -150
}); });
}; };
return ZenMode; return ZenMode;
})(); })();
}).call(window);
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
@mixin maintain-sidebar-dimensions { @mixin maintain-sidebar-dimensions {
display: block; display: block;
width: $gutter-width; width: $gutter-width;
padding: 10px 20px; padding: 10px 0;
} }
.issues-bulk-update.right-sidebar { .issues-bulk-update.right-sidebar {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
color: $gl-text-color; color: $gl-text-color;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
margin-bottom: $gl-padding;
.well-segment { .well-segment {
padding: $gl-padding; padding: $gl-padding;
...@@ -21,6 +22,11 @@ ...@@ -21,6 +22,11 @@
font-size: 12px; font-size: 12px;
} }
} }
&.admin-well h4 {
border-bottom: 1px solid $border-color;
padding-bottom: 8px;
}
} }
.icon-container { .icon-container {
...@@ -53,6 +59,14 @@ ...@@ -53,6 +59,14 @@
padding: 15px; padding: 15px;
} }
.dark-well {
background-color: $gray-normal;
.btn {
width: 100%;
}
}
.well-centered { .well-centered {
h1 { h1 {
font-weight: normal; font-weight: normal;
......
...@@ -200,7 +200,6 @@ ...@@ -200,7 +200,6 @@
right: 0; right: 0;
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 0 20px;
z-index: 200; z-index: 200;
overflow: hidden; overflow: hidden;
...@@ -224,6 +223,10 @@ ...@@ -224,6 +223,10 @@
} }
} }
.issuable-sidebar {
padding: 0 20px;
}
.issuable-sidebar-header { .issuable-sidebar-header {
padding-top: 10px; padding-top: 10px;
} }
......
class OmniauthCallbacksController < Devise::OmniauthCallbacksController class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthenticatesWithTwoFactor include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable
protect_from_forgery except: [:kerberos, :saml, :cas3] protect_from_forgery except: [:kerberos, :saml, :cas3]
...@@ -116,10 +115,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -116,10 +115,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if @user.persisted? && @user.valid? if @user.persisted? && @user.valid?
log_audit_event(@user, with: oauth['provider']) log_audit_event(@user, with: oauth['provider'])
if @user.two_factor_enabled? if @user.two_factor_enabled?
params[:remember_me] = '1' if remember_me?
prompt_for_two_factor(@user) prompt_for_two_factor(@user)
else else
remember_me(@user) if remember_me?
sign_in_and_redirect(@user) sign_in_and_redirect(@user)
end end
else else
...@@ -150,9 +147,4 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -150,9 +147,4 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
AuditEventService.new(user, user, options) AuditEventService.new(user, user, options)
.for_authentication.security_event .for_authentication.security_event
end end
def remember_me?
request_params = request.env['omniauth.params']
(request_params['remember_me'] == '1') if request_params.present?
end
end end
...@@ -5,182 +5,182 @@ ...@@ -5,182 +5,182 @@
.admin-dashboard.prepend-top-default .admin-dashboard.prepend-top-default
.row .row
.col-md-4 .col-md-4
%h4 Statistics .info-well
%hr .well-segment.admin-well
%p %h4 Statistics
Forks %p
%span.light.pull-right Forks
= number_with_delimiter(ForkedProjectLink.count) %span.light.pull-right
%p = number_with_delimiter(ForkedProjectLink.count)
Issues %p
%span.light.pull-right Issues
= number_with_delimiter(Issue.count) %span.light.pull-right
%p = number_with_delimiter(Issue.count)
Merge Requests %p
%span.light.pull-right Merge Requests
= number_with_delimiter(MergeRequest.count) %span.light.pull-right
%p = number_with_delimiter(MergeRequest.count)
Notes %p
%span.light.pull-right Notes
= number_with_delimiter(Note.count) %span.light.pull-right
%p = number_with_delimiter(Note.count)
Snippets %p
%span.light.pull-right Snippets
= number_with_delimiter(Snippet.count) %span.light.pull-right
%p = number_with_delimiter(Snippet.count)
SSH Keys %p
%span.light.pull-right SSH Keys
= number_with_delimiter(Key.count) %span.light.pull-right
%p = number_with_delimiter(Key.count)
Milestones %p
%span.light.pull-right Milestones
= number_with_delimiter(Milestone.count) %span.light.pull-right
%p = number_with_delimiter(Milestone.count)
Active Users %p
%span.light.pull-right Active Users
= number_with_delimiter(User.active.count) %span.light.pull-right
= number_with_delimiter(User.active.count)
.col-md-4 .col-md-4
%h4 .info-well
Features .well-segment.admin-well
%hr %h4 Features
- sign_up = "Sign up" - sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") } %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
= sign_up = sign_up
%span.light.pull-right %span.light.pull-right
= boolean_to_icon signup_enabled? = boolean_to_icon signup_enabled?
- ldap = "LDAP" - ldap = "LDAP"
%p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") } %p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap = ldap
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab.config.ldap.enabled = boolean_to_icon Gitlab.config.ldap.enabled
- gravatar = "Gravatar" - gravatar = "Gravatar"
%p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") } %p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") }
= gravatar = gravatar
%span.light.pull-right %span.light.pull-right
= boolean_to_icon gravatar_enabled? = boolean_to_icon gravatar_enabled?
- omniauth = "OmniAuth" - omniauth = "OmniAuth"
%p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") } %p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") }
= omniauth = omniauth
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab.config.omniauth.enabled = boolean_to_icon Gitlab.config.omniauth.enabled
- reply_email = "Reply by email" - reply_email = "Reply by email"
%p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") } %p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") }
= reply_email = reply_email
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled? = boolean_to_icon Gitlab::IncomingEmail.enabled?
- container_reg = "Container Registry" - container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
= container_reg = container_reg
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled = boolean_to_icon Gitlab.config.registry.enabled
- gitlab_pages = 'GitLab Pages' - gitlab_pages = 'GitLab Pages'
- gitlab_pages_enabled = Gitlab.config.pages.enabled - gitlab_pages_enabled = Gitlab.config.pages.enabled
%p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") } %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
= gitlab_pages = gitlab_pages
%span.light.pull-right %span.light.pull-right
= boolean_to_icon gitlab_pages_enabled = boolean_to_icon gitlab_pages_enabled
- gitlab_shared_runners = 'Shared Runners' - gitlab_shared_runners = 'Shared Runners'
- gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled - gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled
%p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") } %p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") }
= gitlab_shared_runners = gitlab_shared_runners
%span.light.pull-right %span.light.pull-right
= boolean_to_icon gitlab_shared_runners_enabled = boolean_to_icon gitlab_shared_runners_enabled
.col-md-4 .col-md-4
%h4 .info-well
Components .well-segment.admin-well
- if current_application_settings.version_check_enabled %h4
.pull-right Components
= version_status_badge - if current_application_settings.version_check_enabled
.pull-right
%hr = version_status_badge
%p %p
GitLab GitLab
%span.pull-right %span.pull-right
= Gitlab::VERSION = Gitlab::VERSION
%p %p
GitLab Shell GitLab Shell
%span.pull-right %span.pull-right
= Gitlab::Shell.new.version = Gitlab::Shell.new.version
%p %p
GitLab Workhorse GitLab Workhorse
%span.pull-right %span.pull-right
= gitlab_workhorse_version = gitlab_workhorse_version
%p %p
GitLab API GitLab API
%span.pull-right %span.pull-right
= API::API::version = API::API::version
%p %p
Git Git
%span.pull-right %span.pull-right
= Gitlab::Git.version = Gitlab::Git.version
%p %p
Ruby Ruby
%span.pull-right %span.pull-right
#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
%p
%p Rails
Rails %span.pull-right
%span.pull-right #{Rails::VERSION::STRING}
#{Rails::VERSION::STRING} %p
= Gitlab::Database.adapter_name
%p %span.pull-right
= Gitlab::Database.adapter_name = Gitlab::Database.version
%span.pull-right
= Gitlab::Database.version
%hr
.row .row
.col-sm-4 .col-sm-4
.light-well.well-centered .info-well.dark-well
%h4 Projects .well-segment.well-centered
.data
= link_to admin_projects_path do = link_to admin_projects_path do
%h1= number_with_delimiter(Project.cached_count) %h3.text-center
Projects:
= number_with_delimiter(Project.cached_count)
%hr %hr
= link_to('New project', new_project_path, class: "btn btn-new") = link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4 .col-sm-4
.light-well.well-centered .info-well.dark-well
%h4 Users .well-segment.well-centered
.data
= link_to admin_users_path do = link_to admin_users_path do
%h1= number_with_delimiter(User.count) %h3.text-center
Users:
= number_with_delimiter(User.count)
%hr %hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new" = link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4 .col-sm-4
.light-well.well-centered .info-well.dark-well
%h4 Groups .well-segment.well-centered
.data
= link_to admin_groups_path do = link_to admin_groups_path do
%h1= number_with_delimiter(Group.count) %h3.text-center
Groups
= number_with_delimiter(Group.count)
%hr %hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new" = link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row
.row.prepend-top-10
.col-md-4 .col-md-4
%h4 Latest projects .info-well
%hr .well-segment.admin-well
- @projects.each do |project| %h4 Latest projects
%p - @projects.each do |project|
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60' %p
%span.light.pull-right = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
#{time_ago_with_tooltip(project.created_at)} %span.light.pull-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4 .col-md-4
%h4 Latest users .info-well
%hr .well-segment.admin-well
- @users.each do |user| %h4 Latest users
%p - @users.each do |user|
= link_to [:admin, user], class: 'str-truncated-60' do %p
= user.name = link_to [:admin, user], class: 'str-truncated-60' do
%span.light.pull-right = user.name
#{time_ago_with_tooltip(user.created_at)} %span.light.pull-right
#{time_ago_with_tooltip(user.created_at)}
.col-md-4 .col-md-4
%h4 Latest groups .info-well
%hr .well-segment.admin-well
- @groups.each do |group| %h4 Latest groups
%p - @groups.each do |group|
= link_to [:admin, group], class: 'str-truncated-60' do %p
= group.full_name = link_to [:admin, group], class: 'str-truncated-60' do
%span.light.pull-right = group.full_name
#{time_ago_with_tooltip(group.created_at)} %span.light.pull-right
#{time_ago_with_tooltip(group.created_at)}
...@@ -6,7 +6,4 @@ ...@@ -6,7 +6,4 @@
- providers.each do |provider| - providers.each do |provider|
%span.light %span.light
- has_icon = provider_has_icon?(provider) - has_icon = provider_has_icon?(provider)
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn')
%fieldset
= check_box_tag :remember_me
= label_tag :remember_me, 'Remember Me'
---
title: Honor the "Remember me" parameter for OAuth-based login
merge_request: 11963
author:
---
title: Replace 'snippets/snippets.feature' spinach with rspec
merge_request: 12385
author: Alexander Randa @randaalex
---
title: Add wells to admin dashboard overview to fix spacing problems
merge_request:
author:
---
title: fix left & right padding on sidebar
merge_request:
author:
...@@ -619,53 +619,6 @@ test: ...@@ -619,53 +619,6 @@ test:
title: "JIRA" title: "JIRA"
url: https://sample_company.atlassian.net url: https://sample_company.atlassian.net
project_key: PROJECT project_key: PROJECT
omniauth:
enabled: true
allow_single_sign_on: true
external_providers: []
providers:
- { name: 'cas3',
label: 'cas3',
args: { url: 'https://sso.example.com',
disable_ssl_verification: false,
login_url: '/cas/login',
service_validate_url: '/cas/p3/serviceValidate',
logout_url: '/cas/logout'} }
- { name: 'authentiq',
app_id: 'YOUR_CLIENT_ID',
app_secret: 'YOUR_CLIENT_SECRET',
args: { scope: 'aq:name email~rs address aq:push' } }
- { name: 'github',
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET',
url: "https://github.com/",
verify_ssl: false,
args: { scope: 'user:email' } }
- { name: 'bitbucket',
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET' }
- { name: 'gitlab',
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET',
args: { scope: 'api' } }
- { name: 'google_oauth2',
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET',
args: { access_type: 'offline', approval_prompt: '' } }
- { name: 'facebook',
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET' }
- { name: 'twitter',
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET' }
- { name: 'auth0',
args: {
client_id: 'YOUR_AUTH0_CLIENT_ID',
client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
namespace: 'YOUR_AUTH0_DOMAIN' } }
ldap: ldap:
enabled: false enabled: false
servers: servers:
......
require 'flipper/middleware/memoizer' require 'flipper/middleware/memoizer'
Rails.application.config.middleware.use Flipper::Middleware::Memoizer, unless Rails.env.test?
lambda { Feature.flipper } Rails.application.config.middleware.use Flipper::Middleware::Memoizer,
lambda { Feature.flipper }
end
@snippets
Feature: Snippets
Background:
Given I sign in as a user
And I have public "Personal snippet one" snippet
And I have private "Personal snippet private" snippet
@javascript
Scenario: I create new snippet
Given I visit new snippet page
And I submit new snippet "Personal snippet three"
Then I should see snippet "Personal snippet three"
Scenario: I update "Personal snippet one"
Given I visit snippet page "Personal snippet one"
And I click link "Edit"
And I submit new title "Personal snippet new title"
Then I should see "Personal snippet new title"
Scenario: Set "Personal snippet one" public
Given I visit snippet page "Personal snippet one"
And I click link "Edit"
And I uncheck "Private" checkbox
Then I should see "Personal snippet one" public
Scenario: I destroy "Personal snippet one"
Given I visit snippet page "Personal snippet one"
And I click link "Delete"
Then I should not see "Personal snippet one" in snippets
Scenario: I create new internal snippet
Given I logout directly
And I sign in as an admin
Then I visit new snippet page
And I submit new internal snippet
Then I visit snippet page "Internal personal snippet one"
And I logout directly
Then I sign in as a user
Given I visit new snippet page
Then I visit snippet page "Internal personal snippet one"
...@@ -487,10 +487,6 @@ module SharedPaths ...@@ -487,10 +487,6 @@ module SharedPaths
visit explore_snippets_path visit explore_snippets_path
end end
step 'I visit new snippet page' do
visit new_snippet_path
end
def root_ref def root_ref
@project.repository.root_ref @project.repository.root_ref
end end
......
module SharedSnippet
include Spinach::DSL
step 'I have public "Personal snippet one" snippet' do
create(:personal_snippet,
title: "Personal snippet one",
content: "Test content",
file_name: "snippet.rb",
visibility_level: Snippet::PUBLIC,
author: current_user)
end
step 'I have private "Personal snippet private" snippet' do
create(:personal_snippet,
title: "Personal snippet private",
content: "Provate content",
file_name: "private_snippet.rb",
visibility_level: Snippet::PRIVATE,
author: current_user)
end
step 'I have internal "Personal snippet internal" snippet' do
create(:personal_snippet,
title: "Personal snippet internal",
content: "Provate content",
file_name: "internal_snippet.rb",
visibility_level: Snippet::INTERNAL,
author: current_user)
end
step 'I have a public many lined snippet' do
create(:personal_snippet,
title: 'Many lined snippet',
content: <<-END.gsub(/^\s+\|/, ''),
|line one
|line two
|line three
|line four
|line five
|line six
|line seven
|line eight
|line nine
|line ten
|line eleven
|line twelve
|line thirteen
|line fourteen
END
file_name: 'many_lined_snippet.rb',
visibility_level: Snippet::PUBLIC,
author: current_user)
end
step 'There is public "Personal snippet one" snippet' do
create(:personal_snippet,
title: "Personal snippet one",
content: "Test content",
file_name: "snippet.rb",
visibility_level: Snippet::PUBLIC,
author: create(:user))
end
end
class Spinach::Features::Snippets < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
include SharedSnippet
include WaitForRequests
step 'I click link "Personal snippet one"' do
click_link "Personal snippet one"
end
step 'I should not see "Personal snippet one" in snippets' do
expect(page).not_to have_content "Personal snippet one"
end
step 'I click link "Edit"' do
page.within ".detail-page-header" do
first(:link, "Edit").click
end
end
step 'I click link "Delete"' do
first(:link, "Delete").click
end
step 'I submit new snippet "Personal snippet three"' do
fill_in "personal_snippet_title", with: "Personal snippet three"
fill_in "personal_snippet_file_name", with: "my_snippet.rb"
page.within('.file-editor') do
find('.ace_editor').native.send_keys 'Content of snippet three'
end
click_button "Create snippet"
wait_for_requests
end
step 'I submit new internal snippet' do
fill_in "personal_snippet_title", with: "Internal personal snippet one"
fill_in "personal_snippet_file_name", with: "my_snippet.rb"
choose 'personal_snippet_visibility_level_10'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of internal snippet'
end
click_button "Create snippet"
end
step 'I should see snippet "Personal snippet three"' do
expect(page).to have_content "Personal snippet three"
expect(page).to have_content "Content of snippet three"
end
step 'I submit new title "Personal snippet new title"' do
fill_in "personal_snippet_title", with: "Personal snippet new title"
click_button "Save"
end
step 'I should see "Personal snippet new title"' do
expect(page).to have_content "Personal snippet new title"
end
step 'I uncheck "Private" checkbox' do
choose "Internal"
click_button "Save"
end
step 'I should see "Personal snippet one" public' do
expect(page).to have_no_xpath("//i[@class='public-snippet']")
end
step 'I visit snippet page "Personal snippet one"' do
visit snippet_path(snippet)
end
step 'I visit snippet page "Internal personal snippet one"' do
visit snippet_path(internal_snippet)
end
def snippet
@snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one")
end
def internal_snippet
@snippet ||= PersonalSnippet.find_by!(title: "Internal personal snippet one")
end
end
...@@ -42,7 +42,8 @@ namespace :gitlab do ...@@ -42,7 +42,8 @@ namespace :gitlab do
http_clone_url = project.http_url_to_repo http_clone_url = project.http_url_to_repo
ssh_clone_url = project.ssh_url_to_repo ssh_clone_url = project.ssh_url_to_repo
omniauth_providers = Gitlab.config.omniauth.providers.map { |provider| provider['name'] } omniauth_providers = Gitlab.config.omniauth.providers
omniauth_providers.map! { |provider| provider['name'] }
puts "" puts ""
puts "GitLab information".color(:yellow) puts "GitLab information".color(:yellow)
......
FactoryGirl.define do
factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do
end
end
FactoryGirl.define do
factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
project factory: :empty_project
end
end
...@@ -18,4 +18,11 @@ FactoryGirl.define do ...@@ -18,4 +18,11 @@ FactoryGirl.define do
visibility_level Snippet::PRIVATE visibility_level Snippet::PRIVATE
end end
end end
factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
project factory: :empty_project
end
factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do
end
end end
require 'spec_helper'
feature 'OAuth Login', js: true do
def enter_code(code)
fill_in 'user_otp_attempt', with: code
click_button 'Verify code'
end
def stub_omniauth_config(provider)
OmniAuth.config.add_mock(provider, OmniAuth::AuthHash.new(provider: provider.to_s, uid: "12345"))
Rails.application.env_config['devise.mapping'] = Devise.mappings[:user]
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider]
end
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
:facebook, :authentiq, :cas3, :auth0]
before(:all) do
# The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost`
# here), and causes integration tests to fail with 404s. We set the `full_host` by removing the request path (and
# anything after it) from the request URI.
@omniauth_config_full_host = OmniAuth.config.full_host
OmniAuth.config.full_host = ->(request) { request['REQUEST_URI'].sub(/#{request['REQUEST_PATH']}.*/, '') }
end
after(:all) do
OmniAuth.config.full_host = @omniauth_config_full_host
end
providers.each do |provider|
context "when the user logs in using the #{provider} provider" do
context 'when two-factor authentication is disabled' do
it 'logs the user in' do
stub_omniauth_config(provider)
user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
login_via(provider.to_s, user, 'my-uid')
expect(current_path).to eq root_path
end
end
context 'when two-factor authentication is enabled' do
it 'logs the user in' do
stub_omniauth_config(provider)
user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
login_via(provider.to_s, user, 'my-uid')
enter_code(user.current_otp)
expect(current_path).to eq root_path
end
end
context 'when "remember me" is checked' do
context 'when two-factor authentication is disabled' do
it 'remembers the user after a browser restart' do
stub_omniauth_config(provider)
user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
login_via(provider.to_s, user, 'my-uid', remember_me: true)
clear_browser_session
visit(root_path)
expect(current_path).to eq root_path
end
end
context 'when two-factor authentication is enabled' do
it 'remembers the user after a browser restart' do
stub_omniauth_config(provider)
user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
login_via(provider.to_s, user, 'my-uid', remember_me: true)
enter_code(user.current_otp)
clear_browser_session
visit(root_path)
expect(current_path).to eq root_path
end
end
end
context 'when "remember me" is not checked' do
context 'when two-factor authentication is disabled' do
it 'does not remember the user after a browser restart' do
stub_omniauth_config(provider)
user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s)
login_via(provider.to_s, user, 'my-uid', remember_me: false)
clear_browser_session
visit(root_path)
expect(current_path).to eq new_user_session_path
end
end
context 'when two-factor authentication is enabled' do
it 'does not remember the user after a browser restart' do
stub_omniauth_config(provider)
user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s)
login_via(provider.to_s, user, 'my-uid', remember_me: false)
enter_code(user.current_otp)
clear_browser_session
visit(root_path)
expect(current_path).to eq new_user_session_path
end
end
end
end
end
end
require 'rails_helper' require 'rails_helper'
feature 'Create Snippet', :js, feature: true do feature 'User creates snippet', :js, feature: true do
include DropzoneHelper include DropzoneHelper
let(:user) { create(:user) }
before do before do
gitlab_sign_in :user sign_in(user)
visit new_snippet_path visit new_snippet_path
end end
......
require 'rails_helper'
feature 'User deletes snippet', feature: true do
let(:user) { create(:user) }
let(:content) { 'puts "test"' }
let(:snippet) { create(:personal_snippet, :public, content: content, author: user) }
before do
sign_in(user)
visit snippet_path(snippet)
end
it 'deletes the snippet' do
first(:link, 'Delete').click
expect(page).not_to have_content(snippet.title)
end
end
require 'rails_helper' require 'rails_helper'
feature 'Edit Snippet', :js, feature: true do feature 'User edits snippet', :js, feature: true do
include DropzoneHelper include DropzoneHelper
let(:file_name) { 'test.rb' } let(:file_name) { 'test.rb' }
...@@ -10,7 +10,7 @@ feature 'Edit Snippet', :js, feature: true do ...@@ -10,7 +10,7 @@ feature 'Edit Snippet', :js, feature: true do
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, author: user) } let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, author: user) }
before do before do
gitlab_sign_in(user) sign_in(user)
visit edit_snippet_path(snippet) visit edit_snippet_path(snippet)
wait_for_requests wait_for_requests
...@@ -27,7 +27,7 @@ feature 'Edit Snippet', :js, feature: true do ...@@ -27,7 +27,7 @@ feature 'Edit Snippet', :js, feature: true do
it 'updates the snippet with files attached' do it 'updates the snippet with files attached' do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("personal_snippet_description").value).to have_content('banana_sample') expect(page.find_field('personal_snippet_description').value).to have_content('banana_sample')
click_button('Save changes') click_button('Save changes')
wait_for_requests wait_for_requests
...@@ -35,4 +35,24 @@ feature 'Edit Snippet', :js, feature: true do ...@@ -35,4 +35,24 @@ feature 'Edit Snippet', :js, feature: true do
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/uploads/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z}) expect(link).to match(%r{/uploads/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z})
end end
it 'updates the snippet to make it internal' do
choose 'Internal'
click_button 'Save changes'
wait_for_requests
expect(page).to have_no_xpath("//i[@class='fa fa-lock']")
expect(page).to have_xpath("//i[@class='fa fa-shield']")
end
it 'updates the snippet to make it public' do
choose 'Public'
click_button 'Save changes'
wait_for_requests
expect(page).to have_no_xpath("//i[@class='fa fa-lock']")
expect(page).to have_xpath("//i[@class='fa fa-globe']")
end
end end
...@@ -86,6 +86,16 @@ describe('Store', () => { ...@@ -86,6 +86,16 @@ describe('Store', () => {
store.toggleFolder(store.state.environments[1]); store.toggleFolder(store.state.environments[1]);
expect(store.state.environments[1].isOpen).toEqual(false); expect(store.state.environments[1].isOpen).toEqual(false);
}); });
it('should keep folder open when environments are updated', () => {
store.storeEnvironments(serverData);
store.toggleFolder(store.state.environments[1]);
expect(store.state.environments[1].isOpen).toEqual(true);
store.storeEnvironments(serverData);
expect(store.state.environments[1].isOpen).toEqual(true);
});
}); });
describe('setfolderContent', () => { describe('setfolderContent', () => {
...@@ -97,6 +107,17 @@ describe('Store', () => { ...@@ -97,6 +107,17 @@ describe('Store', () => {
expect(store.state.environments[1].children.length).toEqual(serverData.length); expect(store.state.environments[1].children.length).toEqual(serverData.length);
expect(store.state.environments[1].children[0].isChildren).toEqual(true); expect(store.state.environments[1].children[0].isChildren).toEqual(true);
}); });
it('should keep folder content when environments are updated', () => {
store.storeEnvironments(serverData);
store.setfolderContent(store.state.environments[1], serverData);
expect(store.state.environments[1].children.length).toEqual(serverData.length);
// poll
store.storeEnvironments(serverData);
expect(store.state.environments[1].children.length).toEqual(serverData.length);
});
}); });
describe('store pagination', () => { describe('store pagination', () => {
......
#oauth-container
%input#remember_me{ type: "checkbox" }
%a.oauth-login.twitter{ href: "http://example.com/" }
%a.oauth-login.github{ href: "http://example.com/" }
import OAuthRememberMe from '~/oauth_remember_me';
describe('OAuthRememberMe', () => {
preloadFixtures('static/oauth_remember_me.html.raw');
beforeEach(() => {
loadFixtures('static/oauth_remember_me.html.raw');
new OAuthRememberMe({ container: $('#oauth-container') }).bindEvents();
});
it('adds the "remember_me" query parameter to all OAuth login buttons', () => {
$('#oauth-container #remember_me').click();
expect($('#oauth-container .oauth-login.twitter').attr('href')).toBe('http://example.com/?remember_me=1');
expect($('#oauth-container .oauth-login.github').attr('href')).toBe('http://example.com/?remember_me=1');
});
it('removes the "remember_me" query parameter from all OAuth login buttons', () => {
$('#oauth-container #remember_me').click();
$('#oauth-container #remember_me').click();
expect($('#oauth-container .oauth-login.twitter').attr('href')).toBe('http://example.com/');
expect($('#oauth-container .oauth-login.github').attr('href')).toBe('http://example.com/');
});
});
...@@ -35,11 +35,6 @@ module CapybaraHelpers ...@@ -35,11 +35,6 @@ module CapybaraHelpers
visit 'about:blank' visit 'about:blank'
visit url visit url
end end
# Simulate a browser restart by clearing the session cookie.
def clear_browser_session
page.driver.remove_cookie('_gitlab_session')
end
end end
RSpec.configure do |config| RSpec.configure do |config|
......
...@@ -62,16 +62,6 @@ module LoginHelpers ...@@ -62,16 +62,6 @@ module LoginHelpers
Thread.current[:current_user] = user Thread.current[:current_user] = user
end end
def login_via(provider, user, uid, remember_me: false)
mock_auth_hash(provider, uid, user.email)
visit new_user_session_path
expect(page).to have_content('Sign in with')
check 'Remember Me' if remember_me
click_link "oauth-login-#{provider}"
end
def mock_auth_hash(provider, uid, email) def mock_auth_hash(provider, uid, email)
# The mock_auth configuration allows you to set per-provider (or default) # The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing. # authentication hashes to return during integration testing.
...@@ -118,7 +108,6 @@ module LoginHelpers ...@@ -118,7 +108,6 @@ module LoginHelpers
end end
allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config) allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
stub_omniauth_setting(messages) stub_omniauth_setting(messages)
allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml') expect_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
end end
end end
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