Commit 0a074f2e authored by Regis's avatar Regis

fix pipelines/index.html.haml merge conflict

parents 58821935 de25604f
...@@ -425,7 +425,7 @@ notify:slack: ...@@ -425,7 +425,7 @@ notify:slack:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
script: script:
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>" - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/pipelines>"
when: on_failure when: on_failure
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
......
...@@ -7,10 +7,10 @@ exclude: ...@@ -7,10 +7,10 @@ exclude:
linters: linters:
AltText: AltText:
enabled: false enabled: true
ClassAttributeWithStaticValue: ClassAttributeWithStaticValue:
enabled: false enabled: true
ClassesBeforeIds: ClassesBeforeIds:
enabled: false enabled: false
...@@ -29,14 +29,14 @@ linters: ...@@ -29,14 +29,14 @@ linters:
enabled: true enabled: true
FinalNewline: FinalNewline:
enabled: false enabled: true
present: true present: true
HtmlAttributes: HtmlAttributes:
enabled: false enabled: true
ImplicitDiv: ImplicitDiv:
enabled: false enabled: true
LeadingCommentSpace: LeadingCommentSpace:
enabled: false enabled: false
...@@ -80,10 +80,10 @@ linters: ...@@ -80,10 +80,10 @@ linters:
enabled: false enabled: false
SpaceBeforeScript: SpaceBeforeScript:
enabled: false enabled: true
SpaceInsideHashAttributes: SpaceInsideHashAttributes:
enabled: false enabled: true
style: space style: space
Indentation: Indentation:
...@@ -94,7 +94,7 @@ linters: ...@@ -94,7 +94,7 @@ linters:
enabled: true enabled: true
TrailingWhitespace: TrailingWhitespace:
enabled: false enabled: true
UnnecessaryInterpolation: UnnecessaryInterpolation:
enabled: false enabled: false
......
...@@ -2,6 +2,22 @@ ...@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 8.15.2 (2016-12-27)
- Fix finding the latest pipeline. !8301
- Fix mr list timestamp alignment. !8271
- Fix discussion overlap text in regular screens. !8273
- Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari. !8282
- Fix line breaking in nodes of the pipeline graph in firefox. !8292
- Fixes confendential warning text alignment. !8293
- Hide Scroll Top button for failed build page. !8295
- Fix finding the latest pipeline. !8301
- Disable PostgreSQL statement timeouts when removing unneeded services. !8322
- Fix timeout when MR contains large files marked as binary by .gitattributes.
- Rename "autodeploy" to "auto deploy".
- Fixed GFM autocomplete error when no data exists.
- Fixed resolve discussion note button color.
## 8.15.1 (2016-12-23) ## 8.15.1 (2016-12-23)
- Push payloads schedule at most 100 commits, instead of all commits. - Push payloads schedule at most 100 commits, instead of all commits.
......
...@@ -217,8 +217,8 @@ We welcome merge requests with fixes and improvements to GitLab code, tests, ...@@ -217,8 +217,8 @@ We welcome merge requests with fixes and improvements to GitLab code, tests,
and/or documentation. The features we would really like a merge request for are and/or documentation. The features we would really like a merge request for are
listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce] listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note
that if an issue is marked for the current milestone either before or while you that if an issue is marked for the current milestone either before or while you
are working on it, a team member may take over the merge request in order to are working on it, a team member may take over the merge request in order to
ensure the work is finished before the release date. ensure the work is finished before the release date.
If you want to add a new feature that is not labeled it is best to first create If you want to add a new feature that is not labeled it is best to first create
...@@ -300,6 +300,7 @@ you start with a very simple UI? Can you do part of the refactor? The increased ...@@ -300,6 +300,7 @@ you start with a very simple UI? Can you do part of the refactor? The increased
reviewability of small MRs that leads to higher code quality is more important reviewability of small MRs that leads to higher code quality is more important
to us than having a minimal commit log. The smaller an MR is the more likely it to us than having a minimal commit log. The smaller an MR is the more likely it
is it will be merged (quickly). After that you can send more MRs to enhance it. is it will be merged (quickly). After that you can send more MRs to enhance it.
The ['How to get faster PR reviews' document of Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md) also has some great points regarding this.
For examples of feedback on merge requests please look at already For examples of feedback on merge requests please look at already
[closed merge requests][closed-merge-requests]. If you would like quick feedback [closed merge requests][closed-merge-requests]. If you would like quick feedback
......
...@@ -332,7 +332,7 @@ gem 'octokit', '~> 4.3.0' ...@@ -332,7 +332,7 @@ gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.9.0' gem 'mail_room', '~> 0.9.0'
gem 'email_reply_parser', '~> 0.5.8' gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text' gem 'html2text'
gem 'ruby-prof', '~> 0.16.2' gem 'ruby-prof', '~> 0.16.2'
...@@ -347,5 +347,5 @@ gem 'paranoia', '~> 2.2' ...@@ -347,5 +347,5 @@ gem 'paranoia', '~> 2.2'
gem 'health_check', '~> 2.2.0' gem 'health_check', '~> 2.2.0'
# System information # System information
gem 'vmstat', '~> 2.2' gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
...@@ -167,7 +167,7 @@ GEM ...@@ -167,7 +167,7 @@ GEM
railties (>= 4.2) railties (>= 4.2)
dropzonejs-rails (0.7.2) dropzonejs-rails (0.7.2)
rails (> 3.1) rails (> 3.1)
email_reply_parser (0.5.8) email_reply_trimmer (0.1.6)
email_spec (1.6.0) email_spec (1.6.0)
launchy (~> 2.1) launchy (~> 2.1)
mail (~> 2.2) mail (~> 2.2)
...@@ -773,7 +773,7 @@ GEM ...@@ -773,7 +773,7 @@ GEM
coercible (~> 1.0) coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3) descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9) equalizer (~> 0.0, >= 0.0.9)
vmstat (2.2.0) vmstat (2.3.0)
warden (1.2.6) warden (1.2.6)
rack (>= 1.0) rack (>= 1.0)
web-console (2.3.0) web-console (2.3.0)
...@@ -839,7 +839,7 @@ DEPENDENCIES ...@@ -839,7 +839,7 @@ DEPENDENCIES
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.2.0) doorkeeper (~> 4.2.0)
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0) factory_girl_rails (~> 4.7.0)
ffaker (~> 2.0.0) ffaker (~> 2.0.0)
...@@ -982,7 +982,7 @@ DEPENDENCIES ...@@ -982,7 +982,7 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.4) unicorn-worker-killer (~> 0.4.4)
version_sorter (~> 2.1.0) version_sorter (~> 2.1.0)
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.2) vmstat (~> 2.3.0)
web-console (~> 2.0) web-console (~> 2.0)
webmock (~> 1.21.0) webmock (~> 1.21.0)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
......
...@@ -69,7 +69,8 @@ to add details to the issue. ...@@ -69,7 +69,8 @@ to add details to the issue.
- ~UX needs help from a UX designer - ~UX needs help from a UX designer
- ~Frontend needs help from a Front-end engineer. Please follow the - ~Frontend needs help from a Front-end engineer. Please follow the
["Implement design & UI elements" guidelines]. ["Implement design & UI elements" guidelines].
- ~up-for-grabs is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels. - ~"Accepting Merge Requests" is a low priority, well-defined issue that we
encourage people to contribute to. Not exclusive with other labels.
- ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote - ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote
in support or comment for further detail. Do not use `feature request`. in support or comment for further detail. Do not use `feature request`.
- ~bug is an issue reporting undesirable or incorrect behavior. - ~bug is an issue reporting undesirable or incorrect behavior.
......
...@@ -89,6 +89,14 @@ ...@@ -89,6 +89,14 @@
// Set the default path for all cookies to GitLab's root directory // Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/'; Cookies.defaults.path = gon.relative_url_root || '/';
// `hashchange` is not triggered when link target is already in window.location
$body.on('click', 'a[href^="#"]', function() {
var href = this.getAttribute('href');
if (href.substr(1) === gl.utils.getLocationHash()) {
setTimeout(gl.utils.handleLocationHash, 1);
}
});
// prevent default action for disabled buttons // prevent default action for disabled buttons
$('.btn').click(function(e) { $('.btn').click(function(e) {
if ($(this).hasClass('disabled')) { if ($(this).hasClass('disabled')) {
......
...@@ -73,7 +73,7 @@ $(() => { ...@@ -73,7 +73,7 @@ $(() => {
}); });
gl.IssueBoardsSearch = new Vue({ gl.IssueBoardsSearch = new Vue({
el: '#js-boards-seach', el: '#js-boards-search',
data: { data: {
filters: Store.state.filters filters: Store.state.filters
}, },
......
...@@ -71,3 +71,5 @@ class ListIssue { ...@@ -71,3 +71,5 @@ class ListIssue {
return Vue.http.patch(url, data); return Vue.http.patch(url, data);
} }
} }
window.ListIssue = ListIssue;
...@@ -10,3 +10,5 @@ class ListLabel { ...@@ -10,3 +10,5 @@ class ListLabel {
this.priority = (obj.priority !== null) ? obj.priority : Infinity; this.priority = (obj.priority !== null) ? obj.priority : Infinity;
} }
} }
window.ListLabel = ListLabel;
...@@ -148,3 +148,5 @@ class List { ...@@ -148,3 +148,5 @@ class List {
}); });
} }
} }
window.List = List;
...@@ -6,3 +6,5 @@ class ListMilestone { ...@@ -6,3 +6,5 @@ class ListMilestone {
this.title = obj.title; this.title = obj.title;
} }
} }
window.ListMilestone = ListMilestone;
...@@ -8,3 +8,5 @@ class ListUser { ...@@ -8,3 +8,5 @@ class ListUser {
this.avatar = user.avatar_url; this.avatar = user.avatar_url;
} }
} }
window.ListUser = ListUser;
...@@ -65,4 +65,6 @@ class BoardService { ...@@ -65,4 +65,6 @@ class BoardService {
issue issue
}); });
} }
}; }
window.BoardService = BoardService;
...@@ -92,8 +92,8 @@ ...@@ -92,8 +92,8 @@
success: function(buildData) { success: function(buildData) {
$('.js-build-output').html(buildData.trace_html); $('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
this.initScrollMonitor(); this.$buildRefreshAnimation.remove();
return this.$buildRefreshAnimation.remove(); return this.initScrollMonitor();
} }
}.bind(this) }.bind(this)
}); });
......
...@@ -59,9 +59,11 @@ ...@@ -59,9 +59,11 @@
}, },
methods: { methods: {
updateTooltip: function () { updateTooltip: function () {
$(this.$refs.button) this.$nextTick(() => {
.tooltip('hide') $(this.$refs.button)
.tooltip('fixTitle'); .tooltip('hide')
.tooltip('fixTitle');
});
}, },
resolve: function () { resolve: function () {
if (!this.canResolve) return; if (!this.canResolve) return;
...@@ -90,7 +92,7 @@ ...@@ -90,7 +92,7 @@
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert'); new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
} }
this.$nextTick(this.updateTooltip); this.updateTooltip();
}); });
} }
}, },
......
...@@ -92,3 +92,5 @@ class DiscussionModel { ...@@ -92,3 +92,5 @@ class DiscussionModel {
return false; return false;
} }
} }
window.DiscussionModel = DiscussionModel;
...@@ -9,3 +9,5 @@ class NoteModel { ...@@ -9,3 +9,5 @@ class NoteModel {
this.resolved_by = resolved_by; this.resolved_by = resolved_by;
} }
} }
window.NoteModel = NoteModel;
...@@ -64,6 +64,17 @@ ...@@ -64,6 +64,17 @@
new UsernameValidator(); new UsernameValidator();
new ActiveTabMemoizer(); new ActiveTabMemoizer();
break; break;
case 'sessions:create':
if (!gon.u2f) break;
window.gl.u2fAuthenticate = new gl.U2FAuthenticate(
$("#js-authenticate-u2f"),
'#js-login-u2f-form',
gon.u2f,
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form'),
);
window.gl.u2fAuthenticate.start();
break;
case 'projects:boards:show': case 'projects:boards:show':
case 'projects:boards:index': case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -20,3 +20,5 @@ class EnvironmentsService { ...@@ -20,3 +20,5 @@ class EnvironmentsService {
return this.environments.get(); return this.environments.get();
} }
} }
window.EnvironmentsService = EnvironmentsService;
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar;
atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
subtext = subtext.split(' ').pop(); subtext = subtext.split(/\s+/g).pop();
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
_a = decodeURI("%C3%80"); _a = decodeURI("%C3%80");
...@@ -367,7 +367,7 @@ ...@@ -367,7 +367,7 @@
return $input.trigger('keyup'); return $input.trigger('keyup');
}, },
isLoading(data) { isLoading(data) {
if (!data) return false; if (!data || !data.length) return false;
if (Array.isArray(data)) data = data[0]; if (Array.isArray(data)) data = data[0];
return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0]; return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
}, },
......
...@@ -139,15 +139,12 @@ ...@@ -139,15 +139,12 @@
return; return;
} }
return $.getJSON($container.data('path')).error(function() { return $.getJSON($container.data('path')).error(function() {
$container.find('.checking').hide();
$container.find('.unavailable').show(); $container.find('.unavailable').show();
return new Flash('Failed to check if a new branch can be created.', 'alert'); return new Flash('Failed to check if a new branch can be created.', 'alert');
}).success(function(data) { }).success(function(data) {
if (data.can_create_branch) { if (data.can_create_branch) {
$container.find('.checking').hide();
$container.find('.available').show(); $container.find('.available').show();
} else { } else {
$container.find('.checking').hide();
return $container.find('.unavailable').show(); return $container.find('.unavailable').show();
} }
}); });
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, camelcase, prefer-arrow-callback, max-len */ /* eslint-disable func-names, no-var, object-shorthand, comma-dangle, prefer-arrow-callback */
// MarkdownPreview // MarkdownPreview
// //
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview, // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
// and showing a warning when more than `x` users are referenced. // and showing a warning when more than `x` users are referenced.
// //
(function() { (function () {
var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; var lastTextareaPreviewed;
var markdownPreview;
var previewButtonSelector;
var writeButtonSelector;
window.MarkdownPreview = (function() { window.MarkdownPreview = (function () {
function MarkdownPreview() {} function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning // Minimum number of users referenced before triggering a warning
...@@ -16,73 +19,71 @@ ...@@ -16,73 +19,71 @@
MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function(form) { MarkdownPreview.prototype.showPreview = function ($form) {
var mdText, preview; var mdText;
preview = form.find('.js-md-preview'); var preview = $form.find('.js-md-preview');
mdText = form.find('textarea.markdown-area').val(); if (preview.hasClass('md-preview-loading')) {
return;
}
mdText = $form.find('textarea.markdown-area').val();
if (mdText.trim().length === 0) { if (mdText.trim().length === 0) {
preview.text('Nothing to preview.'); preview.text('Nothing to preview.');
return this.hideReferencedUsers(form); this.hideReferencedUsers($form);
} else { } else {
preview.text('Loading...'); preview.addClass('md-preview-loading').text('Loading...');
return this.renderMarkdown(mdText, (function(_this) { this.fetchMarkdownPreview(mdText, (function (response) {
return function(response) { preview.removeClass('md-preview-loading').html(response.body);
preview.html(response.body); preview.renderGFM();
preview.renderGFM(); this.renderReferencedUsers(response.references.users, $form);
return _this.renderReferencedUsers(response.references.users, form); }).bind(this));
};
})(this));
} }
}; };
MarkdownPreview.prototype.renderMarkdown = function(text, success) { MarkdownPreview.prototype.fetchMarkdownPreview = function (text, success) {
if (!window.preview_markdown_path) { if (!window.preview_markdown_path) {
return; return;
} }
if (text === this.ajaxCache.text) { if (text === this.ajaxCache.text) {
return success(this.ajaxCache.response); success(this.ajaxCache.response);
return;
} }
return $.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: window.preview_markdown_path, url: window.preview_markdown_path,
data: { data: {
text: text text: text
}, },
dataType: 'json', dataType: 'json',
success: (function(_this) { success: (function (response) {
return function(response) { this.ajaxCache = {
_this.ajaxCache = { text: text,
text: text, response: response
response: response
};
return success(response);
}; };
})(this) success(response);
}).bind(this)
}); });
}; };
MarkdownPreview.prototype.hideReferencedUsers = function(form) { MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
var referencedUsers; $form.find('.referenced-users').hide();
referencedUsers = form.find('.referenced-users');
return referencedUsers.hide();
}; };
MarkdownPreview.prototype.renderReferencedUsers = function(users, form) { MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) {
var referencedUsers; var referencedUsers;
referencedUsers = form.find('.referenced-users'); referencedUsers = $form.find('.referenced-users');
if (referencedUsers.length) { if (referencedUsers.length) {
if (users.length >= this.referenceThreshold) { if (users.length >= this.referenceThreshold) {
referencedUsers.show(); referencedUsers.show();
return referencedUsers.find('.js-referenced-users-count').text(users.length); referencedUsers.find('.js-referenced-users-count').text(users.length);
} else { } else {
return referencedUsers.hide(); referencedUsers.hide();
} }
} }
}; };
return MarkdownPreview; return MarkdownPreview;
}());
})();
markdownPreview = new window.MarkdownPreview(); markdownPreview = new window.MarkdownPreview();
...@@ -92,19 +93,14 @@ ...@@ -92,19 +93,14 @@
lastTextareaPreviewed = null; lastTextareaPreviewed = null;
$.fn.setupMarkdownPreview = function() { $.fn.setupMarkdownPreview = function () {
var $form, form_textarea; var $form = $(this);
$form = $(this); $form.find('textarea.markdown-area').on('input', function () {
form_textarea = $form.find('textarea.markdown-area'); markdownPreview.hideReferencedUsers($form);
form_textarea.on('input', function() {
return markdownPreview.hideReferencedUsers($form);
});
return form_textarea.on('blur', function() {
return markdownPreview.showPreview($form);
}); });
}; };
$(document).on('markdown-preview:show', function(e, $form) { $(document).on('markdown-preview:show', function (e, $form) {
if (!$form) { if (!$form) {
return; return;
} }
...@@ -115,10 +111,10 @@ ...@@ -115,10 +111,10 @@
// toggle content // toggle content
$form.find('.md-write-holder').hide(); $form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show(); $form.find('.md-preview-holder').show();
return markdownPreview.showPreview($form); markdownPreview.showPreview($form);
}); });
$(document).on('markdown-preview:hide', function(e, $form) { $(document).on('markdown-preview:hide', function (e, $form) {
if (!$form) { if (!$form) {
return; return;
} }
...@@ -129,34 +125,33 @@ ...@@ -129,34 +125,33 @@
// toggle content // toggle content
$form.find('.md-write-holder').show(); $form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus(); $form.find('textarea.markdown-area').focus();
return $form.find('.md-preview-holder').hide(); $form.find('.md-preview-holder').hide();
}); });
$(document).on('markdown-preview:toggle', function(e, keyboardEvent) { $(document).on('markdown-preview:toggle', function (e, keyboardEvent) {
var $target; var $target;
$target = $(keyboardEvent.target); $target = $(keyboardEvent.target);
if ($target.is('textarea.markdown-area')) { if ($target.is('textarea.markdown-area')) {
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]);
return keyboardEvent.preventDefault(); keyboardEvent.preventDefault();
} else if (lastTextareaPreviewed) { } else if (lastTextareaPreviewed) {
$target = lastTextareaPreviewed; $target = lastTextareaPreviewed;
$(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]);
return keyboardEvent.preventDefault(); keyboardEvent.preventDefault();
} }
}); });
$(document).on('click', previewButtonSelector, function(e) { $(document).on('click', previewButtonSelector, function (e) {
var $form; var $form;
e.preventDefault(); e.preventDefault();
$form = $(this).closest('form'); $form = $(this).closest('form');
return $(document).triggerHandler('markdown-preview:show', [$form]); $(document).triggerHandler('markdown-preview:show', [$form]);
}); });
$(document).on('click', writeButtonSelector, function(e) { $(document).on('click', writeButtonSelector, function (e) {
var $form; var $form;
e.preventDefault(); e.preventDefault();
$form = $(this).closest('form'); $form = $(this).closest('form');
return $(document).triggerHandler('markdown-preview:hide', [$form]); $(document).triggerHandler('markdown-preview:hide', [$form]);
}); });
}());
}).call(this);
...@@ -41,15 +41,12 @@ ...@@ -41,15 +41,12 @@
} }
beforeUpdateUsername() { beforeUpdateUsername() {
$('.loading-username').show(); $('.loading-username', this).removeClass('hidden');
$(this).find('.update-success').hide();
return $(this).find('.update-failed').hide();
} }
afterUpdateUsername() { afterUpdateUsername() {
$('.loading-username').hide(); $('.loading-username', this).addClass('hidden');
$(this).find('.btn-save').enable(); $('button[type=submit]', this).enable();
return $(this).find('.loading-gif').hide();
} }
onUpdateNotifs(e, data) { onUpdateNotifs(e, data) {
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
}, },
data: function(term, callback) { data: function(term, callback) {
var finalCallback, projectsCallback; var finalCallback, projectsCallback;
var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) { finalCallback = function(projects) {
return callback(projects); return callback(projects);
}; };
...@@ -34,7 +35,7 @@ ...@@ -34,7 +35,7 @@
if (this.groupId) { if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback); return Api.groupProjects(this.groupId, term, projectsCallback);
} else { } else {
return Api.projects(term, this.orderBy, projectsCallback); return Api.projects(term, orderBy, projectsCallback);
} }
}, },
url: function(project) { url: function(project) {
......
...@@ -76,3 +76,5 @@ class ProtectedBranchDropdown { ...@@ -76,3 +76,5 @@ class ProtectedBranchDropdown {
this.$dropdownFooter.toggleClass('hidden', !branchName); this.$dropdownFooter.toggleClass('hidden', !branchName);
} }
} }
window.ProtectedBranchDropdown = ProtectedBranchDropdown;
...@@ -8,21 +8,26 @@ ...@@ -8,21 +8,26 @@
// State Flow #1: setup -> in_progress -> authenticated -> POST to server // State Flow #1: setup -> in_progress -> authenticated -> POST to server
// State Flow #2: setup -> in_progress -> error -> setup // State Flow #2: setup -> in_progress -> error -> setup
(function() { (function() {
const global = window.gl || (window.gl = {});
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.U2FAuthenticate = (function() { global.U2FAuthenticate = (function() {
function U2FAuthenticate(container, u2fParams) { function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) {
this.container = container; this.container = container;
this.renderNotSupported = bind(this.renderNotSupported, this); this.renderNotSupported = bind(this.renderNotSupported, this);
this.renderAuthenticated = bind(this.renderAuthenticated, this); this.renderAuthenticated = bind(this.renderAuthenticated, this);
this.renderError = bind(this.renderError, this); this.renderError = bind(this.renderError, this);
this.renderInProgress = bind(this.renderInProgress, this); this.renderInProgress = bind(this.renderInProgress, this);
this.renderSetup = bind(this.renderSetup, this);
this.renderTemplate = bind(this.renderTemplate, this); this.renderTemplate = bind(this.renderTemplate, this);
this.authenticate = bind(this.authenticate, this); this.authenticate = bind(this.authenticate, this);
this.start = bind(this.start, this); this.start = bind(this.start, this);
this.appId = u2fParams.app_id; this.appId = u2fParams.app_id;
this.challenge = u2fParams.challenge; this.challenge = u2fParams.challenge;
this.form = form;
this.fallbackButton = fallbackButton;
this.fallbackUI = fallbackUI;
if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this));
this.signRequests = u2fParams.sign_requests.map(function(request) { this.signRequests = u2fParams.sign_requests.map(function(request) {
// The U2F Javascript API v1.1 requires a single challenge, with // The U2F Javascript API v1.1 requires a single challenge, with
// _no challenges per-request_. The U2F Javascript API v1.0 requires a // _no challenges per-request_. The U2F Javascript API v1.0 requires a
...@@ -41,7 +46,7 @@ ...@@ -41,7 +46,7 @@
U2FAuthenticate.prototype.start = function() { U2FAuthenticate.prototype.start = function() {
if (U2FUtil.isU2FSupported()) { if (U2FUtil.isU2FSupported()) {
return this.renderSetup(); return this.renderInProgress();
} else { } else {
return this.renderNotSupported(); return this.renderNotSupported();
} }
...@@ -77,11 +82,6 @@ ...@@ -77,11 +82,6 @@
return this.container.html(template(params)); return this.container.html(template(params));
}; };
U2FAuthenticate.prototype.renderSetup = function() {
this.renderTemplate('setup');
return this.container.find('#js-login-u2f-device').on('click', this.renderInProgress);
};
U2FAuthenticate.prototype.renderInProgress = function() { U2FAuthenticate.prototype.renderInProgress = function() {
this.renderTemplate('inProgress'); this.renderTemplate('inProgress');
return this.authenticate(); return this.authenticate();
...@@ -92,22 +92,29 @@ ...@@ -92,22 +92,29 @@
error_message: error.message(), error_message: error.message(),
error_code: error.errorCode error_code: error.errorCode
}); });
return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress);
}; };
U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
this.renderTemplate('authenticated'); this.renderTemplate('authenticated');
// Prefer to do this instead of interpolating using Underscore templates const container = this.container[0];
// because of JSON escaping issues. container.querySelector('#js-device-response').value = deviceResponse;
return this.container.find("#js-device-response").val(deviceResponse); container.querySelector(this.form).submit();
this.fallbackButton.classList.add('hidden');
}; };
U2FAuthenticate.prototype.renderNotSupported = function() { U2FAuthenticate.prototype.renderNotSupported = function() {
return this.renderTemplate('notSupported'); return this.renderTemplate('notSupported');
}; };
U2FAuthenticate.prototype.switchToFallbackUI = function() {
this.fallbackButton.classList.add('hidden');
this.container[0].classList.add('hidden');
this.fallbackUI.classList.remove('hidden');
};
return U2FAuthenticate; return U2FAuthenticate;
})(); })();
}).call(this); })();
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
.append-bottom-default { margin-bottom: $gl-padding; } .append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block; } .inline { display: inline-block; }
.center { text-align: center; } .center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
.underlined-link { text-decoration: underline; } .underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; } .hint { font-style: italic; color: $hint-color; }
...@@ -57,16 +58,33 @@ pre { ...@@ -57,16 +58,33 @@ pre {
border-radius: 0; border-radius: 0;
color: $well-pre-color; color: $well-pre-color;
} }
&.wrap {
word-break: break-word;
white-space: pre-wrap;
}
} }
hr { hr {
margin: $gl-padding 0; margin: $gl-padding 0;
border-top: 1px solid darken($gray-normal, 8%);
} }
.str-truncated { .str-truncated {
@include str-truncated; @include str-truncated;
} }
.block-truncated {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
> div,
.str-truncated {
display: inline;
}
}
.item-title { font-weight: 600; } .item-title { font-weight: 600; }
/** FLASH message **/ /** FLASH message **/
......
...@@ -80,29 +80,32 @@ ...@@ -80,29 +80,32 @@
} }
} }
.about-gitlab {
color: $color-light;
}
} }
} }
} }
$theme-charcoal: #3d454d; $theme-charcoal-light: #b9bbbe;
$theme-charcoal-light: #485157; $theme-charcoal: #485157;
$theme-charcoal-dark: #383f45; $theme-charcoal-dark: #3d454d;
$theme-charcoal-text: #b9bbbe; $theme-charcoal-darker: #383f45;
$theme-blue-light: #becde9; $theme-blue-light: #becde9;
$theme-blue: #2980b9; $theme-blue: #2980b9;
$theme-blue-dark: #1970a9; $theme-blue-dark: #1970a9;
$theme-blue-darker: #096099; $theme-blue-darker: #096099;
$theme-graphite-lighter: #ccc; $theme-graphite-light: #ccc;
$theme-graphite-light: #777; $theme-graphite: #777;
$theme-graphite: #666; $theme-graphite-dark: #666;
$theme-graphite-dark: #555; $theme-graphite-darker: #555;
$theme-gray-light: #979797; $theme-black-light: #979797;
$theme-gray: #373737; $theme-black: #373737;
$theme-gray-dark: #272727; $theme-black-dark: #272727;
$theme-gray-darker: #222; $theme-black-darker: #222;
$theme-green-light: #adc; $theme-green-light: #adc;
$theme-green: #019875; $theme-green: #019875;
...@@ -120,15 +123,15 @@ body { ...@@ -120,15 +123,15 @@ body {
} }
&.ui_charcoal { &.ui_charcoal {
@include gitlab-theme($theme-charcoal-text, $theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark); @include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
} }
&.ui_graphite { &.ui_graphite {
@include gitlab-theme($theme-graphite-lighter, $theme-graphite-light, $theme-graphite, $theme-graphite-dark); @include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
} }
&.ui_gray { &.ui_black {
@include gitlab-theme($theme-gray-light, $theme-gray, $theme-gray-dark, $theme-gray-darker); @include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
} }
&.ui_green { &.ui_green {
......
...@@ -199,6 +199,7 @@ ul.content-list { ...@@ -199,6 +199,7 @@ ul.content-list {
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
align-items: center;
white-space: nowrap; white-space: nowrap;
} }
...@@ -208,6 +209,11 @@ ul.content-list { ...@@ -208,6 +209,11 @@ ul.content-list {
padding-right: 8px; padding-right: 8px;
} }
.row-fixed-content {
flex: 0 0 auto;
margin-left: auto;
}
.row-title { .row-title {
font-weight: 600; font-weight: 600;
} }
...@@ -239,7 +245,6 @@ ul.content-list { ...@@ -239,7 +245,6 @@ ul.content-list {
} }
ul.controls { ul.controls {
padding-top: 1px;
float: right; float: right;
list-style: none; list-style: none;
......
...@@ -116,8 +116,8 @@ ...@@ -116,8 +116,8 @@
padding-top: 16px; padding-top: 16px;
padding-bottom: 11px; padding-bottom: 11px;
display: inline-block; display: inline-block;
width: 50%;
line-height: 28px; line-height: 28px;
white-space: normal;
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -158,30 +158,24 @@ ...@@ -158,30 +158,24 @@
} }
.nav-controls { .nav-controls {
width: 50%;
display: inline-block; display: inline-block;
float: right; float: right;
text-align: right; text-align: right;
padding: 11px 0; padding: 11px 0;
margin-bottom: 0; margin-bottom: 0;
> .dropdown { > .btn,
margin-right: $gl-padding-top; > .btn-container,
display: inline-block; > .dropdown,
vertical-align: top; > input,
> form {
&:last-child {
margin-right: 0;
}
}
> .btn {
margin-right: $gl-padding-top; margin-right: $gl-padding-top;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
float: right;
} }
} }
...@@ -189,19 +183,21 @@ ...@@ -189,19 +183,21 @@
float: none; float: none;
} }
> form {
display: inline-block;
}
.icon-label { .icon-label {
display: none; display: none;
} }
input { .btn,
.dropdown,
.dropdown-toggle,
input,
form {
height: 35px; height: 35px;
}
input {
display: inline-block; display: inline-block;
position: relative; position: relative;
margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */ /* Medium devices (desktops, 992px and up) */
@media (min-width: $screen-md-min) { width: 200px; } @media (min-width: $screen-md-min) { width: 200px; }
...@@ -225,6 +221,7 @@ ...@@ -225,6 +221,7 @@
.btn, .btn,
form, form,
.dropdown, .dropdown,
.dropdown-toggle,
.dropdown-menu-toggle, .dropdown-menu-toggle,
.form-control { .form-control {
margin: 0 0 10px; margin: 0 0 10px;
...@@ -263,6 +260,10 @@ ...@@ -263,6 +260,10 @@
.nav-text, .nav-text,
.nav-controls { .nav-controls {
width: auto; width: auto;
@media (max-width: $screen-xs-max) {
width: 100%;
}
} }
} }
...@@ -275,6 +276,10 @@ ...@@ -275,6 +276,10 @@
padding: 17px 0; padding: 17px 0;
} }
} }
pre {
width: 100%;
}
} }
.layout-nav { .layout-nav {
...@@ -427,4 +432,41 @@ ...@@ -427,4 +432,41 @@
border-bottom: none; border-bottom: none;
} }
} }
} }
\ No newline at end of file
@media (max-width: $screen-xs-max) {
.top-area {
flex-flow: row wrap;
.nav-controls {
$controls-margin: $btn-xs-side-margin - 2px;
flex: 0 0 100%;
&.controls-flex {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
padding: 0 0 $gl-padding-top;
}
.controls-item,
.controls-item-full,
.controls-item:last-child {
flex: 1 1 35%;
display: block;
width: 100%;
margin: $controls-margin;
.btn,
.dropdown {
margin: 0;
}
}
.controls-item-full {
flex: 1 1 100%;
}
}
}
}
...@@ -101,6 +101,17 @@ ...@@ -101,6 +101,17 @@
padding: 0 8px; padding: 0 8px;
border-radius: 6px; border-radius: 6px;
} }
.about-gitlab {
padding: 7px $gl-sidebar-padding;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
position: absolute;
bottom: 10px;
}
} }
.sidebar-action-buttons { .sidebar-action-buttons {
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
// Labels // Labels
.label { .label {
padding: 4px 5px; padding: 4px 5px;
font-size: 13px; font-size: 12px;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
......
...@@ -489,9 +489,9 @@ $project-network-controls-color: #888; ...@@ -489,9 +489,9 @@ $project-network-controls-color: #888;
*/ */
$runner-state-shared-bg: #32b186; $runner-state-shared-bg: #32b186;
$runner-state-specific-bg: #3498db; $runner-state-specific-bg: #3498db;
$runner-status-online-color: green; $runner-status-online-color: $green-normal;
$runner-status-offline-color: gray; $runner-status-offline-color: $gray-darkest;
$runner-status-paused-color: red; $runner-status-paused-color: $red-normal;
/* /*
Stat Graph Stat Graph
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
.dropdown-content { .dropdown-content {
max-height: 150px; max-height: 150px;
} }
} }
.issue-board-dropdown-content { .issue-board-dropdown-content {
...@@ -109,6 +109,12 @@ ...@@ -109,6 +109,12 @@
&.has-border { &.has-border {
border-top: 3px solid; border-top: 3px solid;
margin-top: -1px;
margin-right: -1px;
margin-left: -1px;
padding-top: 1px;
padding-right: 1px;
padding-left: 1px;
.board-title { .board-title {
padding-top: ($gl-padding - 3px); padding-top: ($gl-padding - 3px);
......
.deploy-keys-list {
width: 100%;
overflow: auto;
table {
border: 1px solid $table-border-color;
}
}
.deploy-keys-title {
padding-bottom: 2px;
line-height: 2;
}
// Limit MR description for side-by-side diff view
.limit-container-width {
.detail-page-header {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
.issuable-details {
.detail-page-description,
.mr-source-target,
.mr-state-widget,
.merge-manually {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
.merge-request-tabs-holder {
&.affix {
border-bottom: 1px solid $border-color;
.nav-links {
border: 0;
}
}
.container-fluid {
padding-left: 0;
padding-right: 0;
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
}
}
.diffs {
.mr-version-controls,
.files-changed {
max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
margin-left: auto;
margin-right: auto;
}
}
}
.issuable-details { .issuable-details {
section { section {
.issuable-discussion { .issuable-discussion {
......
...@@ -98,7 +98,7 @@ ...@@ -98,7 +98,7 @@
} }
.label { .label {
padding: 8px 9px 9px $gl-padding; padding: 8px 9px 9px;
font-size: 14px; font-size: 14px;
} }
} }
......
...@@ -105,19 +105,19 @@ ...@@ -105,19 +105,19 @@
li { li {
flex: 1; flex: 1;
text-align: center; text-align: center;
border-left: 1px solid $border-color;
&:first-of-type { &:first-of-type {
border-left: none;
border-top-left-radius: $border-radius-default; border-top-left-radius: $border-radius-default;
} }
&:last-of-type { &:last-of-type {
border-left: 1px solid $border-color;
border-top-right-radius: $border-radius-default; border-top-right-radius: $border-radius-default;
} }
&:not(.active) { &:not(.active) {
background-color: $gray-light; background-color: $gray-light;
border-left: 1px solid $border-color;
} }
a { a {
......
...@@ -424,11 +424,19 @@ ...@@ -424,11 +424,19 @@
.merge-request-tabs-holder { .merge-request-tabs-holder {
background-color: $white-light; background-color: $white-light;
.container-limited {
max-width: $limited-layout-width;
}
&.affix { &.affix {
top: 100px; top: 100px;
left: 0; left: 0;
z-index: 10; z-index: 10;
transition: right .15s; transition: right .15s;
@media (max-width: $screen-xs-max) {
right: 0;
}
} }
&:not(.affix) .container-fluid { &:not(.affix) .container-fluid {
......
...@@ -109,7 +109,7 @@ ...@@ -109,7 +109,7 @@
margin: auto; margin: auto;
margin-top: 0; margin-top: 0;
text-align: center; text-align: center;
font-size: 13px; font-size: 12px;
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
// On smaller devices the warning becomes the fourth item in the list, // On smaller devices the warning becomes the fourth item in the list,
......
...@@ -557,18 +557,14 @@ ul.notes { ...@@ -557,18 +557,14 @@ ul.notes {
&.is-active { &.is-active {
color: $gl-text-green; color: $gl-text-green;
svg path { svg {
fill: $gl-text-green; fill: $gl-text-green;
} }
} }
svg { svg {
position: relative; position: relative;
color: $gray-darkest; fill: $gray-darkest;
path {
fill: $gray-darkest;
}
} }
} }
......
...@@ -635,8 +635,8 @@ ...@@ -635,8 +635,8 @@
.grouped-pipeline-dropdown { .grouped-pipeline-dropdown {
padding: 0; padding: 0;
width: 191px; width: 195px;
min-width: 191px; min-width: 195px;
left: auto; left: auto;
right: -195px; right: -195px;
top: -4px; top: -4px;
......
...@@ -22,8 +22,8 @@ ...@@ -22,8 +22,8 @@
background: $theme-graphite; background: $theme-graphite;
} }
&.ui_gray { &.ui_black {
background: $theme-gray; background: $theme-black;
} }
&.ui_green { &.ui_green {
......
...@@ -627,6 +627,12 @@ pre.light-well { ...@@ -627,6 +627,12 @@ pre.light-well {
} }
} }
.commits-search-form {
.input-short {
min-width: 200px;
}
}
.project-last-commit { .project-last-commit {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
margin-top: $gl-padding; margin-top: $gl-padding;
......
...@@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController ...@@ -10,7 +10,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
end end
def create def create
@deploy_key = deploy_keys.new(deploy_key_params) @deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user))
if @deploy_key.save if @deploy_key.save
redirect_to admin_deploy_keys_path redirect_to admin_deploy_keys_path
...@@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController ...@@ -39,6 +39,6 @@ class Admin::DeployKeysController < Admin::ApplicationController
end end
def deploy_key_params def deploy_key_params
params.require(:deploy_key).permit(:key, :title) params.require(:deploy_key).permit(:key, :title, :can_push)
end end
end end
class Admin::GroupsController < Admin::ApplicationController class Admin::GroupsController < Admin::ApplicationController
before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update] before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index def index
@groups = Group.all @groups = Group.with_statistics
@groups = @groups.sort(@sort = params[:sort]) @groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page]) @groups = @groups.page(params[:page])
end end
def show def show
@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 = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = AccessRequestsFinder.new(@group).execute(current_user) @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.page(params[:projects_page]) @projects = @group.projects.with_statistics.page(params[:projects_page])
end end
def new def new
......
...@@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer] before_action :group, only: [:show, :transfer]
def index def index
@projects = Project.all @projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.with_push if params[:with_push].present? @projects = @projects.with_push if params[:with_push].present?
......
...@@ -16,9 +16,6 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -16,9 +16,6 @@ class Admin::UsersController < Admin::ApplicationController
@joined_projects = user.projects.joined(@user) @joined_projects = user.projects.joined(@user)
end end
def groups
end
def keys def keys
@keys = user.keys @keys = user.keys
end end
......
...@@ -82,7 +82,7 @@ module CreatesCommit ...@@ -82,7 +82,7 @@ module CreatesCommit
return @merge_request if defined?(@merge_request) return @merge_request if defined?(@merge_request)
@merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened. @merge_request = MergeRequestsFinder.new(current_user, project_id: @mr_target_project.id).execute.opened.
find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch) find_by(source_branch: @mr_source_branch, target_branch: @mr_target_branch, source_project_id: @mr_source_project)
end end
def different_project? def different_project?
......
...@@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -4,6 +4,9 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def index def index
@sort = params[:sort] @sort = params[:sort]
@todos = @todos.page(params[:page]) @todos = @todos.page(params[:page])
if @todos.out_of_range? && @todos.total_pages != 0
redirect_to url_for(params.merge(page: @todos.total_pages))
end
end end
def destroy def destroy
......
...@@ -42,6 +42,8 @@ class GroupsController < Groups::ApplicationController ...@@ -42,6 +42,8 @@ class GroupsController < Groups::ApplicationController
@notification_setting = current_user.notification_settings_for(group) @notification_setting = current_user.notification_settings_for(group)
end end
@nested_groups = group.children
setup_projects setup_projects
respond_to do |format| respond_to do |format|
...@@ -75,7 +77,7 @@ class GroupsController < Groups::ApplicationController ...@@ -75,7 +77,7 @@ class GroupsController < Groups::ApplicationController
end end
def projects def projects
@projects = @group.projects.page(params[:page]) @projects = @group.projects.with_statistics.page(params[:page])
end end
def update def update
......
...@@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end end
def create def create
@key = DeployKey.new(deploy_key_params) @key = DeployKey.new(deploy_key_params.merge(user: current_user))
set_index_vars set_index_vars
if @key.valid? && @project.deploy_keys << @key if @key.valid? && @project.deploy_keys << @key
...@@ -53,6 +53,6 @@ class Projects::DeployKeysController < Projects::ApplicationController ...@@ -53,6 +53,6 @@ class Projects::DeployKeysController < Projects::ApplicationController
end end
def deploy_key_params def deploy_key_params
params.require(:deploy_key).permit(:key, :title) params.require(:deploy_key).permit(:key, :title, :can_push)
end end
end end
...@@ -25,6 +25,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -25,6 +25,9 @@ class Projects::IssuesController < Projects::ApplicationController
def index def index
@issues = issues_collection @issues = issues_collection
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
end
if params[:label_name].present? if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
......
...@@ -38,6 +38,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -38,6 +38,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def index def index
@merge_requests = merge_requests_collection @merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
end
if params[:label_name].present? if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] } labels_params = { project_id: @project.id, title: params[:label_name] }
......
...@@ -26,6 +26,9 @@ class Projects::SnippetsController < Projects::ApplicationController ...@@ -26,6 +26,9 @@ class Projects::SnippetsController < Projects::ApplicationController
scope: params[:scope] scope: params[:scope]
) )
@snippets = @snippets.page(params[:page]) @snippets = @snippets.page(params[:page])
if @snippets.out_of_range? && @snippets.total_pages != 0
redirect_to namespace_project_snippets_path(page: @snippets.total_pages)
end
end end
def new def new
......
...@@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::TagsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy] before_action :authorize_admin_project!, only: [:destroy]
def index def index
params[:sort] = params[:sort].presence || 'name' params[:sort] = params[:sort].presence || sort_value_recently_updated
@sort = params[:sort] @sort = params[:sort]
@tags = TagsFinder.new(@repository, params).execute @tags = TagsFinder.new(@repository, params).execute
......
...@@ -61,7 +61,7 @@ module ProjectsHelper ...@@ -61,7 +61,7 @@ module ProjectsHelper
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
if current_user if current_user
project_link << button_tag(type: 'button', class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" }) do project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
icon("chevron-down") icon("chevron-down")
end end
end end
...@@ -90,10 +90,12 @@ module ProjectsHelper ...@@ -90,10 +90,12 @@ module ProjectsHelper
end end
def project_for_deploy_key(deploy_key) def project_for_deploy_key(deploy_key)
if deploy_key.projects.include?(@project) if deploy_key.has_access_to?(@project)
@project @project
else else
deploy_key.projects.find { |project| can?(current_user, :read_project, project) } deploy_key.projects.find do |project|
can?(current_user, :read_project, project)
end
end end
end end
...@@ -171,48 +173,27 @@ module ProjectsHelper ...@@ -171,48 +173,27 @@ module ProjectsHelper
nav_tabs << :merge_requests nav_tabs << :merge_requests
end end
if can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
if can?(current_user, :read_build, project)
nav_tabs << :builds
end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
nav_tabs << :container_registry nav_tabs << :container_registry
end end
if can?(current_user, :read_environment, project) tab_ability_map = {
nav_tabs << :environments environments: :read_environment,
end milestones: :read_milestone,
pipelines: :read_pipeline,
if can?(current_user, :admin_project, project) snippets: :read_project_snippet,
nav_tabs << :settings settings: :admin_project,
end builds: :read_build,
labels: :read_label,
if can?(current_user, :read_project_member, project) issues: :read_issue,
nav_tabs << :team team: :read_project_member,
end wiki: :read_wiki
}
if can?(current_user, :read_issue, project)
nav_tabs << :issues
end
if can?(current_user, :read_wiki, project)
nav_tabs << :wiki
end
if can?(current_user, :read_project_snippet, project)
nav_tabs << :snippets
end
if can?(current_user, :read_label, project)
nav_tabs << :labels
end
if can?(current_user, :read_milestone, project) tab_ability_map.each do |tab, ability|
nav_tabs << :milestones if can?(current_user, ability, project)
nav_tabs << tab
end
end end
nav_tabs.flatten nav_tabs.flatten
...@@ -246,11 +227,6 @@ module ProjectsHelper ...@@ -246,11 +227,6 @@ module ProjectsHelper
end end
end end
def repository_size(project = @project)
size_in_bytes = project.repository_size * 1.megabyte
number_to_human_size(size_in_bytes, delimiter: ',', precision: 2)
end
def default_url_to_repo(project = @project) def default_url_to_repo(project = @project)
case default_clone_protocol case default_clone_protocol
when 'ssh' when 'ssh'
...@@ -398,20 +374,6 @@ module ProjectsHelper ...@@ -398,20 +374,6 @@ module ProjectsHelper
[@project.path_with_namespace, sha, "readme"].join('-') [@project.path_with_namespace, sha, "readme"].join('-')
end end
def round_commit_count(project)
count = project.commit_count
if count > 10000
'10000+'
elsif count > 5000
'5000+'
elsif count > 1000
'1000+'
else
count
end
end
def current_ref def current_ref
@ref || @repository.try(:root_ref) @ref || @repository.try(:root_ref)
end end
......
...@@ -11,6 +11,7 @@ module SortingHelper ...@@ -11,6 +11,7 @@ module SortingHelper
sort_value_due_date_soon => sort_title_due_date_soon, sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later, sort_value_due_date_later => sort_title_due_date_later,
sort_value_largest_repo => sort_title_largest_repo, sort_value_largest_repo => sort_title_largest_repo,
sort_value_largest_group => sort_title_largest_group,
sort_value_recently_signin => sort_title_recently_signin, sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin, sort_value_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes, sort_value_downvotes => sort_title_downvotes,
...@@ -92,6 +93,10 @@ module SortingHelper ...@@ -92,6 +93,10 @@ module SortingHelper
'Largest repository' 'Largest repository'
end end
def sort_title_largest_group
'Largest group'
end
def sort_title_recently_signin def sort_title_recently_signin
'Recent sign in' 'Recent sign in'
end end
...@@ -193,7 +198,11 @@ module SortingHelper ...@@ -193,7 +198,11 @@ module SortingHelper
end end
def sort_value_largest_repo def sort_value_largest_repo
'repository_size_desc' 'storage_size_desc'
end
def sort_value_largest_group
'storage_size_desc'
end end
def sort_value_recently_signin def sort_value_recently_signin
......
module StorageHelper
def storage_counter(size_in_bytes)
precision = size_in_bytes < 1.megabyte ? 0 : 1
number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false)
end
end
...@@ -43,6 +43,8 @@ module Ci ...@@ -43,6 +43,8 @@ module Ci
before_destroy { project } before_destroy { project }
after_create :execute_hooks after_create :execute_hooks
after_save :update_project_statistics, if: :artifacts_size_changed?
after_destroy :update_project_statistics
class << self class << self
def first_pending def first_pending
...@@ -584,5 +586,9 @@ module Ci ...@@ -584,5 +586,9 @@ module Ci
Ci::MaskSecret.mask!(trace, token) Ci::MaskSecret.mask!(trace, token)
trace trace
end end
def update_project_statistics
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end
end end
end end
...@@ -93,11 +93,8 @@ module Ci ...@@ -93,11 +93,8 @@ module Ci
.select("max(#{quoted_table_name}.id)") .select("max(#{quoted_table_name}.id)")
.group(:ref, :sha) .group(:ref, :sha)
if ref relation = ref ? where(ref: ref) : self
where(id: max_id, ref: ref) relation.where(id: max_id)
else
where(id: max_id)
end
end end
def self.latest_status(ref = nil) def self.latest_status(ref = nil)
...@@ -105,7 +102,7 @@ module Ci ...@@ -105,7 +102,7 @@ module Ci
end end
def self.latest_successful_for(ref) def self.latest_successful_for(ref)
success.latest(ref).first success.latest(ref).order(id: :desc).first
end end
def self.truncate_sha(sha) def self.truncate_sha(sha)
......
...@@ -92,8 +92,9 @@ module Issuable ...@@ -92,8 +92,9 @@ module Issuable
after_save :record_metrics after_save :record_metrics
def update_assignee_cache_counts def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignee # make sure we flush the cache for both the old *and* new assignees(if they exist)
User.find(assignee_id_was).update_cache_counts if assignee_id_was previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
previous_assignee.update_cache_counts if previous_assignee
assignee.update_cache_counts if assignee assignee.update_cache_counts if assignee
end end
......
...@@ -20,4 +20,18 @@ class DeployKey < Key ...@@ -20,4 +20,18 @@ class DeployKey < Key
def destroyed_when_orphaned? def destroyed_when_orphaned?
self.private? self.private?
end end
def has_access_to?(project)
projects.include?(project)
end
def can_push_to?(project)
can_push? && has_access_to?(project)
end
private
# we don't want to notify the user for deploy keys
def notify_user
end
end end
...@@ -48,7 +48,13 @@ class Group < Namespace ...@@ -48,7 +48,13 @@ class Group < Namespace
end end
def sort(method) def sort(method)
order_by(method) if method == 'storage_size_desc'
# storage_size is a virtual column so we need to
# pass a string to avoid AR adding the table name
reorder('storage_size DESC, namespaces.id DESC')
else
order_by(method)
end
end end
def reference_prefix def reference_prefix
...@@ -155,15 +161,17 @@ class Group < Namespace ...@@ -155,15 +161,17 @@ class Group < Namespace
end end
def has_owner?(user) def has_owner?(user)
owners.include?(user) members_with_parents.owners.where(user_id: user).any?
end end
def has_master?(user) def has_master?(user)
members.masters.where(user_id: user).any? members_with_parents.masters.where(user_id: user).any?
end end
# Check if user is a last owner of the group.
# Parent owners are ignored for nested groups.
def last_owner?(user) def last_owner?(user)
has_owner?(user) && owners.size == 1 owners.include?(user) && owners.size == 1
end end
def avatar_type def avatar_type
...@@ -189,6 +197,14 @@ class Group < Namespace ...@@ -189,6 +197,14 @@ class Group < Namespace
end end
def refresh_members_authorized_projects def refresh_members_authorized_projects
UserProjectAccessChangedService.new(users.pluck(:id)).execute UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute
end
def members_with_parents
GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id))
end
def users_with_parents
User.where(id: members_with_parents.select(:user_id))
end end
end end
...@@ -57,10 +57,6 @@ class Key < ActiveRecord::Base ...@@ -57,10 +57,6 @@ class Key < ActiveRecord::Base
) )
end end
def notify_user
run_after_commit { NotificationService.new.new_key(self) }
end
def post_create_hook def post_create_hook
SystemHooksService.new.execute_hooks_for(self, :create) SystemHooksService.new.execute_hooks_for(self, :create)
end end
...@@ -86,4 +82,8 @@ class Key < ActiveRecord::Base ...@@ -86,4 +82,8 @@ class Key < ActiveRecord::Base
self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
end end
def notify_user
run_after_commit { NotificationService.new.new_key(self) }
end
end end
...@@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base ...@@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base
validates :lfs_object_id, presence: true validates :lfs_object_id, presence: true
validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_id, presence: true validates :project_id, presence: true
after_create :update_project_statistics
after_destroy :update_project_statistics
private
def update_project_statistics
ProjectCacheWorker.perform_async(project_id, [], [:lfs_objects_size])
end
end end
...@@ -198,7 +198,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -198,7 +198,9 @@ class MergeRequest < ActiveRecord::Base
end end
def diff_size def diff_size
diffs(diff_options).size opts = diff_options || {}
raw_diffs(opts).size
end end
def diff_base_commit def diff_base_commit
...@@ -574,11 +576,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -574,11 +576,7 @@ class MergeRequest < ActiveRecord::Base
ext = Gitlab::ReferenceExtractor.new(project, current_user) ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(description) ext.analyze(description)
issues = ext.issues ext.issues - closes_issues
closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(description)
issues - closing_issues
end end
def target_project_path def target_project_path
......
...@@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base ...@@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy has_many :projects, dependent: :destroy
has_many :project_statistics
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace" belongs_to :parent, class_name: "Namespace"
...@@ -38,6 +39,18 @@ class Namespace < ActiveRecord::Base ...@@ -38,6 +39,18 @@ class Namespace < ActiveRecord::Base
scope :root, -> { where('type IS NULL') } scope :root, -> { where('type IS NULL') }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
.group('namespaces.id')
.select(
'namespaces.*',
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
)
end
class << self class << self
def by_path(path) def by_path(path)
find_by('lower(path) = :value', value: path.downcase) find_by('lower(path) = :value', value: path.downcase)
......
...@@ -44,6 +44,7 @@ class Project < ActiveRecord::Base ...@@ -44,6 +44,7 @@ class Project < ActiveRecord::Base
after_create :ensure_dir_exist after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature after_create :create_project_feature, unless: :project_feature
after_save :ensure_dir_exist, if: :namespace_id_changed? after_save :ensure_dir_exist, if: :namespace_id_changed?
after_save :update_project_statistics, if: :namespace_id_changed?
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
after_create :set_last_activity_at after_create :set_last_activity_at
...@@ -151,6 +152,7 @@ class Project < ActiveRecord::Base ...@@ -151,6 +152,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
...@@ -220,6 +222,7 @@ class Project < ActiveRecord::Base ...@@ -220,6 +222,7 @@ class Project < ActiveRecord::Base
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
# "enabled" here means "not disabled". It includes private features! # "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) { scope :with_feature_enabled, ->(feature) {
...@@ -332,8 +335,10 @@ class Project < ActiveRecord::Base ...@@ -332,8 +335,10 @@ class Project < ActiveRecord::Base
end end
def sort(method) def sort(method)
if method == 'repository_size_desc' if method == 'storage_size_desc'
reorder(repository_size: :desc, id: :desc) # storage_size is a joined column so we need to
# pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC')
else else
order_by(method) order_by(method)
end end
...@@ -921,7 +926,7 @@ class Project < ActiveRecord::Base ...@@ -921,7 +926,7 @@ class Project < ActiveRecord::Base
Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present"
# we currently doesn't support renaming repository if it contains tags in container registry # we currently doesn't support renaming repository if it contains tags in container registry
raise Exception.new('Project cannot be renamed, because tags are present in its container registry') raise StandardError.new('Project cannot be renamed, because tags are present in its container registry')
end end
if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
...@@ -948,7 +953,7 @@ class Project < ActiveRecord::Base ...@@ -948,7 +953,7 @@ class Project < ActiveRecord::Base
# if we cannot move namespace directory we should rollback # if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs # db changes in order to prevent out of sync between db and fs
raise Exception.new('repository cannot be renamed') raise StandardError.new('repository cannot be renamed')
end end
Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
...@@ -1036,14 +1041,6 @@ class Project < ActiveRecord::Base ...@@ -1036,14 +1041,6 @@ class Project < ActiveRecord::Base
forked? && project == forked_from_project forked? && project == forked_from_project
end end
def update_repository_size
update_attribute(:repository_size, repository.size)
end
def update_commit_count
update_attribute(:commit_count, repository.commit_count)
end
def forks_count def forks_count
forks.count forks.count
end end
...@@ -1322,4 +1319,9 @@ class Project < ActiveRecord::Base ...@@ -1322,4 +1319,9 @@ class Project < ActiveRecord::Base
def full_path_changed? def full_path_changed?
path_changed? || namespace_id_changed? path_changed? || namespace_id_changed?
end end
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
end
end end
class ProjectStatistics < ActiveRecord::Base
belongs_to :project
belongs_to :namespace
before_save :update_storage_size
STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size]
STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
def total_repository_size
repository_size + lfs_objects_size
end
def refresh!(only: nil)
STATISTICS_COLUMNS.each do |column, generator|
if only.blank? || only.include?(column)
public_send("update_#{column}")
end
end
save!
end
def update_commit_count
self.commit_count = project.repository.commit_count
end
def update_repository_size
self.repository_size = project.repository.size
end
def update_lfs_objects_size
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
def update_build_artifacts_size
self.build_artifacts_size = project.builds.sum(:artifacts_size)
end
def update_storage_size
self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute))
end
end
...@@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy ...@@ -4,7 +4,7 @@ class GroupPolicy < BasePolicy
return unless @user return unless @user
globally_viewable = @subject.public? || (@subject.internal? && !@user.external?) globally_viewable = @subject.public? || (@subject.internal? && !@user.external?)
member = @subject.users.include?(@user) member = @subject.users_with_parents.include?(@user)
owner = @user.admin? || @subject.has_owner?(@user) owner = @user.admin? || @subject.has_owner?(@user)
master = owner || @subject.has_master?(@user) master = owner || @subject.has_master?(@user)
......
...@@ -171,9 +171,7 @@ class ProjectPolicy < BasePolicy ...@@ -171,9 +171,7 @@ class ProjectPolicy < BasePolicy
def disabled_features! def disabled_features!
repository_enabled = project.feature_available?(:repository, user) repository_enabled = project.feature_available?(:repository, user)
unless project.feature_available?(:issues, user) block_issues_abilities
cannot!(*named_abilities(:issue))
end
unless project.feature_available?(:merge_requests, user) && repository_enabled unless project.feature_available?(:merge_requests, user) && repository_enabled
cannot!(*named_abilities(:merge_request)) cannot!(*named_abilities(:merge_request))
...@@ -245,11 +243,20 @@ class ProjectPolicy < BasePolicy ...@@ -245,11 +243,20 @@ class ProjectPolicy < BasePolicy
def project_group_member?(user) def project_group_member?(user)
project.group && project.group &&
( (
project.group.members.exists?(user_id: user.id) || project.group.members_with_parents.exists?(user_id: user.id) ||
project.group.requesters.exists?(user_id: user.id) project.group.requesters.exists?(user_id: user.id)
) )
end end
def block_issues_abilities
unless project.feature_available?(:issues, user)
cannot! :read_issue if project.default_issues_tracker?
cannot! :create_issue
cannot! :update_issue
cannot! :admin_issue
end
end
def named_abilities(name) def named_abilities(name)
[ [
:"read_#{name}", :"read_#{name}",
......
...@@ -77,7 +77,7 @@ class GitPushService < BaseService ...@@ -77,7 +77,7 @@ class GitPushService < BaseService
types = [] types = []
end end
ProjectCacheWorker.perform_async(@project.id, types) ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size])
end end
# Schedules processing of commit messages. # Schedules processing of commit messages.
......
...@@ -12,7 +12,7 @@ class GitTagPushService < BaseService ...@@ -12,7 +12,7 @@ class GitTagPushService < BaseService
project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks)
Ci::CreatePipelineService.new(project, current_user, @push_data).execute Ci::CreatePipelineService.new(project, current_user, @push_data).execute
ProjectCacheWorker.perform_async(project.id) ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size])
true true
end end
......
...@@ -12,6 +12,13 @@ module Groups ...@@ -12,6 +12,13 @@ module Groups
return @group return @group
end end
if @group.parent && !can?(current_user, :admin_group, @group.parent)
@group.parent = nil
@group.errors.add(:parent_id, 'manage access required to create subgroup')
return @group
end
@group.name ||= @group.path.dup @group.name ||= @group.path.dup
@group.save @group.save
@group.add_owner(current_user) @group.add_owner(current_user)
......
...@@ -36,14 +36,10 @@ class IssuableBaseService < BaseService ...@@ -36,14 +36,10 @@ class IssuableBaseService < BaseService
end end
end end
def filter_params(issuable_ability_name = :issue) def filter_params(issuable)
filter_assignee ability_name = :"admin_#{issuable.to_ability_name}"
filter_milestone
filter_labels
ability = :"admin_#{issuable_ability_name}" unless can?(current_user, ability_name, project)
unless can?(current_user, ability, project)
params.delete(:milestone_id) params.delete(:milestone_id)
params.delete(:labels) params.delete(:labels)
params.delete(:add_label_ids) params.delete(:add_label_ids)
...@@ -52,14 +48,35 @@ class IssuableBaseService < BaseService ...@@ -52,14 +48,35 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id) params.delete(:assignee_id)
params.delete(:due_date) params.delete(:due_date)
end end
filter_assignee(issuable)
filter_milestone
filter_labels
end end
def filter_assignee def filter_assignee(issuable)
if params[:assignee_id] == IssuableFinder::NONE return unless params[:assignee_id].present?
params[:assignee_id] = ''
assignee_id = params[:assignee_id]
if assignee_id.to_s == IssuableFinder::NONE
params[:assignee_id] = ""
else
params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
end end
end end
def assignee_can_read?(issuable, assignee_id)
new_assignee = User.find_by_id(assignee_id)
return false unless new_assignee.present?
ability_name = :"read_#{issuable.to_ability_name}"
resource = issuable.persisted? ? issuable : project
can?(new_assignee, ability_name, resource)
end
def filter_milestone def filter_milestone
milestone_id = params[:milestone_id] milestone_id = params[:milestone_id]
return unless milestone_id return unless milestone_id
...@@ -138,7 +155,7 @@ class IssuableBaseService < BaseService ...@@ -138,7 +155,7 @@ class IssuableBaseService < BaseService
def create(issuable) def create(issuable)
merge_slash_commands_into_params!(issuable) merge_slash_commands_into_params!(issuable)
filter_params filter_params(issuable)
params.delete(:state_event) params.delete(:state_event)
params[:author] ||= current_user params[:author] ||= current_user
...@@ -180,7 +197,7 @@ class IssuableBaseService < BaseService ...@@ -180,7 +197,7 @@ class IssuableBaseService < BaseService
change_state(issuable) change_state(issuable)
change_subscription(issuable) change_subscription(issuable)
change_todo(issuable) change_todo(issuable)
filter_params filter_params(issuable)
old_labels = issuable.labels.to_a old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a old_mentioned_users = issuable.mentioned_users.to_a
......
...@@ -17,10 +17,6 @@ module Issues ...@@ -17,10 +17,6 @@ module Issues
private private
def filter_params
super(:issue)
end
def execute_hooks(issue, action = 'open') def execute_hooks(issue, action = 'open')
issue_data = hook_data(issue, action) issue_data = hook_data(issue, action)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
......
...@@ -38,10 +38,6 @@ module MergeRequests ...@@ -38,10 +38,6 @@ module MergeRequests
private private
def filter_params
super(:merge_request)
end
def merge_requests_for(branch) def merge_requests_for(branch)
origin_merge_requests = @project.origin_merge_requests origin_merge_requests = @project.origin_merge_requests
.opened.where(source_branch: branch).to_a .opened.where(source_branch: branch).to_a
......
...@@ -41,7 +41,7 @@ module Notes ...@@ -41,7 +41,7 @@ module Notes
# We must add the error after we call #save because errors are reset # We must add the error after we call #save because errors are reset
# when #save is called # when #save is called
if only_commands if only_commands
note.errors.add(:commands_only, 'Your commands have been executed!') note.errors.add(:commands_only, 'Commands applied')
end end
note.commands_changes = command_params.keys note.commands_changes = command_params.keys
......
...@@ -74,7 +74,7 @@ module Users ...@@ -74,7 +74,7 @@ module Users
# remove - The IDs of the authorization rows to remove. # remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]` # add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = []) def update_authorizations(remove = [], add = [])
return if remove.empty? && add.empty? return if remove.empty? && add.empty? && user.authorized_projects_populated
User.transaction do User.transaction do
user.remove_project_authorizations(remove) unless remove.empty? user.remove_project_authorizations(remove) unless remove.empty?
......
%p %p
#{link_to @abuse_report.user.name, user_url(@abuse_report.user)} #{link_to @abuse_report.user.name, user_url(@abuse_report.user)}
(@#{@abuse_report.user.username}) was reported for abuse by (@#{@abuse_report.user.username}) was reported for abuse by
#{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)} #{link_to @abuse_report.reporter.name, user_url(@abuse_report.reporter)}
(@#{@abuse_report.reporter.username}). (@#{@abuse_report.reporter.username}).
%blockquote %blockquote
......
...@@ -321,7 +321,7 @@ ...@@ -321,7 +321,7 @@
= f.text_field :recaptcha_site_key, class: 'form-control' = f.text_field :recaptcha_site_key, class: 'form-control'
.help-block .help-block
Generate site and private keys at Generate site and private keys at
%a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
.form-group .form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2' = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
...@@ -342,7 +342,7 @@ ...@@ -342,7 +342,7 @@
= f.text_field :akismet_api_key, class: 'form-control' = f.text_field :akismet_api_key, class: 'form-control'
.help-block .help-block
Generate API key at Generate API key at
%a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
%fieldset %fieldset
%legend Abuse reports %legend Abuse reports
......
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm' - submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do = form_tag admin_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/ %input{ :name => "_method", :type => "hidden", :value => "delete" }/
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
%th %th
%tbody.oauth-applications %tbody.oauth-applications
- @applications.each do |application| - @applications.each do |application|
%tr{:id => "application_#{application.id}"} %tr{ :id => "application_#{application.id}" }
%td= link_to application.name, admin_application_path(application) %td= link_to application.name, admin_application_path(application)
%td= application.redirect_uri %td= application.redirect_uri
%td= application.access_tokens.map(&:resource_owner_id).uniq.count %td= application.access_tokens.map(&:resource_owner_id).uniq.count
......
...@@ -43,4 +43,4 @@ ...@@ -43,4 +43,4 @@
.panel.panel-default .panel.panel-default
%iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"} %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: none" }
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
%br.clearfix %br.clearfix
-if @broadcast_messages.any? - if @broadcast_messages.any?
%table.table %table.table
%thead %thead
%tr %tr
......
- page_title "Deploy Keys" - page_title "Deploy Keys"
.panel.panel-default.prepend-top-default
.panel-heading %h3.page-title.deploy-keys-title
Public deploy keys (#{@deploy_keys.count}) Public deploy keys (#{@deploy_keys.count})
.controls .pull-right
= link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm" = link_to 'New Deploy Key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted'
- if @deploy_keys.any?
.table-holder - if @deploy_keys.any?
%table.table .table-holder.deploy-keys-list
%thead.panel-heading %table.table
%thead
%tr
%th.col-sm-2 Title
%th.col-sm-4 Fingerprint
%th.col-sm-2 Write access allowed
%th.col-sm-2 Added at
%th.col-sm-2
%tbody
- @deploy_keys.each do |deploy_key|
%tr %tr
%th Title %td
%th Fingerprint %strong= deploy_key.title
%th Added at %td
%th %code.key-fingerprint= deploy_key.fingerprint
%tbody %td
- @deploy_keys.each do |deploy_key| - if deploy_key.can_push?
%tr Yes
%td - else
%strong= deploy_key.title No
%td %td
%code.key-fingerprint= deploy_key.fingerprint %span.cgray
%td added #{time_ago_with_tooltip(deploy_key.created_at)}
%span.cgray %td
added #{time_ago_with_tooltip(deploy_key.created_at)} = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key pull-right'
%td
= link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
...@@ -16,6 +16,14 @@ ...@@ -16,6 +16,14 @@
Paste a machine public key here. Read more about how to generate it Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh/README") = link_to "here", help_page_path("ssh/README")
= f.text_area :key, class: "form-control thin_area", rows: 5 = f.text_area :key, class: "form-control thin_area", rows: 5
.form-group
.control-label
.col-sm-10
= f.label :can_push do
= f.check_box :can_push
%strong Write access allowed
%p.light.append-bottom-0
Allow this key to push to repository as well? (Default only allows pull access.)
.form-actions .form-actions
= f.submit 'Create', class: "btn-create btn" = f.submit 'Create', class: "btn-create btn"
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
= link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn' = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats .stats
%span.badge
= storage_counter(group.storage_size)
%span %span
= icon('bookmark') = icon('bookmark')
= number_with_delimiter(group.projects.count) = number_with_delimiter(group.projects.count)
...@@ -13,7 +16,7 @@ ...@@ -13,7 +16,7 @@
= icon('users') = icon('users')
= number_with_delimiter(group.users.count) = number_with_delimiter(group.users.count)
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
= visibility_level_icon(group.visibility_level, fw: false) = visibility_level_icon(group.visibility_level, fw: false)
.avatar-container.s40 .avatar-container.s40
......
...@@ -27,6 +27,8 @@ ...@@ -27,6 +27,8 @@
= sort_title_recently_updated = sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
= sort_title_oldest_updated = sort_title_oldest_updated
= link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do
= sort_title_largest_group
= link_to new_admin_group_path, class: "btn btn-new" do = link_to new_admin_group_path, class: "btn btn-new" do
New Group New Group
%ul.content-list %ul.content-list
......
...@@ -38,6 +38,18 @@ ...@@ -38,6 +38,18 @@
%strong %strong
= @group.created_at.to_s(:medium) = @group.created_at.to_s(:medium)
%li
%span.light Storage:
%strong= storage_counter(@group.storage_size)
(
= storage_counter(@group.repository_size)
repositories,
= storage_counter(@group.build_artifacts_size)
build artifacts,
= storage_counter(@group.lfs_objects_size)
LFS
)
%li %li
%span.light Group Git LFS status: %span.light Group Git LFS status:
%strong %strong
...@@ -55,8 +67,8 @@ ...@@ -55,8 +67,8 @@
%li %li
%strong %strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
%span.label.label-gray %span.badge
= repository_size(project) = storage_counter(project.statistics.storage_size)
%span.pull-right.light %span.pull-right.light
%span.monospace= project.path_with_namespace + ".git" %span.monospace= project.path_with_namespace + ".git"
.panel-footer .panel-footer
...@@ -73,8 +85,8 @@ ...@@ -73,8 +85,8 @@
%li %li
%strong %strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
%span.label.label-gray %span.badge
= repository_size(project) = storage_counter(project.statistics.storage_size)
%span.pull-right.light %span.pull-right.light
%span.monospace= project.path_with_namespace + ".git" %span.monospace= project.path_with_namespace + ".git"
...@@ -91,7 +103,7 @@ ...@@ -91,7 +103,7 @@
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div %div
= users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
%div.prepend-top-10 .prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr %hr
= button_tag 'Add users to group', class: "btn btn-create" = button_tag 'Add users to group', class: "btn btn-create"
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
System hook will be triggered on set of events like creating project System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events. or adding ssh key. But you can also enable extra triggers like Push events.
%div.prepend-top-default .prepend-top-default
= f.check_box :push_events, class: 'pull-left' = f.check_box :push_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :push_events, class: 'list-label' do = f.label :push_events, class: 'list-label' do
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
= f.submit "Add System Hook", class: "btn btn-create" = f.submit "Add System Hook", class: "btn btn-create"
%hr %hr
-if @hooks.any? - if @hooks.any?
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
System hooks (#{@hooks.count}) System hooks (#{@hooks.count})
...@@ -70,4 +70,3 @@ ...@@ -70,4 +70,3 @@
- if hook.send(trigger) - if hook.send(trigger)
%span.label.label-gray= trigger.titleize %span.label.label-gray= trigger.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
%li{id: dom_id(label)} %li{ id: dom_id(label) }
.label-row .label-row
= render_colored_label(label, tooltip: false) = render_colored_label(label, tooltip: false)
= markdown_field(label, :description) = markdown_field(label, :description)
......
...@@ -69,8 +69,8 @@ ...@@ -69,8 +69,8 @@
.controls .controls
- if project.archived - if project.archived
%span.label.label-warning archived %span.label.label-warning archived
%span.label.label-gray %span.badge
= repository_size(project) = storage_counter(project.statistics.storage_size)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
.title .title
......
...@@ -65,9 +65,16 @@ ...@@ -65,9 +65,16 @@
= @project.repository.path_to_repo = @project.repository.path_to_repo
%li %li
%span.light Size %span.light Storage:
%strong %strong= storage_counter(@project.statistics.storage_size)
= repository_size(@project) (
= storage_counter(@project.statistics.repository_size)
repository,
= storage_counter(@project.statistics.build_artifacts_size)
build artifacts,
= storage_counter(@project.statistics.lfs_objects_size)
LFS
)
%li %li
%span.light last commit: %span.light last commit:
......
%tr{id: dom_id(runner)} %tr{ id: dom_id(runner) }
%td %td
- if runner.shared? - if runner.shared?
%span.label.label-success shared %span.label.label-success shared
......
...@@ -15,10 +15,8 @@ ...@@ -15,10 +15,8 @@
%ul.nav-links %ul.nav-links
= nav_link(path: 'users#show') do = nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user) = link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#groups') do
= link_to "Groups", groups_admin_user_path(@user)
= nav_link(path: 'users#projects') do = nav_link(path: 'users#projects') do
= link_to "Projects", projects_admin_user_path(@user) = link_to "Groups and projects", projects_admin_user_path(@user)
= nav_link(path: 'users#keys') do = nav_link(path: 'users#keys') do
= link_to "SSH keys", keys_admin_user_path(@user) = link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do = nav_link(controller: :identities) do
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn'
- unless user == current_user - unless user == current_user
.dropdown.inline .dropdown.inline
%a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } } %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', data: { toggle: 'dropdown' } }
= icon('cog') = icon('cog')
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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