Commit 16c0ac7e authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 4249-show-results-from-docker-image-scan-in-the-merge-request-widget

* master: (39 commits)
  Port of 28869-es6-modules to EE
  Docs: update admin docs
  Fix spec by avoiding monkeypatching
  fix broken empty state assets for environment monitoring page
  Clean up new dropdown styles (EE port)
  Adds i18n for empty state
  [EE] Present member collection at the controller level
  Resolve "Geo: cache results in issue and message count being incorrect"
  Fix an exception in Geo scheduler workers
  Remove legacy page call from node factory in QA
  Remove noisy notification from QA base page
  Remove legacy page from hashed storage QA scenario
  Update license admin area images and instructions
  Replace license_admin_area.png to show new UI
  Replace admin_wrench.png to match new UI
  Remove legacy code from Geo test scenario
  Remove redundant blank line from geo replication specs
  Use new Geo nodes settings pages in Gitlab QA
  Fix successful rebase throwing flash error message
  Fix new license factory in GitLab QA
  ...
parents 46343cf9 2f1fd0d9
...@@ -9,7 +9,7 @@ export default class ContextualSidebar { ...@@ -9,7 +9,7 @@ export default class ContextualSidebar {
} }
initDomElements() { initDomElements() {
this.$page = $('.page-with-sidebar'); this.$page = $('.layout-page');
this.$sidebar = $('.nav-sidebar'); this.$sidebar = $('.nav-sidebar');
this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
this.$overlay = $('.mobile-overlay'); this.$overlay = $('.mobile-overlay');
......
...@@ -15,7 +15,7 @@ import GroupLabelSubscription from './group_label_subscription'; ...@@ -15,7 +15,7 @@ import GroupLabelSubscription from './group_label_subscription';
import BuildArtifacts from './build_artifacts'; import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor'; import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select'; import groupsSelect from './groups_select';
/* global Search */ import Search from './search';
/* global Admin */ /* global Admin */
import NamespaceSelect from './namespace_select'; import NamespaceSelect from './namespace_select';
import NewCommitForm from './new_commit_form'; import NewCommitForm from './new_commit_form';
...@@ -25,7 +25,7 @@ import projectAvatar from './project_avatar'; ...@@ -25,7 +25,7 @@ import projectAvatar from './project_avatar';
import Compare from './compare'; import Compare from './compare';
import initCompareAutocomplete from './compare_autocomplete'; import initCompareAutocomplete from './compare_autocomplete';
/* global PathLocks */ /* global PathLocks */
/* global ProjectFindFile */ import ProjectFindFile from './project_find_file';
import ProjectNew from './project_new'; import ProjectNew from './project_new';
import projectImport from './project_import'; import projectImport from './project_import';
import Labels from './labels'; import Labels from './labels';
...@@ -96,6 +96,7 @@ import DueDateSelectors from './due_date_select'; ...@@ -96,6 +96,7 @@ import DueDateSelectors from './due_date_select';
import Diff from './diff'; import Diff from './diff';
import ProjectLabelSubscription from './project_label_subscription'; import ProjectLabelSubscription from './project_label_subscription';
import ProjectVariables from './project_variables'; import ProjectVariables from './project_variables';
import SearchAutocomplete from './search_autocomplete';
// EE-only // EE-only
import ApproversSelect from './approvers_select'; import ApproversSelect from './approvers_select';
...@@ -777,7 +778,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -777,7 +778,7 @@ import initGroupAnalytics from './init_group_analytics';
Dispatcher.prototype.initSearch = function() { Dispatcher.prototype.initSearch = function() {
// Only when search form is present // Only when search form is present
if ($('.search').length) { if ($('.search').length) {
return new gl.SearchAutocomplete(); return new SearchAutocomplete();
} }
}; };
......
...@@ -21,7 +21,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -21,7 +21,7 @@ export default class IssuableBulkUpdateSidebar {
} }
initDomElements() { initDomElements() {
this.$page = $('.page-with-sidebar'); this.$page = $('.layout-page');
this.$sidebar = $('.right-sidebar'); this.$sidebar = $('.right-sidebar');
this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar'); this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar');
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide'); this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
......
...@@ -60,15 +60,10 @@ import './notifications_dropdown'; ...@@ -60,15 +60,10 @@ import './notifications_dropdown';
import './notifications_form'; import './notifications_form';
import './pager'; import './pager';
import './preview_markdown'; import './preview_markdown';
import './project_find_file';
import './project_import'; import './project_import';
import './projects_dropdown'; import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
import './render_gfm'; import './render_gfm';
import './right_sidebar'; import './right_sidebar';
import './search';
import './search_autocomplete';
import initBreadcrumbs from './breadcrumb'; import initBreadcrumbs from './breadcrumb';
// EE-only scripts // EE-only scripts
...@@ -134,7 +129,7 @@ $(function () { ...@@ -134,7 +129,7 @@ $(function () {
}); });
if (bootstrapBreakpoint === 'xs') { if (bootstrapBreakpoint === 'xs') {
const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar'); const $rightSidebar = $('aside.right-sidebar, .layout-page');
$rightSidebar $rightSidebar
.removeClass('right-sidebar-expanded') .removeClass('right-sidebar-expanded')
...@@ -194,7 +189,7 @@ $(function () { ...@@ -194,7 +189,7 @@ $(function () {
trigger: 'focus', trigger: 'focus',
// set the viewport to the main content, excluding the navigation bar, so // set the viewport to the main content, excluding the navigation bar, so
// the navigation can't overlap the popover // the navigation can't overlap the popover
viewport: '.page-with-sidebar' viewport: '.layout-page'
}); });
$('.trigger-submit').on('change', function () { $('.trigger-submit').on('change', function () {
return $(this).parents('form').submit(); return $(this).parents('form').submit();
......
...@@ -10,6 +10,7 @@ import './mixins/line_conflict_actions'; ...@@ -10,6 +10,7 @@ import './mixins/line_conflict_actions';
import './components/diff_file_editor'; import './components/diff_file_editor';
import './components/inline_conflict_lines'; import './components/inline_conflict_lines';
import './components/parallel_conflict_lines'; import './components/parallel_conflict_lines';
import syntaxHighlight from '../syntax_highlight';
$(() => { $(() => {
const INTERACTIVE_RESOLVE_MODE = 'interactive'; const INTERACTIVE_RESOLVE_MODE = 'interactive';
...@@ -53,7 +54,7 @@ $(() => { ...@@ -53,7 +54,7 @@ $(() => {
mergeConflictsStore.setLoadingState(false); mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => { this.$nextTick(() => {
$('.js-syntax-highlight').syntaxHighlight(); syntaxHighlight($('.js-syntax-highlight'));
}); });
}); });
}, },
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
import { getLocationHash } from './lib/utils/url_utility'; import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab'; import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff'; import Diff from './diff';
import syntaxHighlight from './syntax_highlight';
/* eslint-disable max-len */ /* eslint-disable max-len */
// MergeRequestTabs // MergeRequestTabs
...@@ -295,7 +296,7 @@ import Diff from './diff'; ...@@ -295,7 +296,7 @@ import Diff from './diff';
} }
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight(); syntaxHighlight($('#diffs .js-syntax-highlight'));
if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
this.expandViewContainer(); this.expandViewContainer();
......
...@@ -2,11 +2,34 @@ ...@@ -2,11 +2,34 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
(function() { // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
this.ProjectFindFile = (function() { const highlighter = function(element, text, matches) {
var highlighter; var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
lastIndex = 0;
highlightText = "";
matchedChars = [];
for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
unmatched = text.substring(lastIndex, matchIndex);
if (unmatched) {
if (matchedChars.length) {
element.append(matchedChars.join("").bold());
}
matchedChars = [];
element.append(document.createTextNode(unmatched));
}
matchedChars.push(text[matchIndex]);
lastIndex = matchIndex + 1;
}
if (matchedChars.length) {
element.append(matchedChars.join("").bold());
}
return element.append(document.createTextNode(text.substring(lastIndex)));
};
export default class ProjectFindFile {
function ProjectFindFile(element1, options) { constructor (element1, options) {
this.element = element1; this.element = element1;
this.options = options; this.options = options;
this.goToBlob = this.goToBlob.bind(this); this.goToBlob = this.goToBlob.bind(this);
...@@ -23,7 +46,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; ...@@ -23,7 +46,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
this.load(this.options.url); this.load(this.options.url);
} }
ProjectFindFile.prototype.initEvent = function() { initEvent() {
this.inputElement.off("keyup"); this.inputElement.off("keyup");
this.inputElement.on("keyup", (function(_this) { this.inputElement.on("keyup", (function(_this) {
return function(event) { return function(event) {
...@@ -38,18 +61,18 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; ...@@ -38,18 +61,18 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
} }
}; };
})(this)); })(this));
}; }
ProjectFindFile.prototype.findFile = function() { findFile() {
var result, searchText; var result, searchText;
searchText = this.inputElement.val(); searchText = this.inputElement.val();
result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
return this.renderList(result, searchText); return this.renderList(result, searchText);
// find file // find file
}; }
// files pathes load // files pathes load
ProjectFindFile.prototype.load = function(url) { load(url) {
return $.ajax({ return $.ajax({
url: url, url: url,
method: "get", method: "get",
...@@ -63,10 +86,10 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; ...@@ -63,10 +86,10 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
}; };
})(this) })(this)
}); });
}; }
// render result // render result
ProjectFindFile.prototype.renderList = function(filePaths, searchText) { renderList(filePaths, searchText) {
var blobItemUrl, filePath, html, i, j, len, matches, results; var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty(); this.element.find(".tree-table > tbody").empty();
results = []; results = [];
...@@ -79,39 +102,14 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; ...@@ -79,39 +102,14 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
matches = fuzzaldrinPlus.match(filePath, searchText); matches = fuzzaldrinPlus.match(filePath, searchText);
} }
blobItemUrl = this.options.blobUrlTemplate + "/" + filePath; blobItemUrl = this.options.blobUrlTemplate + "/" + filePath;
html = this.makeHtml(filePath, matches, blobItemUrl); html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find(".tree-table > tbody").append(html)); results.push(this.element.find(".tree-table > tbody").append(html));
} }
return results; return results;
};
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
highlighter = function(element, text, matches) {
var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
lastIndex = 0;
highlightText = "";
matchedChars = [];
for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
unmatched = text.substring(lastIndex, matchIndex);
if (unmatched) {
if (matchedChars.length) {
element.append(matchedChars.join("").bold());
}
matchedChars = [];
element.append(document.createTextNode(unmatched));
}
matchedChars.push(text[matchIndex]);
lastIndex = matchIndex + 1;
}
if (matchedChars.length) {
element.append(matchedChars.join("").bold());
} }
return element.append(document.createTextNode(text.substring(lastIndex)));
};
// make tbody row html // make tbody row html
ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) { static makeHtml(filePath, matches, blobItemUrl) {
var $tr; var $tr;
$tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>"); $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
if (matches) { if (matches) {
...@@ -121,9 +119,9 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; ...@@ -121,9 +119,9 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
$tr.find(".str-truncated").text(filePath); $tr.find(".str-truncated").text(filePath);
} }
return $tr; return $tr;
}; }
ProjectFindFile.prototype.selectRow = function(type) { selectRow(type) {
var next, rows, selectedRow; var next, rows, selectedRow;
rows = this.element.find(".files-slider tr.tree-item"); rows = this.element.find(".files-slider tr.tree-item");
selectedRow = this.element.find(".files-slider tr.tree-item.selected"); selectedRow = this.element.find(".files-slider tr.tree-item.selected");
...@@ -143,28 +141,25 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; ...@@ -143,28 +141,25 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
} }
return selectedRow.addClass("selected").focus(); return selectedRow.addClass("selected").focus();
} }
}; }
ProjectFindFile.prototype.selectRowUp = function() { selectRowUp() {
return this.selectRow("UP"); return this.selectRow("UP");
}; }
ProjectFindFile.prototype.selectRowDown = function() { selectRowDown() {
return this.selectRow("DOWN"); return this.selectRow("DOWN");
}; }
ProjectFindFile.prototype.goToTree = function() { goToTree() {
return location.href = this.options.treeUrl; return location.href = this.options.treeUrl;
}; }
ProjectFindFile.prototype.goToBlob = function() { goToBlob() {
var $link = this.element.find(".tree-item.selected .tree-item-file-name a"); var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
if ($link.length) { if ($link.length) {
$link.get(0).click(); $link.get(0).click();
} }
}; }
}
return ProjectFindFile;
})();
}).call(window);
import renderMath from './render_math'; import renderMath from './render_math';
import renderMermaid from './render_mermaid'; import renderMermaid from './render_mermaid';
import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown // Render Gitlab flavoured Markdown
// //
// Delegates to syntax highlight and render math & mermaid diagrams. // Delegates to syntax highlight and render math & mermaid diagrams.
// //
$.fn.renderGFM = function renderGFM() { $.fn.renderGFM = function renderGFM() {
this.find('.js-syntax-highlight').syntaxHighlight(); syntaxHighlight(this.find('.js-syntax-highlight'));
renderMath(this.find('.js-render-math')); renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid')); renderMermaid(this.find('.js-render-mermaid'));
return this; return this;
......
<script> <script>
/* global LineHighlighter */ /* global LineHighlighter */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import syntaxHighlight from '../../syntax_highlight';
export default { export default {
computed: { computed: {
...@@ -13,7 +14,7 @@ export default { ...@@ -13,7 +14,7 @@ export default {
}, },
methods: { methods: {
highlightFile() { highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight(); syntaxHighlight($(this.$el).find('.file-content'));
}, },
}, },
mounted() { mounted() {
......
...@@ -42,11 +42,11 @@ import Cookies from 'js-cookie'; ...@@ -42,11 +42,11 @@ import Cookies from 'js-cookie';
if ($thisIcon.hasClass('fa-angle-double-right')) { if ($thisIcon.hasClass('fa-angle-double-right')) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
} else { } else {
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
$('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
if (gl.lazyLoader) gl.lazyLoader.loadCheck(); if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} }
...@@ -173,7 +173,7 @@ import Cookies from 'js-cookie'; ...@@ -173,7 +173,7 @@ import Cookies from 'js-cookie';
Sidebar.prototype.setCollapseAfterUpdate = function($block) { Sidebar.prototype.setCollapseAfterUpdate = function($block) {
$block.addClass('collapse-after-update'); $block.addClass('collapse-after-update');
return $('.page-with-sidebar').addClass('with-overlay'); return $('.layout-page').addClass('with-overlay');
}; };
Sidebar.prototype.onSidebarDropdownHidden = function(e) { Sidebar.prototype.onSidebarDropdownHidden = function(e) {
...@@ -187,7 +187,7 @@ import Cookies from 'js-cookie'; ...@@ -187,7 +187,7 @@ import Cookies from 'js-cookie';
Sidebar.prototype.sidebarDropdownHidden = function($block) { Sidebar.prototype.sidebarDropdownHidden = function($block) {
if ($block.hasClass('collapse-after-update')) { if ($block.hasClass('collapse-after-update')) {
$block.removeClass('collapse-after-update'); $block.removeClass('collapse-after-update');
$('.page-with-sidebar').removeClass('with-overlay'); $('.layout-page').removeClass('with-overlay');
return this.toggleSidebar('hide'); return this.toggleSidebar('hide');
} }
}; };
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
import Flash from './flash'; import Flash from './flash';
import Api from './api'; import Api from './api';
(function() { export default class Search {
this.Search = (function() { constructor() {
function Search() { const $groupDropdown = $('.js-search-group-dropdown');
var $groupDropdown, $projectDropdown; const $projectDropdown = $('.js-search-project-dropdown');
$groupDropdown = $('.js-search-group-dropdown');
$projectDropdown = $('.js-search-project-dropdown'); this.searchInput = '.js-search-input';
this.searchClear = '.js-search-clear';
this.groupId = $groupDropdown.data('group-id'); this.groupId = $groupDropdown.data('group-id');
this.eventListeners(); this.eventListeners();
$groupDropdown.glDropdown({ $groupDropdown.glDropdown({
selectable: true, selectable: true,
filterable: true, filterable: true,
fieldName: 'group_id', fieldName: 'group_id',
search: { search: {
fields: ['full_name'] fields: ['full_name'],
}, },
data: function(term, callback) { data(term, callback) {
return Api.groups(term, {}, function(data) { return Api.groups(term, {}, (data) => {
data.unshift({ data.unshift({
full_name: 'Any' full_name: 'Any',
}); });
data.splice(1, 0, 'divider'); data.splice(1, 0, 'divider');
return callback(data); return callback(data);
}); });
}, },
id: function(obj) { id(obj) {
return obj.id; return obj.id;
}, },
text: function(obj) { text(obj) {
return obj.full_name; return obj.full_name;
}, },
toggleLabel: function(obj) { toggleLabel(obj) {
return ($groupDropdown.data('default-label')) + " " + obj.full_name; return `${($groupDropdown.data('default-label'))} ${obj.full_name}`;
}, },
clicked: (function(_this) { clicked: () => Search.submitSearch(),
return function() {
return _this.submitSearch();
};
})(this)
}); });
$projectDropdown.glDropdown({ $projectDropdown.glDropdown({
selectable: true, selectable: true,
filterable: true, filterable: true,
fieldName: 'project_id', fieldName: 'project_id',
search: { search: {
fields: ['name'] fields: ['name'],
}, },
data: (term, callback) => { data: (term, callback) => {
this.getProjectsData(term) this.getProjectsData(term)
.then((data) => { .then((data) => {
data.unshift({ data.unshift({
name_with_namespace: 'Any' name_with_namespace: 'Any',
}); });
data.splice(1, 0, 'divider'); data.splice(1, 0, 'divider');
...@@ -61,47 +60,46 @@ import Api from './api'; ...@@ -61,47 +60,46 @@ import Api from './api';
.then(data => callback(data)) .then(data => callback(data))
.catch(() => new Flash('Error fetching projects')); .catch(() => new Flash('Error fetching projects'));
}, },
id: function(obj) { id(obj) {
return obj.id; return obj.id;
}, },
text: function(obj) { text(obj) {
return obj.name_with_namespace; return obj.name_with_namespace;
}, },
toggleLabel: function(obj) { toggleLabel(obj) {
return ($projectDropdown.data('default-label')) + " " + obj.name_with_namespace; return `${($projectDropdown.data('default-label'))} ${obj.name_with_namespace}`;
}, },
clicked: (function(_this) { clicked: () => Search.submitSearch(),
return function() {
return _this.submitSearch();
};
})(this)
}); });
} }
Search.prototype.eventListeners = function() { eventListeners() {
$(document).off('keyup', '.js-search-input').on('keyup', '.js-search-input', this.searchKeyUp); $(document)
return $(document).off('click', '.js-search-clear').on('click', '.js-search-clear', this.clearSearchField); .off('keyup', this.searchInput)
}; .on('keyup', this.searchInput, this.searchKeyUp);
$(document)
.off('click', this.searchClear)
.on('click', this.searchClear, this.clearSearchField);
}
Search.prototype.submitSearch = function() { static submitSearch() {
return $('.js-search-form').submit(); return $('.js-search-form').submit();
}; }
Search.prototype.searchKeyUp = function() { searchKeyUp() {
var $input; const $input = $(this);
$input = $(this);
if ($input.val() === '') { if ($input.val() === '') {
return $('.js-search-clear').addClass('hidden'); $('.js-search-clear').addClass('hidden');
} else { } else {
return $('.js-search-clear').removeClass('hidden'); $('.js-search-clear').removeClass('hidden');
}
} }
};
Search.prototype.clearSearchField = function() { clearSearchField() {
return $('.js-search-input').val('').trigger('keyup').focus(); return $(this.searchInput).val('').trigger('keyup').focus();
}; }
Search.prototype.getProjectsData = function(term) { getProjectsData(term) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (this.groupId) { if (this.groupId) {
Api.groupProjects(this.groupId, term, resolve); Api.groupProjects(this.groupId, term, resolve);
...@@ -111,8 +109,5 @@ import Api from './api'; ...@@ -111,8 +109,5 @@ import Api from './api';
}, resolve); }, resolve);
} }
}); });
}; }
}
return Search;
})();
}).call(window);
/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ /* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
((global) => { const KEYCODE = {
const KEYCODE = {
ESCAPE: 27, ESCAPE: 27,
BACKSPACE: 8, BACKSPACE: 8,
ENTER: 13, ENTER: 13,
UP: 38, UP: 38,
DOWN: 40 DOWN: 40
};
function setSearchOptions() {
var $projectOptionsDataEl = $('.js-search-project-options');
var $groupOptionsDataEl = $('.js-search-group-options');
var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
var projectPath = $projectOptionsDataEl.data('project-path');
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path')
}; };
}
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
class SearchAutocomplete { var groupPath = $groupOptionsDataEl.data('group-path');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'),
mrPath: $groupOptionsDataEl.data('mr-path')
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
mrPath: $dashboardOptionsDataEl.data('mr-path')
};
}
}
export default class SearchAutocomplete {
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
setSearchOptions();
this.bindEventContext(); this.bindEventContext();
this.wrap = wrap || $('.search'); this.wrap = wrap || $('.search');
this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
...@@ -402,45 +440,4 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. ...@@ -402,45 +440,4 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
return this.searchInput.val('').focus(); return this.searchInput.val('').focus();
} }
} }
} }
global.SearchAutocomplete = SearchAutocomplete;
$(function() {
var $projectOptionsDataEl = $('.js-search-project-options');
var $groupOptionsDataEl = $('.js-search-group-options');
var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
var projectPath = $projectOptionsDataEl.data('project-path');
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path')
};
}
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
var groupPath = $groupOptionsDataEl.data('group-path');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'),
mrPath: $groupOptionsDataEl.data('mr-path')
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
mrPath: $dashboardOptionsDataEl.data('mr-path')
};
}
});
})(window.gl || (window.gl = {}));
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import imageDiffHelper from './image_diff/helpers/index'; import imageDiffHelper from './image_diff/helpers/index';
import syntaxHighlight from './syntax_highlight';
const WRAPPER = '<div class="diff-content"></div>'; const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
...@@ -64,7 +65,7 @@ export default class SingleFileDiff { ...@@ -64,7 +65,7 @@ export default class SingleFileDiff {
_this.loadingContent.hide(); _this.loadingContent.hide();
if (data.html) { if (data.html) {
_this.content = $(data.html); _this.content = $(data.html);
_this.content.syntaxHighlight(); syntaxHighlight(_this.content);
} else { } else {
_this.hasError = true; _this.hasError = true;
_this.content = $(ERROR_HTML); _this.content = $(ERROR_HTML);
......
...@@ -10,17 +10,15 @@ ...@@ -10,17 +10,15 @@
// <div class="js-syntax-highlight"></div> // <div class="js-syntax-highlight"></div>
// //
$.fn.syntaxHighlight = function() { export default function syntaxHighlight(el) {
var $children; if ($(el).hasClass('js-syntax-highlight')) {
if ($(this).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting // Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme); return $(el).addClass(gon.user_color_scheme);
} else { } else {
// Given a parent element, recurse to any of its applicable children // Given a parent element, recurse to any of its applicable children
$children = $(this).find('.js-syntax-highlight'); const $children = $(el).find('.js-syntax-highlight');
if ($children.length) { if ($children.length) {
return $children.syntaxHighlight(); return syntaxHighlight($children);
} }
} }
}; }
...@@ -143,20 +143,48 @@ ...@@ -143,20 +143,48 @@
} }
} }
@mixin dropdown-item-hover {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
outline: 0;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
.avatar {
border-color: $white-light;
}
}
@mixin dropdown-link { @mixin dropdown-link {
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
display: block; display: block;
font-weight: $gl-font-weight-normal;
position: relative; position: relative;
padding: 5px 8px; padding: 8px 16px;
color: $gl-text-color; color: $gl-text-color;
line-height: initial; line-height: normal;
border-radius: 2px; white-space: normal;
white-space: nowrap;
overflow: hidden; overflow: hidden;
text-align: left;
width: 100%;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
&:hover, &:hover,
&:active,
&:focus, &:focus,
&.is-focused { &.is-focused {
background-color: $dropdown-link-hover-bg; @include dropdown-item-hover;
text-decoration: none; text-decoration: none;
.badge { .badge {
...@@ -166,6 +194,13 @@ ...@@ -166,6 +194,13 @@
&.dropdown-menu-user-link { &.dropdown-menu-user-link {
line-height: 16px; line-height: 16px;
padding-top: 10px;
padding-bottom: 7px;
white-space: nowrap;
.dropdown-menu-user-username {
display: block;
}
} }
.icon-play { .icon-play {
...@@ -187,8 +222,8 @@ ...@@ -187,8 +222,8 @@
z-index: 300; z-index: 300;
min-width: 240px; min-width: 240px;
max-width: 500px; max-width: 500px;
margin-top: 2px; margin-top: $dropdown-vertical-offset;
margin-bottom: 2px; margin-bottom: 24px;
font-size: 14px; font-size: 14px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
padding: 8px 0; padding: 8px 0;
...@@ -197,6 +232,10 @@ ...@@ -197,6 +232,10 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
&.dropdown-open-top {
margin-bottom: $dropdown-vertical-offset;
}
&.dropdown-open-left { &.dropdown-open-left {
right: 0; right: 0;
left: auto; left: auto;
...@@ -227,16 +266,27 @@ ...@@ -227,16 +266,27 @@
} }
li { li {
display: block;
text-align: left; text-align: left;
list-style: none; list-style: none;
padding: 0 10px; padding: 0 1px;
a,
button,
.menu-item {
@include dropdown-link;
}
} }
.divider { .divider {
height: 1px; height: 1px;
margin: 6px 10px; margin: 6px 0;
padding: 0; padding: 0;
background-color: $dropdown-divider-color; background-color: $dropdown-divider-color;
&:hover {
background-color: $dropdown-divider-color;
}
} }
.separator { .separator {
...@@ -247,10 +297,6 @@ ...@@ -247,10 +297,6 @@
background-color: $dropdown-divider-color; background-color: $dropdown-divider-color;
} }
a {
@include dropdown-link;
}
.dropdown-menu-empty-item a { .dropdown-menu-empty-item a {
&:hover, &:hover,
&:focus { &:focus {
...@@ -262,7 +308,7 @@ ...@@ -262,7 +308,7 @@
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: 13px; font-size: 13px;
line-height: 22px; line-height: 22px;
padding: 0 16px; padding: 8px 16px;
} }
&.capitalize-header .dropdown-header { &.capitalize-header .dropdown-header {
...@@ -277,7 +323,7 @@ ...@@ -277,7 +323,7 @@
.separator + .dropdown-header, .separator + .dropdown-header,
.separator + .dropdown-bold-header { .separator + .dropdown-bold-header {
padding-top: 2px; padding-top: 10px;
} }
.unclickable { .unclickable {
...@@ -298,48 +344,28 @@ ...@@ -298,48 +344,28 @@
} }
.dropdown-menu li { .dropdown-menu li {
padding: $gl-btn-padding;
cursor: pointer; cursor: pointer;
&.droplab-item-active button {
@include dropdown-item-hover;
}
> a, > a,
> button { > button {
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0;
border-radius: 0;
text-overflow: inherit; text-overflow: inherit;
background-color: inherit;
color: inherit;
border: inherit;
text-align: left; text-align: left;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
&.btn .fa:not(:last-child) { &.btn .fa:not(:last-child) {
margin-left: 5px; margin-left: 5px;
} }
} }
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected i { &.droplab-item-selected i {
visibility: visible; visibility: visible;
} }
&.divider {
margin: 0 8px;
padding: 0;
border-top: $gray-darkest;
}
.icon { .icon {
visibility: hidden; visibility: hidden;
} }
...@@ -431,11 +457,6 @@ ...@@ -431,11 +457,6 @@
} }
} }
.dropdown-menu-user-link {
padding-top: 10px;
padding-bottom: 7px;
}
.dropdown-menu-user-full-name { .dropdown-menu-user-full-name {
display: block; display: block;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
...@@ -464,23 +485,22 @@ ...@@ -464,23 +485,22 @@
.dropdown-menu-align-right { .dropdown-menu-align-right {
left: auto; left: auto;
right: 0; right: 0;
margin-top: -5px;
} }
.dropdown-menu-selectable { .dropdown-menu-selectable {
li {
a { a {
padding-left: 26px; padding: 8px 40px;
position: relative; position: relative;
&.is-indeterminate, &.is-indeterminate,
&.is-active { &.is-active {
font-weight: $gl-font-weight-bold;
color: $gl-text-color; color: $gl-text-color;
&::before { &::before {
position: absolute; position: absolute;
left: 6px; left: 16px;
top: 50%; top: 16px;
transform: translateY(-50%); transform: translateY(-50%);
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
font-size: inherit; font-size: inherit;
...@@ -488,6 +508,12 @@ ...@@ -488,6 +508,12 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
&.dropdown-menu-user-link {
&::before {
top: 50%;
}
}
} }
&.is-indeterminate::before { &.is-indeterminate::before {
...@@ -496,9 +522,7 @@ ...@@ -496,9 +522,7 @@
&.is-active::before { &.is-active::before {
content: "\f00c"; content: "\f00c";
position: absolute; }
top: 50%;
transform: translateY(-50%);
} }
} }
} }
...@@ -735,136 +759,6 @@ ...@@ -735,136 +759,6 @@
} }
} }
@mixin dropdown-item-hover {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
// TODO: change global style and remove mixin
@mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav {
margin-bottom: 24px;
&.dropdown-open-top {
margin-bottom: $dropdown-vertical-offset;
}
li {
display: block;
padding: 0 1px;
&:hover {
background-color: transparent;
}
&.divider {
margin: 6px 0;
&:hover {
background-color: $dropdown-divider-color;
}
}
&.dropdown-header {
padding: 8px 16px;
}
&.droplab-item-active button {
@include dropdown-item-hover;
}
a,
button,
.menu-item {
margin-bottom: 0;
border-radius: 0;
box-shadow: none;
padding: 8px 16px;
text-align: left;
white-space: normal;
width: 100%;
font-weight: $gl-font-weight-normal;
line-height: normal;
&.dropdown-menu-user-link {
white-space: nowrap;
.dropdown-menu-user-username {
display: block;
}
}
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
&.is-focused,
&:hover,
&:active,
&:focus {
@include dropdown-item-hover;
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
}
&.is-active {
font-weight: inherit;
&::before {
top: 16px;
}
&.dropdown-menu-user-link::before {
top: 50%;
transform: translateY(-50%);
}
}
}
&.dropdown-menu-empty-item a {
&:hover,
&:focus {
background-color: transparent;
}
}
}
&.dropdown-menu-selectable {
li {
a {
padding: 8px 40px;
&.is-indeterminate::before,
&.is-active::before {
left: 16px;
}
}
}
}
}
#{$selector}.dropdown-menu-align-right {
margin-top: 2px;
}
.open {
#{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav {
@media (max-width: $screen-xs-max) {
max-width: 100%;
}
}
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.navbar-gitlab { .navbar-gitlab {
li.header-projects, li.header-projects,
...@@ -891,9 +785,6 @@ ...@@ -891,9 +785,6 @@
} }
} }
@include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + ');
header.header-content .dropdown-menu.projects-dropdown-menu { header.header-content .dropdown-menu.projects-dropdown-menu {
padding: 0; padding: 0;
} }
...@@ -1031,35 +922,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -1031,35 +922,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
} }
.new-epic-dropdown {
.dropdown-menu {
padding-left: $gl-padding-top;
padding-right: $gl-padding-top;
}
.form-control {
width: 100%;
}
.btn-save {
display: flex;
margin-top: $gl-btn-padding;
}
}
.empty-state .new-epic-dropdown {
display: inline-flex;
.btn-save {
margin-left: 0;
margin-bottom: 0;
}
.btn-new {
margin: 0;
}
}
.dropdown-content-faded-mask { .dropdown-content-faded-mask {
position: relative; position: relative;
......
...@@ -50,8 +50,6 @@ ...@@ -50,8 +50,6 @@
} }
.filtered-search-wrapper { .filtered-search-wrapper {
@include new-style-dropdown;
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
...@@ -165,16 +163,6 @@ ...@@ -165,16 +163,6 @@
} }
} }
.droplab-dropdown li.filtered-search-token {
padding: 0;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
.filtered-search-term { .filtered-search-term {
.name { .name {
background-color: inherit; background-color: inherit;
...@@ -336,21 +324,12 @@ ...@@ -336,21 +324,12 @@
.filtered-search-history-dropdown-content { .filtered-search-history-dropdown-content {
max-height: none; max-height: none;
}
.filtered-search-history-dropdown-item,
.filtered-search-history-clear-button {
@include dropdown-link;
overflow: hidden;
width: 100%;
margin: 0.5em 0;
background-color: transparent; .filtered-search-history-dropdown-item,
border: 0; .filtered-search-history-clear-button {
text-align: left;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
}
} }
.filtered-search-history-dropdown-token { .filtered-search-history-dropdown-token {
...@@ -402,24 +381,9 @@ ...@@ -402,24 +381,9 @@
} }
} }
%filter-dropdown-item-btn-hover {
text-decoration: none;
outline: 0;
.avatar {
border-color: $white-light;
}
}
.droplab-dropdown .dropdown-menu .filter-dropdown-item { .droplab-dropdown .dropdown-menu .filter-dropdown-item {
.btn { .btn {
border: 0;
width: 100%;
text-align: left;
padding: 8px 16px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden;
border-radius: 0;
.fa { .fa {
width: 15px; width: 15px;
...@@ -433,11 +397,6 @@ ...@@ -433,11 +397,6 @@
width: 17px; width: 17px;
height: 17px; height: 17px;
} }
&:hover,
&:focus {
@extend %filter-dropdown-item-btn-hover;
}
} }
.dropdown-light-content { .dropdown-light-content {
...@@ -458,17 +417,9 @@ ...@@ -458,17 +417,9 @@
word-break: break-all; word-break: break-all;
} }
} }
&.droplab-item-active .btn {
@extend %filter-dropdown-item-btn-hover;
}
} }
.filter-dropdown-loading { .filter-dropdown-loading {
padding: 8px 16px; padding: 8px 16px;
text-align: center; text-align: center;
} }
.issues-details-filters {
@include new-style-dropdown;
}
.content-wrapper.page-with-new-nav {
margin-top: $header-height;
}
.navbar-gitlab { .navbar-gitlab {
@include new-style-dropdown;
&.navbar-gitlab { &.navbar-gitlab {
padding: 0 16px; padding: 0 16px;
z-index: 1000; z-index: 1000;
......
...@@ -24,6 +24,7 @@ body { ...@@ -24,6 +24,7 @@ body {
} }
.content-wrapper { .content-wrapper {
margin-top: $header-height;
padding-bottom: 100px; padding-bottom: 100px;
} }
...@@ -105,11 +106,11 @@ body { ...@@ -105,11 +106,11 @@ body {
} }
} }
.page-with-sidebar > .content-wrapper { .layout-page > .content-wrapper {
min-height: calc(100vh - #{$header-height}); min-height: calc(100vh - #{$header-height});
} }
.with-performance-bar .page-with-sidebar { .with-performance-bar .layout-page {
margin-top: $header-height + $performance-bar-height; margin-top: $header-height + $performance-bar-height;
} }
......
...@@ -132,8 +132,6 @@ ul.content-list { ...@@ -132,8 +132,6 @@ ul.content-list {
} }
.controls { .controls {
@include new-style-dropdown;
float: right; float: right;
> .control-text { > .control-text {
......
...@@ -86,8 +86,6 @@ ...@@ -86,8 +86,6 @@
} }
.nav-controls { .nav-controls {
@include new-style-dropdown;
display: inline-block; display: inline-block;
float: right; float: right;
text-align: right; text-align: right;
......
...@@ -144,10 +144,6 @@ ...@@ -144,10 +144,6 @@
} }
} }
.issuable-sidebar {
@include new-style-dropdown;
}
.pikaday-container { .pikaday-container {
.pika-single { .pika-single {
margin-top: 2px; margin-top: 2px;
......
...@@ -343,8 +343,6 @@ a > code { ...@@ -343,8 +343,6 @@ a > code {
@extend .ref-name; @extend .ref-name;
} }
@include new-style-dropdown('.git-revision-dropdown');
/** /**
* Apply Markdown typography * Apply Markdown typography
* *
......
...@@ -482,7 +482,7 @@ ...@@ -482,7 +482,7 @@
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
} }
.page-with-contextual-sidebar.page-with-sidebar .issue-boards-sidebar { .page-with-contextual-sidebar.layout-page .issue-boards-sidebar {
.issuable-sidebar-header { .issuable-sidebar-header {
position: relative; position: relative;
} }
...@@ -699,8 +699,6 @@ ...@@ -699,8 +699,6 @@
} }
.boards-switcher { .boards-switcher {
@include new-style-dropdown;
padding-right: 10px; padding-right: 10px;
} }
......
...@@ -323,8 +323,6 @@ ...@@ -323,8 +323,6 @@
} }
.build-dropdown { .build-dropdown {
@include new-style-dropdown;
margin: $gl-padding 0; margin: $gl-padding 0;
padding: 0; padding: 0;
......
...@@ -13,8 +13,6 @@ ...@@ -13,8 +13,6 @@
max-width: 100%; max-width: 100%;
} }
@include new-style-dropdown('.clusters-dropdown ');
.clusters-container { .clusters-container {
.nav-bar-right { .nav-bar-right {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding;
......
#cycle-analytics { #cycle-analytics {
@include new-style-dropdown;
max-width: 1000px; max-width: 1000px;
margin: 24px auto 0; margin: 24px auto 0;
position: relative; position: relative;
......
...@@ -32,8 +32,6 @@ ...@@ -32,8 +32,6 @@
} }
.detail-page-header-actions { .detail-page-header-actions {
@include new-style-dropdown;
align-self: center; align-self: center;
flex-shrink: 0; flex-shrink: 0;
flex: 0 0 auto; flex: 0 0 auto;
......
...@@ -581,8 +581,6 @@ ...@@ -581,8 +581,6 @@
} }
.commit-stat-summary { .commit-stat-summary {
@include new-style-dropdown;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
margin-left: -$gl-padding; margin-left: -$gl-padding;
padding-left: $gl-padding; padding-left: $gl-padding;
......
...@@ -204,8 +204,6 @@ ...@@ -204,8 +204,6 @@
.gitlab-ci-yml-selector, .gitlab-ci-yml-selector,
.dockerfile-selector, .dockerfile-selector,
.template-type-selector { .template-type-selector {
@include new-style-dropdown;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
font-family: $regular_font; font-family: $regular_font;
......
...@@ -12,8 +12,6 @@ ...@@ -12,8 +12,6 @@
.environments-container { .environments-container {
.ci-table { .ci-table {
@include new-style-dropdown;
.deployment-column { .deployment-column {
> span { > span {
word-break: break-all; word-break: break-all;
......
...@@ -489,12 +489,6 @@ ...@@ -489,12 +489,6 @@
} }
} }
.dropdown-content {
a:hover {
color: inherit;
}
}
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 100%; width: 100%;
padding-top: 6px; padding-top: 6px;
...@@ -513,10 +507,6 @@ ...@@ -513,10 +507,6 @@
} }
} }
.sidebar-move-issue-dropdown {
@include new-style-dropdown;
}
.sidebar-move-issue-confirmation-button { .sidebar-move-issue-confirmation-button {
width: 100%; width: 100%;
......
...@@ -142,8 +142,6 @@ ul.related-merge-requests > li { ...@@ -142,8 +142,6 @@ ul.related-merge-requests > li {
} }
.issue-form { .issue-form {
@include new-style-dropdown;
.select2-container { .select2-container {
width: 250px !important; width: 250px !important;
} }
......
...@@ -116,8 +116,6 @@ ...@@ -116,8 +116,6 @@
} }
.manage-labels-list { .manage-labels-list {
@include new-style-dropdown;
> li:not(.empty-message):not(.is-not-draggable) { > li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light; background-color: $white-light;
cursor: move; cursor: move;
......
...@@ -64,8 +64,6 @@ ...@@ -64,8 +64,6 @@
} }
.member-form-control { .member-form-control {
@include new-style-dropdown;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
padding-bottom: 5px; padding-bottom: 5px;
margin-left: 0; margin-left: 0;
...@@ -79,8 +77,6 @@ ...@@ -79,8 +77,6 @@
} }
.member-search-form { .member-search-form {
@include new-style-dropdown;
position: relative; position: relative;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
......
...@@ -471,8 +471,6 @@ ...@@ -471,8 +471,6 @@
} }
.mr-source-target { .mr-source-target {
@include new-style-dropdown;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
...@@ -594,8 +592,6 @@ ...@@ -594,8 +592,6 @@
} }
.mr-version-controls { .mr-version-controls {
@include new-style-dropdown;
position: relative; position: relative;
background: $gray-light; background: $gray-light;
color: $gl-text-color; color: $gl-text-color;
...@@ -818,7 +814,3 @@ ...@@ -818,7 +814,3 @@
} }
} }
} }
.merge-request-form {
@include new-style-dropdown;
}
...@@ -23,8 +23,6 @@ ...@@ -23,8 +23,6 @@
.new-note, .new-note,
.note-edit-form { .note-edit-form {
.note-form-actions { .note-form-actions {
@include new-style-dropdown;
position: relative; position: relative;
margin: $gl-padding 0 0; margin: $gl-padding 0 0;
} }
......
...@@ -490,8 +490,6 @@ ul.notes { ...@@ -490,8 +490,6 @@ ul.notes {
} }
.note-actions { .note-actions {
@include new-style-dropdown;
align-self: flex-start; align-self: flex-start;
flex-shrink: 0; flex-shrink: 0;
display: inline-flex; display: inline-flex;
......
...@@ -14,7 +14,3 @@ ...@@ -14,7 +14,3 @@
font-size: 18px; font-size: 18px;
} }
} }
.notification-form {
@include new-style-dropdown;
}
...@@ -321,8 +321,6 @@ ...@@ -321,8 +321,6 @@
// Pipeline visualization // Pipeline visualization
.pipeline-actions { .pipeline-actions {
@include new-style-dropdown;
border-bottom: 0; border-bottom: 0;
} }
...@@ -730,9 +728,6 @@ a.linked-pipeline-mini-item { ...@@ -730,9 +728,6 @@ a.linked-pipeline-mini-item {
} }
} }
@include new-style-dropdown('.big-pipeline-graph-dropdown-menu');
@include new-style-dropdown('.mini-pipeline-graph-dropdown-menu');
// dropdown content for big and mini pipeline // dropdown content for big and mini pipeline
.big-pipeline-graph-dropdown-menu, .big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu { .mini-pipeline-graph-dropdown-menu {
...@@ -831,7 +826,6 @@ a.linked-pipeline-mini-item { ...@@ -831,7 +826,6 @@ a.linked-pipeline-mini-item {
font-weight: normal; font-weight: normal;
line-height: $line-height-base; line-height: $line-height-base;
white-space: nowrap; white-space: nowrap;
border-radius: 3px;
.ci-job-name-component { .ci-job-name-component {
align-items: center; align-items: center;
......
...@@ -331,8 +331,6 @@ ...@@ -331,8 +331,6 @@
} }
.project-repo-buttons { .project-repo-buttons {
@include new-style-dropdown;
.project-action-button .dropdown-menu { .project-action-button .dropdown-menu {
max-height: 250px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
...@@ -910,8 +908,6 @@ a.allowed-to-push { ...@@ -910,8 +908,6 @@ a.allowed-to-push {
.new-protected-branch, .new-protected-branch,
.new-protected-tag { .new-protected-tag {
@include new-style-dropdown;
label { label {
margin-top: 6px; margin-top: 6px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
...@@ -938,8 +934,6 @@ a.allowed-to-push { ...@@ -938,8 +934,6 @@ a.allowed-to-push {
.protected-branches-list, .protected-branches-list,
.protected-tags-list { .protected-tags-list {
@include new-style-dropdown;
margin-bottom: 30px; margin-bottom: 30px;
.settings-message { .settings-message {
......
...@@ -116,11 +116,6 @@ input[type="checkbox"]:hover { ...@@ -116,11 +116,6 @@ input[type="checkbox"]:hover {
opacity: 0; opacity: 0;
display: block; display: block;
left: -5px; left: -5px;
padding: 0;
ul {
padding: 10px 0;
}
} }
.dropdown-content { .dropdown-content {
...@@ -185,8 +180,6 @@ input[type="checkbox"]:hover { ...@@ -185,8 +180,6 @@ input[type="checkbox"]:hover {
} }
.search-holder { .search-holder {
@include new-style-dropdown;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
......
...@@ -265,7 +265,3 @@ ...@@ -265,7 +265,3 @@
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
} }
.todos-filters {
@include new-style-dropdown;
}
.tree-holder { .tree-holder {
@include new-style-dropdown;
.nav-block { .nav-block {
margin: 10px 0; margin: 10px 0;
......
class Admin::GroupsController < Admin::ApplicationController class Admin::GroupsController < Admin::ApplicationController
include MembersPresentation
prepend EE::Admin::GroupsController prepend EE::Admin::GroupsController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
...@@ -12,8 +13,10 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -12,8 +13,10 @@ class Admin::GroupsController < Admin::ApplicationController
def show def show
@group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id]) @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id])
@members = @group.members.order("access_level DESC").page(params[:members_page]) @members = present_members(
@requesters = AccessRequestsFinder.new(@group).execute(current_user) @group.members.order("access_level DESC").page(params[:members_page]))
@requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user))
@projects = @group.projects.with_statistics.page(params[:projects_page]) @projects = @group.projects.with_statistics.page(params[:projects_page])
end end
......
class Admin::ProjectsController < Admin::ApplicationController class Admin::ProjectsController < Admin::ApplicationController
include MembersPresentation
before_action :project, only: [:show, :transfer, :repository_check] before_action :project, only: [:show, :transfer, :repository_check]
before_action :group, only: [:show, :transfer] before_action :group, only: [:show, :transfer]
...@@ -19,11 +21,14 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -19,11 +21,14 @@ class Admin::ProjectsController < Admin::ApplicationController
def show def show
if @group if @group
@group_members = @group.members.order("access_level DESC").page(params[:group_members_page]) @group_members = present_members(
@group.members.order("access_level DESC").page(params[:group_members_page]))
end end
@project_members = @project.members.page(params[:project_members_page]) @project_members = present_members(
@requesters = AccessRequestsFinder.new(@project).execute(current_user) @project.members.page(params[:project_members_page]))
@requesters = present_members(
AccessRequestsFinder.new(@project).execute(current_user))
end end
def transfer def transfer
......
module MembersPresentation
extend ActiveSupport::Concern
def present_members(members)
Gitlab::View::Presenter::Factory.new(
members,
current_user: current_user,
presenter_class: MembersPresenter
).fabricate!
end
end
...@@ -2,6 +2,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -2,6 +2,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
prepend EE::Groups::GroupMembersController prepend EE::Groups::GroupMembersController
include MembershipActions include MembershipActions
include MembersPresentation
include SortingHelper include SortingHelper
# Authorize # Authorize
...@@ -17,15 +18,17 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -17,15 +18,17 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.search(params[:search]) if params[:search].present? @members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort) @members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50) @members = @members.page(params[:page]).per(50)
@members.includes(:user) @members = present_members(@members.includes(:user))
@requesters = AccessRequestsFinder.new(@group).execute(current_user) @requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user))
@group_member = @group.group_members.new @group_member = @group.group_members.new
end end
def update def update
@group_member = @group.group_members.find(params[:id]) @group_member = @group.group_members.find(params[:id])
.present(current_user: current_user)
return render_403 unless can?(current_user, :update_group_member, @group_member) return render_403 unless can?(current_user, :update_group_member, @group_member)
......
class Projects::ProjectMembersController < Projects::ApplicationController class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions include MembershipActions
include MembersPresentation
include SortingHelper include SortingHelper
# Authorize # Authorize
...@@ -20,13 +21,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -20,13 +21,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end end
@project_members = @project_members.sort(@sort).page(params[:page]) @project_members = present_members(@project_members.sort(@sort).page(params[:page]))
@requesters = AccessRequestsFinder.new(@project).execute(current_user) @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
@project_member = @project.project_members.new @project_member = @project.project_members.new
end end
def update def update
@project_member = @project.project_members.find(params[:id]) @project_member = @project.project_members.find(params[:id])
.present(current_user: current_user)
return render_403 unless can?(current_user, :update_project_member, @project_member) return render_403 unless can?(current_user, :update_project_member, @project_member)
......
module MembersHelper module MembersHelper
# Returns a `<action>_<source>_member` association, e.g.:
# - admin_project_member, update_project_member, destroy_project_member
# - admin_group_member, update_group_member, destroy_group_member, override_group_member
def action_member_permission(action, member)
"#{action}_#{member.type.underscore}".to_sym
end
def remove_member_message(member, user: nil) def remove_member_message(member, user: nil)
user = current_user if defined?(current_user) user = current_user if defined?(current_user)
......
...@@ -4,6 +4,7 @@ class Member < ActiveRecord::Base ...@@ -4,6 +4,7 @@ class Member < ActiveRecord::Base
include Importable include Importable
include Expirable include Expirable
include Gitlab::Access include Gitlab::Access
include Presentable
attr_accessor :raw_invite_token attr_accessor :raw_invite_token
attr_accessor :skip_notification attr_accessor :skip_notification
......
class GroupMemberPresenter < MemberPresenter
prepend EE::GroupMemberPresenter
private
def admin_member_permission
:admin_group_member
end
def update_member_permission
:update_group_member
end
def destroy_member_permission
:destroy_group_member
end
end
class MemberPresenter < Gitlab::View::Presenter::Delegated
prepend EE::MemberPresenter
presents :member
def access_level_roles
member.class.access_level_roles
end
def can_resend_invite?
invite? &&
can?(current_user, admin_member_permission, source)
end
def can_update?
can?(current_user, update_member_permission, member)
end
def can_remove?
can?(current_user, destroy_member_permission, member)
end
def can_approve?
request? && can_update?
end
private
def admin_member_permission
raise NotImplementedError
end
def update_member_permission
raise NotImplementedError
end
def destroy_member_permission
raise NotImplementedError
end
end
class MembersPresenter < Gitlab::View::Presenter::Delegated
include Enumerable
presents :members
def to_ary
to_a
end
def each
members.each do |member|
yield member.present(current_user: current_user)
end
end
end
class ProjectMemberPresenter < MemberPresenter
prepend EE::ProjectMemberPresenter
private
def admin_member_permission
:admin_project_member
end
def update_member_permission
:update_project_member
end
def destroy_member_permission
:destroy_project_member
end
end
# Base class for services that count a single resource such as the number of # Base class for services that count a single resource such as the number of
# issues for a project. # issues for a project.
class BaseCountService class BaseCountService
prepend ::EE::BaseCountService
def relation_for_count def relation_for_count
raise( raise(
NotImplementedError, NotImplementedError,
......
...@@ -35,8 +35,17 @@ module Members ...@@ -35,8 +35,17 @@ module Members
def can_update_access_requester?(access_requester, opts = {}) def can_update_access_requester?(access_requester, opts = {})
access_requester && ( access_requester && (
opts[:force] || opts[:force] ||
can?(current_user, action_member_permission(:update, access_requester), access_requester) can?(current_user, update_member_permission(access_requester), access_requester)
) )
end end
def update_member_permission(member)
case member
when GroupMember
:update_group_member
when ProjectMember
:update_project_member
end
end
end end
end end
...@@ -41,7 +41,16 @@ module Members ...@@ -41,7 +41,16 @@ module Members
end end
def can_destroy_member?(member) def can_destroy_member?(member)
member && can?(current_user, action_member_permission(:destroy, member), member) member && can?(current_user, destroy_member_permission(member), member)
end
def destroy_member_permission(member)
case member
when GroupMember
:destroy_group_member
when ProjectMember
:destroy_project_member
end
end end
end end
end end
.page-with-sidebar{ class: page_with_sidebar_class } .layout-page{ class: page_with_sidebar_class }
- if defined?(nav) && nav - if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}" = render "layouts/nav/sidebar/#{nav}"
.content-wrapper.page-with-new-nav .content-wrapper
.mobile-overlay .mobile-overlay
.alert-wrapper .alert-wrapper
= render "layouts/header/ee_license_banner" = render "layouts/header/ee_license_banner"
......
...@@ -3,15 +3,15 @@ ...@@ -3,15 +3,15 @@
Template Template
.template-selector-dropdowns-wrap .template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden .template-type-selector.js-template-type-selector-wrap.hidden
= dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } ) = dropdown_tag("Choose type", options: { toggle_class: 'js-template-type-selector', title: "Choose a template type" } )
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a license template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) = dropdown_tag("Apply a license template", options: { toggle_class: 'js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
.template-selectors-undo-menu.hidden .template-selectors-undo-menu.hidden
%span.text-info Template applied %span.text-info Template applied
%button.btn.btn-sm.btn-info Undo %button.btn.btn-sm.btn-info Undo
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
.col-sm-10.create-from .col-sm-10.create-from
.dropdown .dropdown
= hidden_field_tag :ref, default_ref = hidden_field_tag :ref, default_ref
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
.text-left.dropdown-toggle-text= default_ref .text-left.dropdown-toggle-text= default_ref
= icon('chevron-down') = icon('chevron-down')
= render 'shared/ref_dropdown', dropdown_class: 'wide' = render 'shared/ref_dropdown', dropdown_class: 'wide'
......
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'), #prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started'), "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading'), "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect'), "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json), "additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json),
"has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } } "has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } }
- project = local_assigns.fetch(:project)
- members = local_assigns.fetch(:members)
.panel.panel-default .panel.panel-default
.panel-heading.flex-project-members-panel .panel-heading.flex-project-members-panel
%span.flex-project-title %span.flex-project-title
Members of Members of
%strong %strong= project.name
#{@project.name} %span.badge= members.total_count
%span.badge= @project_members.total_count = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
= form_tag project_project_members_path(@project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group .form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
......
...@@ -39,5 +39,5 @@ ...@@ -39,5 +39,5 @@
- if @group_links.any? - if @group_links.any?
= render 'projects/project_members/groups', group_links: @group_links = render 'projects/project_members/groups', group_links: @group_links
= render 'projects/project_members/team', members: @project_members = render 'projects/project_members/team', project: @project, members: @project_members
= paginate @project_members, theme: "gitlab" = paginate @project_members, theme: "gitlab"
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
.col-sm-10.create-from .col-sm-10.create-from
.dropdown .dropdown
= hidden_field_tag :ref, default_ref = hidden_field_tag :ref, default_ref
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
.text-left.dropdown-toggle-text= default_ref .text-left.dropdown-toggle-text= default_ref
= render 'shared/ref_dropdown', dropdown_class: 'wide' = render 'shared/ref_dropdown', dropdown_class: 'wide'
.help-block .help-block
......
...@@ -35,7 +35,12 @@ ...@@ -35,7 +35,12 @@
%li %li
= link_to 'Edit', edit_label_path(label) = link_to 'Edit', edit_label_path(label)
%li %li
= link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, data: {confirm: 'Remove this label? Are you sure?'} = link_to 'Delete',
destroy_label_path(label),
title: 'Delete',
method: :delete,
data: {confirm: 'Remove this label? Are you sure?'},
class: 'text-danger'
.pull-right.hidden-xs.hidden-sm.hidden-md .pull-right.hidden-xs.hidden-sm.hidden-md
= link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do
......
- show_roles = local_assigns.fetch(:show_roles, true) - show_roles = local_assigns.fetch(:show_roles, true)
- show_controls = local_assigns.fetch(:show_controls, true) - show_controls = local_assigns.fetch(:show_controls, true)
- force_mobile_view = local_assigns.fetch(:force_mobile_view, false) - force_mobile_view = local_assigns.fetch(:force_mobile_view, false)
- member = local_assigns.fetch(:member)
- user = local_assigns.fetch(:user, member.user) - user = local_assigns.fetch(:user, member.user)
- source = member.source - source = member.source
- can_admin_member = can?(current_user, action_member_permission(:update, member), member)
-# EE-only
- can_override_member = can?(current_user, action_member_permission(:override, member), member)
%li.member{ class: [dom_class(member), ("is-overriden" if member.override)], id: dom_id(member) } %li.member{ class: [dom_class(member), ("is-overriden" if member.override)], id: dom_id(member) }
%span.list-item-name %span.list-item-name
...@@ -51,21 +48,21 @@ ...@@ -51,21 +48,21 @@
- if show_roles - if show_roles
- current_resource = @project || @group - current_resource = @project || @group
.controls.member-controls .controls.member-controls
= render 'shared/members/ee/ldap_tag', can_override: can_override_member, visible: false = render 'shared/members/ee/ldap_tag', can_override: member.can_override?, visible: false
- if show_controls && member.source == current_resource - if show_controls && member.source == current_resource
- if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) - if member.can_resend_invite?
= link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]), = link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]),
method: :post, method: :post,
class: 'btn btn-default prepend-left-10 hidden-xs', class: 'btn btn-default prepend-left-10 hidden-xs',
title: 'Resend invite' title: 'Resend invite'
- if user != current_user && (can_admin_member || can_override_member) - if user != current_user && member.can_update?
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level = f.hidden_field :access_level
.member-form-control.dropdown.append-right-5 .member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
disabled: !can_admin_member, disabled: member.can_override?,
data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } } data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } }
%span.dropdown-toggle-text %span.dropdown-toggle-text
= member.human_access = member.human_access
...@@ -74,24 +71,27 @@ ...@@ -74,24 +71,27 @@
= dropdown_title("Change permissions") = dropdown_title("Change permissions")
.dropdown-content .dropdown-content
%ul %ul
- member.class.access_level_roles.each do |role, role_id| - member.access_level_roles.each do |role, role_id|
%li %li
= link_to role, "javascript:void(0)", = link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id), class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member) } data: { id: role_id, el_id: dom_id(member) }
= render 'shared/members/ee/revert_ldap_group_sync_option', group: @group, member: member, can_override: can_override_member = render 'shared/members/ee/revert_ldap_group_sync_option',
group: @group,
member: member,
can_override: member.can_override?
.prepend-left-5.clearable-input.member-form-control .prepend-left-5.clearable-input.member-form-control
= f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) } = f.text_field :expires_at,
disabled: member.can_override?,
class: 'form-control js-access-expiration-date js-member-update-control',
placeholder: 'Expiration date',
id: "member_expires_at_#{member.id}",
data: { el_id: dom_id(member) }
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
- else - else
%span.member-access-text= member.human_access %span.member-access-text= member.human_access
- if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) - if member.can_approve?
= link_to 'Resend invite', polymorphic_path([:resend_invite, member]),
method: :post,
class: 'btn btn-default prepend-left-10 visible-xs-block'
- elsif member.request? && can_admin_member
= link_to polymorphic_path([:approve_access_request, member]), = link_to polymorphic_path([:approve_access_request, member]),
method: :post, method: :post,
class: 'btn btn-success prepend-left-10', class: 'btn btn-success prepend-left-10',
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
- unless force_mobile_view - unless force_mobile_view
= icon('check inverse', class: 'hidden-xs') = icon('check inverse', class: 'hidden-xs')
- if can?(current_user, action_member_permission(:destroy, member), member) - if member.can_remove?
- if current_user == user - if current_user == user
= link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]),
method: :delete, method: :delete,
...@@ -117,8 +117,8 @@ ...@@ -117,8 +117,8 @@
Delete Delete
- unless force_mobile_view - unless force_mobile_view
= icon('trash', class: 'hidden-xs') = icon('trash', class: 'hidden-xs')
= render 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :edit, can_override: can_override_member = render 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :edit, can_override: member.can_override?
- else - else
%span.member-access-text= member.human_access %span.member-access-text= member.human_access
= render 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :confirm, can_override: can_override_member = render 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :confirm, can_override: member.can_override?
- membership_source = local_assigns.fetch(:membership_source)
- requesters = local_assigns.fetch(:requesters)
- force_mobile_view = local_assigns.fetch(:force_mobile_view, false) - force_mobile_view = local_assigns.fetch(:force_mobile_view, false)
- if requesters.any? - return if requesters.empty?
.panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) }
.panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) }
.panel-heading .panel-heading
Users requesting access to Users requesting access to
%strong= membership_source.name %strong= membership_source.name
......
...@@ -124,6 +124,8 @@ module Geo ...@@ -124,6 +124,8 @@ module Geo
def schedule_jobs def schedule_jobs
capacity = max_capacity capacity = max_capacity
num_to_schedule = [capacity - scheduled_job_ids.size, pending_resources.size].min num_to_schedule = [capacity - scheduled_job_ids.size, pending_resources.size].min
num_to_schedule = 0 if num_to_schedule < 0
to_schedule = pending_resources.shift(num_to_schedule) to_schedule = pending_resources.shift(num_to_schedule)
scheduled = to_schedule.map do |args| scheduled = to_schedule.map do |args|
......
---
title: Issue count now refreshes quicker on geo secondary
merge_request: 3639
author:
type: fixed
---
title: Fix successful rebase throwing flash error message
merge_request: 3727
author:
type: fixed
---
title: Fix an exception in Geo scheduler workers
merge_request: 3740
author:
type: fixed
---
title: Refactor member view using a Presenter
merge_request: 9645
author: TM Lee
...@@ -9,6 +9,7 @@ providers. ...@@ -9,6 +9,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, - [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server and 389 Server
- **(EES/EEP)** [LDAP for GitLab EE](ldap-ee.md): LDAP additions to GitLab Enterprise Editions
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
......
...@@ -207,11 +207,7 @@ Make sure you install the necessary dependencies from step 1, ...@@ -207,11 +207,7 @@ Make sure you install the necessary dependencies from step 1,
add GitLab package repository from step 2. add GitLab package repository from step 2.
When installing the GitLab package, do not supply `EXTERNAL_URL` value. When installing the GitLab package, do not supply `EXTERNAL_URL` value.
### Initial node configuration ### Configuring the Consul nodes
Each node needs to be configured to run only the services it needs.
#### Configuring the Consul nodes
On each Consul node perform the following: On each Consul node perform the following:
...@@ -249,9 +245,28 @@ On each Consul node perform the following: ...@@ -249,9 +245,28 @@ On each Consul node perform the following:
1. [Reconfigure GitLab] for the changes to take effect. 1. [Reconfigure GitLab] for the changes to take effect.
After this is completed on each Consul server node, proceed further. #### Consul Checkpoint
Before moving on, make sure Consul is configured correctly. Run the following
command to verify all server nodes are communicating:
```
/opt/gitlab/embedded/bin/consul members
```
The output should be similar to:
```
Node Address Status Type Build Protocol DC
CONSUL_NODE_ONE XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
CONSUL_NODE_TWO XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
CONSUL_NODE_THREE XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
```
If any of the nodes isn't `alive` or if any of the three nodes are missing,
check the [Troubleshooting section](#troubleshooting) before proceeding.
#### Configuring the Database nodes ### Configuring the Database nodes
On each database node perform the following: On each database node perform the following:
...@@ -321,105 +336,6 @@ On each database node perform the following: ...@@ -321,105 +336,6 @@ On each database node perform the following:
you also need to specify: `postgresql['pgbouncer_user'] = PGBOUNCER_USERNAME` in you also need to specify: `postgresql['pgbouncer_user'] = PGBOUNCER_USERNAME` in
your configuration your configuration
#### Configuring the Pgbouncer node
1. Make sure you collect [`CONSUL_SERVER_NODES`](#consul_information), [`CONSUL_PASSWORD_HASH`](#consul_information), and [`PGBOUNCER_PASSWORD_HASH`](#pgbouncer_information) before executing the next step.
1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
```ruby
# Disable all components except Pgbouncer and Consul agent
bootstrap['enable'] = false
gitaly['enable'] = false
mailroom['enable'] = false
nginx['enable'] = false
redis['enable'] = false
prometheus['enable'] = false
postgresql['enable'] = false
unicorn['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
gitlab_rails['auto_migrate'] = false
pgbouncer['enable'] = true
consul['enable'] = true
# Configure Pgbouncer
pgbouncer['admin_users'] = %w(pgbouncer gitlab-consul)
# Configure Consul agent
consul['watchers'] = %w(postgresql)
# START user configuration
# Please set the real values as explained in Required Information section
# Replace CONSUL_PASSWORD_HASH with with a generated md5 value
# Replace PGBOUNCER_PASSWORD_HASH with with a generated md5 value
pgbouncer['users'] = {
'gitlab-consul': {
password: 'CONSUL_PASSWORD_HASH'
},
'pgbouncer': {
password: 'PGBOUNCER_PASSWORD_HASH'
}
}
# Replace placeholders:
#
# Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
# with the addresses gathered for CONSUL_SERVER_NODES
consul['configuration'] = {
retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z)
}
#
# END user configuration
```
1. [Reconfigure GitLab] for the changes to take effect.
#### Configuring the Application nodes
These will be the nodes running the `gitlab-rails` service. You may have other
attributes set, but the following need to be set.
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
# Disable PostgreSQL on the application node
postgresql['enable'] = false
gitlab_rails['db_host'] = 'PGBOUNCER_NODE'
gitlab_rails['db_port'] = 6432
gitlab_rails['db_password'] = 'POSTGRESQL_USER_PASSWORD'
gitlab_rails['auto_migrate'] = false
```
1. [Reconfigure GitLab] for the changes to take effect.
### Node post-configuration
After reconfigure successfully runs, the following steps must be completed to
get the cluster up and running.
#### Consul nodes post-configuration
Verify the nodes are all communicating:
```sh
/opt/gitlab/embedded/bin/consul members
```
The output should be similar to:
```
Node Address Status Type Build Protocol DC
CONSUL_NODE_ONE XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
CONSUL_NODE_TWO XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
CONSUL_NODE_THREE XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
DATABASE_NODE_ONE XXX.XXX.XXX.YYY:8301 alive client 0.9.2 2 gitlab_consul
DATABASE_NODE_TWO XXX.XXX.XXX.YYY:8301 alive client 0.9.2 2 gitlab_consul
DATABASE_NODE_THREE XXX.XXX.XXX.YYY:8301 alive client 0.9.2 2 gitlab_consul
PGBOUNCER_NODE XXX.XXX.XXX.YYY:8301 alive client 0.9.0 2 gitlab_consul
```
#### Database nodes post-configuration #### Database nodes post-configuration
##### Primary node ##### Primary node
...@@ -453,7 +369,11 @@ Select one node as a primary node. ...@@ -453,7 +369,11 @@ Select one node as a primary node.
----------+----------|----------|---------------------------------------- ----------+----------|----------|----------------------------------------
* master | HOSTNAME | | host=HOSTNAME user=gitlab_repmgr dbname=gitlab_repmgr * master | HOSTNAME | | host=HOSTNAME user=gitlab_repmgr dbname=gitlab_repmgr
``` ```
1. Note down the value in the `Name` column. We will refer to it in the next section as `MASTER_NODE_NAME`.
1. Note down the hostname/ip in the connection string: `host=HOSTNAME`. We will
refer to the hostname in the next section as `MASTER_NODE_NAME`. If the value
is not an IP address, it will need to be a resolvable name (via DNS or
`/etc/hosts`)
##### Secondary nodes ##### Secondary nodes
...@@ -499,22 +419,104 @@ Select one node as a primary node. ...@@ -499,22 +419,104 @@ Select one node as a primary node.
Repeat the above steps on all secondary nodes. Repeat the above steps on all secondary nodes.
#### Pgbouncer node post-configuration #### Database checkpoint
Before moving on, make sure the databases are configured correctly. Run the
following command on the **primary** node to verify that replication is working
properly:
```
gitlab-ctl repmgr cluster show
```
The output should be similar to:
```
Role | Name | Upstream | Connection String
----------+--------------|--------------|--------------------------------------------------------------------
* master | MASTER | | host=MASTER port=5432 user=gitlab_repmgr dbname=gitlab_repmgr
standby | STANDBY | MASTER | host=STANDBY port=5432 user=gitlab_repmgr dbname=gitlab_repmgr
```
If the 'Role' column for any node says "FAILED", check the
[Troubleshooting section](#troubleshooting) before proceeding.
### Configuring the Pgbouncer node
1. Make sure you collect [`CONSUL_SERVER_NODES`](#consul_information), [`CONSUL_PASSWORD_HASH`](#consul_information), and [`PGBOUNCER_PASSWORD_HASH`](#pgbouncer_information) before executing the next step.
1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
```ruby
# Disable all components except Pgbouncer and Consul agent
bootstrap['enable'] = false
gitaly['enable'] = false
mailroom['enable'] = false
nginx['enable'] = false
redis['enable'] = false
prometheus['enable'] = false
postgresql['enable'] = false
unicorn['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
gitlab_rails['auto_migrate'] = false
pgbouncer['enable'] = true
consul['enable'] = true
# Configure Pgbouncer
pgbouncer['admin_users'] = %w(pgbouncer gitlab-consul)
# Configure Consul agent
consul['watchers'] = %w(postgresql)
1. Create a `.pgpass` file user for the `CONSUL_USER` account to be able to # START user configuration
reload pgbouncer. Confirm `PGBOUNCER_PASSWORD` twice when asked: # Please set the real values as explained in Required Information section
# Replace CONSUL_PASSWORD_HASH with with a generated md5 value
# Replace PGBOUNCER_PASSWORD_HASH with with a generated md5 value
pgbouncer['users'] = {
'gitlab-consul': {
password: 'CONSUL_PASSWORD_HASH'
},
'pgbouncer': {
password: 'PGBOUNCER_PASSWORD_HASH'
}
}
# Replace placeholders:
#
# Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
# with the addresses gathered for CONSUL_SERVER_NODES
consul['configuration'] = {
retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z)
}
#
# END user configuration
```
1. [Reconfigure GitLab] for the changes to take effect.
1. Create a `.pgpass` file so Consule is able to
reload pgbouncer. Enter the `PGBOUNCER_PASSWORD` twice when asked:
```sh ```sh
gitlab-ctl write-pgpass --host 127.0.0.1 --database pgbouncer --user pgbouncer --hostuser gitlab-consul gitlab-ctl write-pgpass --host 127.0.0.1 --database pgbouncer --user pgbouncer --hostuser gitlab-consul
``` ```
#### PGBouncer Checkpoint
1. Ensure the node is talking to the current master: 1. Ensure the node is talking to the current master:
```sh ```sh
gitlab-ctl pgb-console # You will be prompted for PGBOUNCER_PASSWORD gitlab-ctl pgb-console # You will be prompted for PGBOUNCER_PASSWORD
``` ```
Then run: If there is an error `psql: ERROR: Auth failed` after typing in the
password, ensure you previously generated the MD5 password hashes with the correct
format. The correct format is to concatenate the password and the username:
`PASSWORDUSERNAME`. For example, `Sup3rS3cr3tpgbouncer` would be the text
needed to generate an MD5 password hash for the `pgbouncer` user.
1. Once the console prompt is available, run the following queries:
```sh ```sh
show databases ; show clients ; show databases ; show clients ;
...@@ -531,11 +533,29 @@ Repeat the above steps on all secondary nodes. ...@@ -531,11 +533,29 @@ Repeat the above steps on all secondary nodes.
type | user | database | state | addr | port | local_addr | local_port | connect_time | request_time | ptr | link | remote_pid | tls type | user | database | state | addr | port | local_addr | local_port | connect_time | request_time | ptr | link | remote_pid | tls
------+-----------+---------------------+---------+----------------+-------+------------+------------+---------------------+---------------------+-----------+------+------------+----- ------+-----------+---------------------+---------+----------------+-------+------------+------------+---------------------+---------------------+-----------+------+------------+-----
C | (nouser) | gitlabhq_production | waiting | IP_OF_APP_NODE | 56512 | 127.0.0.1 | 6432 | 2017-08-21 18:08:51 | 2017-08-21 18:08:51 | 0x22b3700 | | 0 |
C | pgbouncer | pgbouncer | active | 127.0.0.1 | 56846 | 127.0.0.1 | 6432 | 2017-08-21 18:09:59 | 2017-08-21 18:10:48 | 0x22b3880 | | 0 | C | pgbouncer | pgbouncer | active | 127.0.0.1 | 56846 | 127.0.0.1 | 6432 | 2017-08-21 18:09:59 | 2017-08-21 18:10:48 | 0x22b3880 | | 0 |
(2 rows) (2 rows)
``` ```
### Configuring the Application nodes
These will be the nodes running the `gitlab-rails` service. You may have other
attributes set, but the following need to be set.
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
# Disable PostgreSQL on the application node
postgresql['enable'] = false
gitlab_rails['db_host'] = 'PGBOUNCER_NODE'
gitlab_rails['db_port'] = 6432
gitlab_rails['db_password'] = 'POSTGRESQL_USER_PASSWORD'
gitlab_rails['auto_migrate'] = false
```
1. [Reconfigure GitLab] for the changes to take effect.
#### Application node post-configuration #### Application node post-configuration
Ensure that all migrations ran: Ensure that all migrations ran:
...@@ -547,7 +567,8 @@ gitlab-rake gitlab:db:configure ...@@ -547,7 +567,8 @@ gitlab-rake gitlab:db:configure
#### Ensure GitLab is running #### Ensure GitLab is running
At this point, your GitLab instance should be up and running. Verify you are At this point, your GitLab instance should be up and running. Verify you are
able to login, and create issues and merge requests. If you have troubles check the [Troubleshooting section](#troubleshooting). able to login, and create issues and merge requests. If you have troubles check
the [Troubleshooting section](#troubleshooting).
### Example configuration ### Example configuration
......
...@@ -32,7 +32,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. ...@@ -32,7 +32,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc. - [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc.
- [Polling](polling.md): Configure how often the GitLab UI polls for updates. - [Polling](polling.md): Configure how often the GitLab UI polls for updates.
- [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages. - [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages.
- [GitLab Pages configuration for installations from the source](pages/source.md): Enable and configure GitLab Pages on - [GitLab Pages configuration for GitLab source installations](pages/source.md): Enable and configure GitLab Pages on
[source installations](../install/installation.md#installation-from-source). [source installations](../install/installation.md#installation-from-source).
- [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab. - [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
- **(EES/EEP)** [Elasticsearch](../integration/elasticsearch.md): Enable Elasticsearch to empower GitLab's Advanced Global Search. Useful when you deal with a huge amount of data. - **(EES/EEP)** [Elasticsearch](../integration/elasticsearch.md): Enable Elasticsearch to empower GitLab's Advanced Global Search. Useful when you deal with a huge amount of data.
...@@ -85,13 +85,13 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email. ...@@ -85,13 +85,13 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email.
- [Issue closing pattern](issue_closing_pattern.md): Customize how to close an issue from commit messages. - [Issue closing pattern](issue_closing_pattern.md): Customize how to close an issue from commit messages.
- [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service. - [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service.
- [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project. - [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project.
- **(EES/EEP)** [Limit project size](../user/admin_area/settings/account_and_limit_settings.md): Set a hard limit for your repositories' size.
### Repository settings ### Repository settings
- [Repository checks](repository_checks.md): Periodic Git repository checks. - [Repository checks](repository_checks.md): Periodic Git repository checks.
- [Repository storage paths](repository_storage_paths.md): Manage the paths used to store repositories. - [Repository storage paths](repository_storage_paths.md): Manage the paths used to store repositories.
- [Repository storage rake tasks](raketasks/storage.md): A collection of rake tasks to list and migrate existing projects and attachments associated with it from Legacy storage to Hashed storage. - [Repository storage rake tasks](raketasks/storage.md): A collection of rake tasks to list and migrate existing projects and attachments associated with it from Legacy storage to Hashed storage.
- **(EES/EEP)** [Limit repository size](../user/admin_area/settings/account_and_limit_settings.md): Set a hard limit for your repositories' size.
## Continuous Integration settings ## Continuous Integration settings
......
# Speed up SSH operations # Speed up SSH operations
>
- [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/250) in GitLab Enterprise Edition 8.7.
- Available in GitLab Enterprise Edition Starter.
## The problem ## The problem
SSH operations become slow as the number of users grows. SSH operations become slow as the number of users grows.
......
...@@ -29,10 +29,4 @@ ...@@ -29,10 +29,4 @@
item! item!
``` ```
1. Include the mixin in CSS
```SCSS
@include new-style-dropdown('.my-dropdown ');
```
[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns [bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
# Push Rules # Push Rules
> Available in [GitLab Enterprise Edition Starter][ee]. > Available in [GitLab Enterprise Editions][ee].
Gain additional control over pushes to your repository. Gain additional control over pushes to your repository.
...@@ -61,16 +61,16 @@ The following options are available. ...@@ -61,16 +61,16 @@ The following options are available.
| Push rule | GitLab version | Description | | Push rule | GitLab version | Description |
| --------- | :------------: | ----------- | | --------- | :------------: | ----------- |
| Removal of tags with `git push` | 7.10 | Forbid users to remove git tags with `git push`. Tags will still be able to be deleted through the web UI. | | Removal of tags with `git push` | **EES** 7.10 | Forbid users to remove git tags with `git push`. Tags will still be able to be deleted through the web UI. |
| Check whether author is a GitLab user | 7.10 | Restrict commits by author (email) to existing GitLab users. | | Check whether author is a GitLab user | **EES** 7.10 | Restrict commits by author (email) to existing GitLab users. |
| Check whether committer is the current authenticated user | 10.2 | GitLab will reject any commit that was not committed by the current authenticated user | | Check whether committer is the current authenticated user | **EEP** 10.2 | GitLab will reject any commit that was not committed by the current authenticated user |
| Check whether commit is signed through GPG | 10.1 | Reject commit when it is not signed through GPG. Read [signing commits with GPG][signing-commits]. | | Check whether commit is signed through GPG | **EEP** 10.1 | Reject commit when it is not signed through GPG. Read [signing commits with GPG][signing-commits]. |
| Prevent committing secrets to Git | 8.12 | GitLab will reject any files that are likely to contain secrets. Read [what files are forbidden](#prevent-pushing-secrets-to-the-repository). | | Prevent committing secrets to Git | **EES** 8.12 | GitLab will reject any files that are likely to contain secrets. Read [what files are forbidden](#prevent-pushing-secrets-to-the-repository). |
| Restrict by commit message | 7.10 | Only commit messages that match this Ruby regular expression are allowed to be pushed. Leave empty to allow any commit message. | | Restrict by commit message | **EES** 7.10 | Only commit messages that match this Ruby regular expression are allowed to be pushed. Leave empty to allow any commit message. |
| Restrict by branch name | 9.3 | Only branch names that match this Ruby regular expression are allowed to be pushed. Leave empty to allow any branch name. | | Restrict by branch name | **EES** 9.3 | Only branch names that match this Ruby regular expression are allowed to be pushed. Leave empty to allow any branch name. |
| Restrict by commit author's email | 7.10 | Only commit author's email that match this Ruby regular expression are allowed to be pushed. Leave empty to allow any email. | | Restrict by commit author's email | **EES** 7.10 | Only commit author's email that match this Ruby regular expression are allowed to be pushed. Leave empty to allow any email. |
| Prohibited file names | 7.10 | Any committed filenames that match this Ruby regular expression are not allowed to be pushed. Leave empty to allow any filenames. | | Prohibited file names | **EES** 7.10 | Any committed filenames that match this Ruby regular expression are not allowed to be pushed. Leave empty to allow any filenames. |
| Maximum file size | 7.12 | Pushes that contain added or updated files that exceed this file size (in MB) are rejected. Set to 0 to allow files of any size. | | Maximum file size | **EES** 7.12 | Pushes that contain added or updated files that exceed this file size (in MB) are rejected. Set to 0 to allow files of any size. |
>**Tip:** >**Tip:**
You can check your regular expressions at <http://rubular.com>. You can check your regular expressions at <http://rubular.com>.
......
...@@ -20,20 +20,14 @@ License admin area. ...@@ -20,20 +20,14 @@ License admin area.
Otherwise, you can: Otherwise, you can:
1. Navigate manually to the **Admin Area** by clicking the wrench icon in the 1. Navigate manually to the **Admin Area** by clicking the wrench icon in the menu bar.
upper right corner.
![Admin area icon](img/admin_wrench.png) ![Admin area icon](img/admin_wrench.png)
1. And then going to the **License** tab. 1. And then going to the **License** tab and click on **Upload New License**.
![License admin area](img/license_admin_area.png) ![License admin area](img/license_admin_area.png)
>**Note:**
If you don't see the banner mentioned above, that means that either you are not
logged in as admin or a license is already uploaded.
--- ---
If you've received a `.gitlab-license` file, you should have already downloaded If you've received a `.gitlab-license` file, you should have already downloaded
......
...@@ -50,7 +50,15 @@ ...@@ -50,7 +50,15 @@
this.service.rebase() this.service.rebase()
.then(() => { .then(() => {
simplePoll((continuePolling, stopPolling) => { simplePoll(this.checkRebaseStatus);
})
.catch((error) => {
this.rebasingError = error.merge_error;
this.isMakingRequest = false;
Flash('Something went wrong. Please try again.');
});
},
checkRebaseStatus(continuePolling, stopPolling) {
this.service.poll() this.service.poll()
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then((res) => {
...@@ -59,7 +67,7 @@ ...@@ -59,7 +67,7 @@
} else { } else {
this.isMakingRequest = false; this.isMakingRequest = false;
if (res.merge_error.length) { if (res.merge_error && res.merge_error.length) {
this.rebasingError = res.merge_error; this.rebasingError = res.merge_error;
Flash('Something went wrong. Please try again.'); Flash('Something went wrong. Please try again.');
} }
...@@ -73,13 +81,6 @@ ...@@ -73,13 +81,6 @@
Flash('Something went wrong. Please try again.'); Flash('Something went wrong. Please try again.');
stopPolling(); stopPolling();
}); });
});
})
.catch((error) => {
this.rebasingError = error.merge_error;
this.isMakingRequest = false;
Flash('Something went wrong. Please try again.');
});
}, },
}, },
}; };
......
.new-epic-dropdown {
.dropdown-menu {
padding-left: $gl-padding-top;
padding-right: $gl-padding-top;
}
.form-control {
width: 100%;
}
.btn-save {
display: flex;
margin-top: $gl-btn-padding;
}
}
.empty-state .new-epic-dropdown {
display: inline-flex;
.btn-save {
margin-left: 0;
margin-bottom: 0;
}
.btn-new {
margin: 0;
}
}
.service-desk-issues { .service-desk-issues {
.empty-state {
max-width: 450px;
text-align: center;
}
.non-empty-state { .non-empty-state {
text-align: left; text-align: left;
padding-bottom: $gl-padding-top; padding-bottom: $gl-padding-top;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
.service-desk-graphic {
margin-top: $gl-padding;
}
.media-body {
margin-top: $gl-padding-top;
margin-left: $gl-padding;
}
}
.turn-on-btn-container {
margin-top: $gl-padding-top;
} }
} }
module EE
module GroupMemberPresenter
private
def override_member_permission
:override_group_member
end
end
end
module EE
module MemberPresenter
def can_update?
super || can_override?
end
def can_override?
can?(current_user, override_member_permission, member)
end
private
def override_member_permission
raise NotImplementedError
end
end
end
module EE
module ProjectMemberPresenter
private
def override_member_permission
:override_project_member
end
end
end
module EE
module BaseCountService
# geo secondary cache should expire quicker than primary, otherwise various counts
# could be incorrect for 2 weeks.
def cache_options
raise NotImplementedError.new unless defined?(super)
value = super
value[:expires_in] = 20.minutes if ::Gitlab::Geo.secondary?
value
end
end
end
...@@ -4,22 +4,26 @@ ...@@ -4,22 +4,26 @@
- callout_selector = is_empty_state ? 'empty-state' : 'non-empty-state media' - callout_selector = is_empty_state ? 'empty-state' : 'non-empty-state media'
- svg_path = !is_empty_state ? 'shared/empty_states/icons/service_desk_callout.svg' : 'shared/empty_states/icons/service_desk_empty_state.svg' - svg_path = !is_empty_state ? 'shared/empty_states/icons/service_desk_callout.svg' : 'shared/empty_states/icons/service_desk_empty_state.svg'
- can_edit_project_settings = can?(current_user, :admin_project, @project) - can_edit_project_settings = can?(current_user, :admin_project, @project)
- title_text = _("Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab")
%div{ class: "#{callout_selector}" } %div{ class: "#{callout_selector}" }
.service-desk-graphic .svg-content
= render svg_path = render svg_path
.media-body %div{ class: is_empty_state ? "text-content" : "prepend-top-10 prepend-left-default" }
%h5 Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab. - if is_empty_state
%h4= title_text
- else
%h5= title_text
- if service_desk_enabled - if service_desk_enabled
%p %p
Have your users email = _("Have your users email")
%code= @project.service_desk_address %code= @project.service_desk_address
%span Those emails automatically become issues (with the comments becoming the email conversation) listed here. %span= _("Those emails automatically become issues (with the comments becoming the email conversation) listed here.")
= link_to 'Read more', help_page_path('user/project/service_desk') = link_to _('Read more'), help_page_path('user/project/service_desk')
- if can_edit_project_settings && !service_desk_enabled - if can_edit_project_settings && !service_desk_enabled
.turn-on-btn-container %div{ class: is_empty_state ? "text-center" : "prepend-top-10" }
= link_to "Turn on Service Desk", edit_project_path(@project), class: 'btn btn-new btn-inverted' = link_to _("Turn on Service Desk"), edit_project_path(@project), class: 'btn btn-success'
...@@ -16,7 +16,7 @@ module Gitlab ...@@ -16,7 +16,7 @@ module Gitlab
attr_reader :subject, :attributes attr_reader :subject, :attributes
def presenter_class def presenter_class
"#{subject.class.name}Presenter".constantize attributes.delete(:presenter_class) { "#{subject.class.name}Presenter".constantize }
end end
end end
end end
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-12-05 20:31+0100\n" "POT-Creation-Date: 2017-12-12 18:31+0000\n"
"PO-Revision-Date: 2017-12-05 20:31+0100\n" "PO-Revision-Date: 2017-12-12 18:31+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -105,12 +105,18 @@ msgstr "" ...@@ -105,12 +105,18 @@ msgstr ""
msgid "Activity" msgid "Activity"
msgstr "" msgstr ""
msgid "Add"
msgstr ""
msgid "Add Changelog" msgid "Add Changelog"
msgstr "" msgstr ""
msgid "Add Contribution guide" msgid "Add Contribution guide"
msgstr "" msgstr ""
msgid "Add Group Webhooks and GitLab Enterprise Edition."
msgstr ""
msgid "Add License" msgid "Add License"
msgstr "" msgstr ""
...@@ -129,6 +135,9 @@ msgstr "" ...@@ -129,6 +135,9 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription" msgid "An error occurred when toggling the notification subscription"
msgstr "" msgstr ""
msgid "An error occurred when updating the issue weight"
msgstr ""
msgid "An error occurred while fetching sidebar data" msgid "An error occurred while fetching sidebar data"
msgstr "" msgstr ""
...@@ -216,6 +225,60 @@ msgstr "" ...@@ -216,6 +225,60 @@ msgstr ""
msgid "Available" msgid "Available"
msgstr "" msgstr ""
msgid "Billing"
msgstr ""
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
msgstr ""
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
msgstr ""
msgid "BillingPlans|Current plan"
msgstr ""
msgid "BillingPlans|Customer Support"
msgstr ""
msgid "BillingPlans|Downgrade"
msgstr ""
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
msgstr ""
msgid "BillingPlans|Manage plan"
msgstr ""
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
msgstr ""
msgid "BillingPlans|See all %{plan_name} features"
msgstr ""
msgid "BillingPlans|This group uses the plan associated with its parent group."
msgstr ""
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr ""
msgid "BillingPlans|Upgrade"
msgstr ""
msgid "BillingPlans|You are currently on the %{plan_link} plan."
msgstr ""
msgid "BillingPlans|frequently asked questions"
msgstr ""
msgid "BillingPlans|monthly"
msgstr ""
msgid "BillingPlans|paid annually at %{price_per_year}"
msgstr ""
msgid "BillingPlans|per user"
msgstr ""
msgid "Branch" msgid "Branch"
msgid_plural "Branches" msgid_plural "Branches"
msgstr[0] "" msgstr[0] ""
...@@ -293,6 +356,9 @@ msgstr "" ...@@ -293,6 +356,9 @@ msgstr ""
msgid "Branches|Sort by" msgid "Branches|Sort by"
msgstr "" msgstr ""
msgid "Branches|The branch could not be updated automatically because it has diverged from its upstream counterpart."
msgstr ""
msgid "Branches|The default branch cannot be deleted" msgid "Branches|The default branch cannot be deleted"
msgstr "" msgstr ""
...@@ -305,9 +371,15 @@ msgstr "" ...@@ -305,9 +371,15 @@ msgstr ""
msgid "Branches|To confirm, type %{branch_name_confirmation}:" msgid "Branches|To confirm, type %{branch_name_confirmation}:"
msgstr "" msgstr ""
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
msgstr ""
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}." msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "" msgstr ""
msgid "Branches|diverged from upstream"
msgstr ""
msgid "Branches|merged" msgid "Branches|merged"
msgstr "" msgstr ""
...@@ -347,6 +419,9 @@ msgstr "" ...@@ -347,6 +419,9 @@ msgstr ""
msgid "Cancel edit" msgid "Cancel edit"
msgstr "" msgstr ""
msgid "Change Weight"
msgstr ""
msgid "ChangeTypeActionLabel|Pick into branch" msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "" msgstr ""
...@@ -377,13 +452,10 @@ msgstr "" ...@@ -377,13 +452,10 @@ msgstr ""
msgid "Cherry-pick this commit" msgid "Cherry-pick this commit"
msgstr "" msgstr ""
msgid "Checking branch availability..." msgid "Cherry-pick this merge request"
msgstr ""
msgid "Cherry-pick this commit"
msgstr "" msgstr ""
msgid "Cherry-pick this merge request" msgid "Choose which groups you wish to replicate to this secondary node. Leave blank to replicate all."
msgstr "" msgstr ""
msgid "CiStatusLabel|canceled" msgid "CiStatusLabel|canceled"
...@@ -446,10 +518,10 @@ msgstr "" ...@@ -446,10 +518,10 @@ msgstr ""
msgid "Clone repository" msgid "Clone repository"
msgstr "" msgstr ""
msgid "Cluster" msgid "Close"
msgstr "" msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your cluster" msgid "Cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your cluster" msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
...@@ -823,13 +895,13 @@ msgstr "" ...@@ -823,13 +895,13 @@ msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready." msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr "" msgstr ""
msgid "ContributorsPage|Building repository graph." msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr "" msgstr ""
msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits." msgid "Control the maximum concurrency of repository backfill for this secondary node"
msgstr "" msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready." msgid "Copy SSH public key to clipboard"
msgstr "" msgstr ""
msgid "Copy URL to clipboard" msgid "Copy URL to clipboard"
...@@ -850,6 +922,9 @@ msgstr "" ...@@ -850,6 +922,9 @@ msgstr ""
msgid "Create empty bare repository" msgid "Create empty bare repository"
msgstr "" msgstr ""
msgid "Create epic"
msgstr ""
msgid "Create file" msgid "Create file"
msgstr "" msgstr ""
...@@ -877,6 +952,9 @@ msgstr "" ...@@ -877,6 +952,9 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token" msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "" msgstr ""
msgid "Creating epic"
msgstr ""
msgid "Cron Timezone" msgid "Cron Timezone"
msgstr "" msgstr ""
...@@ -945,6 +1023,9 @@ msgstr "" ...@@ -945,6 +1023,9 @@ msgstr ""
msgid "Description" msgid "Description"
msgstr "" msgstr ""
msgid "Description templates allow you to define context-specific templates for issue and merge request description fields for your project."
msgstr ""
msgid "Details" msgid "Details"
msgstr "" msgstr ""
...@@ -957,6 +1038,9 @@ msgstr "" ...@@ -957,6 +1038,9 @@ msgstr ""
msgid "Dismiss Cycle Analytics introduction box" msgid "Dismiss Cycle Analytics introduction box"
msgstr "" msgstr ""
msgid "Dismiss Merge Request promotion"
msgstr ""
msgid "Don't show again" msgid "Don't show again"
msgstr "" msgstr ""
...@@ -1047,6 +1131,18 @@ msgstr "" ...@@ -1047,6 +1131,18 @@ msgstr ""
msgid "Environments|You don't have any environments right now." msgid "Environments|You don't have any environments right now."
msgstr "" msgstr ""
msgid "Epic will be removed! Are you sure?"
msgstr ""
msgid "Epics"
msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
msgid "Error creating epic"
msgstr ""
msgid "Error occurred when toggling the notification subscription" msgid "Error occurred when toggling the notification subscription"
msgstr "" msgstr ""
...@@ -1139,6 +1235,36 @@ msgstr "" ...@@ -1139,6 +1235,36 @@ msgstr ""
msgid "GPG Keys" msgid "GPG Keys"
msgstr "" msgstr ""
msgid "Geo Nodes"
msgstr ""
msgid "GeoNodeSyncStatus|Failed"
msgstr ""
msgid "GeoNodeSyncStatus|Node is failing or broken."
msgstr ""
msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
msgstr ""
msgid "GeoNodeSyncStatus|Out of sync"
msgstr ""
msgid "GeoNodeSyncStatus|Synced"
msgstr ""
msgid "Geo|File sync capacity"
msgstr ""
msgid "Geo|Groups to replicate"
msgstr ""
msgid "Geo|Repository sync capacity"
msgstr ""
msgid "Geo|Select groups to replicate."
msgstr ""
msgid "Git storage health information has been reset" msgid "Git storage health information has been reset"
msgstr "" msgstr ""
...@@ -1190,9 +1316,6 @@ msgstr "" ...@@ -1190,9 +1316,6 @@ msgstr ""
msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group." msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
msgstr "" msgstr ""
msgid "GroupsTreeRole|as"
msgstr ""
msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?" msgid "GroupsTree|Are you sure you want to leave the \"${this.group.fullName}\" group?"
msgstr "" msgstr ""
...@@ -1223,6 +1346,9 @@ msgstr "" ...@@ -1223,6 +1346,9 @@ msgstr ""
msgid "GroupsTree|Sorry, no groups or projects matched your search" msgid "GroupsTree|Sorry, no groups or projects matched your search"
msgstr "" msgstr ""
msgid "Have your users email"
msgstr ""
msgid "Health Check" msgid "Health Check"
msgstr "" msgstr ""
...@@ -1250,46 +1376,48 @@ msgstr "" ...@@ -1250,46 +1376,48 @@ msgstr ""
msgid "Import repository" msgid "Import repository"
msgstr "" msgstr ""
msgid "Install a Runner compatible with GitLab CI" msgid "Improve Issue boards with GitLab Enterprise Edition."
msgstr "" msgstr ""
msgid "Internal - The group and any internal projects can be viewed by any logged in user." msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
msgstr "" msgstr ""
msgid "Internal - The project can be accessed by any logged in user." msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
msgstr "" msgstr ""
msgid "Interval Pattern" msgid "Install a Runner compatible with GitLab CI"
msgstr "" msgstr ""
msgid "Introducing Cycle Analytics" msgid "Instance"
msgstr "" msgid_plural "Instances"
msgstr[0] ""
msgstr[1] ""
msgid "Issue events" msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr "" msgstr ""
msgid "IssueBoards|Board" msgid "Internal - The project can be accessed by any logged in user."
msgstr "" msgstr ""
msgid "Issues" msgid "Interval Pattern"
msgstr "" msgstr ""
msgid "Jan" msgid "Introducing Cycle Analytics"
msgstr "" msgstr ""
msgid "January" msgid "Issue board focus mode"
msgstr "" msgstr ""
msgid "Jul" msgid "Issue events"
msgstr "" msgstr ""
msgid "July" msgid "IssueBoards|Board"
msgstr "" msgstr ""
msgid "Jun" msgid "IssueBoards|Boards"
msgstr "" msgstr ""
msgid "June" msgid "Issues"
msgstr "" msgstr ""
msgid "Jan" msgid "Jan"
...@@ -1363,6 +1491,9 @@ msgstr "" ...@@ -1363,6 +1491,9 @@ msgstr ""
msgid "Leave project" msgid "Leave project"
msgstr "" msgstr ""
msgid "License"
msgstr ""
msgid "Limited to showing %d event at most" msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most" msgid_plural "Limited to showing %d events at most"
msgstr[0] "" msgstr[0] ""
...@@ -1374,13 +1505,10 @@ msgstr "" ...@@ -1374,13 +1505,10 @@ msgstr ""
msgid "Locked" msgid "Locked"
msgstr "" msgstr ""
msgid "Login" msgid "Locked Files"
msgstr ""
msgid "Mar"
msgstr "" msgstr ""
msgid "March" msgid "Login"
msgstr "" msgstr ""
msgid "Mar" msgid "Mar"
...@@ -1422,6 +1550,9 @@ msgstr "" ...@@ -1422,6 +1550,9 @@ msgstr ""
msgid "More information is available|here" msgid "More information is available|here"
msgstr "" msgstr ""
msgid "Multiple issue boards"
msgstr ""
msgid "New Cluster" msgid "New Cluster"
msgstr "" msgstr ""
...@@ -1442,6 +1573,9 @@ msgstr "" ...@@ -1442,6 +1573,9 @@ msgstr ""
msgid "New directory" msgid "New directory"
msgstr "" msgstr ""
msgid "New epic"
msgstr ""
msgid "New file" msgid "New file"
msgstr "" msgstr ""
...@@ -1571,6 +1705,9 @@ msgstr "" ...@@ -1571,6 +1705,9 @@ msgstr ""
msgid "Only project members can comment." msgid "Only project members can comment."
msgstr "" msgstr ""
msgid "Opened"
msgstr ""
msgid "OpenedNDaysAgo|Opened" msgid "OpenedNDaysAgo|Opened"
msgstr "" msgstr ""
...@@ -1616,6 +1753,9 @@ msgstr "" ...@@ -1616,6 +1753,9 @@ msgstr ""
msgid "Pipeline Schedules" msgid "Pipeline Schedules"
msgstr "" msgstr ""
msgid "Pipeline quota"
msgstr ""
msgid "PipelineCharts|Failed:" msgid "PipelineCharts|Failed:"
msgstr "" msgstr ""
...@@ -1700,6 +1840,9 @@ msgstr "" ...@@ -1700,6 +1840,9 @@ msgstr ""
msgid "Pipeline|with stages" msgid "Pipeline|with stages"
msgstr "" msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
msgid "Preferences" msgid "Preferences"
msgstr "" msgstr ""
...@@ -1802,16 +1945,28 @@ msgstr "" ...@@ -1802,16 +1945,28 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph" msgid "ProjectNetworkGraph|Graph"
msgstr "" msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
msgid "ProjectSettings|Immediately run a pipeline on the default branch" msgid "ProjectSettings|Immediately run a pipeline on the default branch"
msgstr "" msgstr ""
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript" msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
msgstr "" msgstr ""
msgid "ProjectSettings|Immediately run a pipeline on the default branch" msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
msgstr "" msgstr ""
msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript" msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr "" msgstr ""
msgid "Projects" msgid "Projects"
...@@ -1874,43 +2029,16 @@ msgstr "" ...@@ -1874,43 +2029,16 @@ msgstr ""
msgid "Public - The group and any public projects can be viewed without any authentication." msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "" msgstr ""
msgid "PrometheusService|Finding and configuring metrics..." msgid "Public - The project can be accessed without any authentication."
msgstr ""
msgid "PrometheusService|Metrics"
msgstr ""
msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
msgstr ""
msgid "PrometheusService|Missing environment variable"
msgstr ""
msgid "PrometheusService|Monitored"
msgstr ""
msgid "PrometheusService|More information"
msgstr ""
msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
msgstr ""
msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
msgstr ""
msgid "PrometheusService|Prometheus monitoring"
msgstr ""
msgid "PrometheusService|View environments"
msgstr "" msgstr ""
msgid "Public - The group and any public projects can be viewed without any authentication." msgid "Push Rules"
msgstr "" msgstr ""
msgid "Public - The project can be accessed without any authentication." msgid "Push events"
msgstr "" msgstr ""
msgid "Push events" msgid "PushRule|Committer restriction"
msgstr "" msgstr ""
msgid "Read more" msgid "Read more"
...@@ -1925,6 +2053,9 @@ msgstr "" ...@@ -1925,6 +2053,9 @@ msgstr ""
msgid "RefSwitcher|Tags" msgid "RefSwitcher|Tags"
msgstr "" msgstr ""
msgid "Registry"
msgstr ""
msgid "Related Commits" msgid "Related Commits"
msgstr "" msgstr ""
...@@ -1991,6 +2122,9 @@ msgstr "" ...@@ -1991,6 +2122,9 @@ msgstr ""
msgid "Scheduling Pipelines" msgid "Scheduling Pipelines"
msgstr "" msgstr ""
msgid "Scoped issue boards"
msgstr ""
msgid "Search branches and tags" msgid "Search branches and tags"
msgstr "" msgstr ""
...@@ -2050,6 +2184,21 @@ msgid_plural "Showing %d events" ...@@ -2050,6 +2184,21 @@ msgid_plural "Showing %d events"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Sidebar|Change weight"
msgstr ""
msgid "Sidebar|Edit"
msgstr ""
msgid "Sidebar|No"
msgstr ""
msgid "Sidebar|None"
msgstr ""
msgid "Sidebar|Weight"
msgstr ""
msgid "Snippets" msgid "Snippets"
msgstr "" msgstr ""
...@@ -2107,6 +2256,9 @@ msgstr "" ...@@ -2107,6 +2256,9 @@ msgstr ""
msgid "SortOptions|Least popular" msgid "SortOptions|Least popular"
msgstr "" msgstr ""
msgid "SortOptions|Less weight"
msgstr ""
msgid "SortOptions|Milestone" msgid "SortOptions|Milestone"
msgstr "" msgstr ""
...@@ -2116,6 +2268,9 @@ msgstr "" ...@@ -2116,6 +2268,9 @@ msgstr ""
msgid "SortOptions|Milestone due soon" msgid "SortOptions|Milestone due soon"
msgstr "" msgstr ""
msgid "SortOptions|More weight"
msgstr ""
msgid "SortOptions|Most popular" msgid "SortOptions|Most popular"
msgstr "" msgstr ""
...@@ -2155,6 +2310,9 @@ msgstr "" ...@@ -2155,6 +2310,9 @@ msgstr ""
msgid "SortOptions|Start soon" msgid "SortOptions|Start soon"
msgstr "" msgstr ""
msgid "SortOptions|Weight"
msgstr ""
msgid "Source" msgid "Source"
msgstr "" msgstr ""
...@@ -2277,76 +2435,13 @@ msgstr "" ...@@ -2277,76 +2435,13 @@ msgstr ""
msgid "Target Branch" msgid "Target Branch"
msgstr "" msgstr ""
msgid "TagsPage|Browse files" msgid "Team"
msgstr ""
msgid "TagsPage|Can't find HEAD commit for this tag"
msgstr ""
msgid "TagsPage|Cancel"
msgstr ""
msgid "TagsPage|Create tag"
msgstr ""
msgid "TagsPage|Delete tag"
msgstr ""
msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
msgstr ""
msgid "TagsPage|Edit release notes"
msgstr ""
msgid "TagsPage|Existing branch name, tag, or commit SHA"
msgstr ""
msgid "TagsPage|Filter by tag name"
msgstr ""
msgid "TagsPage|New Tag"
msgstr ""
msgid "TagsPage|New tag"
msgstr ""
msgid "TagsPage|Optionally, add a message to the tag."
msgstr ""
msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
msgstr ""
msgid "TagsPage|Release notes"
msgstr ""
msgid "TagsPage|Repository has no tags yet."
msgstr ""
msgid "TagsPage|Sort by"
msgstr ""
msgid "TagsPage|Tags"
msgstr ""
msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
msgstr ""
msgid "TagsPage|This tag has no release notes."
msgstr ""
msgid "TagsPage|Use git tag command to add a new one:"
msgstr ""
msgid "TagsPage|Write your release notes or drag files here..."
msgstr ""
msgid "TagsPage|protected"
msgstr "" msgstr ""
msgid "Target Branch" msgid "Thanks! Don't show me this again"
msgstr "" msgstr ""
msgid "Team" 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 "" msgstr ""
msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold" msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
...@@ -2418,6 +2513,9 @@ msgstr "" ...@@ -2418,6 +2513,9 @@ msgstr ""
msgid "There are problems accessing Git storage: " msgid "There are problems accessing Git storage: "
msgstr "" msgstr ""
msgid "This board\\'s scope is reduced"
msgstr ""
msgid "This branch has changed since you started editing. Would you like to create a new branch?" msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "" msgstr ""
...@@ -2439,6 +2537,9 @@ msgstr "" ...@@ -2439,6 +2537,9 @@ msgstr ""
msgid "This merge request is locked." msgid "This merge request is locked."
msgstr "" msgstr ""
msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
msgstr ""
msgid "Time before an issue gets scheduled" msgid "Time before an issue gets scheduled"
msgstr "" msgstr ""
...@@ -2587,6 +2688,9 @@ msgstr[1] "" ...@@ -2587,6 +2688,9 @@ msgstr[1] ""
msgid "Time|s" msgid "Time|s"
msgstr "" msgstr ""
msgid "Title"
msgstr ""
msgid "Total Time" msgid "Total Time"
msgstr "" msgstr ""
...@@ -2596,7 +2700,13 @@ msgstr "" ...@@ -2596,7 +2700,13 @@ msgstr ""
msgid "Total test time for all commits/merges" msgid "Total test time for all commits/merges"
msgstr "" msgstr ""
msgid "Total test time for all commits/merges" msgid "Track activity with Contribution Analytics."
msgstr ""
msgid "Track groups of issues that share a theme, across projects and milestones"
msgstr ""
msgid "Turn on Service Desk"
msgstr "" msgstr ""
msgid "Unlock" msgid "Unlock"
...@@ -2611,6 +2721,21 @@ msgstr "" ...@@ -2611,6 +2721,21 @@ msgstr ""
msgid "Unsubscribe" msgid "Unsubscribe"
msgstr "" msgstr ""
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
msgid "Upgrade your plan to activate Contribution Analytics."
msgstr ""
msgid "Upgrade your plan to activate Group Webhooks."
msgstr ""
msgid "Upgrade your plan to activate Issue weight."
msgstr ""
msgid "Upgrade your plan to improve Issue boards."
msgstr ""
msgid "Upload New File" msgid "Upload New File"
msgstr "" msgstr ""
...@@ -2620,6 +2745,9 @@ msgstr "" ...@@ -2620,6 +2745,9 @@ msgstr ""
msgid "UploadLink|click to upload" msgid "UploadLink|click to upload"
msgstr "" msgstr ""
msgid "Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab"
msgstr ""
msgid "Use the following registration token during setup:" msgid "Use the following registration token during setup:"
msgstr "" msgstr ""
...@@ -2653,6 +2781,15 @@ msgstr "" ...@@ -2653,6 +2781,15 @@ msgstr ""
msgid "We don't have enough data to show this stage." msgid "We don't have enough data to show this stage."
msgstr "" msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
msgid "Weight"
msgstr ""
msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable" msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable"
msgstr "" msgstr ""
...@@ -2758,6 +2895,9 @@ msgstr "" ...@@ -2758,6 +2895,9 @@ msgstr ""
msgid "Wiki|Wiki Pages" msgid "Wiki|Wiki Pages"
msgstr "" msgstr ""
msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
msgstr ""
msgid "Withdraw Access Request" msgid "Withdraw Access Request"
msgstr "" msgstr ""
...@@ -2773,10 +2913,10 @@ msgstr "" ...@@ -2773,10 +2913,10 @@ msgstr ""
msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?" msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "" msgstr ""
msgid "You are on a read-only GitLab instance." msgid "You can only add files when you are on a branch"
msgstr "" msgstr ""
msgid "You can only add files when you are on a branch" msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
msgstr "" msgstr ""
msgid "You cannot write to this read-only GitLab instance." msgid "You cannot write to this read-only GitLab instance."
...@@ -2830,7 +2970,10 @@ msgstr "" ...@@ -2830,7 +2970,10 @@ msgstr ""
msgid "branch name" msgid "branch name"
msgstr "" msgstr ""
msgid "branch name" msgid "by"
msgstr ""
msgid "commit"
msgstr "" msgstr ""
msgid "day" msgid "day"
...@@ -2858,7 +3001,7 @@ msgstr "" ...@@ -2858,7 +3001,7 @@ msgstr ""
msgid "source" msgid "source"
msgstr "" msgstr ""
msgid "source" msgid "to help your contributors communicate effectively!"
msgstr "" msgstr ""
msgid "username" msgid "username"
......
...@@ -9,6 +9,7 @@ module QA ...@@ -9,6 +9,7 @@ module QA
autoload :User, 'qa/runtime/user' autoload :User, 'qa/runtime/user'
autoload :Namespace, 'qa/runtime/namespace' autoload :Namespace, 'qa/runtime/namespace'
autoload :Scenario, 'qa/runtime/scenario' autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser'
end end
## ##
...@@ -65,7 +66,6 @@ module QA ...@@ -65,7 +66,6 @@ module QA
autoload :Base, 'qa/page/base' autoload :Base, 'qa/page/base'
module Main module Main
autoload :Entry, 'qa/page/main/entry'
autoload :Login, 'qa/page/main/login' autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu' autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth' autoload :OAuth, 'qa/page/main/oauth'
......
...@@ -6,7 +6,13 @@ module QA ...@@ -6,7 +6,13 @@ module QA
module Page module Page
module Admin module Admin
autoload :License, 'qa/ee/page/admin/license' autoload :License, 'qa/ee/page/admin/license'
autoload :GeoNodes, 'qa/ee/page/admin/geo_nodes'
module Geo
module Nodes
autoload :Show, 'qa/ee/page/admin/geo/nodes/show'
autoload :New, 'qa/ee/page/admin/geo/nodes/new'
end
end
end end
end end
......
module QA
module EE
module Page
module Admin
module Geo
module Nodes
class New < QA::Page::Base
def set_node_address(address)
fill_in 'URL', with: address
end
def add_node!
click_button 'Add Node'
end
end
end
end
end
end
end
end
...@@ -2,13 +2,13 @@ module QA ...@@ -2,13 +2,13 @@ module QA
module EE module EE
module Page module Page
module Admin module Admin
class GeoNodes < QA::Page::Base module Geo
def set_node_address(address) module Nodes
fill_in 'URL', with: address class Show < QA::Page::Base
def new_node!
click_link 'New node'
end
end end
def add_node!
click_button 'Add Node'
end end
end end
end end
......
...@@ -6,12 +6,12 @@ module QA ...@@ -6,12 +6,12 @@ module QA
attr_accessor :address attr_accessor :address
def perform def perform
QA::Page::Main::Entry.act { visit_login_page }
QA::Page::Main::Login.act { sign_in_using_credentials } QA::Page::Main::Login.act { sign_in_using_credentials }
QA::Page::Main::Menu.act { go_to_admin_area } QA::Page::Main::Menu.act { go_to_admin_area }
QA::Page::Admin::Menu.act { go_to_geo_nodes } QA::Page::Admin::Menu.act { go_to_geo_nodes }
EE::Page::Admin::Geo::Nodes::Show.act { new_node! }
EE::Page::Admin::GeoNodes.perform do |page| EE::Page::Admin::Geo::Nodes::New.perform do |page|
raise ArgumentError if @address.nil? raise ArgumentError if @address.nil?
page.set_node_address(@address) page.set_node_address(@address)
......
...@@ -4,7 +4,6 @@ module QA ...@@ -4,7 +4,6 @@ module QA
module License module License
class Add < QA::Scenario::Template class Add < QA::Scenario::Template
def perform(license) def perform(license)
QA::Page::Main::Entry.act { visit_login_page }
QA::Page::Main::Login.act { sign_in_using_credentials } QA::Page::Main::Login.act { sign_in_using_credentials }
QA::Page::Main::Menu.act { go_to_admin_area } QA::Page::Main::Menu.act { go_to_admin_area }
QA::Page::Admin::Menu.act { go_to_license } QA::Page::Admin::Menu.act { go_to_license }
......
...@@ -12,13 +12,7 @@ module QA ...@@ -12,13 +12,7 @@ module QA
attribute :geo_skip_setup?, '--without-setup' attribute :geo_skip_setup?, '--without-setup'
def perform(**args) def perform(**args)
QA::Specs::Config.act { configure_capybara! }
unless args[:geo_skip_setup?] unless args[:geo_skip_setup?]
# TODO, Factory::License -> gitlab-org/gitlab-qa#86
#
QA::Runtime::Scenario.define(:gitlab_address, args[:geo_primary_address])
Geo::Primary.act do Geo::Primary.act do
add_license add_license
enable_hashed_storage enable_hashed_storage
...@@ -49,26 +43,32 @@ module QA ...@@ -49,26 +43,32 @@ module QA
# #
puts 'Adding GitLab EE license ...' puts 'Adding GitLab EE license ...'
QA::Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Scenario::License::Add.perform(ENV['EE_LICENSE']) Scenario::License::Add.perform(ENV['EE_LICENSE'])
end end
end
def enable_hashed_storage def enable_hashed_storage
# TODO, Factory::HashedStorage - gitlab-org/gitlab-qa#86 # TODO, Factory::HashedStorage - gitlab-org/gitlab-qa#86
# #
puts 'Enabling hashed repository storage setting ...' puts 'Enabling hashed repository storage setting ...'
QA::Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
QA::Scenario::Gitlab::Admin::HashedStorage.perform(:enabled) QA::Scenario::Gitlab::Admin::HashedStorage.perform(:enabled)
end end
end
def add_secondary_node def add_secondary_node
# TODO, Factory::Geo::Node - gitlab-org/gitlab-qa#86 # TODO, Factory::Geo::Node - gitlab-org/gitlab-qa#86
# #
puts 'Adding new Geo secondary node ...' puts 'Adding new Geo secondary node ...'
QA::Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Scenario::Geo::Node.perform do |node| Scenario::Geo::Node.perform do |node|
node.address = QA::Runtime::Scenario.geo_secondary_address node.address = QA::Runtime::Scenario.geo_secondary_address
end end
end end
end
def set_replication_password def set_replication_password
puts 'Setting replication password on primary node ...' puts 'Setting replication password on primary node ...'
......
...@@ -7,18 +7,12 @@ module QA ...@@ -7,18 +7,12 @@ module QA
require 'qa/ee' require 'qa/ee'
end end
##
# TODO generic solution for screenshot in factories
#
# gitlab-org/gitlab-qa#86
#
def perform_before_hooks def perform_before_hooks
return unless ENV['EE_LICENSE'] return unless ENV['EE_LICENSE']
QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login) do
EE::Scenario::License::Add.perform(ENV['EE_LICENSE']) EE::Scenario::License::Add.perform(ENV['EE_LICENSE'])
rescue end
Capybara::Screenshot.screenshot_and_save_page
raise
end end
end end
end end
......
...@@ -10,6 +10,18 @@ module QA ...@@ -10,6 +10,18 @@ module QA
visit current_url visit current_url
end end
def wait(css = '.application', time: 60)
Time.now.tap do |start|
while Time.now - start < time
break if page.has_css?(css, wait: 5)
refresh
end
end
yield if block_given?
end
def scroll_to(selector, text: nil) def scroll_to(selector, text: nil)
page.execute_script <<~JS page.execute_script <<~JS
var elements = Array.from(document.querySelectorAll('#{selector}')); var elements = Array.from(document.querySelectorAll('#{selector}'));
...@@ -24,6 +36,10 @@ module QA ...@@ -24,6 +36,10 @@ module QA
page.within(selector) { yield } if block_given? page.within(selector) { yield } if block_given?
end end
def self.path
raise NotImplementedError
end
end end
end end
end end
...@@ -4,6 +4,7 @@ module QA ...@@ -4,6 +4,7 @@ module QA
class New < Page::Base class New < Page::Base
def set_path(path) def set_path(path)
fill_in 'group_path', with: path fill_in 'group_path', with: path
fill_in 'group_name', with: path
end end
def set_description(description) def set_description(description)
......
module QA
module Page
module Main
class Entry < Page::Base
def visit_login_page
visit("#{Runtime::Scenario.gitlab_address}/users/sign_in")
wait_for_instance_to_be_ready
end
private
def wait_for_instance_to_be_ready
# This resolves cold boot / background tasks problems
#
start = Time.now
while Time.now - start < 1000
break if page.has_css?('.application', wait: 10)
refresh
end
end
end
end
end
end
...@@ -2,6 +2,10 @@ module QA ...@@ -2,6 +2,10 @@ module QA
module Page module Page
module Main module Main
class Login < Page::Base class Login < Page::Base
def initialize
wait('.application', time: 500)
end
def sign_in_using_credentials def sign_in_using_credentials
if page.has_content?('Change your password') if page.has_content?('Change your password')
fill_in :user_password, with: Runtime::User.password fill_in :user_password, with: Runtime::User.password
...@@ -13,6 +17,10 @@ module QA ...@@ -13,6 +17,10 @@ module QA
fill_in :user_password, with: Runtime::User.password fill_in :user_password, with: Runtime::User.password
click_button 'Sign in' click_button 'Sign in'
end end
def self.path
'/users/sign_in'
end
end end
end end
end end
......
...@@ -2,10 +2,6 @@ module QA ...@@ -2,10 +2,6 @@ module QA
module Page module Page
module Mattermost module Mattermost
class Login < Page::Base class Login < Page::Base
def initialize
visit(Runtime::Scenario.mattermost_address + '/login')
end
def sign_in_using_oauth def sign_in_using_oauth
click_link class: 'btn btn-custom-login gitlab' click_link class: 'btn btn-custom-login gitlab'
...@@ -13,6 +9,10 @@ module QA ...@@ -13,6 +9,10 @@ module QA
click_button 'Authorize' click_button 'Authorize'
end end
end end
def self.path
'/login'
end
end end
end end
end end
......
...@@ -3,37 +3,39 @@ require 'capybara/rspec' ...@@ -3,37 +3,39 @@ require 'capybara/rspec'
require 'capybara-screenshot/rspec' require 'capybara-screenshot/rspec'
require 'selenium-webdriver' require 'selenium-webdriver'
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/LineLength
module QA module QA
module Specs module Runtime
class Config < Scenario::Template class Browser
include Scenario::Actable include QA::Scenario::Actable
def perform def initialize
configure_rspec! self.class.configure!
configure_capybara!
end end
def configure_rspec! ##
RSpec.configure do |config| # Visit a page that belongs to a GitLab instance under given address.
config.expect_with :rspec do |expectations| #
expectations.include_chain_clauses_in_custom_matcher_descriptions = true # Example:
#
# visit(:gitlab, Page::Main::Login)
# visit('http://gitlab.example/users/sign_in')
#
# In case of an address that is a symbol we will try to guess address
# based on `Runtime::Scenario#something_address`.
#
def visit(address, page, &block)
Browser::Session.new(address, page).tap do |session|
session.perform(&block)
end end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end end
config.order = :random def self.visit(address, page, &block)
Kernel.srand config.seed new.visit(address, page, &block)
config.formatter = :documentation
config.color = true
end
end end
def configure_capybara! def self.configure!
return if Capybara.drivers.include?(:chrome)
Capybara.register_driver :chrome do |app| Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
'chromeOptions' => { 'chromeOptions' => {
...@@ -53,11 +55,55 @@ module QA ...@@ -53,11 +55,55 @@ module QA
config.default_driver = :chrome config.default_driver = :chrome
config.javascript_driver = :chrome config.javascript_driver = :chrome
config.default_max_wait_time = 10 config.default_max_wait_time = 10
# https://github.com/mattheworiordan/capybara-screenshot/issues/164 # https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = 'tmp' config.save_path = 'tmp'
end end
end end
class Session
include Capybara::DSL
def initialize(instance, page = nil)
@instance = instance
@address = host + page&.path
end
def host
if @instance.is_a?(Symbol)
Runtime::Scenario.send("#{@instance}_address")
else
@instance.to_s
end
end
def perform(&block)
visit(@address)
yield if block_given?
rescue
raise if block.nil?
# RSpec examples will take care of screenshots on their own
#
unless block.binding.receiver.is_a?(RSpec::Core::ExampleGroup)
screenshot_and_save_page
end
raise
ensure
clear! if block_given?
end
##
# Selenium allows to reset session cookies for current domain only.
#
# See gitlab-org/gitlab-qa#102
#
def clear!
visit(@address)
reset_session!
end
end
end end
end end
end end
...@@ -8,7 +8,6 @@ module QA ...@@ -8,7 +8,6 @@ module QA
include Bootable include Bootable
def perform(address, *files) def perform(address, *files)
Specs::Config.act { configure_capybara! }
Runtime::Scenario.define(:gitlab_address, address) Runtime::Scenario.define(:gitlab_address, address)
## ##
......
...@@ -6,7 +6,6 @@ module QA ...@@ -6,7 +6,6 @@ module QA
def perform(*traits) def perform(*traits)
raise ArgumentError unless traits.include?(:enabled) raise ArgumentError unless traits.include?(:enabled)
Page::Main::Entry.act { visit_login_page }
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Page::Main::Menu.act { go_to_admin_area } Page::Main::Menu.act { go_to_admin_area }
Page::Admin::Menu.act { go_to_settings } Page::Admin::Menu.act { go_to_settings }
......
module QA module QA
feature 'GitLab Geo replication', :geo do feature 'GitLab Geo replication', :geo do
scenario 'users pushes code to the primary node' do scenario 'users pushes code to the primary node' do
Page::Main::Entry.act { visit(Runtime::Scenario.geo_primary_address) } Runtime::Browser.visit(:geo_primary, QA::Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario| Scenario::Gitlab::Project::Create.perform do |scenario|
...@@ -29,8 +29,7 @@ module QA ...@@ -29,8 +29,7 @@ module QA
end end
end end
Page::Main::Entry.act { visit(Runtime::Scenario.geo_secondary_address) } Runtime::Browser.visit(:geo_secondary, QA::Page::Main::Login) do
Page::Main::OAuth.act do Page::Main::OAuth.act do
authorize! if needs_authorization? authorize! if needs_authorization?
end end
...@@ -55,4 +54,6 @@ module QA ...@@ -55,4 +54,6 @@ module QA
end end
end end
end end
end
end
end end
module QA module QA
feature 'standard root login', :core do feature 'standard user login', :core do
scenario 'user logs in using credentials' do scenario 'user logs in using credentials' do
Page::Main::Entry.act { visit_login_page } Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
# TODO, since `Signed in successfully` message was removed # TODO, since `Signed in successfully` message was removed
......
module QA module QA
feature 'create a new group', :mattermost do feature 'create a new group', :mattermost do
scenario 'creating a group with a mattermost team' do scenario 'creating a group with a mattermost team' do
Page::Main::Entry.act { visit_login_page } Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Page::Main::Menu.act { go_to_groups } Page::Main::Menu.act { go_to_groups }
......
module QA module QA
feature 'logging in to Mattermost', :mattermost do feature 'logging in to Mattermost', :mattermost do
scenario 'can use gitlab oauth' do scenario 'can use gitlab oauth' do
Page::Main::Entry.act { visit_login_page } Runtime::Browser.visit(:gitlab, Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do
Page::Mattermost::Login.act { sign_in_using_oauth } Page::Mattermost::Login.act { sign_in_using_oauth }
Page::Mattermost::Main.perform do |page| Page::Mattermost::Main.perform do |page|
expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
end end
end end
end
##
# TODO, temporary workaround for gitlab-org/gitlab-qa#102.
#
after do
visit Runtime::Scenario.mattermost_address
reset_session!
visit Runtime::Scenario.gitlab_address
reset_session!
end end
end end
end end
module QA module QA
feature 'create a new project', :core do feature 'create a new project', :core do
scenario 'user creates a new project' do scenario 'user creates a new project' do
Page::Main::Entry.act { visit_login_page } Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |project| Scenario::Gitlab::Project::Create.perform do |project|
......
...@@ -9,7 +9,7 @@ module QA ...@@ -9,7 +9,7 @@ module QA
end end
before do before do
Page::Main::Entry.act { visit_login_page } Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario| Scenario::Gitlab::Project::Create.perform do |scenario|
......
...@@ -2,7 +2,7 @@ module QA ...@@ -2,7 +2,7 @@ module QA
feature 'push code to repository', :core do feature 'push code to repository', :core do
context 'with regular account over http' do context 'with regular account over http' do
scenario 'user pushes code to the repository' do scenario 'user pushes code to the repository' do
Page::Main::Entry.act { visit_login_page } Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario| Scenario::Gitlab::Project::Create.perform do |scenario|
......
...@@ -17,7 +17,7 @@ module QA ...@@ -17,7 +17,7 @@ module QA
tags.to_a.each { |tag| args.push(['-t', tag.to_s]) } tags.to_a.each { |tag| args.push(['-t', tag.to_s]) }
args.push(files) args.push(files)
Specs::Config.perform Runtime::Browser.configure!
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero? abort if status.nonzero?
......
...@@ -23,7 +23,7 @@ describe 'epics list', :js do ...@@ -23,7 +23,7 @@ describe 'epics list', :js do
end end
it 'renders the list correctly' do it 'renders the list correctly' do
page.within('.page-with-new-nav .content') do page.within('.content-wrapper .content') do
expect(find('.top-area')).to have_content('All 2') expect(find('.top-area')).to have_content('All 2')
within('.issuable-list') do within('.issuable-list') do
expect(page).to have_content(epics.first.title) expect(page).to have_content(epics.first.title)
...@@ -33,7 +33,7 @@ describe 'epics list', :js do ...@@ -33,7 +33,7 @@ describe 'epics list', :js do
end end
it 'renders the epic detail correctly after clicking the link' do it 'renders the epic detail correctly after clicking the link' do
page.within('.page-with-new-nav .content .issuable-list') do page.within('.content-wrapper .content .issuable-list') do
click_link(epics.first.title) click_link(epics.first.title)
end end
......
require 'spec_helper'
describe GroupMemberPresenter do
let(:user) { double(:user) }
let(:group) { double(:group) }
let(:group_member) { double(:group_member, source: group) }
let(:presenter) { described_class.new(group_member, current_user: user) }
describe '#can_update?' do
context 'when user cannot update_group_member but can override_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(true)
end
it { expect(presenter.can_update?).to eq(true) }
end
context 'when user cannot update_group_member and cannot override_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false)
end
it { expect(presenter.can_update?).to eq(false) }
end
end
end
require 'spec_helper'
describe ProjectMemberPresenter do
let(:user) { double(:user) }
let(:project) { double(:project) }
let(:project_member) { double(:project_member, source: project) }
let(:presenter) { described_class.new(project_member, current_user: user) }
describe '#can_update?' do
context 'when user cannot update_project_member but can override_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(true)
end
it { expect(presenter.can_update?).to eq(true) }
end
context 'when user cannot update_project_member and cannot override_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false)
end
it { expect(presenter.can_update?).to eq(false) }
end
end
end
require 'spec_helper'
describe BaseCountService do
include ::EE::GeoHelpers
describe '#cache_options' do
subject { described_class.new.cache_options }
it 'returns the default' do
stub_current_geo_node(nil)
is_expected.to include(:raw)
is_expected.not_to include(:expires_in)
end
it 'returns default on a Geo primary' do
stub_current_geo_node(create(:geo_node, :primary))
is_expected.to include(:raw)
is_expected.not_to include(:expires_in)
end
it 'returns cache of 20 mins on a Geo secondary' do
stub_current_geo_node(create(:geo_node))
is_expected.to include(:raw)
is_expected.to include(expires_in: 20.minutes)
end
end
end
FactoryGirl.define do FactoryGirl.define do
factory :geo_node do factory :geo_node do
sequence(:url) do |port| # Start at a number higher than the current port to avoid the GeoNode
# "lock out" validation
sequence(:url, Gitlab.config.gitlab.port + 1) do |port|
uri = URI.parse("http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.relative_url_root}") uri = URI.parse("http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.relative_url_root}")
uri.port = port uri.port = port
uri.path += '/' unless uri.path.end_with?('/') uri.path += '/' unless uri.path.end_with?('/')
......
require 'spec_helper' require 'spec_helper'
describe MembersHelper do describe MembersHelper do
describe '#action_member_permission' do
let(:project_member) { build(:project_member) }
let(:group_member) { build(:group_member) }
it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member }
it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
end
describe '#remove_member_message' do describe '#remove_member_message' do
let(:requester) { create(:user) } let(:requester) { create(:user) }
let(:project) { create(:project, :public, :access_requestable) } let(:project) { create(:project, :public, :access_requestable) }
......
...@@ -41,7 +41,7 @@ import '~/right_sidebar'; ...@@ -41,7 +41,7 @@ import '~/right_sidebar';
loadFixtures(fixtureName); loadFixtures(fixtureName);
this.sidebar = new Sidebar; this.sidebar = new Sidebar;
$aside = $('.right-sidebar'); $aside = $('.right-sidebar');
$page = $('.page-with-sidebar'); $page = $('.layout-page');
$icon = $aside.find('i'); $icon = $aside.find('i');
$toggle = $aside.find('.js-sidebar-toggle'); $toggle = $aside.find('.js-sidebar-toggle');
return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
......
/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */
import '~/gl_dropdown'; import '~/gl_dropdown';
import '~/search_autocomplete'; import SearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
...@@ -128,7 +128,7 @@ import * as urlUtils from '~/lib/utils/url_utility'; ...@@ -128,7 +128,7 @@ import * as urlUtils from '~/lib/utils/url_utility';
window.gon.current_user_id = userId; window.gon.current_user_id = userId;
window.gon.current_username = userName; window.gon.current_username = userName;
return widget = new gl.SearchAutocomplete; return widget = new SearchAutocomplete;
}); });
afterEach(function() { afterEach(function() {
......
/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */ /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */
import '~/syntax_highlight'; import syntaxHighlight from '~/syntax_highlight';
(function() { describe('Syntax Highlighter', function() {
describe('Syntax Highlighter', function() {
var stubUserColorScheme; var stubUserColorScheme;
stubUserColorScheme = function(value) { stubUserColorScheme = function(value) {
if (window.gon == null) { if (window.gon == null) {
...@@ -17,7 +16,7 @@ import '~/syntax_highlight'; ...@@ -17,7 +16,7 @@ import '~/syntax_highlight';
}); });
return it('applies syntax highlighting', function() { return it('applies syntax highlighting', function() {
stubUserColorScheme('monokai'); stubUserColorScheme('monokai');
$('.js-syntax-highlight').syntaxHighlight(); syntaxHighlight($('.js-syntax-highlight'));
return expect($('.js-syntax-highlight')).toHaveClass('monokai'); return expect($('.js-syntax-highlight')).toHaveClass('monokai');
}); });
}); });
...@@ -27,7 +26,7 @@ import '~/syntax_highlight'; ...@@ -27,7 +26,7 @@ import '~/syntax_highlight';
}); });
it('applies highlighting to all applicable children', function() { it('applies highlighting to all applicable children', function() {
stubUserColorScheme('monokai'); stubUserColorScheme('monokai');
$('.parent').syntaxHighlight(); syntaxHighlight($('.parent'));
expect($('.parent, .foo')).not.toHaveClass('monokai'); expect($('.parent, .foo')).not.toHaveClass('monokai');
return expect($('.monokai').length).toBe(2); return expect($('.monokai').length).toBe(2);
}); });
...@@ -35,10 +34,9 @@ import '~/syntax_highlight'; ...@@ -35,10 +34,9 @@ import '~/syntax_highlight';
var highlight; var highlight;
setFixtures('<div></div>'); setFixtures('<div></div>');
highlight = function() { highlight = function() {
return $('div').syntaxHighlight(); return syntaxHighlight($('div'));
}; };
return expect(highlight).not.toThrow(); return expect(highlight).not.toThrow();
}); });
}); });
}); });
}).call(window);
import Vue from 'vue'; import Vue from 'vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import component from 'ee/vue_merge_request_widget/components/states/mr_widget_rebase.vue'; import component from 'ee/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import mountComponent from '../../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
...@@ -75,4 +76,42 @@ describe('Merge request widget rebase component', () => { ...@@ -75,4 +76,42 @@ describe('Merge request widget rebase component', () => {
expect(text).toContain('to allow this merge request to be merged.'); expect(text).toContain('to allow this merge request to be merged.');
}); });
}); });
describe('methods', () => {
it('checkRebaseStatus', (done) => {
spyOn(eventHub, '$emit');
vm = mountComponent(Component, {
mr: {},
service: {
rebase() {
return Promise.resolve();
},
poll() {
return Promise.resolve({
json() {
return {
rebase_in_progress: false,
merge_error: null,
};
},
});
},
},
});
vm.rebase();
// Wait for the rebase request
vm.$nextTick()
// Wait for the polling request
.then(vm.$nextTick())
// Wait for the eventHub to be called
.then(vm.$nextTick())
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
})
.then(done)
.catch(done.fail);
});
});
}); });
...@@ -182,13 +182,22 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do ...@@ -182,13 +182,22 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do
end end
context 'for a pre-Markdown Note attachment file path' do context 'for a pre-Markdown Note attachment file path' do
class Note < ActiveRecord::Base let(:model) { create(:note, :with_attachment) }
has_many :uploads, as: :model, dependent: :destroy let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') }
let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) }
before do
Upload.where(model_type: 'Note', model_id: model.id).delete_all
end end
let(:model) { create(:note, :with_attachment) } # Can't use the shared example because Note doesn't have an `uploads` association
it 'creates an Upload record' do
expect do
subject.perform(1, untracked_files_for_uploads.last.id)
end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1)
it_behaves_like 'non_markdown_file' expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs)
end
end end
context 'for a user avatar file path' do context 'for a user avatar file path' do
......
...@@ -27,5 +27,13 @@ describe Gitlab::View::Presenter::Factory do ...@@ -27,5 +27,13 @@ describe Gitlab::View::Presenter::Factory do
expect(presenter).to be_a(Ci::BuildPresenter) expect(presenter).to be_a(Ci::BuildPresenter)
end end
it 'uses the presenter_class if given on #initialize' do
MyCustomPresenter = Class.new(described_class)
presenter = described_class.new(build, presenter_class: MyCustomPresenter).fabricate!
expect(presenter).to be_a(MyCustomPresenter)
end
end end
end end
require 'spec_helper'
describe GroupMemberPresenter do
let(:user) { double(:user) }
let(:group) { double(:group) }
let(:group_member) { double(:group_member, source: group) }
let(:presenter) { described_class.new(group_member, current_user: user) }
describe '#can_resend_invite?' do
context 'when group_member is invited' do
before do
expect(group_member).to receive(:invite?).and_return(true)
end
context 'and user can admin_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
end
it { expect(presenter.can_resend_invite?).to eq(true) }
end
context 'and user cannot admin_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
end
it { expect(presenter.can_resend_invite?).to eq(false) }
end
end
context 'when group_member is not invited' do
before do
expect(group_member).to receive(:invite?).and_return(false)
end
context 'and user can admin_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
end
it { expect(presenter.can_resend_invite?).to eq(false) }
end
context 'and user cannot admin_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
end
it { expect(presenter.can_resend_invite?).to eq(false) }
end
end
end
describe '#can_update?' do
context 'when user can update_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
end
it { expect(presenter.can_update?).to eq(true) }
end
context 'when user cannot update_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false)
end
it { expect(presenter.can_update?).to eq(false) }
end
end
describe '#can_remove?' do
context 'when user can destroy_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(true)
end
it { expect(presenter.can_remove?).to eq(true) }
end
context 'when user cannot destroy_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(false)
end
it { expect(presenter.can_remove?).to eq(false) }
end
end
describe '#can_approve?' do
context 'when group_member has request an invite' do
before do
expect(group_member).to receive(:request?).and_return(true)
end
context 'when user can update_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
end
it { expect(presenter.can_approve?).to eq(true) }
end
context 'when user cannot update_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false)
end
it { expect(presenter.can_approve?).to eq(false) }
end
end
context 'when group_member did not request an invite' do
before do
expect(group_member).to receive(:request?).and_return(false)
end
context 'when user can update_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
end
it { expect(presenter.can_approve?).to eq(false) }
end
context 'when user cannot update_group_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
end
it { expect(presenter.can_approve?).to eq(false) }
end
end
end
end
require 'spec_helper'
describe ProjectMemberPresenter do
let(:user) { double(:user) }
let(:project) { double(:project) }
let(:project_member) { double(:project_member, source: project) }
let(:presenter) { described_class.new(project_member, current_user: user) }
describe '#can_resend_invite?' do
context 'when project_member is invited' do
before do
expect(project_member).to receive(:invite?).and_return(true)
end
context 'and user can admin_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true)
end
it { expect(presenter.can_resend_invite?).to eq(true) }
end
context 'and user cannot admin_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false)
end
it { expect(presenter.can_resend_invite?).to eq(false) }
end
end
context 'when project_member is not invited' do
before do
expect(project_member).to receive(:invite?).and_return(false)
end
context 'and user can admin_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true)
end
it { expect(presenter.can_resend_invite?).to eq(false) }
end
context 'and user cannot admin_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false)
end
it { expect(presenter.can_resend_invite?).to eq(false) }
end
end
end
describe '#can_update?' do
context 'when user can update_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
end
it { expect(presenter.can_update?).to eq(true) }
end
context 'when user cannot update_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false)
end
it { expect(presenter.can_update?).to eq(false) }
end
end
describe '#can_remove?' do
context 'when user can destroy_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(true)
end
it { expect(presenter.can_remove?).to eq(true) }
end
context 'when user cannot destroy_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(false)
end
it { expect(presenter.can_remove?).to eq(false) }
end
end
describe '#can_approve?' do
context 'when project_member has request an invite' do
before do
expect(project_member).to receive(:request?).and_return(true)
end
context 'and user can update_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
end
it { expect(presenter.can_approve?).to eq(true) }
end
context 'and user cannot update_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false)
end
it { expect(presenter.can_approve?).to eq(false) }
end
end
context 'when project_member did not request an invite' do
before do
expect(project_member).to receive(:request?).and_return(false)
end
context 'and user can update_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
end
it { expect(presenter.can_approve?).to eq(false) }
end
context 'and user cannot update_project_member' do
before do
allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
end
it { expect(presenter.can_approve?).to eq(false) }
end
end
end
end
...@@ -15,14 +15,12 @@ describe Users::KeysCountService, :use_clean_rails_memory_store_caching do ...@@ -15,14 +15,12 @@ describe Users::KeysCountService, :use_clean_rails_memory_store_caching do
expect(service.count).to eq(1) expect(service.count).to eq(1)
end end
it 'caches the number of keys in Redis' do it 'caches the number of keys in Redis', :request_store do
service.delete_cache
control_count = ActiveRecord::QueryRecorder.new { service.count }.count
service.delete_cache service.delete_cache
recorder = ActiveRecord::QueryRecorder.new do expect { 2.times { service.count } }.not_to exceed_query_limit(control_count)
2.times { service.count }
end
expect(recorder.count).to eq(1)
end end
end end
......
...@@ -175,6 +175,15 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach ...@@ -175,6 +175,15 @@ describe Geo::RepositoryShardSyncWorker, :geo, :delete, :clean_gitlab_redis_cach
Sidekiq::Testing.inline! { subject.perform(shard_name) } Sidekiq::Testing.inline! { subject.perform(shard_name) }
end end
end end
context 'number of scheduled jobs exceeds capacity' do
it 'schedules 0 jobs' do
is_expected.to receive(:scheduled_job_ids).and_return(1..1000).at_least(:once)
is_expected.not_to receive(:schedule_job)
Sidekiq::Testing.inline! { subject.perform(shard_name) }
end
end
end end
describe 'when PostgreSQL FDW is available', :geo do describe 'when PostgreSQL FDW is available', :geo do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment