Commit 5e2f7f25 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into '33580-fix-api-scoping'

# Conflicts:
#   lib/api/users.rb
parents afbc7520 016b9f25
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
"gon": false, "gon": false,
"localStorage": false "localStorage": false
}, },
"parser": "babel-eslint",
"plugins": [ "plugins": [
"filenames", "filenames",
"import", "import",
......
...@@ -63,7 +63,7 @@ stages: ...@@ -63,7 +63,7 @@ stages:
.only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql .only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql
only: only:
- /mysql/ - /mysql/
- /-stable$/ - /-stable/
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
...@@ -474,8 +474,8 @@ codeclimate: ...@@ -474,8 +474,8 @@ codeclimate:
services: services:
- docker:dind - docker:dind
script: script:
- docker pull codeclimate/codeclimate - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
artifacts: artifacts:
paths: [codeclimate.json] paths: [codeclimate.json]
...@@ -550,3 +550,9 @@ cache gems: ...@@ -550,3 +550,9 @@ cache gems:
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
gitlab_git_test:
variables:
SETUP_DB: "false"
script:
- spec/support/prepare-gitlab-git-test-for-commit --check-for-changes
...@@ -2,6 +2,19 @@ ...@@ -2,6 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.3.4 (2017-07-03)
- No changes.
## 9.3.3 (2017-06-30)
- Fix head pipeline stored in merge request for external pipelines. !12478
- Bring back branches badge to main project page. !12548
- Fix diff of requirements.txt file by not matching newlines as part of package names.
- Perform housekeeping only when an import of a fresh project is completed.
- Fixed issue boards closed list not showing all closed issues.
- Fixed multi-line markdown tooltip buttons in issue edit form.
## 9.3.2 (2017-06-27) ## 9.3.2 (2017-06-27)
- API: Fix optional arugments for POST :id/variables. !12474 - API: Fix optional arugments for POST :id/variables. !12474
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
/* global Flash */ /* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import * as Emoji from './emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
...@@ -24,27 +23,9 @@ const categoryLabelMap = { ...@@ -24,27 +23,9 @@ const categoryLabelMap = {
flags: 'Flags', flags: 'Flags',
}; };
function renderCategory(name, emojiList, opts = {}) { class AwardsHandler {
return ` constructor(emoji) {
<h5 class="emoji-menu-title"> this.emoji = emoji;
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${Emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
</li>
`).join('\n')}
</ul>
`;
}
export default class AwardsHandler {
constructor() {
this.eventListeners = []; this.eventListeners = [];
// If the user shows intent let's pre-build the menu // If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
...@@ -78,10 +59,10 @@ export default class AwardsHandler { ...@@ -78,10 +59,10 @@ export default class AwardsHandler {
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon'); const $spriteIconElement = $target.find('.icon');
const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
$target.closest('.js-awards-block').addClass('current'); $target.closest('.js-awards-block').addClass('current');
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
}); });
} }
...@@ -139,16 +120,16 @@ export default class AwardsHandler { ...@@ -139,16 +120,16 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true; this.isCreatingEmojiMenu = true;
// Render the first category // Render the first category
const categoryMap = Emoji.getEmojiCategoryMap(); const categoryMap = this.emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0]; const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey]; const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
// Render the frequently used // Render the frequently used
const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
let frequentlyUsedCatgegory = ''; let frequentlyUsedCatgegory = '';
if (frequentlyUsedEmojis.length > 0) { if (frequentlyUsedEmojis.length > 0) {
frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
menuListClass: 'frequent-emojis', menuListClass: 'frequent-emojis',
}); });
} }
...@@ -179,7 +160,7 @@ export default class AwardsHandler { ...@@ -179,7 +160,7 @@ export default class AwardsHandler {
} }
this.isAddingRemainingEmojiMenuCategories = true; this.isAddingRemainingEmojiMenuCategories = true;
const categoryMap = Emoji.getEmojiCategoryMap(); const categoryMap = this.emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately // Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive // This will take more time, but makes UI more responsive
...@@ -191,7 +172,7 @@ export default class AwardsHandler { ...@@ -191,7 +172,7 @@ export default class AwardsHandler {
promiseChain.then(() => promiseChain.then(() =>
new Promise((resolve) => { new Promise((resolve) => {
const emojisInCategory = categoryMap[categoryNameKey]; const emojisInCategory = categoryMap[categoryNameKey];
const categoryMarkup = renderCategory( const categoryMarkup = this.renderCategory(
categoryLabelMap[categoryNameKey], categoryLabelMap[categoryNameKey],
emojisInCategory, emojisInCategory,
); );
...@@ -216,6 +197,25 @@ export default class AwardsHandler { ...@@ -216,6 +197,25 @@ export default class AwardsHandler {
}); });
} }
renderCategory(name, emojiList, opts = {}) {
return `
<h5 class="emoji-menu-title">
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${this.emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
</li>
`).join('\n')}
</ul>
`;
}
positionMenu($menu, $addBtn) { positionMenu($menu, $addBtn) {
const position = $addBtn.data('position'); const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element // The menu could potentially be off-screen or in a hidden overflow element
...@@ -234,7 +234,7 @@ export default class AwardsHandler { ...@@ -234,7 +234,7 @@ export default class AwardsHandler {
} }
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const normalizedEmoji = Emoji.normalizeEmojiName(emoji); const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
...@@ -249,7 +249,7 @@ export default class AwardsHandler { ...@@ -249,7 +249,7 @@ export default class AwardsHandler {
this.checkMutuality(votesBlock, emoji); this.checkMutuality(votesBlock, emoji);
} }
this.addEmojiToFrequentlyUsedList(emoji); this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = Emoji.normalizeEmojiName(emoji); const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) { if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) { if (this.isActive($emojiButton)) {
...@@ -374,7 +374,7 @@ export default class AwardsHandler { ...@@ -374,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = ` const buttonHtml = `
<button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom">
${Emoji.glEmojiTag(emojiName)} ${this.emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span> <span class="award-control-text js-counter">1</span>
</button> </button>
`; `;
...@@ -440,7 +440,7 @@ export default class AwardsHandler { ...@@ -440,7 +440,7 @@ export default class AwardsHandler {
} }
addEmojiToFrequentlyUsedList(emoji) { addEmojiToFrequentlyUsedList(emoji) {
if (Emoji.isEmojiNameValid(emoji)) { if (this.emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
} }
...@@ -450,7 +450,7 @@ export default class AwardsHandler { ...@@ -450,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => { return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => Emoji.isEmojiNameValid(inputName), inputName => this.emoji.isEmojiNameValid(inputName),
); );
return this.frequentlyUsedEmojis; return this.frequentlyUsedEmojis;
...@@ -493,7 +493,7 @@ export default class AwardsHandler { ...@@ -493,7 +493,7 @@ export default class AwardsHandler {
} }
findMatchingEmojiElements(query) { findMatchingEmojiElements(query) {
const emojiMatches = Emoji.filterEmojiNamesByAlias(query); const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0); .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
...@@ -507,3 +507,12 @@ export default class AwardsHandler { ...@@ -507,3 +507,12 @@ export default class AwardsHandler {
$('.emoji-menu').remove(); $('.emoji-menu').remove();
} }
} }
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
.then(Emoji => new AwardsHandler(Emoji));
}
return awardsHandlerPromise;
}
import autosize from 'vendor/autosize'; import autosize from 'vendor/autosize';
$(() => { document.addEventListener('DOMContentLoaded', () => {
const $fields = $('.js-autosize'); const autosizeEls = document.querySelectorAll('.js-autosize');
$fields.on('autosize:resized', function resized() { autosize(autosizeEls);
const $field = $(this); autosize.update(autosizeEls);
$field.data('height', $field.outerHeight());
});
$fields.on('resize.autosize', function resize() {
const $field = $(this);
if ($field.data('height') !== $field.outerHeight()) {
$field.data('height', $field.outerHeight());
autosize.destroy($field);
$field.css('max-height', window.outerHeight);
}
});
autosize($fields);
autosize.update($fields);
$fields.css('resize', 'vertical');
}); });
import installCustomElements from 'document-register-element'; import installCustomElements from 'document-register-element';
import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support'; import isEmojiUnicodeSupported from '../emoji/support';
installCustomElements(window); installCustomElements(window);
...@@ -32,12 +31,20 @@ export default function installGlEmojiElement() { ...@@ -32,12 +31,20 @@ export default function installGlEmojiElement() {
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) { } else {
import(/* webpackChunkName: 'emoji' */ '../emoji')
.then(({ emojiImageTag, emojiFallbackImageSrc }) => {
if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc); this.innerHTML = emojiImageTag(name, fallbackSrc);
} else { } else {
const src = emojiFallbackImageSrc(name); const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src); this.innerHTML = emojiImageTag(name, src);
} }
})
.catch(() => {
// do nothing
});
}
} }
}; };
......
...@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({
}, },
milestoneTitle() { milestoneTitle() {
return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
} },
canRemove() {
return !this.list.preset;
},
}, },
watch: { watch: {
detail: { detail: {
......
...@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ ...@@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
}, },
template: ` template: `
<div <div
class="block list" class="block list">
v-if="list.type !== 'closed'">
<button <button
class="btn btn-default btn-block" class="btn btn-default btn-block"
type="button" type="button"
......
...@@ -85,9 +85,8 @@ window.Build = (function () { ...@@ -85,9 +85,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) { if (!this.hasBeenScrolled) {
this.scrollToBottom(); this.scrollToBottom();
} }
}); })
.then(() => this.verifyTopPosition());
this.verifyTopPosition();
} }
Build.prototype.canScroll = function () { Build.prototype.canScroll = function () {
...@@ -176,7 +175,7 @@ window.Build = (function () { ...@@ -176,7 +175,7 @@ window.Build = (function () {
} }
if ($flashError.length) { if ($flashError.length) {
topPostion += $flashError.outerHeight(); topPostion += $flashError.outerHeight() + prependTopDefault;
} }
this.$buildTrace.css({ this.$buildTrace.css({
...@@ -196,6 +195,7 @@ window.Build = (function () { ...@@ -196,6 +195,7 @@ window.Build = (function () {
}) })
.done((log) => { .done((log) => {
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
} }
...@@ -220,7 +220,11 @@ window.Build = (function () { ...@@ -220,7 +220,11 @@ window.Build = (function () {
} }
if (!log.complete) { if (!log.complete) {
if (!this.hasBeenScrolled) {
this.toggleScrollAnimation(true); this.toggleScrollAnimation(true);
} else {
this.toggleScrollAnimation(false);
}
Build.timeout = setTimeout(() => { Build.timeout = setTimeout(() => {
//eslint-disable-next-line //eslint-disable-next-line
...@@ -229,7 +233,8 @@ window.Build = (function () { ...@@ -229,7 +233,8 @@ window.Build = (function () {
if (!this.hasBeenScrolled) { if (!this.hasBeenScrolled) {
this.scrollToBottom(); this.scrollToBottom();
} }
}); })
.then(() => this.verifyTopPosition());
}, 4000); }, 4000);
} else { } else {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import './lib/utils/url_utility'; import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
const UNFOLD_COUNT = 20; const UNFOLD_COUNT = 20;
let isBound = false; let isBound = false;
...@@ -8,8 +9,10 @@ let isBound = false; ...@@ -8,8 +9,10 @@ let isBound = false;
class Diff { class Diff {
constructor() { constructor() {
const $diffFile = $('.files .diff-file'); const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff(); $diffFile.singleFileDiff();
$diffFile.filesCommentButton();
FilesCommentButton.init($diffFile);
$diffFile.each((index, file) => new gl.ImageFile(file)); $diffFile.each((index, file) => new gl.ImageFile(file));
......
...@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({
const notesCount = this.notesCount; const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container') $(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0) .toggleClass('no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container') .nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0); .toggleClass('no-comment-btn', notesCount > 0);
}, },
toggleDiscussionsToggleState() { toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
......
...@@ -209,8 +209,8 @@ import initExperimentalFlags from './experimental_flags'; ...@@ -209,8 +209,8 @@ import initExperimentalFlags from './experimental_flags';
new MilestoneSelect(); new MilestoneSelect();
new gl.IssuableTemplateSelectors(); new gl.IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:creations:new':
case 'projects:merge_requests:new_diffs': case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
new gl.Diff(); new gl.Diff();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -247,10 +247,6 @@ import initExperimentalFlags from './experimental_flags'; ...@@ -247,10 +247,6 @@ import initExperimentalFlags from './experimental_flags';
shortcut_handler = new ShortcutsIssuable(true); shortcut_handler = new ShortcutsIssuable(true);
new ZenMode(); new ZenMode();
break; break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
break;
case 'dashboard:activity': case 'dashboard:activity':
new gl.Activities(); new gl.Activities();
break; break;
...@@ -319,7 +315,7 @@ import initExperimentalFlags from './experimental_flags'; ...@@ -319,7 +315,7 @@ import initExperimentalFlags from './experimental_flags';
new gl.Members(); new gl.Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:members:show': case 'projects:settings:members:show':
new gl.MemberExpirationDate('.js-access-expiration-date-groups'); new gl.MemberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect(); new GroupsSelect();
new gl.MemberExpirationDate(); new gl.MemberExpirationDate();
...@@ -386,7 +382,7 @@ import initExperimentalFlags from './experimental_flags'; ...@@ -386,7 +382,7 @@ import initExperimentalFlags from './experimental_flags';
case 'search:show': case 'search:show':
new Search(); new Search();
break; break;
case 'projects:repository:show': case 'projects:settings:repository:show':
// Initialize Protected Branch Settings // Initialize Protected Branch Settings
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
...@@ -396,7 +392,7 @@ import initExperimentalFlags from './experimental_flags'; ...@@ -396,7 +392,7 @@ import initExperimentalFlags from './experimental_flags';
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
break; break;
case 'projects:ci_cd:show': case 'projects:settings:ci_cd:show':
new gl.ProjectVariables(); new gl.ProjectVariables();
break; break;
case 'ci:lints:create': case 'ci:lints:create':
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */
/* global notes */ /* global notes */
let $commentButtonTemplate; /* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
window.FilesCommentButton = (function() { * bottleneck for pages with large diffs. For a comprehensive list of what
var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
COMMENT_BUTTON_CLASS = '.add-diff-note';
const LINE_NUMBER_CLASS = 'diff-line-num';
LINE_HOLDER_CLASS = '.line_holder'; const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
LINE_NUMBER_CLASS = 'diff-line-num'; const EMPTY_CELL_CLASS = 'empty-cell';
const OLD_LINE_CLASS = 'old_line';
LINE_CONTENT_CLASS = 'line_content'; const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`;
const DIFF_CONTAINER_SELECTOR = '.files';
UNFOLDABLE_LINE_CLASS = 'js-unfold'; const DIFF_EXPANDED_CLASS = 'diff-expanded';
EMPTY_CELL_CLASS = 'empty-cell'; export default {
init($diffFile) {
OLD_LINE_CLASS = 'old_line'; /* Caching is used only when the following members are *true*. This is because there are likely to be
* differently configured versions of diffs in the same session. However if these values are true, they
LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; * will be true in all cases */
TEXT_FILE_SELECTOR = '.text-file'; if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
function FilesCommentButton(filesContainerElement) { this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
this.render = this.render.bind(this);
this.hideButton = this.hideButton.bind(this);
this.isParallelView = notes.isParallelView();
filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
} }
FilesCommentButton.prototype.render = function(e) { if (typeof notes !== 'undefined' && !this.isParallelView) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; this.isParallelView = notes.isParallelView && notes.isParallelView();
$currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('js-no-comment-btn')) return;
lineContentElement = this.getLineContent($currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
$button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
buttonParentElement.addClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
if ($button.length) {
return;
} }
textFileElement = this.getTextFileElement($currentTarget); if (this.userCanCreateNote) {
buttonParentElement.append(this.buildButton({ $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
discussionID: lineContentElement.attr('data-discussion-id'), .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
lineType: lineContentElement.attr('data-line-type'),
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
commitID: textFileElement.attr('data-commit-id'),
noteType: lineContentElement.attr('data-note-type'),
// LegacyDiffNote
lineCode: lineContentElement.attr('data-line-code'),
// DiffNote
position: lineContentElement.attr('data-position')
}));
};
FilesCommentButton.prototype.hideButton = function(e) {
var $currentTarget = $(e.currentTarget);
var buttonParentElement = this.getButtonParent($currentTarget);
buttonParentElement.removeClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
};
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
return $commentButtonTemplate.clone().attr({
'data-discussion-id': buttonAttributes.discussionID,
'data-line-type': buttonAttributes.lineType,
'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID,
'data-note-type': buttonAttributes.noteType,
// LegacyDiffNote
'data-line-code': buttonAttributes.lineCode,
// DiffNote
'data-position': buttonAttributes.position
});
};
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
return hoveredElement.closest(TEXT_FILE_SELECTOR);
};
FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement;
}
if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
} }
}; },
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { showButton(isParallelView, e) {
if (!this.isParallelView) { const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement;
}
return hoveredElement.parent().find("." + OLD_LINE_CLASS);
} else {
if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
return hoveredElement;
}
return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
}
};
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { if (!this.validateButtonParent(buttonParentElement)) return;
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
};
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { buttonParentElement.classList.add('is-over');
return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== ''; buttonParentElement.nextElementSibling.classList.add('is-over');
}; },
return FilesCommentButton; hideButton(isParallelView, e) {
})(); const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView);
$.fn.filesCommentButton = function() { buttonParentElement.classList.remove('is-over');
$commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); buttonParentElement.nextElementSibling.classList.remove('is-over');
},
if (!(this && (this.parent().data('can-create-note') != null))) { getButtonParent(hoveredElement, isParallelView) {
return; if (isParallelView) {
if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) {
return hoveredElement.previousElementSibling;
} }
return this.each(function() { } else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) {
if (!$.data(this, 'filesCommentButton')) { return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`);
return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
} }
}); return hoveredElement;
},
validateButtonParent(buttonParentElement) {
return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
!buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) &&
!buttonParentElement.classList.contains(NO_COMMENT_CLASS) &&
!buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS);
},
}; };
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import AjaxFilter from '~/droplab/plugins/ajax_filter'; import AjaxFilter from '~/droplab/plugins/ajax_filter';
import './filtered_search_dropdown'; import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class DropdownUser extends gl.FilteredSearchDropdown { class DropdownUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, tokenKeys, filter) { constructor(droplab, dropdown, input, tokenKeys, filter) {
...@@ -32,8 +33,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { ...@@ -32,8 +33,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
} }
hideCurrentUser() { hideCurrentUser() {
const currenUserItem = this.dropdown.querySelector('.js-current-user'); addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden');
currenUserItem.classList.add('hidden');
} }
itemClicked(e) { itemClicked(e) {
......
...@@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root'; ...@@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service'; import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class FilteredSearchManager { class FilteredSearchManager {
constructor(page) { constructor(page) {
...@@ -227,11 +228,7 @@ class FilteredSearchManager { ...@@ -227,11 +228,7 @@ class FilteredSearchManager {
} }
addInputContainerFocus() { addInputContainerFocus() {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus');
if (inputContainer) {
inputContainer.classList.add('focus');
}
} }
removeInputContainerFocus(e) { removeInputContainerFocus(e) {
......
import { validEmojiNames, glEmojiTag } from './emoji';
import glRegexp from './lib/utils/regexp'; import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache'; import AjaxCache from './lib/utils/ajax_cache';
...@@ -373,7 +372,12 @@ class GfmAutoComplete { ...@@ -373,7 +372,12 @@ class GfmAutoComplete {
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
import(/* webpackChunkName: 'emoji' */ './emoji')
.then(({ validEmojiNames, glEmojiTag }) => {
this.loadData($input, at, validEmojiNames); this.loadData($input, at, validEmojiNames);
GfmAutoComplete.glEmojiTag = glEmojiTag;
})
.catch(() => { this.isLoadingData[at] = false; });
} else { } else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => { .then((data) => {
...@@ -396,6 +400,13 @@ class GfmAutoComplete { ...@@ -396,6 +400,13 @@ class GfmAutoComplete {
this.cachedData = {}; this.cachedData = {};
} }
destroy() {
this.input.each((i, input) => {
const $input = $(input);
$input.atwho('destroy');
});
}
static isLoading(data) { static isLoading(data) {
let dataToInspect = data; let dataToInspect = data;
if (data && data.length > 0) { if (data && data.length > 0) {
...@@ -421,12 +432,14 @@ GfmAutoComplete.atTypeMap = { ...@@ -421,12 +432,14 @@ GfmAutoComplete.atTypeMap = {
}; };
// Emoji // Emoji
GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = { GfmAutoComplete.Emoji = {
templateFunction(name) { templateFunction(name) {
return `<li> // glEmojiTag helper is loaded on-demand in fetchData()
${name} ${glEmojiTag(name)} if (GfmAutoComplete.glEmojiTag) {
</li> return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`;
`; }
return `<li>${name}</li>`;
}, },
}; };
// Team Members // Team Members
......
...@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) { ...@@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) {
GLForm.prototype.destroy = function() { GLForm.prototype.destroy = function() {
// Clean form listeners // Clean form listeners
this.clearEventListeners(); this.clearEventListeners();
if (this.autoComplete) {
this.autoComplete.destroy();
}
return this.form.data('gl-form', null); return this.form.data('gl-form', null);
}; };
...@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() { ...@@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() {
this.form.addClass('gfm-form'); this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes // remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), { this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), {
emojis: true, emojis: true,
members: this.enableGFM, members: this.enableGFM,
issues: this.enableGFM, issues: this.enableGFM,
......
import Cookies from 'js-cookie';
import _ from 'underscore'; import _ from 'underscore';
export default class GroupName { export default class GroupName {
constructor() { constructor() {
this.titleContainer = document.querySelector('.title-container'); this.titleContainer = document.querySelector('.js-title-container');
this.title = document.querySelector('.title'); this.title = this.titleContainer.querySelector('.title');
this.titleWidth = this.title.offsetWidth; this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title'); this.groupTitle = this.titleContainer.querySelector('.group-title');
this.groups = document.querySelectorAll('.group-path'); this.groups = this.titleContainer.querySelectorAll('.group-path');
this.toggle = null; this.toggle = null;
this.isHidden = false; this.isHidden = false;
this.init(); this.init();
...@@ -33,11 +33,20 @@ export default class GroupName { ...@@ -33,11 +33,20 @@ export default class GroupName {
createToggle() { createToggle() {
this.toggle = document.createElement('button'); this.toggle = document.createElement('button');
this.toggle.setAttribute('type', 'button');
this.toggle.className = 'text-expander group-name-toggle'; this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path'); this.toggle.setAttribute('aria-label', 'Toggle full path');
if (Cookies.get('new_nav') === 'true') {
this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>';
} else {
this.toggle.innerHTML = '...'; this.toggle.innerHTML = '...';
}
this.toggle.addEventListener('click', this.toggleGroups.bind(this)); this.toggle.addEventListener('click', this.toggleGroups.bind(this));
if (Cookies.get('new_nav') === 'true') {
this.title.insertBefore(this.toggle, this.groupTitle);
} else {
this.titleContainer.insertBefore(this.toggle, this.title); this.titleContainer.insertBefore(this.toggle, this.title);
}
this.toggleGroups(); this.toggleGroups();
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
/* global SubscriptionSelect */ /* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; const DISABLED_CONTENT_CLASS = 'disabled-content';
...@@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar {
return navbarHeight + layoutNavHeight + subNavScroll; return navbarHeight + layoutNavHeight + subNavScroll;
} }
initSidebar() {
if (!this.navHeight) {
this.navHeight = this.getNavHeight();
}
if (!this.sidebarInitialized) {
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
this.sidebarInitialized = true;
}
}
setupBulkUpdateActions() { setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData(); IssuableBulkUpdateActions.setOriginalDropdownData();
} }
...@@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar { ...@@ -97,7 +86,7 @@ export default class IssuableBulkUpdateSidebar {
this.toggleCheckboxDisplay(enable); this.toggleCheckboxDisplay(enable);
if (enable) { if (enable) {
this.initSidebar(); SidebarHeightManager.init();
} }
} }
...@@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -143,17 +132,6 @@ export default class IssuableBulkUpdateSidebar {
this.$bulkEditSubmitBtn.enable(); this.$bulkEditSubmitBtn.enable();
} }
} }
// loosely based on method of the same name in right_sidebar.js
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.navHeight - currentScrollDepth;
if (diff > 0) {
this.$sidebar.outerHeight(window.innerHeight - diff);
} else {
this.$sidebar.outerHeight('100%');
}
}
static getCheckedIssueIds() { static getCheckedIssueIds() {
const $checkedIssues = $('.selected_issue:checked'); const $checkedIssues = $('.selected_issue:checked');
......
...@@ -39,6 +39,17 @@ ...@@ -39,6 +39,17 @@
runnerId() { runnerId() {
return `#${this.job.runner.id}`; return `#${this.job.runner.id}`;
}, },
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path;
},
}, },
}; };
</script> </script>
...@@ -63,7 +74,7 @@ ...@@ -63,7 +74,7 @@
Retry Retry
</a> </a>
</div> </div>
<div class="block"> <div :class="{block : renderBlock }">
<p <p
class="build-detail-row js-job-mr" class="build-detail-row js-job-mr"
v-if="job.merge_request"> v-if="job.merge_request">
......
...@@ -112,29 +112,11 @@ window.dateFormat = dateFormat; ...@@ -112,29 +112,11 @@ window.dateFormat = dateFormat;
return timefor; return timefor;
}; };
w.gl.utils.cachedTimeagoElements = [];
w.gl.utils.renderTimeago = function($els) { w.gl.utils.renderTimeago = function($els) {
if (!$els && !w.gl.utils.cachedTimeagoElements.length) { const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
} else if ($els) {
w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
}
w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
};
w.gl.utils.updateTimeagoText = function(el) {
const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang);
if (el.textContent !== formattedDate) {
el.textContent = formattedDate;
}
};
w.gl.utils.initTimeagoTimeout = function() {
gl.utils.renderTimeago();
gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000); // timeago.js sets timeouts internally for each timeago value to be updated in real time
gl.utils.getTimeago().render(timeagoEls, lang);
}; };
w.gl.utils.getDayDifference = function(a, b) { w.gl.utils.getDayDifference = function(a, b) {
......
/* eslint-disable import/prefer-default-export */
export const addClassIfElementExists = (element, className) => {
if (element) {
element.classList.add(className);
}
};
This diff is collapsed.
...@@ -70,7 +70,7 @@ import './ajax_loading_spinner'; ...@@ -70,7 +70,7 @@ import './ajax_loading_spinner';
import './api'; import './api';
import './aside'; import './aside';
import './autosave'; import './autosave';
import AwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './breakpoints'; import './breakpoints';
import './broadcast_message'; import './broadcast_message';
import './build'; import './build';
...@@ -355,10 +355,10 @@ $(function () { ...@@ -355,10 +355,10 @@ $(function () {
$window.off('resize.app').on('resize.app', function () { $window.off('resize.app').on('resize.app', function () {
return fitSidebarForSize(); return fitSidebarForSize();
}); });
gl.awardsHandler = new AwardsHandler(); loadAwardsHandler();
new Aside(); new Aside();
gl.utils.initTimeagoTimeout(); gl.utils.renderTimeago();
$(document).trigger('init.scrolling-tabs'); $(document).trigger('init.scrolling-tabs');
}); });
...@@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
this.resetViewContainer(); this.resetViewContainer();
this.mountPipelinesView(); this.mountPipelinesView();
} else { } else {
if (Breakpoints.get().getBreakpointSize() !== 'xs') {
this.expandView(); this.expandView();
}
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView(); this.destroyPipelinesView();
} }
...@@ -168,9 +170,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -168,9 +170,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Activate a tab based on the current action // Activate a tab based on the current action
activateTab(action) { activateTab(action) {
const activate = action === 'show' ? 'notes' : action;
// important note: the .tab('show') method triggers 'shown.bs.tab' event itself // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
$(`.merge-request-tabs a[data-action='${activate}']`).tab('show'); $(`.merge-request-tabs a[data-action='${action}']`).tab('show');
} }
// Replaces the current Merge Request-specific action in the URL with a new one // Replaces the current Merge Request-specific action in the URL with a new one
...@@ -185,7 +186,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -185,7 +186,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// location.pathname # => "/namespace/project/merge_requests/1/diffs" // location.pathname # => "/namespace/project/merge_requests/1/diffs"
// //
// location.pathname # => "/namespace/project/merge_requests/1/diffs" // location.pathname # => "/namespace/project/merge_requests/1/diffs"
// setCurrentAction('notes') // setCurrentAction('show')
// location.pathname # => "/namespace/project/merge_requests/1" // location.pathname # => "/namespace/project/merge_requests/1"
// //
// location.pathname # => "/namespace/project/merge_requests/1/diffs" // location.pathname # => "/namespace/project/merge_requests/1/diffs"
...@@ -194,13 +195,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; ...@@ -194,13 +195,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// //
// Returns the new URL String // Returns the new URL String
setCurrentAction(action) { setCurrentAction(action) {
this.currentAction = action === 'show' ? 'notes' : action; this.currentAction = action;
// Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' // Remove a trailing '/commits' '/diffs' '/pipelines'
let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes' // Append the new action if we're on a tab other than 'notes'
if (this.currentAction !== 'notes') { if (this.currentAction !== 'show' && this.currentAction !== 'new') {
newState += `/${this.currentAction}`; newState += `/${this.currentAction}`;
} }
......
<script>
/* global Flash */
import _ from 'underscore';
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service';
import monitoringRow from './monitoring_row.vue';
import monitoringState from './monitoring_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
export default {
data() {
const metricsData = document.querySelector('#prometheus-graphs').dataset;
const store = new MonitoringStore();
return {
store,
state: 'gettingStarted',
hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
endpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
showEmptyState: true,
backOffRequestCounter: 0,
updateAspectRatio: false,
updatedAspectRatios: 0,
resizeThrottled: {},
};
},
components: {
monitoringRow,
monitoringState,
},
methods: {
getGraphsData() {
const maxNumberOfRequests = 3;
this.state = 'loading';
gl.utils.backOff((next, stop) => {
this.service.get().then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
} else {
stop(new Error('Failed to connect to the prometheus server'));
}
} else {
stop(resp);
}
}).catch(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.state = 'unableToConnect';
return false;
}
return resp.json();
})
.then((metricGroupsData) => {
if (!metricGroupsData) return false;
this.store.storeMetrics(metricGroupsData.data);
return this.getDeploymentData();
})
.then((deploymentData) => {
if (deploymentData !== false) {
this.store.storeDeploymentData(deploymentData.deployments);
this.showEmptyState = false;
}
return {};
})
.catch(() => {
this.state = 'unableToConnect';
});
},
getDeploymentData() {
return this.service.getDeploymentData(this.deploymentEndpoint)
.then(resp => resp.json())
.catch(() => new Flash('Error getting deployment information.'));
},
resize() {
this.updateAspectRatio = true;
},
toggleAspectRatio() {
this.updatedAspectRatios = this.updatedAspectRatios += 1;
if (this.store.getMetricsCount() === this.updatedAspectRatios) {
this.updateAspectRatio = !this.updateAspectRatio;
this.updatedAspectRatios = 0;
}
},
},
created() {
this.service = new MonitoringService(this.endpoint);
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
},
beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
this.resizeThrottled = _.throttle(this.resize, 600);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
}
},
};
</script>
<template>
<div
class="prometheus-graphs"
v-if="!showEmptyState">
<div
class="row"
v-for="(groupData, index) in store.groups"
:key="index">
<div
class="col-md-12">
<div
class="panel panel-default prometheus-panel">
<div
class="panel-heading">
<h4>{{groupData.group}}</h4>
</div>
<div
class="panel-body">
<monitoring-row
v-for="(row, index) in groupData.metrics"
:key="index"
:row-data="row"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
</div>
</div>
</div>
</div>
</div>
<monitoring-state
:selected-state="state"
:documentation-path="documentationPath"
:settings-path="settingsPath"
v-else
/>
</template>
<script>
/* global Breakpoints */
import d3 from 'd3';
import monitoringLegends from './monitoring_legends.vue';
import monitoringFlag from './monitoring_flag.vue';
import monitoringDeployment from './monitoring_deployment.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
const bisectDate = d3.bisector(d => d.time).left;
export default {
props: {
columnData: {
type: Object,
required: true,
},
classType: {
type: String,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
},
mixins: [MonitoringMixin],
data() {
return {
graphHeight: 500,
graphWidth: 600,
graphHeightOffset: 120,
xScale: {},
yScale: {},
margin: {},
data: [],
breakpointHandler: Breakpoints.get(),
unitOfDisplay: '',
areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1',
yAxisLabel: '',
legendTitle: '',
reducedDeploymentData: [],
area: '',
line: '',
measurements: measurements.large,
currentData: {
time: new Date(),
value: 0,
},
currentYCoordinate: 0,
currentXCoordinate: 0,
currentFlagPosition: 0,
metricUsage: '',
showFlag: false,
showDeployInfo: true,
};
},
components: {
monitoringLegends,
monitoringFlag,
monitoringDeployment,
},
computed: {
outterViewBox() {
return `0 0 ${this.graphWidth} ${this.graphHeight}`;
},
innerViewBox() {
if ((this.graphWidth - 150) > 0) {
return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`;
}
return '0 0 0 0';
},
axisTransform() {
return `translate(70, ${this.graphHeight - 100})`;
},
paddingBottomRootSvg() {
return (Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0;
},
},
methods: {
draw() {
const breakpointSize = this.breakpointHandler.getBreakpointSize();
const query = this.columnData.queries[0];
this.margin = measurements.large.margin;
if (breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
this.margin = measurements.small.margin;
this.measurements = measurements.small;
}
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || 'N/A';
this.yAxisLabel = this.columnData.y_label || 'Values';
this.legendTitle = query.legend || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
if (this.data !== undefined) {
this.renderAxesPaths();
this.formatDeployments();
}
},
handleMouseOverGraph(e) {
let point = this.$refs.graphData.createSVGPoint();
point.x = e.clientX;
point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x = point.x += 7;
const timeValueOverlay = this.xScale.invert(point.x);
const overlayIndex = bisectDate(this.data, timeValueOverlay, 1);
const d0 = this.data[overlayIndex - 1];
const d1 = this.data[overlayIndex];
if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0;
this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x);
this.currentYCoordinate = this.yScale(this.currentData.value);
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
if (currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
},
renderAxesPaths() {
const axisXScale = d3.time.scale()
.range([0, this.graphWidth]);
this.yScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
axisXScale.domain(d3.extent(this.data, d => d.time));
this.yScale.domain([0, d3.max(this.data.map(d => d.value))]);
const xAxis = d3.svg.axis()
.scale(axisXScale)
.ticks(measurements.ticks)
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(this.yScale)
.ticks(measurements.ticks)
.orient('left');
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
const width = this.graphWidth;
d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
.selectAll('.tick')
.each(function createTickLines() {
d3.select(this).select('line').attr('x2', width);
}); // This will select all of the ticks once they're rendered
this.xScale = d3.time.scale()
.range([0, this.graphWidth - 70]);
this.xScale.domain(d3.extent(this.data, d => d.time));
const areaFunction = d3.svg.area()
.x(d => this.xScale(d.time))
.y0(this.graphHeight - this.graphHeightOffset)
.y1(d => this.yScale(d.value))
.interpolate('linear');
const lineFunction = d3.svg.line()
.x(d => this.xScale(d.time))
.y(d => this.yScale(d.value));
this.line = lineFunction(this.data);
this.area = areaFunction(this.data);
},
},
watch: {
updateAspectRatio() {
if (this.updateAspectRatio) {
this.graphHeight = 500;
this.graphWidth = 600;
this.measurements = measurements.large;
this.draw();
eventHub.$emit('toggleAspectRatio');
}
},
},
mounted() {
this.draw();
},
};
</script>
<template>
<div
:class="classType">
<h5
class="text-center">
{{columnData.title}}
</h5>
<div
class="prometheus-svg-container">
<svg
:viewBox="outterViewBox"
:style="{ 'padding-bottom': paddingBottomRootSvg }"
ref="baseSvg">
<g
class="x-axis"
:transform="axisTransform">
</g>
<g
class="y-axis"
transform="translate(70, 20)">
</g>
<monitoring-legends
:graph-width="graphWidth"
:graph-height="graphHeight"
:margin="margin"
:measurements="measurements"
:area-color-rgb="areaColorRgb"
:legend-title="legendTitle"
:y-axis-label="yAxisLabel"
:metric-usage="metricUsage"
/>
<svg
class="graph-data"
:viewBox="innerViewBox"
ref="graphData">
<path
class="metric-area"
:d="area"
:fill="areaColorRgb"
transform="translate(-5, 20)">
</path>
<path
class="metric-line"
:d="line"
:stroke="lineColorRgb"
fill="none"
stroke-width="2"
transform="translate(-5, 20)">
</path>
<rect
class="prometheus-graph-overlay"
:width="(graphWidth - 70)"
:height="(graphHeight - 100)"
transform="translate(-5, 20)"
ref="graphOverlay"
@mousemove="handleMouseOverGraph($event)">
</rect>
<monitoring-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
<monitoring-flag
v-if="showFlag"
:current-x-coordinate="currentXCoordinate"
:current-y-coordinate="currentYCoordinate"
:current-data="currentData"
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
:graph-height-offset="graphHeightOffset"
/>
</svg>
</svg>
</div>
</div>
</template>
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
export default {
props: {
showDeployInfo: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
},
computed: {
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
},
methods: {
refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
},
formatTime(deploymentTime) {
return timeFormat(deploymentTime);
},
formatDate(deploymentTime) {
return dateFormat(deploymentTime);
},
nameDeploymentClass(deployment) {
return `deploy-info-${deployment.id}`;
},
transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
},
},
};
</script>
<template>
<g
class="deploy-info"
v-if="showDeployInfo">
<g
v-for="(deployment, index) in deploymentData"
:key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
<rect
x="0"
y="0"
:height="calculatedHeight"
width="3"
fill="url(#shadow-gradient)">
</rect>
<line
class="deployment-line"
x1="0"
y1="0"
x2="0"
:y2="calculatedHeight"
stroke="#000">
</line>
<svg
v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box"
x="3"
y="0"
width="92"
height="60">
<rect
class="rect-text-metric deploy-info-rect rect-metric"
x="1"
y="1"
rx="2"
width="90"
height="58">
</rect>
<g
transform="translate(5, 2)">
<text
class="deploy-info-text text-metric-bold">
{{refText(deployment)}}
</text>
</g>
<text
class="deploy-info-text"
y="18"
transform="translate(5, 2)">
{{formatDate(deployment.time)}}
</text>
<text
class="deploy-info-text text-metric-bold"
y="38"
transform="translate(5, 2)">
{{formatTime(deployment.time)}}
</text>
</svg>
</g>
<svg
height="0"
width="0">
<defs>
<linearGradient
id="shadow-gradient">
<stop
offset="0%"
stop-color="#000"
stop-opacity="0.4">
</stop>
<stop
offset="100%"
stop-color="#000"
stop-opacity="0">
</stop>
</linearGradient>
</defs>
</svg>
</g>
</template>
<script>
import {
dateFormat,
timeFormat,
} from '../constants';
export default {
props: {
currentXCoordinate: {
type: Number,
required: true,
},
currentYCoordinate: {
type: Number,
required: true,
},
currentFlagPosition: {
type: Number,
required: true,
},
currentData: {
type: Object,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
graphHeightOffset: {
type: Number,
required: true,
},
},
data() {
return {
circleColorRgb: '#8fbce8',
};
},
computed: {
formatTime() {
return timeFormat(this.currentData.time);
},
formatDate() {
return dateFormat(this.currentData.time);
},
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
},
},
};
</script>
<template>
<g class="mouse-over-flag">
<line
class="selected-metric-line"
:x1="currentXCoordinate"
:y1="0"
:x2="currentXCoordinate"
:y2="calculatedHeight"
transform="translate(-5, 20)">
</line>
<circle
class="circle-metric"
:fill="circleColorRgb"
stroke="#000"
:cx="currentXCoordinate"
:cy="currentYCoordinate"
r="5"
transform="translate(-5, 20)">
</circle>
<svg
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
<rect
class="rect-metric"
x="4"
y="1"
rx="2"
width="90"
height="40"
transform="translate(-3, 20)">
</rect>
<text
class="text-metric text-metric-bold"
x="8"
y="35"
transform="translate(-5, 20)">
{{formatTime}}
</text>
<text
class="text-metric-date"
x="8"
y="15"
transform="translate(-5, 20)">
{{formatDate}}
</text>
</svg>
</g>
</template>
<script>
export default {
props: {
graphWidth: {
type: Number,
required: true,
},
graphHeight: {
type: Number,
required: true,
},
margin: {
type: Object,
required: true,
},
measurements: {
type: Object,
required: true,
},
areaColorRgb: {
type: String,
required: true,
},
legendTitle: {
type: String,
required: true,
},
yAxisLabel: {
type: String,
required: true,
},
metricUsage: {
type: String,
required: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
};
},
computed: {
textTransform() {
const yCoordinate = (((this.graphHeight - this.margin.top)
+ this.measurements.axisLabelLineOffset) / 2) || 0;
return `translate(15, ${yCoordinate}) rotate(-90)`;
},
rectTransform() {
const yCoordinate = ((this.graphHeight - this.margin.top) / 2)
+ (this.yLabelWidth / 2) + 10 || 0;
return `translate(0, ${yCoordinate}) rotate(-90)`;
},
xPosition() {
return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2)
- this.margin.right) || 0;
},
yPosition() {
return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
},
},
mounted() {
this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox();
this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5;
});
},
};
</script>
<template>
<g
class="axis-label-container">
<line
class="label-x-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
:y1="yPosition"
:x2="graphWidth + 20"
:y2="yPosition">
</line>
<line
class="label-y-axis-line"
stroke="#000000"
stroke-width="1"
x1="10"
y1="0"
:x2="10"
:y2="yPosition">
</line>
<rect
class="rect-axis-text"
:transform="rectTransform"
:width="yLabelWidth"
:height="yLabelHeight">
</rect>
<text
class="label-axis-text y-label-text"
text-anchor="middle"
:transform="textTransform"
ref="ylabel">
{{yAxisLabel}}
</text>
<rect
class="rect-axis-text"
:x="xPosition + 50"
:y="graphHeight - 80"
width="50"
height="50">
</rect>
<text
class="label-axis-text"
:x="xPosition + 60"
:y="yPosition"
dy=".35em">
Time
</text>
<rect
:fill="areaColorRgb"
:width="measurements.legends.width"
:height="measurements.legends.height"
x="20"
:y="graphHeight - measurements.legendOffset">
</rect>
<text
class="text-metric-title"
x="50"
:y="graphHeight - 40">
{{legendTitle}}
</text>
<text
class="text-metric-usage"
x="50"
:y="graphHeight - 25">
{{metricUsage}}
</text>
</g>
</template>
<script>
import monitoringColumn from './monitoring_column.vue';
export default {
props: {
rowData: {
type: Array,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
},
components: {
monitoringColumn,
},
computed: {
bootstrapClass() {
return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12';
},
},
};
</script>
<template>
<div
class="prometheus-row row">
<monitoring-column
v-for="(column, index) in rowData"
:column-data="column"
:class-type="bootstrapClass"
:key="index"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="deploymentData"
/>
</div>
</template>
<script>
import gettingStartedSvg from 'empty_states/monitoring/_getting_started.svg';
import loadingSvg from 'empty_states/monitoring/_loading.svg';
import unableToConnectSvg from 'empty_states/monitoring/_unable_to_connect.svg';
export default {
props: {
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: false,
default: '',
},
selectedState: {
type: String,
required: true,
},
},
data() {
return {
states: {
gettingStarted: {
svg: gettingStartedSvg,
title: 'Get started with performance monitoring',
description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.',
buttonText: 'Configure Prometheus',
},
loading: {
svg: loadingSvg,
title: 'Waiting for performance data',
description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.',
buttonText: 'View documentation',
},
unableToConnect: {
svg: unableToConnectSvg,
title: 'Unable to connect to Prometheus server',
description: 'Ensure connectivity is available from the GitLab server to the ',
buttonText: 'View documentation',
},
},
};
},
computed: {
currentState() {
return this.states[this.selectedState];
},
buttonPath() {
if (this.selectedState === 'gettingStarted') {
return this.settingsPath;
}
return this.documentationPath;
},
showButtonDescription() {
if (this.selectedState === 'unableToConnect') return true;
return false;
},
},
};
</script>
<template>
<div
class="prometheus-state">
<div
class="row">
<div
class="col-md-4 col-md-offset-4 state-svg"
v-html="currentState.svg">
</div>
</div>
<div
class="row">
<div
class="col-md-6 col-md-offset-3">
<h4
class="text-center state-title">
{{currentState.title}}
</h4>
</div>
</div>
<div
class="row">
<div
class="col-md-6 col-md-offset-3">
<div
class="description-text text-center state-description">
{{currentState.description}}
<a
:href="settingsPath"
v-if="showButtonDescription">
Prometheus server
</a>
</div>
</div>
</div>
<div
class="row state-button-section">
<div
class="col-md-4 col-md-offset-4 text-center state-button">
<a
class="btn btn-success"
:href="buttonPath">
{{currentState.buttonText}}
</a>
</div>
</div>
</div>
</template>
/* global Flash */
import d3 from 'd3';
import {
dateFormat,
timeFormat,
} from './constants';
export default class Deployments {
constructor(width, height) {
this.width = width;
this.height = height;
this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
this.createGradientDef();
}
init(chartData) {
this.chartData = chartData;
this.x = d3.time.scale().range([0, this.width]);
this.x.domain(d3.extent(this.chartData, d => d.time));
this.charts = d3.selectAll('.prometheus-graph');
this.getData();
}
getData() {
$.ajax({
url: this.endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error getting deployment information.'))
.done((data) => {
this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.x(time));
time.setSeconds(this.chartData[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
});
}
return deploymentDataArray;
}, []);
this.plotData();
});
}
plotData() {
this.charts.each((d, i) => {
const svg = d3.select(this.charts[0][i]);
const chart = svg.select('.graph-container');
const key = svg.node().getAttribute('graph-type');
this.createLine(chart, key);
this.createDeployInfoBox(chart, key);
});
}
createGradientDef() {
const defs = d3.select('body')
.append('svg')
.attr({
height: 0,
width: 0,
})
.append('defs');
defs.append('linearGradient')
.attr({
id: 'shadow-gradient',
})
.append('stop')
.attr({
offset: '0%',
'stop-color': '#000',
'stop-opacity': 0.4,
})
.select(this.selectParentNode)
.append('stop')
.attr({
offset: '100%',
'stop-color': '#000',
'stop-opacity': 0,
});
}
createLine(chart, key) {
chart.append('g')
.attr({
class: 'deploy-info',
})
.selectAll('.deploy-info')
.data(this.data)
.enter()
.append('g')
.attr({
class: d => `deploy-info-${d.id}-${key}`,
transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
})
.append('rect')
.attr({
x: 1,
y: 0,
height: this.height + 1,
width: 3,
fill: 'url(#shadow-gradient)',
})
.select(this.selectParentNode)
.append('line')
.attr({
class: 'deployment-line',
x1: 0,
x2: 0,
y1: 0,
y2: this.height + 1,
});
}
createDeployInfoBox(chart, key) {
chart.selectAll('.deploy-info')
.selectAll('.js-deploy-info-box')
.data(this.data)
.enter()
.select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
.append('svg')
.attr({
class: 'js-deploy-info-box hidden',
x: 3,
y: 0,
width: 92,
height: 60,
})
.append('rect')
.attr({
class: 'rect-text-metric deploy-info-rect rect-metric',
x: 1,
y: 1,
rx: 2,
width: 90,
height: 58,
})
.select(this.selectParentNode)
.append('g')
.attr({
transform: 'translate(5, 2)',
})
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
})
.text(Deployments.refText)
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text',
y: 18,
})
.text(d => dateFormat(d.time))
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
y: 38,
})
.text(d => timeFormat(d.time));
}
static toggleDeployTextbox(deploy, key, showInfoBox) {
d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
.classed('hidden', !showInfoBox);
}
mouseOverDeployInfo(mouseXPos, key) {
if (!this.data) return false;
let dataFound = false;
this.data.forEach((d) => {
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
Deployments.toggleDeployTextbox(d, key, true);
} else {
Deployments.toggleDeployTextbox(d, key, false);
}
});
return dataFound;
}
/* `this` is bound to the D3 node */
selectParentNode() {
return this.parentNode;
}
static refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
}
}
import Vue from 'vue';
export default new Vue();
const mixins = {
methods: {
mouseOverDeployInfo(mouseXPos) {
if (!this.reducedDeploymentData) return false;
let dataFound = false;
this.reducedDeploymentData = this.reducedDeploymentData.map((d) => {
const deployment = d;
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
deployment.showDeploymentFlag = true;
} else {
deployment.showDeploymentFlag = false;
}
return deployment;
});
return dataFound;
},
formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.xScale(time));
time.setSeconds(this.data[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
showDeploymentFlag: false,
});
}
return deploymentDataArray;
}, []);
},
},
};
export default mixins;
import PrometheusGraph from './prometheus_graph'; import Vue from 'vue';
import Monitoring from './components/monitoring.vue';
document.addEventListener('DOMContentLoaded', function onLoad() { document.addEventListener('DOMContentLoaded', () => new Vue({
document.removeEventListener('DOMContentLoaded', onLoad, false); el: '#prometheus-graphs',
return new PrometheusGraph(); components: {
}, false); 'monitoring-dashboard': Monitoring,
},
render: createElement => createElement('monitoring-dashboard'),
}));
This diff is collapsed.
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class MonitoringService {
constructor(endpoint) {
this.graphs = Vue.resource(endpoint);
}
get() {
return this.graphs.get();
}
// eslint-disable-next-line class-methods-use-this
getDeploymentData(endpoint) {
return Vue.http.get(endpoint);
}
}
import _ from 'underscore';
class MonitoringStore {
constructor() {
this.groups = [];
this.deploymentData = [];
}
// eslint-disable-next-line class-methods-use-this
createArrayRows(metrics = []) {
const currentMetrics = metrics;
const availableMetrics = [];
let metricsRow = [];
let index = 1;
Object.keys(currentMetrics).forEach((key) => {
const metricValues = currentMetrics[key].queries[0].result[0].values;
if (metricValues != null) {
const literalMetrics = metricValues.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
}));
currentMetrics[key].queries[0].result[0].values = literalMetrics;
metricsRow.push(currentMetrics[key]);
if (index % 2 === 0) {
availableMetrics.push(metricsRow);
metricsRow = [];
}
index = index += 1;
}
});
if (metricsRow.length > 0) {
availableMetrics.push(metricsRow);
}
return availableMetrics;
}
storeMetrics(groups = []) {
this.groups = groups.map((group) => {
const currentGroup = group;
currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value();
currentGroup.metrics = this.createArrayRows(currentGroup.metrics);
return currentGroup;
});
}
storeDeploymentData(deploymentData = []) {
this.deploymentData = deploymentData;
}
getMetricsCount() {
let metricsCount = 0;
this.groups.forEach((group) => {
group.metrics.forEach((metric) => {
metricsCount = metricsCount += metric.length;
});
});
return metricsCount;
}
}
export default MonitoringStore;
export default {
small: { // Covers both xs and sm screen sizes
margin: {
top: 40,
right: 40,
bottom: 50,
left: 40,
},
legends: {
width: 15,
height: 30,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
legendOffset: 52,
},
large: { // This covers both md and lg screen sizes
margin: {
top: 80,
right: 80,
bottom: 100,
left: 80,
},
legends: {
width: 20,
height: 35,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
legendOffset: 55,
},
ticks: 3,
};
...@@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho ...@@ -18,6 +18,7 @@ import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho'; import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
import loadAwardsHandler from './awards_handler';
import './autosave'; import './autosave';
import './dropzone_input'; import './dropzone_input';
import './task_list'; import './task_list';
...@@ -291,8 +292,13 @@ export default class Notes { ...@@ -291,8 +292,13 @@ export default class Notes {
if ('emoji_award' in noteEntity.commands_changes) { if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0); votesBlock = $('.js-awards-block').eq(0);
gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
return gl.awardsHandler.scrollToAwards(); loadAwardsHandler().then((awardsHandler) => {
awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
awardsHandler.scrollToAwards();
}).catch(() => {
// ignore
});
} }
} }
} }
...@@ -337,6 +343,10 @@ export default class Notes { ...@@ -337,6 +343,10 @@ export default class Notes {
if (!noteEntity.valid) { if (!noteEntity.valid) {
if (noteEntity.errors.commands_only) { if (noteEntity.errors.commands_only) {
if (noteEntity.commands_changes &&
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline);
this.refresh(); this.refresh();
} }
...@@ -829,6 +839,8 @@ export default class Notes { ...@@ -829,6 +839,8 @@ export default class Notes {
*/ */
setupDiscussionNoteForm(dataHolder, form) { setupDiscussionNoteForm(dataHolder, form) {
// setup note target // setup note target
const diffFileData = dataHolder.closest('.text-file');
var discussionID = dataHolder.data('discussionId'); var discussionID = dataHolder.data('discussionId');
if (discussionID) { if (discussionID) {
...@@ -839,9 +851,10 @@ export default class Notes { ...@@ -839,9 +851,10 @@ export default class Notes {
form.attr('data-line-code', dataHolder.data('lineCode')); form.attr('data-line-code', dataHolder.data('lineCode'));
form.find('#line_type').val(dataHolder.data('lineType')); form.find('#line_type').val(dataHolder.data('lineType'));
form.find('#note_noteable_type').val(dataHolder.data('noteableType')); form.find('#note_noteable_type').val(diffFileData.data('noteableType'));
form.find('#note_noteable_id').val(dataHolder.data('noteableId')); form.find('#note_noteable_id').val(diffFileData.data('noteableId'));
form.find('#note_commit_id').val(dataHolder.data('commitId')); form.find('#note_commit_id').val(diffFileData.data('commitId'));
form.find('#note_type').val(dataHolder.data('noteType')); form.find('#note_type').val(dataHolder.data('noteType'));
// LegacyDiffNote // LegacyDiffNote
...@@ -1485,7 +1498,7 @@ export default class Notes { ...@@ -1485,7 +1498,7 @@ export default class Notes {
const cachedNoteBodyText = $noteBodyText.html(); const cachedNoteBodyText = $noteBodyText.html();
// Show updated comment content temporarily // Show updated comment content temporarily
$noteBodyText.html(_.escape(formContent)); $noteBodyText.html(formContent);
$editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half'); $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
$editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>'); $editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>');
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import SidebarHeightManager from './sidebar_height_manager';
(function() { (function() {
this.Sidebar = (function() { this.Sidebar = (function() {
...@@ -8,10 +9,6 @@ import Cookies from 'js-cookie'; ...@@ -8,10 +9,6 @@ import Cookies from 'js-cookie';
this.toggleTodo = this.toggleTodo.bind(this); this.toggleTodo = this.toggleTodo.bind(this);
this.sidebar = $('aside'); this.sidebar = $('aside');
this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab');
this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners(); this.removeListeners();
this.addEventListeners(); this.addEventListeners();
} }
...@@ -25,16 +22,14 @@ import Cookies from 'js-cookie'; ...@@ -25,16 +22,14 @@ import Cookies from 'js-cookie';
}; };
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
SidebarHeightManager.init();
const $document = $(document); const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20);
const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => debouncedSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) { $document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
...@@ -212,18 +207,6 @@ import Cookies from 'js-cookie'; ...@@ -212,18 +207,6 @@ import Cookies from 'js-cookie';
} }
}; };
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = this.$navGitlab.outerHeight();
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
this.$rightSidebar.outerHeight($(window).height() - diff);
this.$sidebarInner.height('100%');
} else {
this.$rightSidebar.outerHeight('100%');
this.$sidebarInner.height('');
}
};
Sidebar.prototype.isOpen = function() { Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded'); return this.sidebar.is('.right-sidebar-expanded');
}; };
......
export default {
init() {
if (!this.initialized) {
this.$window = $(window);
this.$rightSidebar = $('.js-right-sidebar');
this.$navHeight = $('.navbar-gitlab').outerHeight() +
$('.layout-nav').outerHeight() +
$('.sub-nav-scroll').outerHeight();
const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20);
const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200);
this.$window.on('scroll', throttledSetSidebarHeight);
this.$window.on('resize', debouncedSetSidebarHeight);
this.initialized = true;
}
},
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.$navHeight - currentScrollDepth;
if (diff > 0) {
const newSidebarHeight = window.innerHeight - diff;
this.$rightSidebar.outerHeight(newSidebarHeight);
this.sidebarHeightIsCustom = true;
} else if (this.sidebarHeightIsCustom) {
this.$rightSidebar.outerHeight('100%');
this.sidebarHeightIsCustom = false;
}
},
};
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import FilesCommentButton from './files_comment_button';
(function() { (function() {
window.SingleFileDiff = (function() { window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
...@@ -78,6 +80,8 @@ ...@@ -78,6 +80,8 @@
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
FilesCommentButton.init($(_this.file));
if (cb) cb(); if (cb) cb();
}; };
})(this)); })(this));
......
...@@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) { ...@@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) {
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
var isAuthorFilter;
isAuthorFilter = $('.js-author-search');
return _this.users(term, options, function(users) { return _this.users(term, options, function(users) {
// GitLabDropdownFilter returns this.instance // GitLabDropdownFilter returns this.instance
// GitLabDropdownRemote returns this.options.instance // GitLabDropdownRemote returns this.options.instance
......
...@@ -12,9 +12,18 @@ ...@@ -12,9 +12,18 @@
required: false, required: false,
default: '1', default: '1',
}, },
inline: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
rootElementType() {
return this.inline ? 'span' : 'div';
},
cssClass() { cssClass() {
return `fa-${this.size}x`; return `fa-${this.size}x`;
}, },
...@@ -22,12 +31,14 @@ ...@@ -22,12 +31,14 @@
}; };
</script> </script>
<template> <template>
<div class="text-center"> <component
:is="this.rootElementType"
class="text-center">
<i <i
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
:class="cssClass" :class="cssClass"
aria-hidden="true" aria-hidden="true"
:aria-label="label"> :aria-label="label">
</i> </i>
</div> </component>
</template> </template>
...@@ -64,6 +64,12 @@ ...@@ -64,6 +64,12 @@
*/ */
return new gl.GLForm($(this.$refs['gl-form']), true); return new gl.GLForm($(this.$refs['gl-form']), true);
}, },
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
if (glForm) {
glForm.destroy();
}
},
}; };
</script> </script>
......
/**
* This is the first script loaded by webpack's runtime. It is used to manually configure
* config.output.publicPath to account for relative_url_root or CDN settings which cannot be
* baked-in to our webpack bundles.
*/
if (gon && gon.webpack_public_path) {
__webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line
}
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
max-width: $limited-layout-width-sm; max-width: $limited-layout-width-sm;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-top: 64px;
padding-bottom: 64px;
} }
} }
......
...@@ -74,6 +74,8 @@ $red-700: #a62d19; ...@@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615; $red-800: #8b2615;
$red-900: #711e11; $red-900: #711e11;
$purple-600: #6e49cb;
$purple-650: #5c35ae;
$purple-700: #4a2192; $purple-700: #4a2192;
$purple-800: #2c0a5c; $purple-800: #2c0a5c;
$purple-900: #380d75; $purple-900: #380d75;
...@@ -103,6 +105,7 @@ $well-light-text-color: #5b6169; ...@@ -103,6 +105,7 @@ $well-light-text-color: #5b6169;
*/ */
$gl-font-size: 14px; $gl-font-size: 14px;
$gl-text-color: rgba(0, 0, 0, .85); $gl-text-color: rgba(0, 0, 0, .85);
$gl-text-color-light: rgba(0, 0, 0, .7);
$gl-text-color-secondary: rgba(0, 0, 0, .55); $gl-text-color-secondary: rgba(0, 0, 0, .55);
$gl-text-color-disabled: rgba(0, 0, 0, .35); $gl-text-color-disabled: rgba(0, 0, 0, .35);
$gl-text-color-inverted: rgba(255, 255, 255, 1.0); $gl-text-color-inverted: rgba(255, 255, 255, 1.0);
......
...@@ -11,20 +11,19 @@ header.navbar-gitlab-new { ...@@ -11,20 +11,19 @@ header.navbar-gitlab-new {
padding-left: 0; padding-left: 0;
.title-container { .title-container {
align-items: stretch;
padding-top: 0; padding-top: 0;
overflow: visible; overflow: visible;
} }
.title { .title {
display: block; display: flex;
height: 100%;
padding-right: 0; padding-right: 0;
color: currentColor; color: currentColor;
> a { > a {
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%;
padding-top: 3px; padding-top: 3px;
padding-right: $gl-padding; padding-right: $gl-padding;
padding-left: $gl-padding; padding-left: $gl-padding;
...@@ -265,3 +264,127 @@ header.navbar-gitlab-new { ...@@ -265,3 +264,127 @@ header.navbar-gitlab-new {
} }
} }
} }
.breadcrumbs {
display: flex;
min-height: 60px;
padding-top: $gl-padding-top;
padding-bottom: $gl-padding-top;
color: $gl-text-color;
border-bottom: 1px solid $border-color;
.dropdown-toggle-caret {
position: relative;
top: -1px;
padding: 0 5px;
color: rgba($black, .65);
font-size: 10px;
line-height: 1;
background: none;
border: 0;
&:focus {
outline: 0;
}
}
}
.breadcrumbs-container {
display: flex;
width: 100%;
position: relative;
.dropdown-menu-projects {
margin-top: -$gl-padding;
margin-left: $gl-padding;
}
}
.breadcrumbs-links {
flex: 1;
align-self: center;
color: $black-transparent;
a {
color: rgba($black, .65);
&:not(:first-child),
&.group-path {
margin-left: 4px;
}
&:not(:last-of-type),
&.group-path {
margin-right: 3px;
}
}
.title {
white-space: nowrap;
> a {
&:last-of-type {
font-weight: 600;
}
}
}
.avatar-tile {
margin-right: 5px;
border: 1px solid $border-color;
border-radius: 50%;
vertical-align: sub;
&.identicon {
float: left;
width: 16px;
height: 16px;
margin-top: 2px;
font-size: 10px;
}
}
.text-expander {
margin-left: 4px;
margin-right: 4px;
> i {
position: relative;
top: 1px;
}
}
}
.breadcrumbs-extra {
flex: 0 0 auto;
margin-left: auto;
}
.breadcrumbs-sub-title {
margin: 2px 0 0;
font-size: 16px;
font-weight: normal;
ul {
margin: 0;
}
li {
display: inline-block;
&:not(:last-child) {
&::after {
content: "/";
margin: 0 2px 0 5px;
}
}
&:last-child a {
font-weight: 600;
}
}
a {
color: $gl-text-color;
}
}
@import "framework/variables";
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
$new-sidebar-width: 220px;
.page-with-new-sidebar {
@media (min-width: $screen-sm-min) {
padding-left: $new-sidebar-width;
}
// Override position: absolute
.right-sidebar {
position: fixed;
height: 100%;
}
}
.context-header {
background-color: $gray-normal;
border-bottom: 1px solid $border-color;
font-weight: 600;
display: flex;
align-items: center;
padding: 10px 14px;
.avatar-container {
flex: 0 0 40px;
}
&:hover {
background-color: $border-color;
}
}
.settings-avatar {
background-color: $white-light;
i {
font-size: 20px;
width: 100%;
color: $gl-text-color-secondary;
text-align: center;
align-self: center;
}
}
.nav-sidebar {
position: fixed;
z-index: 400;
width: $new-sidebar-width;
transition: width $sidebar-transition-duration;
top: 50px;
bottom: 0;
left: 0;
overflow: auto;
background-color: $gray-light;
border-right: 1px solid $border-color;
ul {
padding: 0;
list-style: none;
}
li {
white-space: nowrap;
a {
display: block;
padding: 12px 14px;
}
}
a {
color: $gl-text-color;
text-decoration: none;
}
@media (max-width: $screen-xs-max) {
width: 0;
}
}
.sidebar-sub-level-items {
display: none;
> li {
a {
padding: 12px 24px;
color: $gl-text-color-light;
&:hover {
color: $gl-text-color;
background-color: $border-color;
}
}
&.active {
> a {
color: $purple-650;
font-weight: 600;
}
}
}
}
.sidebar-top-level-items {
> li {
.badge {
float: right;
background-color: $border-color;
color: $gl-text-color;
}
&.active {
> a {
background-color: $purple-600;
color: $white-light;
font-weight: 600;
}
.badge {
background-color: $purple-700;
color: $white-light;
}
.sidebar-sub-level-items {
background-color: $gray-normal;
border-left: 6px solid $purple-600;
display: block;
}
}
&:not(.active) > a:hover {
background-color: $border-color;
.badge {
transition: background-color 100ms linear;
background-color: $gray-normal;
}
}
}
}
// Make issue boards full-height now that sub-nav is gone
.boards-list {
height: calc(100vh - 50px);
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
// scss-lint:disable DuplicateProperty
height: calc(100vh - 120px);
// scss-lint:enable DuplicateProperty
}
}
...@@ -147,10 +147,9 @@ ...@@ -147,10 +147,9 @@
top: 35px; top: 35px;
left: 10px; left: 10px;
bottom: 0; bottom: 0;
overflow-y: scroll;
overflow-x: hidden;
padding: 10px 20px 20px 5px; padding: 10px 20px 20px 5px;
white-space: pre; white-space: pre-wrap;
overflow: auto;
} }
.environment-information { .environment-information {
...@@ -399,6 +398,7 @@ ...@@ -399,6 +398,7 @@
.build-light-text { .build-light-text {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
word-wrap: break-word;
} }
.build-gutter-toggle { .build-gutter-toggle {
......
...@@ -20,8 +20,6 @@ ...@@ -20,8 +20,6 @@
} }
.diff-content { .diff-content {
overflow: auto;
overflow-y: hidden;
background: $white-light; background: $white-light;
color: $gl-text-color; color: $gl-text-color;
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
...@@ -476,6 +474,7 @@ ...@@ -476,6 +474,7 @@
height: 19px; height: 19px;
width: 19px; width: 19px;
margin-left: -15px; margin-left: -15px;
z-index: 100;
&:hover { &:hover {
.diff-comment-avatar, .diff-comment-avatar,
...@@ -491,7 +490,7 @@ ...@@ -491,7 +490,7 @@
transform: translateX((($i * $x-pos) - $x-pos)); transform: translateX((($i * $x-pos) - $x-pos));
&:hover { &:hover {
transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); transform: translateX((($i * $x-pos) - $x-pos));
} }
} }
} }
...@@ -542,6 +541,7 @@ ...@@ -542,6 +541,7 @@
height: 19px; height: 19px;
padding: 0; padding: 0;
transition: transform .1s ease-out; transition: transform .1s ease-out;
z-index: 100;
svg { svg {
position: absolute; position: absolute;
...@@ -555,10 +555,6 @@ ...@@ -555,10 +555,6 @@
fill: $white-light; fill: $white-light;
} }
&:hover {
transform: scale(1.2);
}
&:focus { &:focus {
outline: 0; outline: 0;
} }
......
...@@ -140,23 +140,6 @@ ...@@ -140,23 +140,6 @@
} }
} }
.prometheus-graph {
text {
fill: $gl-text-color;
stroke-width: 0;
}
.label-axis-text,
.text-metric-usage {
fill: $black;
font-weight: 500;
}
.legend-axis-text {
fill: $black;
}
}
.x-axis path, .x-axis path,
.y-axis path, .y-axis path,
.label-x-axis-line, .label-x-axis-line,
...@@ -205,6 +188,7 @@ ...@@ -205,6 +188,7 @@
.text-metric { .text-metric {
font-weight: 600; font-weight: 600;
font-size: 14px;
} }
.selected-metric-line { .selected-metric-line {
...@@ -214,20 +198,15 @@ ...@@ -214,20 +198,15 @@
.deployment-line { .deployment-line {
stroke: $black; stroke: $black;
stroke-width: 2; stroke-width: 1;
} }
.deploy-info-text { .deploy-info-text {
dominant-baseline: text-before-edge; dominant-baseline: text-before-edge;
} }
.text-metric-bold {
font-weight: 600;
}
.prometheus-state { .prometheus-state {
margin-top: 10px; margin-top: 10px;
display: none;
.state-button-section { .state-button-section {
margin-top: 10px; margin-top: 10px;
...@@ -242,3 +221,59 @@ ...@@ -242,3 +221,59 @@
width: 38px; width: 38px;
} }
} }
.prometheus-panel {
margin-top: 20px;
}
.prometheus-svg-container {
position: relative;
height: 0;
width: 100%;
padding: 0;
padding-bottom: 100%;
.text-metric-bold {
font-weight: 600;
}
}
.prometheus-svg-container > svg {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
text {
fill: $gl-text-color;
stroke-width: 0;
}
.label-axis-text,
.text-metric-usage {
fill: $black;
font-weight: 500;
font-size: 14px;
}
.legend-axis-text {
fill: $black;
}
.tick > text {
font-size: 14px;
}
@media (max-width: $screen-sm-max) {
.label-axis-text,
.text-metric-usage,
.legend-axis-text {
font-size: 8px;
}
.tick > text {
font-size: 8px;
}
}
}
...@@ -597,7 +597,38 @@ ...@@ -597,7 +597,38 @@
.issue-info-container { .issue-info-container {
-webkit-flex: 1; -webkit-flex: 1;
flex: 1; flex: 1;
display: flex;
padding-right: $gl-padding; padding-right: $gl-padding;
.issue-main-info {
flex: 1 auto;
margin-right: 10px;
}
.issuable-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
flex: 1 0 auto;
.controls {
margin-bottom: 2px;
line-height: 20px;
padding: 0;
}
.issue-updated-at {
line-height: 20px;
}
}
@media(max-width: $screen-xs-max) {
.issuable-meta {
.controls li {
margin-right: 0;
}
}
}
} }
.issue-check { .issue-check {
...@@ -609,6 +640,30 @@ ...@@ -609,6 +640,30 @@
vertical-align: text-top; vertical-align: text-top;
} }
} }
.issuable-milestone,
.issuable-info,
.task-status,
.issuable-updated-at {
font-weight: normal;
color: $gl-text-color-secondary;
a {
color: $gl-text-color;
.fa {
color: $gl-text-color-secondary;
}
}
}
@media(max-width: $screen-md-max) {
.task-status,
.issuable-due-date,
.project-ref-path {
display: none;
}
}
} }
} }
......
...@@ -279,5 +279,9 @@ ...@@ -279,5 +279,9 @@
.label-link { .label-link {
display: inline-block; display: inline-block;
vertical-align: text-top; vertical-align: top;
.label {
vertical-align: inherit;
}
} }
...@@ -628,8 +628,14 @@ ul.notes { ...@@ -628,8 +628,14 @@ ul.notes {
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
.line_holder .is-over:not(.no-comment-btn) {
.add-diff-note {
opacity: 1;
}
}
.add-diff-note { .add-diff-note {
display: none; opacity: 0;
margin-top: -2px; margin-top: -2px;
border-radius: 50%; border-radius: 50%;
background: $white-light; background: $white-light;
...@@ -642,13 +648,11 @@ ul.notes { ...@@ -642,13 +648,11 @@ ul.notes {
width: 23px; width: 23px;
height: 23px; height: 23px;
border: 1px solid $blue-500; border: 1px solid $blue-500;
transition: transform .1s ease-in-out;
&:hover { &:hover {
background: $blue-500; background: $blue-500;
border-color: $blue-600; border-color: $blue-600;
color: $white-light; color: $white-light;
transform: scale(1.15);
} }
&:active { &:active {
......
...@@ -377,6 +377,7 @@ a.deploy-project-label { ...@@ -377,6 +377,7 @@ a.deploy-project-label {
} }
.breadcrumb.repo-breadcrumb { .breadcrumb.repo-breadcrumb {
flex: 1;
padding: 0; padding: 0;
background: transparent; background: transparent;
border: none; border: none;
...@@ -482,11 +483,12 @@ a.deploy-project-label { ...@@ -482,11 +483,12 @@ a.deploy-project-label {
.project-stats { .project-stats {
font-size: 0; font-size: 0;
text-align: center; text-align: center;
max-width: 100%;
border-bottom: 1px solid $border-color;
.nav { .nav {
padding-top: 12px; padding-top: 12px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid $border-color;
} }
.nav > li { .nav > li {
......
...@@ -33,3 +33,20 @@ ...@@ -33,3 +33,20 @@
font-weight: normal; font-weight: normal;
} }
} }
.admin-runner-btn-group-cell {
min-width: 150px;
.btn-sm {
padding: 4px 9px;
}
.btn-default {
color: $gl-text-color-secondary;
}
.fa-pause,
.fa-play {
font-size: 11px;
}
}
.tree-holder { .tree-holder {
.nav-block { .nav-block {
margin: 10px 0; margin: 10px 0;
...@@ -15,6 +16,11 @@ ...@@ -15,6 +16,11 @@
.btn-group { .btn-group {
margin-left: 10px; margin-left: 10px;
} }
.control {
float: left;
margin-left: 10px;
}
} }
.tree-ref-holder { .tree-ref-holder {
...@@ -70,7 +76,8 @@ ...@@ -70,7 +76,8 @@
} }
.file-finder { .file-finder {
width: 50%; max-width: 500px;
width: 100%;
.file-finder-input { .file-finder-input {
width: 95%; width: 95%;
......
class AbuseReportsController < ApplicationController class AbuseReportsController < ApplicationController
before_action :set_user, only: [:new]
def new def new
@abuse_report = AbuseReport.new @abuse_report = AbuseReport.new
@abuse_report.user_id = params[:user_id] @abuse_report.user_id = @user.id
@ref_url = params.fetch(:ref_url, '') @ref_url = params.fetch(:ref_url, '')
end end
...@@ -27,4 +29,14 @@ class AbuseReportsController < ApplicationController ...@@ -27,4 +29,14 @@ class AbuseReportsController < ApplicationController
user_id user_id
)) ))
end end
def set_user
@user = User.find_by(id: params[:user_id])
if @user.nil?
redirect_to root_path, alert: "Cannot create the abuse report. The user has been deleted."
elsif @user.blocked?
redirect_to @user, alert: "Cannot create the abuse report. This user has been blocked."
end
end
end end
...@@ -78,7 +78,7 @@ module CreatesCommit ...@@ -78,7 +78,7 @@ module CreatesCommit
end end
def new_merge_request_path def new_merge_request_path
new_namespace_project_merge_request_path( namespace_project_new_merge_request_path(
@project_to_commit_into.namespace, @project_to_commit_into.namespace,
@project_to_commit_into, @project_to_commit_into,
merge_request: { merge_request: {
......
...@@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController
@milestone_states = GlobalMilestone.states_count(@projects) @milestone_states = GlobalMilestone.states_count(@projects)
@milestones = Kaminari.paginate_array(milestones).page(params[:page]) @milestones = Kaminari.paginate_array(milestones).page(params[:page])
end end
format.json do
render json: milestones.map { |m| m.for_display.slice(:title, :name) }
end
end end
end end
......
...@@ -267,10 +267,22 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -267,10 +267,22 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def issue_params def issue_params
params.require(:issue).permit( params.require(:issue).permit(*issue_params_attributes)
:title, :assignee_id, :position, :description, :confidential, end
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [], assignee_ids: []
) def issue_params_attributes
%i[
title
assignee_id
position
description
confidential
milestone_id
due_date
state_event
task_num
lock_version
] + [{ label_ids: [], assignee_ids: [] }]
end end
def authenticate_user! def authenticate_user!
......
class Projects::MergeRequests::ApplicationController < Projects::ApplicationController
before_action :check_merge_requests_available!
before_action :merge_request
before_action :authorize_read_merge_request!
before_action :ensure_ref_fetched
private
def merge_request
@issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
# Make sure merge requests created before 8.0
# have head file in refs/merge-requests/
def ensure_ref_fetched
@merge_request.ensure_ref_fetched
end
def merge_request_params
params.require(:merge_request)
.permit(merge_request_params_attributes)
end
def merge_request_params_attributes
[
:assignee_id,
:description,
:force_remove_source_branch,
:lock_version,
:milestone_id,
:source_branch,
:source_project_id,
:state_event,
:target_branch,
:target_project_id,
:task_num,
:title,
label_ids: []
]
end
def set_pipeline_variables
@pipelines = @merge_request.all_pipelines
@pipeline = @merge_request.head_pipeline
@statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0
end
end
class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::ApplicationController
include IssuableActions
before_action :authorize_can_resolve_conflicts!
def show
respond_to do |format|
format.html do
labels
end
format.json do
if @conflicts_list.can_be_resolved_in_ui?
render json: @conflicts_list
elsif @merge_request.can_be_merged?
render json: {
message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.',
type: 'error'
}
else
render json: {
message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.',
type: 'error'
}
end
end
end
end
def conflict_for_path
return render_404 unless @conflicts_list.can_be_resolved_in_ui?
file = @conflicts_list.file_for_path(params[:old_path], params[:new_path])
return render_404 unless file
render json: file, full_content: true
end
def resolve_conflicts
return render_404 unless @conflicts_list.can_be_resolved_in_ui?
if @merge_request.can_be_merged?
render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' }
return
end
begin
::MergeRequests::Conflicts::ResolveService
.new(merge_request)
.execute(current_user, params)
flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
rescue Gitlab::Conflict::ResolutionError => e
render status: :bad_request, json: { message: e.message }
end
end
def authorize_can_resolve_conflicts!
@conflicts_list = ::MergeRequests::Conflicts::ListService.new(@merge_request)
return render_404 unless @conflicts_list.can_be_resolved_by?(current_user)
end
end
class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController
include DiffForPath
include DiffHelper
skip_before_action :merge_request
skip_before_action :ensure_ref_fetched
before_action :authorize_create_merge_request!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
def new
define_new_vars
end
def create
@target_branches ||= []
@merge_request = ::MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
if @merge_request.valid?
redirect_to(merge_request_path(@merge_request))
else
@source_project = @merge_request.source_project
@target_project = @merge_request.target_project
define_new_vars
render action: "new"
end
end
def pipelines
@pipelines = @merge_request.all_pipelines
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: {
pipelines: PipelineSerializer
.new(project: @project, current_user: @current_user)
.represent(@pipelines)
}
end
def diffs
@diffs = if @merge_request.can_be_created
@merge_request.diffs(diff_options)
else
[]
end
@diff_notes_disabled = true
@environment = @merge_request.environments_for(current_user).last
render json: { html: view_to_html_string('projects/merge_requests/creations/_diffs', diffs: @diffs, environment: @environment) }
end
def diff_for_path
@diffs = @merge_request.diffs(diff_options)
@diff_notes_disabled = true
render_diff_for_path(@diffs)
end
def branch_from
# This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
if params[:ref].present?
@ref = params[:ref]
@commit = @repository.commit("refs/heads/#{@ref}")
end
render layout: false
end
def branch_to
@target_project = selected_target_project
if params[:ref].present?
@ref = params[:ref]
@commit = @target_project.commit("refs/heads/#{@ref}")
end
render layout: false
end
def update_branches
@target_project = selected_target_project
@target_branches = @target_project.repository.branch_names
render layout: false
end
private
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
def define_new_vars
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@merge_request.target_project.repository.branch_names
else
[]
end
@target_project = @merge_request.target_project
@source_project = @merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@note_counts = Note.where(commit_id: @commits.map(&:id))
.group(:commit_id).count
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
set_pipeline_variables
end
def selected_target_project
if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil?
@project
else
@project.forked_project_link.forked_from_project
end
end
end
class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController
include DiffForPath
include DiffHelper
include RendersNotes
before_action :apply_diff_view_cookie!
before_action :define_diff_vars
before_action :define_diff_comment_vars
def show
@environment = @merge_request.environments_for(current_user).last
render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
end
def diff_for_path
render_diff_for_path(@diffs)
end
private
def define_diff_vars
@merge_request_diff =
if params[:diff_id]
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else
@merge_request.merge_request_diff
end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@start_sha = params[:start_sha]
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
@start_sha = @merge_request_diff.head_commit_sha
@start_version = @merge_request_diff
end
end
@compare =
if @start_sha
@merge_request_diff.compare_with(@start_sha)
else
@merge_request_diff
end
@diffs = @compare.diffs(diff_options)
end
def define_diff_comment_vars
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
noteable_id: @merge_request.id
}
@diff_notes_disabled = false
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs)
@notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes))
end
end
class Projects::PipelineSchedulesController < Projects::ApplicationController class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_read_pipeline_schedule! before_action :authorize_read_pipeline_schedule!
before_action :authorize_create_pipeline_schedule!, only: [:new, :create, :edit, :take_ownership, :update] before_action :authorize_create_pipeline_schedule!, only: [:new, :create]
before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy] before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
before_action :schedule, only: [:edit, :update, :destroy, :take_ownership] before_action :schedule, only: [:edit, :update, :destroy, :take_ownership]
......
...@@ -97,7 +97,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -97,7 +97,7 @@ class ProjectsController < Projects::ApplicationController
end end
if @project.pending_delete? if @project.pending_delete?
flash[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name } flash.now[:alert] = _("Project '%{project_name}' queued for deletion.") % { project_name: @project.name }
end end
respond_to do |format| respond_to do |format|
......
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
# #
class IssuableFinder class IssuableFinder
NONE = '0'.freeze NONE = '0'.freeze
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
...@@ -62,7 +63,7 @@ class IssuableFinder ...@@ -62,7 +63,7 @@ class IssuableFinder
# grouping and counting within that query. # grouping and counting within that query.
# #
def count_by_state def count_by_state
count_params = params.merge(state: nil, sort: nil) count_params = params.merge(state: nil, sort: nil, for_counting: true)
labels_count = label_names.any? ? label_names.count : 1 labels_count = label_names.any? ? label_names.count : 1
finder = self.class.new(current_user, count_params) finder = self.class.new(current_user, count_params)
counts = Hash.new(0) counts = Hash.new(0)
...@@ -86,6 +87,10 @@ class IssuableFinder ...@@ -86,6 +87,10 @@ class IssuableFinder
execute.find_by!(*params) execute.find_by!(*params)
end end
def state_counter_cache_key(state)
Digest::SHA1.hexdigest(state_counter_cache_key_components(state).flatten.join('-'))
end
def group def group
return @group if defined?(@group) return @group if defined?(@group)
...@@ -418,4 +423,13 @@ class IssuableFinder ...@@ -418,4 +423,13 @@ class IssuableFinder
def current_user_related? def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end end
def state_counter_cache_key_components(state)
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
opts.delete_if { |_, value| value.blank? }
['issuables_count', klass.to_ability_name, opts.sort]
end
end end
...@@ -16,14 +16,72 @@ ...@@ -16,14 +16,72 @@
# sort: string # sort: string
# #
class IssuesFinder < IssuableFinder class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
def klass def klass
Issue Issue
end end
def with_confidentiality_access_check
return Issue.all if user_can_see_all_confidential_issues?
return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues?
Issue.where('
issues.confidential IS NOT TRUE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
OR issues.project_id IN(:project_ids)))',
user_id: current_user.id,
project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id))
end
private private
def init_collection def init_collection
IssuesFinder.not_restricted_by_confidentiality(current_user) with_confidentiality_access_check
end
def user_can_see_all_confidential_issues?
return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues)
return @user_can_see_all_confidential_issues = false if current_user.blank?
return @user_can_see_all_confidential_issues = true if current_user.full_private_access?
@user_can_see_all_confidential_issues =
project? &&
project &&
project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
end
# Anonymous users can't see any confidential issues.
#
# Users without access to see _all_ confidential issues (as in
# `user_can_see_all_confidential_issues?`) are more complicated, because they
# can see confidential issues where:
# 1. They are an assignee.
# 2. They are an author.
#
# That's fine for most cases, but if we're just counting, we need to cache
# effectively. If we cached this accurately, we'd have a cache key for every
# authenticated user without sufficient access to the project. Instead, when
# we are counting, we treat them as if they can't see any confidential issues.
#
# This does mean the counts may be wrong for those users, but avoids an
# explosion in cache keys.
def user_cannot_see_confidential_issues?(for_counting: false)
return false if user_can_see_all_confidential_issues?
current_user.blank? || for_counting || params[:for_counting]
end
def state_counter_cache_key_components(state)
extra_components = [
user_can_see_all_confidential_issues?,
user_cannot_see_confidential_issues?(for_counting: true)
]
super + extra_components
end end
def by_assignee(items) def by_assignee(items)
...@@ -38,21 +96,6 @@ class IssuesFinder < IssuableFinder ...@@ -38,21 +96,6 @@ class IssuesFinder < IssuableFinder
end end
end end
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NOT TRUE') if user.blank?
return Issue.all if user.full_private_access?
Issue.where('
issues.confidential IS NOT TRUE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
OR issues.project_id IN(:project_ids)))',
user_id: user.id,
project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
end
def item_project_ids(items) def item_project_ids(items)
items&.reorder(nil)&.select(:project_id) items&.reorder(nil)&.select(:project_id)
end end
......
...@@ -83,7 +83,12 @@ class LabelsFinder < UnionFinder ...@@ -83,7 +83,12 @@ class LabelsFinder < UnionFinder
def projects def projects
return @projects if defined?(@projects) return @projects if defined?(@projects)
@projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute @projects = if skip_authorization
Project.all
else
ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute
end
@projects = @projects.in_namespace(params[:group_id]) if group? @projects = @projects.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil) @projects = @projects.reorder(nil)
......
...@@ -83,6 +83,8 @@ class TodosFinder ...@@ -83,6 +83,8 @@ class TodosFinder
if project? if project?
@project = Project.find(params[:project_id]) @project = Project.find(params[:project_id])
@project = nil if @project.pending_delete?
unless Ability.allowed?(current_user, :read_project, @project) unless Ability.allowed?(current_user, :read_project, @project)
@project = nil @project = nil
end end
......
...@@ -60,13 +60,13 @@ class UsersFinder ...@@ -60,13 +60,13 @@ class UsersFinder
end end
def by_external_identity(users) def by_external_identity(users)
return users unless current_user.admin? && params[:extern_uid] && params[:provider] return users unless current_user&.admin? && params[:extern_uid] && params[:provider]
users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid]))
end end
def by_external(users) def by_external(users)
return users = users.where.not(external: true) unless current_user.admin? return users = users.where.not(external: true) unless current_user&.admin?
return users unless params[:external] return users unless params[:external]
users.external users.external
......
...@@ -131,10 +131,7 @@ module ApplicationHelper ...@@ -131,10 +131,7 @@ module ApplicationHelper
end end
def body_data_page def body_data_page
path = controller.controller_path.split('/') [*controller.controller_path.split('/'), controller.action_name].compact.join(':')
namespace = path.first if path.second
[namespace, controller.controller_name, controller.action_name].compact.join(':')
end end
# shortcut for gitlab config # shortcut for gitlab config
......
...@@ -284,7 +284,7 @@ module BlobHelper ...@@ -284,7 +284,7 @@ module BlobHelper
merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project)) merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project))
if merge_project if merge_project
options << link_to("create a merge request", new_namespace_project_merge_request_path(project.namespace, project)) options << link_to("create a merge request", namespace_project_new_merge_request_path(project.namespace, project))
end end
options options
......
...@@ -9,7 +9,7 @@ module CompareHelper ...@@ -9,7 +9,7 @@ module CompareHelper
end end
def create_mr_path(from = params[:from], to = params[:to], project = @project) def create_mr_path(from = params[:from], to = params[:to], project = @project)
new_namespace_project_merge_request_path( namespace_project_new_merge_request_path(
project.namespace, project.namespace,
project, project,
merge_request: { merge_request: {
......
...@@ -16,8 +16,8 @@ module FormHelper ...@@ -16,8 +16,8 @@ module FormHelper
end end
end end
def issue_dropdown_options(issuable, has_multiple_assignees = true) def issue_assignees_dropdown_options
options = { {
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee', title: 'Select assignee',
filter: true, filter: true,
...@@ -27,8 +27,8 @@ module FormHelper ...@@ -27,8 +27,8 @@ module FormHelper
first_user: current_user&.username, first_user: current_user&.username,
null_user: true, null_user: true,
current_user: true, current_user: true,
project_id: issuable.project.try(:id), project_id: @project.id,
field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]", field_name: 'issue[assignee_ids][]',
default_label: 'Unassigned', default_label: 'Unassigned',
'max-select': 1, 'max-select': 1,
'dropdown-header': 'Assignee', 'dropdown-header': 'Assignee',
...@@ -38,13 +38,5 @@ module FormHelper ...@@ -38,13 +38,5 @@ module FormHelper
current_user_info: current_user.to_json(only: [:id, :name]) current_user_info: current_user.to_json(only: [:id, :name])
} }
} }
if has_multiple_assignees
options[:title] = 'Select assignee(s)'
options[:data][:'dropdown-header'] = 'Assignee(s)'
options[:data].delete(:'max-select')
end
options
end end
end end
...@@ -16,11 +16,12 @@ module GroupsHelper ...@@ -16,11 +16,12 @@ module GroupsHelper
full_title = '' full_title = ''
group.ancestors.reverse.each do |parent| group.ancestors.reverse.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable') full_title += group_title_link(parent, hidable: true)
full_title += '<span class="hidable"> / </span>'.html_safe full_title += '<span class="hidable"> / </span>'.html_safe
end end
full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path') full_title += group_title_link(group)
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
content_tag :span, class: 'group-title' do content_tag :span, class: 'group-title' do
...@@ -56,4 +57,20 @@ module GroupsHelper ...@@ -56,4 +57,20 @@ module GroupsHelper
def group_issues(group) def group_issues(group)
IssuesFinder.new(current_user, group_id: group.id).execute IssuesFinder.new(current_user, group_id: group.id).execute
end end
private
def group_title_link(group, hidable: false)
link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do
output =
if show_new_nav?
image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16)
else
""
end
output << simple_sanitize(group.name)
output.html_safe
end
end
end end
...@@ -165,11 +165,7 @@ module IssuablesHelper ...@@ -165,11 +165,7 @@ module IssuablesHelper
} }
state_title = titles[state] || state.to_s.humanize state_title = titles[state] || state.to_s.humanize
count = issuables_count_for_state(issuable_type, state)
count =
Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
issuables_count_for_state(issuable_type, state)
end
html = content_tag(:span, state_title) html = content_tag(:span, state_title)
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge') html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
...@@ -237,6 +233,18 @@ module IssuablesHelper ...@@ -237,6 +233,18 @@ module IssuablesHelper
} }
end end
def issuables_count_for_state(issuable_type, state, finder: nil)
finder ||= public_send("#{issuable_type}_finder")
cache_key = finder.state_counter_cache_key(state)
@counts ||= {}
@counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
finder.count_by_state
end
@counts[cache_key][state]
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
...@@ -255,24 +263,6 @@ module IssuablesHelper ...@@ -255,24 +263,6 @@ module IssuablesHelper
end end
end end
def issuables_count_for_state(issuable_type, state)
@counts ||= {}
@counts[issuable_type] ||= public_send("#{issuable_type}_finder").count_by_state
@counts[issuable_type][state]
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page].freeze
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
def issuables_state_counter_cache_key(issuable_type, state)
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
opts.delete_if { |_, value| value.blank? }
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
def issuable_templates(issuable) def issuable_templates(issuable)
@issuable_templates ||= @issuable_templates ||=
case issuable case issuable
......
module MergeRequestsHelper module MergeRequestsHelper
def new_mr_path_from_push_event(event) def new_mr_path_from_push_event(event)
target_project = event.project.default_merge_request_target target_project = event.project.default_merge_request_target
new_namespace_project_merge_request_path( namespace_project_new_merge_request_path(
event.project.namespace, event.project.namespace,
event.project, event.project,
new_mr_from_push_event(event, target_project) new_mr_from_push_event(event, target_project)
...@@ -48,7 +48,7 @@ module MergeRequestsHelper ...@@ -48,7 +48,7 @@ module MergeRequestsHelper
end end
def mr_change_branches_path(merge_request) def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path( namespace_project_new_merge_request_path(
@project.namespace, @project, @project.namespace, @project,
merge_request: { merge_request: {
source_project_id: merge_request.source_project_id, source_project_id: merge_request.source_project_id,
......
...@@ -74,6 +74,8 @@ module MilestonesHelper ...@@ -74,6 +74,8 @@ module MilestonesHelper
project = @target_project || @project project = @target_project || @project
if project if project
namespace_project_milestones_path(project.namespace, project, :json) namespace_project_milestones_path(project.namespace, project, :json)
elsif @group
group_milestones_path(@group, :json)
else else
dashboard_milestones_path(:json) dashboard_milestones_path(:json)
end end
......
module NavHelper module NavHelper
def page_gutter_class def page_gutter_class
if current_path?('merge_requests#show') || if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') || current_path?('projects/merge_requests/conflicts#show') ||
current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
current_path?('issues#show') || current_path?('issues#show') ||
current_path?('milestones#show') current_path?('milestones#show')
if cookies[:collapsed_gutter] == 'true' if cookies[:collapsed_gutter] == 'true'
......
...@@ -47,6 +47,18 @@ module NotesHelper ...@@ -47,6 +47,18 @@ module NotesHelper
data data
end end
def add_diff_note_button(line_code, position, line_type)
return if @diff_notes_disabled
button_tag '',
class: 'add-diff-note js-add-diff-note-button',
type: 'submit', name: 'button',
data: diff_view_line_data(line_code, position, line_type),
title: 'Add a comment to this line' do
icon('comment-o')
end
end
def link_to_reply_discussion(discussion, line_type = nil) def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user return unless current_user
......
...@@ -58,7 +58,17 @@ module ProjectsHelper ...@@ -58,7 +58,17 @@ module ProjectsHelper
link_to(simple_sanitize(owner.name), user_path(owner)) link_to(simple_sanitize(owner.name), user_path(owner))
end end
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" } project_link = link_to project_path(project), { class: "project-item-select-holder" } do
output =
if show_new_nav?
project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16)
else
""
end
output << simple_sanitize(project.name)
output.html_safe
end
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', order_by: 'last_activity_at' }) 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
......
...@@ -126,6 +126,18 @@ module SearchHelper ...@@ -126,6 +126,18 @@ module SearchHelper
search_path(options) search_path(options)
end end
def search_filter_input_options(type)
{
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
data: {
'project-id' => @project.id,
'username-params' => @users.to_json(only: [:id, :username]),
'base-endpoint' => namespace_project_path(@project.namespace, @project)
}
}
end
# Sanitize a HTML field for search display. Most tags are stripped out and the # Sanitize a HTML field for search display. Most tags are stripped out and the
# maximum length is set to 200 characters. # maximum length is set to 200 characters.
def search_md_sanitize(object, field) def search_md_sanitize(object, field)
......
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.
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