Commit b23a7bca authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2017-06-28

# Conflicts:
#	app/assets/javascripts/locale/bg/app.js
#	app/assets/javascripts/locale/de/app.js
#	app/assets/javascripts/locale/en/app.js
#	app/assets/javascripts/locale/fr/app.js
#	app/assets/javascripts/locale/pt_BR/app.js
#	app/assets/javascripts/locale/zh_HK/app.js
#	app/assets/javascripts/locale/zh_TW/app.js
#	doc/README.md
#	lib/gitlab/ldap/access.rb
#	spec/lib/gitlab/ldap/access_spec.rb
#	spec/models/project_spec.rb
[ci skip]
parents 75ac0420 d3c3200c
...@@ -21,6 +21,7 @@ eslint-report.html ...@@ -21,6 +21,7 @@ eslint-report.html
/.yarn-cache /.yarn-cache
/.byebug_history /.byebug_history
/Vagrantfile /Vagrantfile
/app/assets/javascripts/locale/**/app.js
/backups/* /backups/*
/config/aws.yml /config/aws.yml
/config/database.yml /config/database.yml
......
...@@ -199,6 +199,7 @@ setup-test-env: ...@@ -199,6 +199,7 @@ setup-test-env:
script: script:
- node --version - node --version
- yarn install --pure-lockfile --cache-folder .yarn-cache - yarn install --pure-lockfile --cache-folder .yarn-cache
- bundle exec rake gettext:po_to_json
- bundle exec rake gitlab:assets:compile - bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts: artifacts:
...@@ -419,6 +420,7 @@ gitlab:assets:compile: ...@@ -419,6 +420,7 @@ gitlab:assets:compile:
NO_COMPRESSION: "true" NO_COMPRESSION: "true"
script: script:
- yarn install --pure-lockfile --production --cache-folder .yarn-cache - yarn install --pure-lockfile --production --cache-folder .yarn-cache
- bundle exec rake gettext:po_to_json
- bundle exec rake gitlab:assets:compile - bundle exec rake gitlab:assets:compile
artifacts: artifacts:
name: webpack-report name: webpack-report
...@@ -436,6 +438,7 @@ karma: ...@@ -436,6 +438,7 @@ karma:
BABEL_ENV: "coverage" BABEL_ENV: "coverage"
CHROME_LOG_FILE: "chrome_debug.log" CHROME_LOG_FILE: "chrome_debug.log"
script: script:
- bundle exec rake gettext:po_to_json
- bundle exec rake karma - bundle exec rake karma
coverage: '/^Statements *: (\d+\.\d+%)/' coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts: artifacts:
...@@ -459,6 +462,7 @@ codeclimate: ...@@ -459,6 +462,7 @@ codeclimate:
script: script:
- docker pull codeclimate/codeclimate - 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 > 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
- sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' codeclimate.json
artifacts: artifacts:
paths: [codeclimate.json] paths: [codeclimate.json]
...@@ -533,3 +537,9 @@ cache gems: ...@@ -533,3 +537,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
...@@ -365,7 +365,7 @@ group :test do ...@@ -365,7 +365,7 @@ group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0' gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2' gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.24.0' gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1' gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0' gem 'timecop', '~> 0.8.0'
......
...@@ -395,7 +395,7 @@ GEM ...@@ -395,7 +395,7 @@ GEM
temple (~> 0.7.6) temple (~> 0.7.6)
thor thor
tilt tilt
hashdiff (0.3.2) hashdiff (0.3.4)
hashie (3.5.5) hashie (3.5.5)
hashie-forbidden_attributes (0.1.1) hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0) hashie (>= 3.0)
...@@ -491,7 +491,7 @@ GEM ...@@ -491,7 +491,7 @@ GEM
mimemagic (0.3.0) mimemagic (0.3.0)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.6) mmap2 (2.2.7)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
msgpack (1.1.0) msgpack (1.1.0)
multi_json (1.12.1) multi_json (1.12.1)
...@@ -919,7 +919,7 @@ GEM ...@@ -919,7 +919,7 @@ GEM
vmstat (2.3.0) vmstat (2.3.0)
warden (1.2.6) warden (1.2.6)
rack (>= 1.0) rack (>= 1.0)
webmock (1.24.6) webmock (2.3.2)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
...@@ -1158,7 +1158,7 @@ DEPENDENCIES ...@@ -1158,7 +1158,7 @@ DEPENDENCIES
version_sorter (~> 2.1.0) version_sorter (~> 2.1.0)
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
webmock (~> 1.24.0) webmock (~> 2.3.2)
webpack-rails (~> 0.9.10) webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
......
...@@ -2,11 +2,7 @@ ...@@ -2,11 +2,7 @@
/* global Flash */ /* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import * as Emoji from './emoji';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from './behaviors/gl_emoji';
import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
...@@ -17,8 +13,6 @@ const requestAnimationFrame = window.requestAnimationFrame || ...@@ -17,8 +13,6 @@ const requestAnimationFrame = window.requestAnimationFrame ||
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
let categoryMap = null;
const categoryLabelMap = { const categoryLabelMap = {
activity: 'Activity', activity: 'Activity',
people: 'People', people: 'People',
...@@ -30,26 +24,6 @@ const categoryLabelMap = { ...@@ -30,26 +24,6 @@ const categoryLabelMap = {
flags: 'Flags', flags: 'Flags',
}; };
function buildCategoryMap() {
return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => {
const emojiInfo = emojiMap[emojiNameKey];
if (currentCategoryMap[emojiInfo.category]) {
currentCategoryMap[emojiInfo.category].push(emojiNameKey);
}
return currentCategoryMap;
}, {
activity: [],
people: [],
nature: [],
food: [],
travel: [],
objects: [],
symbols: [],
flags: [],
});
}
function renderCategory(name, emojiList, opts = {}) { function renderCategory(name, emojiList, opts = {}) {
return ` return `
<h5 class="emoji-menu-title"> <h5 class="emoji-menu-title">
...@@ -59,7 +33,7 @@ function renderCategory(name, emojiList, opts = {}) { ...@@ -59,7 +33,7 @@ function renderCategory(name, emojiList, opts = {}) {
${emojiList.map(emojiName => ` ${emojiList.map(emojiName => `
<li class="emoji-menu-list-item"> <li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${glEmojiTag(emojiName, { ${Emoji.glEmojiTag(emojiName, {
sprite: true, sprite: true,
})} })}
</button> </button>
...@@ -72,7 +46,6 @@ function renderCategory(name, emojiList, opts = {}) { ...@@ -72,7 +46,6 @@ function renderCategory(name, emojiList, opts = {}) {
export default class AwardsHandler { export default class AwardsHandler {
constructor() { constructor() {
this.eventListeners = []; this.eventListeners = [];
this.aliases = emojiAliases;
// 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', () => {
const $menu = $('.emoji-menu'); const $menu = $('.emoji-menu');
...@@ -81,8 +54,6 @@ export default class AwardsHandler { ...@@ -81,8 +54,6 @@ export default class AwardsHandler {
this.createEmojiMenu(); this.createEmojiMenu();
}); });
} }
// Prebuild the categoryMap
categoryMap = categoryMap || buildCategoryMap();
}); });
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
e.stopPropagation(); e.stopPropagation();
...@@ -168,7 +139,7 @@ export default class AwardsHandler { ...@@ -168,7 +139,7 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true; this.isCreatingEmojiMenu = true;
// Render the first category // Render the first category
categoryMap = categoryMap || buildCategoryMap(); const categoryMap = 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 = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
...@@ -208,7 +179,7 @@ export default class AwardsHandler { ...@@ -208,7 +179,7 @@ export default class AwardsHandler {
} }
this.isAddingRemainingEmojiMenuCategories = true; this.isAddingRemainingEmojiMenuCategories = true;
categoryMap = categoryMap || buildCategoryMap(); const categoryMap = 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
...@@ -262,14 +233,8 @@ export default class AwardsHandler { ...@@ -262,14 +233,8 @@ export default class AwardsHandler {
return $menu.css(css); return $menu.css(css);
} }
addAward( addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
votesBlock, const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
awardUrl,
emoji,
checkMutuality,
callback,
) {
const normalizedEmoji = this.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);
...@@ -279,16 +244,12 @@ export default class AwardsHandler { ...@@ -279,16 +244,12 @@ export default class AwardsHandler {
$('.js-add-award.is-active').removeClass('is-active'); $('.js-add-award.is-active').removeClass('is-active');
} }
addAwardToEmojiBar( addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
votesBlock,
emoji,
checkForMutuality,
) {
if (checkForMutuality || checkForMutuality === null) { if (checkForMutuality || checkForMutuality === null) {
this.checkMutuality(votesBlock, emoji); this.checkMutuality(votesBlock, emoji);
} }
this.addEmojiToFrequentlyUsedList(emoji); this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = this.normalizeEmojiName(emoji); const normalizedEmoji = 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)) {
...@@ -413,7 +374,7 @@ export default class AwardsHandler { ...@@ -413,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">
${glEmojiTag(emojiName)} ${Emoji.glEmojiTag(emojiName)}
<span class="award-control-text js-counter">1</span> <span class="award-control-text js-counter">1</span>
</button> </button>
`; `;
...@@ -478,12 +439,8 @@ export default class AwardsHandler { ...@@ -478,12 +439,8 @@ export default class AwardsHandler {
return $('body, html').animate(options, 200); return $('body, html').animate(options, 200);
} }
normalizeEmojiName(emoji) {
return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
}
addEmojiToFrequentlyUsedList(emoji) { addEmojiToFrequentlyUsedList(emoji) {
if (isEmojiNameValid(emoji)) { if (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 });
} }
...@@ -493,7 +450,7 @@ export default class AwardsHandler { ...@@ -493,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 => isEmojiNameValid(inputName), inputName => Emoji.isEmojiNameValid(inputName),
); );
return this.frequentlyUsedEmojis; return this.frequentlyUsedEmojis;
...@@ -535,21 +492,11 @@ export default class AwardsHandler { ...@@ -535,21 +492,11 @@ export default class AwardsHandler {
} }
} }
findMatchingEmojiElements(term) { findMatchingEmojiElements(query) {
const safeTerm = term.toLowerCase(); const emojiMatches = Emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const namesMatchingAlias = []; const $matchingElements = $emojiElements
Object.keys(emojiAliases).forEach((alias) => { .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
if (alias.indexOf(safeTerm) >= 0) {
namesMatchingAlias.push(emojiAliases[alias]);
}
});
const $matchingElements = namesMatchingAlias.concat(safeTerm)
.reduce(
($result, searchTerm) =>
$result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)),
$([]),
);
return $matchingElements.closest('li').clone(); return $matchingElements.closest('li').clone();
} }
......
import installCustomElements from 'document-register-element'; import installCustomElements from 'document-register-element';
import emojiMap from 'emojis/digests.json'; import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import emojiAliases from 'emojis/aliases.json'; import isEmojiUnicodeSupported from '../emoji/support';
import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
installCustomElements(window); installCustomElements(window);
const generatedUnicodeSupportMap = getUnicodeSupportMap(); export default function installGlEmojiElement() {
function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
function assembleFallbackImageSrc(inputName) {
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
return fallbackImageSrc;
}
const glEmojiTagDefaults = {
sprite: false,
forceFallback: false,
};
function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options);
let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
const fallbackImageSrc = assembleFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}
function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype); const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() { GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim(); const emojiUnicode = this.textContent.trim();
...@@ -90,7 +25,7 @@ function installGlEmojiElement() { ...@@ -90,7 +25,7 @@ function installGlEmojiElement() {
if ( if (
emojiUnicode && emojiUnicode &&
isEmojiUnicode && isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) { ) {
// CSS sprite fallback takes precedence over image fallback // CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (hasCssSpriteFalback) {
...@@ -100,7 +35,7 @@ function installGlEmojiElement() { ...@@ -100,7 +35,7 @@ function installGlEmojiElement() {
} else if (hasImageFallback) { } else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc); this.innerHTML = emojiImageTag(name, fallbackSrc);
} else { } else {
const src = assembleFallbackImageSrc(name); const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src); this.innerHTML = emojiImageTag(name, src);
} }
} }
...@@ -110,9 +45,3 @@ function installGlEmojiElement() { ...@@ -110,9 +45,3 @@ function installGlEmojiElement() {
prototype: GlEmojiElementProto, prototype: GlEmojiElementProto,
}); });
} }
export {
installGlEmojiElement,
glEmojiTag,
emojiImageTag,
};
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
function isEmojiNameValid(inputName) {
const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
return name && emojiMap[name];
}
export default isEmojiNameValid;
import './autosize'; import './autosize';
import './bind_in_out'; import './bind_in_out';
import './details_behavior'; import './details_behavior';
import { installGlEmojiElement } from './gl_emoji'; import installGlEmojiElement from './gl_emoji';
import './quick_submit'; import './quick_submit';
import './requires_input'; import './requires_input';
import './toggler_behavior'; import './toggler_behavior';
......
...@@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { ...@@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
e.preventDefault(); e.preventDefault();
const $form = $(e.target).closest('form'); const $form = $(e.target).closest('form');
const $submitButton = $form.find('input[type=submit], button[type=submit]'); const $submitButton = $form.find('input[type=submit], button[type=submit]').first();
if (!$submitButton.attr('disabled')) { if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]); $submitButton.trigger('click', [e]);
......
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
export const validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
}
export function isEmojiNameValid(name) {
return validEmojiNames.indexOf(name) >= 0;
}
export function filterEmojiNames(filter) {
const match = filter.toLowerCase();
return validEmojiNames.filter(name => name.indexOf(match) >= 0);
}
export function filterEmojiNamesByAlias(filter) {
return _.uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name)));
}
let emojiCategoryMap;
export function getEmojiCategoryMap() {
if (!emojiCategoryMap) {
emojiCategoryMap = {
activity: [],
people: [],
nature: [],
food: [],
travel: [],
objects: [],
symbols: [],
flags: [],
};
Object.keys(emojiMap).forEach((name) => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name);
}
});
}
return emojiCategoryMap;
}
export function getEmojiInfo(query) {
let name = normalizeEmojiName(query);
let emojiInfo = emojiMap[name];
// Fallback to question mark for unknown emojis
if (!emojiInfo) {
name = 'grey_question';
emojiInfo = emojiMap[name];
}
return { ...emojiInfo, name };
}
export function emojiFallbackImageSrc(inputName) {
const { name, digest } = getEmojiInfo(inputName);
return `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${digest}.png`;
}
export function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
export function glEmojiTag(inputName, options) {
const opts = { sprite: false, forceFallback: false, ...options };
const { name, ...emojiInfo } = getEmojiInfo(inputName);
const fallbackImageSrc = emojiFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
title="${emojiInfo.description}"
>
${contents}
</gl-emoji>
`;
}
import isEmojiUnicodeSupported from './is_emoji_unicode_supported';
import getUnicodeSupportMap from './unicode_support_map';
// cache browser support map between calls
let browserUnicodeSupportMap;
export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) {
browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap();
return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion);
}
...@@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe ...@@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
} }
export { export {
isEmojiUnicodeSupported, isEmojiUnicodeSupported as default,
isFlagEmoji, isFlagEmoji,
isKeycapEmoji, isKeycapEmoji,
isSkinToneComboEmoji, isSkinToneComboEmoji,
......
...@@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) { ...@@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) {
return resultMap; return resultMap;
} }
function getUnicodeSupportMap() { export default function getUnicodeSupportMap() {
let unicodeSupportMap; let unicodeSupportMap;
let userAgentFromCache; let userAgentFromCache;
...@@ -165,8 +165,3 @@ function getUnicodeSupportMap() { ...@@ -165,8 +165,3 @@ function getUnicodeSupportMap() {
return unicodeSupportMap; return unicodeSupportMap;
} }
export {
getUnicodeSupportMap,
generateUnicodeSupportMap,
};
...@@ -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) {
...@@ -231,11 +232,7 @@ class FilteredSearchManager { ...@@ -231,11 +232,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 emojiMap from 'emojis/digests.json'; import { validEmojiNames, glEmojiTag } from './emoji';
import emojiAliases from 'emojis/aliases.json'; import glRegexp from './lib/utils/regexp';
import { glEmojiTag } from '~/behaviors/gl_emoji'; import AjaxCache from './lib/utils/ajax_cache';
import glRegexp from '~/lib/utils/regexp';
import AjaxCache from '~/lib/utils/ajax_cache';
function sanitize(str) { function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, ''); return str.replace(/<(?:.|\n)*?>/gm, '');
...@@ -375,7 +373,7 @@ class GfmAutoComplete { ...@@ -375,7 +373,7 @@ 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') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); this.loadData($input, at, validEmojiNames);
} else { } else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.then((data) => { .then((data) => {
......
/* eslint-disable import/prefer-default-export */
export const addClassIfElementExists = (element, className) => {
if (element) {
element.classList.add(className);
}
};
...@@ -94,8 +94,8 @@ gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { ...@@ -94,8 +94,8 @@ gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null) { if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected); insertText = this.blockTagText(text, textArea, blockTag, selected);
} else { } else {
insertText = selectedSplit.map(function(val) { insertText = selectedSplit.map(function(val) {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<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_axis || '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,
};
...@@ -1485,7 +1485,7 @@ export default class Notes { ...@@ -1485,7 +1485,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>');
......
import Vue from 'vue';
import Translate from '../../vue_shared/translate';
Vue.use(Translate);
const inputNameAttribute = 'schedule[cron]';
export default {
props: {
initialCronInterval: {
type: String,
required: false,
default: '',
},
},
data() {
return {
inputNameAttribute,
cronInterval: this.initialCronInterval,
cronIntervalPresets: {
everyDay: '0 4 * * *',
everyWeek: '0 4 * * 0',
everyMonth: '0 4 1 * *',
},
cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
customInputEnabled: false,
};
},
computed: {
intervalIsPreset() {
return _.contains(this.cronIntervalPresets, this.cronInterval);
},
// The text input is editable when there's a custom interval, or when it's
// a preset interval and the user clicks the 'custom' radio button
isEditable() {
return !!(this.customInputEnabled || !this.intervalIsPreset);
},
},
methods: {
toggleCustomInput(shouldEnable) {
this.customInputEnabled = shouldEnable;
if (shouldEnable) {
// We need to change the value so other radios don't remain selected
// because the model (cronInterval) hasn't changed. The server trims it.
this.cronInterval = `${this.cronInterval} `;
}
},
},
created() {
if (this.intervalIsPreset) {
this.enableCustomInput = false;
}
},
watch: {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
Vue.nextTick(() => {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
});
},
},
template: `
<div class="interval-pattern-form-group">
<div class="cron-preset-radio-input">
<input
id="custom"
class="label-light"
type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
@click="toggleCustomInput(true)"
/>
<label for="custom">
{{ s__('PipelineSheduleIntervalPattern|Custom') }}
</label>
<span class="cron-syntax-link-wrap">
(<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>)
</span>
</div>
<div class="cron-preset-radio-input">
<input
id="every-day"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-day">
{{ __('Every day (at 4:00am)') }}
</label>
</div>
<div class="cron-preset-radio-input">
<input
id="every-week"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-week">
{{ __('Every week (Sundays at 4:00am)') }}
</label>
</div>
<div class="cron-preset-radio-input">
<input
id="every-month"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-month">
{{ __('Every month (on the 1st at 4:00am)') }}
</label>
</div>
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
class="form-control inline cron-interval-input"
type="text"
:placeholder="__('Define a custom pattern with cron syntax')"
required="true"
v-model="cronInterval"
:name="inputNameAttribute"
:disabled="!isEditable"
/>
</div>
</div>
`,
};
<script>
export default {
props: {
initialCronInterval: {
type: String,
required: false,
default: '',
},
},
data() {
return {
inputNameAttribute: 'schedule[cron]',
cronInterval: this.initialCronInterval,
cronIntervalPresets: {
everyDay: '0 4 * * *',
everyWeek: '0 4 * * 0',
everyMonth: '0 4 1 * *',
},
cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
customInputEnabled: false,
};
},
computed: {
intervalIsPreset() {
return _.contains(this.cronIntervalPresets, this.cronInterval);
},
// The text input is editable when there's a custom interval, or when it's
// a preset interval and the user clicks the 'custom' radio button
isEditable() {
return !!(this.customInputEnabled || !this.intervalIsPreset);
},
},
methods: {
toggleCustomInput(shouldEnable) {
this.customInputEnabled = shouldEnable;
if (shouldEnable) {
// We need to change the value so other radios don't remain selected
// because the model (cronInterval) hasn't changed. The server trims it.
this.cronInterval = `${this.cronInterval} `;
}
},
},
created() {
if (this.intervalIsPreset) {
this.enableCustomInput = false;
}
},
watch: {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
this.$nextTick(() => {
gl.pipelineScheduleFieldErrors.updateFormValidityState();
});
},
},
};
</script>
<template>
<div class="interval-pattern-form-group">
<div class="cron-preset-radio-input">
<input
id="custom"
class="label-light"
type="radio"
:name="inputNameAttribute"
:value="cronInterval"
:checked="isEditable"
@click="toggleCustomInput(true)"
/>
<label for="custom">
{{ s__('PipelineSheduleIntervalPattern|Custom') }}
</label>
<span class="cron-syntax-link-wrap">
(<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>)
</span>
</div>
<div class="cron-preset-radio-input">
<input
id="every-day"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyDay"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-day">
{{ __('Every day (at 4:00am)') }}
</label>
</div>
<div class="cron-preset-radio-input">
<input
id="every-week"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyWeek"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-week">
{{ __('Every week (Sundays at 4:00am)') }}
</label>
</div>
<div class="cron-preset-radio-input">
<input
id="every-month"
class="label-light"
type="radio"
v-model="cronInterval"
:name="inputNameAttribute"
:value="cronIntervalPresets.everyMonth"
@click="toggleCustomInput(false)"
/>
<label class="label-light" for="every-month">
{{ __('Every month (on the 1st at 4:00am)') }}
</label>
</div>
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
class="form-control inline cron-interval-input"
type="text"
:placeholder="__('Define a custom pattern with cron syntax')"
required="true"
v-model="cronInterval"
:name="inputNameAttribute"
:disabled="!isEditable"
/>
</div>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import IntervalPatternInput from './components/interval_pattern_input'; import Translate from '../vue_shared/translate';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown'; import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown';
document.addEventListener('DOMContentLoaded', () => { Vue.use(Translate);
const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input'); const intervalPatternMount = document.getElementById('interval-pattern-input');
const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : ''; const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : '';
new IntervalPatternInputComponent({ return new Vue({
propsData: { el: intervalPatternMount,
initialCronInterval, components: {
intervalPatternInput,
}, },
}).$mount(intervalPatternMount); render(createElement) {
return createElement('interval-pattern-input', {
props: {
initialCronInterval,
},
});
},
});
}
document.addEventListener('DOMContentLoaded', () => {
/* Most of the form is written in haml, but for fields with more complex behaviors,
* you should mount individual Vue components here. If at some point components need
* to share state, it may make sense to refactor the whole form to Vue */
initIntervalPatternInput();
// Initialize non-Vue JS components in the form
const formElement = document.getElementById('new-pipeline-schedule-form'); const formElement = document.getElementById('new-pipeline-schedule-form');
gl.timezoneDropdown = new TimezoneDropdown(); gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
......
...@@ -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);
......
@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;
}
.right-sidebar {
position: fixed;
height: 100%;
}
}
.nav-sidebar {
position: fixed;
z-index: 400;
width: $new-sidebar-width;
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 {
a {
display: block;
padding: 12px 14px;
}
}
a {
color: $gl-text-color;
text-decoration: none;
}
}
.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
}
}
...@@ -290,23 +290,6 @@ ...@@ -290,23 +290,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,
...@@ -355,6 +338,7 @@ ...@@ -355,6 +338,7 @@
.text-metric { .text-metric {
font-weight: 600; font-weight: 600;
font-size: 14px;
} }
.selected-metric-line { .selected-metric-line {
...@@ -364,20 +348,15 @@ ...@@ -364,20 +348,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;
...@@ -392,3 +371,59 @@ ...@@ -392,3 +371,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;
}
}
}
...@@ -473,7 +473,7 @@ ul.notes { ...@@ -473,7 +473,7 @@ ul.notes {
} }
.more-actions { .more-actions {
display: inline; display: inline-block;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
......
...@@ -385,6 +385,7 @@ a.deploy-project-label { ...@@ -385,6 +385,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;
......
...@@ -70,7 +70,8 @@ ...@@ -70,7 +70,8 @@
} }
.file-finder { .file-finder {
width: 50%; max-width: 500px;
width: 100%;
.file-finder-input { .file-finder-input {
width: 95%; width: 95%;
......
...@@ -113,7 +113,7 @@ module Network ...@@ -113,7 +113,7 @@ module Network
opts[:ref] = @commit.id if @filter_ref opts[:ref] = @commit.id if @filter_ref
@repo.find_commits(opts) Gitlab::Git::Commit.find_all(@repo.raw_repository, opts)
end end
def commits_sort_by_ref def commits_sort_by_ref
......
...@@ -705,7 +705,7 @@ class Project < ActiveRecord::Base ...@@ -705,7 +705,7 @@ class Project < ActiveRecord::Base
end end
def last_activity_date def last_activity_date
last_activity_at || updated_at last_repository_updated_at || last_activity_at || updated_at
end end
def project_id def project_id
......
...@@ -612,22 +612,6 @@ class Repository ...@@ -612,22 +612,6 @@ class Repository
end end
end end
# Returns url for submodule
#
# Ex.
# @repository.submodule_url_for('master', 'rack')
# # => git@localhost:rack.git
#
def submodule_url_for(ref, path)
if submodules(ref).any?
submodule = submodules(ref)[path]
if submodule
submodule['url']
end
end
end
def last_commit_for_path(sha, path) def last_commit_for_path(sha, path)
sha = last_commit_id_for_path(sha, path) sha = last_commit_id_for_path(sha, path)
commit(sha) commit(sha)
......
...@@ -3,7 +3,7 @@ module Boards ...@@ -3,7 +3,7 @@ module Boards
class ListService < BaseService class ListService < BaseService
def execute def execute
issues = IssuesFinder.new(current_user, filter_params).execute issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list? issues = without_board_labels(issues) unless movable_list? || closed_list?
issues = with_list_label(issues) if movable_list? issues = with_list_label(issues) if movable_list?
issues.order_by_position_and_priority issues.order_by_position_and_priority
end end
...@@ -21,7 +21,15 @@ module Boards ...@@ -21,7 +21,15 @@ module Boards
end end
def movable_list? def movable_list?
@movable_list ||= list.present? && list.movable? return @movable_list if defined?(@movable_list)
@movable_list = list.present? && list.movable?
end
def closed_list?
return @closed_list if defined?(@closed_list)
@closed_list = list.present? && list.closed?
end end
def filter_params def filter_params
......
...@@ -11,7 +11,7 @@ class NotificationRecipientService ...@@ -11,7 +11,7 @@ class NotificationRecipientService
def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true) def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
recipients = target.participants(current_user) recipients = participants(target, current_user)
recipients = add_project_watchers(recipients) recipients = add_project_watchers(recipients)
recipients = add_custom_notifications(recipients, custom_action) recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients) recipients = reject_mention_users(recipients)
...@@ -86,12 +86,7 @@ class NotificationRecipientService ...@@ -86,12 +86,7 @@ class NotificationRecipientService
mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) } mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
# Add all users participating in the thread (author, assignee, comment authors) # Add all users participating in the thread (author, assignee, comment authors)
recipients = recipients = participants(target, note.author) || mentioned_users
if target.respond_to?(:participants)
target.participants(note.author)
else
mentioned_users
end
unless note.for_personal_snippet? unless note.for_personal_snippet?
# Merge project watchers # Merge project watchers
...@@ -123,6 +118,14 @@ class NotificationRecipientService ...@@ -123,6 +118,14 @@ class NotificationRecipientService
protected protected
# Ensure that if we modify this array, we aren't modifying the memoised
# participants on the target.
def participants(target, user)
return unless target.respond_to?(:participants)
target.participants(user).dup
end
# Get project/group users with CUSTOM notification level # Get project/group users with CUSTOM notification level
def add_custom_notifications(recipients, action) def add_custom_notifications(recipients, action)
user_ids = [] user_ids = []
......
- page_title "Edit", @application.name, "Applications" - page_title "Edit", @application.name, "Applications"
- @content_class = "limit-container-width" unless fluid_layout
%h3.page-title Edit application %h3.page-title Edit application
= render 'form', application: @application = render 'form', application: @application
- page_title "Applications" - page_title "Applications"
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
= page_title = page_title
%p %p
...@@ -10,7 +11,7 @@ ...@@ -10,7 +11,7 @@
and applications that you've authorized to use your account. and applications that you've authorized to use your account.
- else - else
Manage applications that you've authorized to use your account. Manage applications that you've authorized to use your account.
.col-lg-9 .col-lg-8
- if user_oauth_applications? - if user_oauth_applications?
%h5.prepend-top-0 %h5.prepend-top-0
Add new application Add new application
......
- page_title @application.name, "Applications" - page_title @application.name, "Applications"
- @content_class = "limit-container-width" unless fluid_layout
%h3.page-title %h3.page-title
Application: #{@application.name} Application: #{@application.name}
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
- if show_new_nav? - if show_new_nav?
= stylesheet_link_tag "new_nav", media: "all" = stylesheet_link_tag "new_nav", media: "all"
= stylesheet_link_tag "new_sidebar", media: "all"
= Gon::Base.render_data = Gon::Base.render_data
......
.page-with-sidebar{ class: page_gutter_class } .page-with-sidebar{ class: "#{('page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar)} #{page_gutter_class}" }
- if defined?(nav) && nav - if show_new_nav?
.layout-nav - if defined?(nav) && nav
.container-fluid = render "layouts/nav/#{nav}"
= render "layouts/nav/#{nav}" - else
- if content_for?(:sub_nav) - if defined?(nav) && nav
= yield :sub_nav .layout-nav
.content-wrapper{ class: layout_nav_class } .container-fluid
= render "layouts/nav/#{nav}"
- if content_for?(:sub_nav)
= yield :sub_nav
.content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" }
.alert-wrapper .alert-wrapper
= render "layouts/broadcast" = render "layouts/broadcast"
= render "layouts/flash" = render "layouts/flash"
......
- page_title "Admin Area" - page_title "Admin Area"
- header_title "Admin Area", admin_root_path - header_title "Admin Area", admin_root_path
- nav "admin" - if show_new_nav?
- nav "new_admin_sidebar"
- @new_sidebar = true
- else
- nav "admin"
= render template: "layouts/application" = render template: "layouts/application"
- page_title @group.name - page_title @group.name
- page_description @group.description unless page_description - page_description @group.description unless page_description
- header_title group_title(@group) unless header_title - header_title group_title(@group) unless header_title
- nav "group" - if show_new_nav?
- nav "new_group_sidebar"
- @new_sidebar = true
- else
- nav "group"
= render template: "layouts/application" = render template: "layouts/application"
.nav-sidebar
%ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
Overview
%ul.sidebar-sub-level-items
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview' do
%span
Overview
= nav_link(controller: [:admin, :projects]) do
= link_to admin_projects_path, title: 'Projects' do
%span
Projects
= nav_link(controller: :users) do
= link_to admin_users_path, title: 'Users' do
%span
Users
= nav_link(controller: :groups) do
= link_to admin_groups_path, title: 'Groups' do
%span
Groups
= nav_link path: 'builds#index' do
= link_to admin_jobs_path, title: 'Jobs' do
%span
Jobs
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
%span
Runners
= nav_link path: 'cohorts#index' do
= link_to admin_cohorts_path, title: 'Cohorts' do
%span
Cohorts
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do
= link_to admin_conversational_development_index_path, title: 'Monitoring' do
%span
Monitoring
%ul.sidebar-sub-level-items
= nav_link(controller: :conversational_development_index) do
= link_to admin_conversational_development_index_path, title: 'ConvDev Index' do
%span
ConvDev Index
= nav_link(controller: :system_info) do
= link_to admin_system_info_path, title: 'System Info' do
%span
System Info
= nav_link(controller: :background_jobs) do
= link_to admin_background_jobs_path, title: 'Background Jobs' do
%span
Background Jobs
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do
%span
Logs
= nav_link(controller: :health_check) do
= link_to admin_health_check_path, title: 'Health Check' do
%span
Health Check
= nav_link(controller: :requests_profiles) do
= link_to admin_requests_profiles_path, title: 'Requests Profiles' do
%span
Requests Profiles
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path, title: 'Messages' do
%span
Messages
= nav_link(controller: [:hooks, :hook_logs]) do
= link_to admin_hooks_path, title: 'Hooks' do
%span
System Hooks
= nav_link(controller: :applications) do
= link_to admin_applications_path, title: 'Applications' do
%span
Applications
= nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path, title: "Spam Logs" do
%span
Spam Logs
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path, title: 'Deploy Keys' do
%span
Deploy Keys
= nav_link(controller: :services) do
= link_to admin_application_settings_services_path, title: 'Service Templates' do
%span
Service Templates
= nav_link(controller: :labels) do
= link_to admin_labels_path, title: 'Labels' do
%span
Labels
= nav_link(controller: :appearances) do
= link_to admin_appearances_path, title: 'Appearances' do
%span
Appearance
%li.divider
= nav_link(controller: :application_settings) do
= link_to admin_application_settings_path, title: 'Settings' do
%span
Settings
.nav-sidebar
%ul.sidebar-top-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Home' do
%span
Group
%ul.sidebar-sub-level-items
= nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
= link_to group_path(@group), title: 'Group Home' do
%span
Home
= nav_link(path: 'groups#activity') do
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
= link_to issues_group_path(@group), title: 'Issues' do
%span
Issues
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(issues.count)
%ul.sidebar-sub-level-items
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: 'List' do
%span
List
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: 'Labels' do
%span
Labels
= nav_link(path: 'milestones#index') do
= link_to group_milestones_path(@group), title: 'Milestones' do
%span
Milestones
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span
Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count)
= nav_link(path: 'group_members#index') do
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit]) do
= link_to edit_group_path(@group), title: 'Settings' do
%span
Settings
.nav-sidebar
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
%span
Profile
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account' do
%span
Account
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path, title: 'Applications' do
%span
Applications
= nav_link(controller: :chat_names) do
= link_to profile_chat_names_path, title: 'Chat' do
%span
Chat
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
%span
Emails
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
%span
Password
= nav_link(controller: :notifications) do
= link_to profile_notifications_path, title: 'Notifications' do
%span
Notifications
= nav_link(controller: :keys) do
= link_to profile_keys_path, title: 'SSH Keys' do
%span
SSH Keys
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
%span
Preferences
= nav_link(path: 'profiles#audit_log') do
= link_to audit_log_profile_path, title: 'Authentication log' do
%span
Authentication log
This diff is collapsed.
- page_title "User Settings" - page_title "User Settings"
- header_title "User Settings", profile_path unless header_title - header_title "User Settings", profile_path unless header_title
- sidebar "dashboard" - sidebar "dashboard"
- nav "profile" - if show_new_nav?
- nav "new_profile_sidebar"
- @new_sidebar = true
- else
- nav "profile"
= render template: "layouts/application" = render template: "layouts/application"
- page_title @project.name_with_namespace - page_title @project.name_with_namespace
- page_description @project.description unless page_description - page_description @project.description unless page_description
- header_title project_title(@project) unless header_title - header_title project_title(@project) unless header_title
- nav "project" - if show_new_nav?
- nav "new_project_sidebar"
- @new_sidebar = true
- else
- nav "project"
- content_for :project_javascripts do - content_for :project_javascripts do
- project = @target_project || @project - project = @target_project || @project
......
- page_title "Account" - page_title "Account"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
- if current_user.ldap_user? - if current_user.ldap_user?
...@@ -6,13 +7,13 @@ ...@@ -6,13 +7,13 @@
Some options are unavailable for LDAP accounts Some options are unavailable for LDAP accounts
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Private Tokens Private Tokens
%p %p
Keep these tokens secret, anyone with access to them can interact with Keep these tokens secret, anyone with access to them can interact with
GitLab as if they were you. GitLab as if they were you.
.col-lg-9.private-tokens-reset .col-lg-8.private-tokens-reset
= render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' } = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' }
= render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' } = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' }
...@@ -22,12 +23,12 @@ ...@@ -22,12 +23,12 @@
%hr %hr
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Two-Factor Authentication Two-Factor Authentication
%p %p
Increase your account's security by enabling Two-Factor Authentication (2FA). Increase your account's security by enabling Two-Factor Authentication (2FA).
.col-lg-9 .col-lg-8
%p %p
Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
- if current_user.two_factor_enabled? - if current_user.two_factor_enabled?
...@@ -43,12 +44,12 @@ ...@@ -43,12 +44,12 @@
%hr %hr
- if button_based_providers.any? - if button_based_providers.any?
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Social sign-in Social sign-in
%p %p
Activate signin with one of the following services Activate signin with one of the following services
.col-lg-9 .col-lg-8
%label.label-light %label.label-light
Connected Accounts Connected Accounts
%p Click on icon to activate signin with one of the following services %p Click on icon to activate signin with one of the following services
...@@ -69,12 +70,12 @@ ...@@ -69,12 +70,12 @@
%hr %hr
- if current_user.can_change_username? - if current_user.can_change_username?
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0.warning-title %h4.prepend-top-0.warning-title
Change username Change username
%p %p
Changing your username will change path to all personal projects! Changing your username will change path to all personal projects!
.col-lg-9 .col-lg-8
= form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f| = form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
.form-group .form-group
= f.label :username, "Path", class: "label-light" = f.label :username, "Path", class: "label-light"
...@@ -93,10 +94,10 @@ ...@@ -93,10 +94,10 @@
- if signup_enabled? - if signup_enabled?
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0.danger-title %h4.prepend-top-0.danger-title
Remove account Remove account
.col-lg-9 .col-lg-8
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p %p
Deleting an account has the following effects: Deleting an account has the following effects:
......
- page_title "Authentication log" - page_title "Authentication log"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h3.prepend-top-0 %h3.prepend-top-0
= page_title = page_title
%p %p
This is a security log of important events involving your account. This is a security log of important events involving your account.
.col-lg-9 .col-lg-8
= render 'event_table', events: @events = render 'event_table', events: @events
- page_title 'Chat' - page_title 'Chat'
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
= page_title = page_title
%p %p
You can see your Chat accounts. You can see your Chat accounts.
.col-lg-9 .col-lg-8
%h5 Active chat names (#{@chat_names.size}) %h5 Active chat names (#{@chat_names.size})
- if @chat_names.present? - if @chat_names.present?
......
- page_title "Emails" - page_title "Emails"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
= page_title = page_title
%p %p
Control emails linked to your account Control emails linked to your account
.col-lg-9 .col-lg-8
%h4.prepend-top-0 %h4.prepend-top-0
Add email address Add email address
= form_for 'email', url: profile_emails_path do |f| = form_for 'email', url: profile_emails_path do |f|
......
- page_title "SSH Keys" - page_title "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
= page_title = page_title
%p %p
SSH keys allow you to establish a secure connection between your computer and GitLab. SSH keys allow you to establish a secure connection between your computer and GitLab.
.col-lg-9 .col-lg-8
%h5.prepend-top-0 %h5.prepend-top-0
Add an SSH key Add an SSH key
%p.profile-settings-content %p.profile-settings-content
......
- page_title @key.title, "SSH Keys" - page_title @key.title, "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
= render "key_details" = render "key_details"
- page_title "Notifications" - page_title "Notifications"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
%div %div
...@@ -10,14 +11,14 @@ ...@@ -10,14 +11,14 @@
= hidden_field_tag :notification_type, 'global' = hidden_field_tag :notification_type, 'global'
.row .row
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4 %h4
= page_title = page_title
%p %p
You can specify notification level per group or per project. You can specify notification level per group or per project.
%p %p
By default, all projects and groups will use the global notifications setting. By default, all projects and groups will use the global notifications setting.
.col-lg-9 .col-lg-8
%h5 %h5
Global notification settings Global notification settings
......
- page_title "Password" - page_title "Password"
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
= page_title = page_title
%p %p
After a successful password update, you will be redirected to the login page where you can log in with your new password. After a successful password update, you will be redirected to the login page where you can log in with your new password.
.col-lg-9 .col-lg-8
%h5.prepend-top-0 %h5.prepend-top-0
Change your password Change your password
- unless @user.password_automatically_set? - unless @user.password_automatically_set?
......
- page_title "Personal Access Tokens" - page_title "Personal Access Tokens"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
= page_title = page_title
%p %p
...@@ -11,7 +12,7 @@ ...@@ -11,7 +12,7 @@
You can also use personal access tokens to authenticate against Git over HTTP. You can also use personal access tokens to authenticate against Git over HTTP.
They are the only accepted password when you have Two-Factor Authentication (2FA) enabled. They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.
.col-lg-9 .col-lg-8
- if flash[:personal_access_token] - if flash[:personal_access_token]
.created-personal-access-token-container .created-personal-access-token-container
......
- page_title 'Preferences' - page_title 'Preferences'
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Syntax highlighting theme Syntax highlighting theme
%p %p
This setting allows you to customize the appearance of the syntax. This setting allows you to customize the appearance of the syntax.
= succeed '.' do = succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank' = link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank'
.col-lg-9.syntax-theme .col-lg-8.syntax-theme
- Gitlab::ColorSchemes.each do |scheme| - Gitlab::ColorSchemes.each do |scheme|
= label_tag do = label_tag do
.preview= image_tag "#{scheme.css_class}-scheme-preview.png" .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
...@@ -36,14 +37,14 @@ ...@@ -36,14 +37,14 @@
New New
.col-sm-12 .col-sm-12
%hr %hr
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Behavior Behavior
%p %p
This setting allows you to customize the behavior of the system layout and default views. This setting allows you to customize the behavior of the system layout and default views.
= succeed '.' do = succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank' = link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank'
.col-lg-9 .col-lg-8
.form-group .form-group
= f.label :layout, class: 'label-light' do = f.label :layout, class: 'label-light' do
Layout width Layout width
......
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default' }, authenticity_token: true do |f| = bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default' }, authenticity_token: true do |f|
= form_errors(@user) = form_errors(@user)
.row .row
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Public Avatar Public Avatar
%p %p
...@@ -16,7 +17,7 @@ ...@@ -16,7 +17,7 @@
You can upload an avatar here You can upload an avatar here
- if gravatar_enabled? - if gravatar_enabled?
or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host} or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host}
.col-lg-9 .col-lg-8
.clearfix.avatar-image.append-bottom-default .clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
...@@ -34,14 +35,14 @@ ...@@ -34,14 +35,14 @@
= link_to 'Remove avatar', profile_avatar_path, data: { confirm: 'Avatar will be removed. Are you sure?' }, method: :delete, class: 'btn btn-gray' = link_to 'Remove avatar', profile_avatar_path, data: { confirm: 'Avatar will be removed. Are you sure?' }, method: :delete, class: 'btn btn-gray'
%hr %hr
.row .row
.col-lg-3.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Main settings Main settings
%p %p
This information will appear on your profile. This information will appear on your profile.
- if current_user.ldap_user? - if current_user.ldap_user?
Some options are unavailable for LDAP accounts Some options are unavailable for LDAP accounts
.col-lg-9 .col-lg-8
.row .row
= f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, = f.text_field :name, required: true, wrapper: { class: 'col-md-9' },
help: 'Enter your name, so people you know can recognize you.' help: 'Enter your name, so people you know can recognize you.'
......
- page_title 'Two-Factor Authentication', 'Account' - page_title 'Two-Factor Authentication', 'Account'
- header_title "Two-Factor Authentication", profile_two_factor_auth_path - header_title "Two-Factor Authentication", profile_two_factor_auth_path
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head' = render 'profiles/head'
- if inject_u2f_api? - if inject_u2f_api?
...@@ -7,12 +8,12 @@ ...@@ -7,12 +8,12 @@
= page_specific_javascript_bundle_tag('u2f') = page_specific_javascript_bundle_tag('u2f')
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
Register Two-Factor Authentication App Register Two-Factor Authentication App
%p %p
Use an app on your mobile device to enable two-factor authentication (2FA). Use an app on your mobile device to enable two-factor authentication (2FA).
.col-lg-9 .col-lg-8
- if current_user.two_factor_otp_enabled? - if current_user.two_factor_otp_enabled?
= icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page." = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page."
- else - else
...@@ -20,9 +21,9 @@ ...@@ -20,9 +21,9 @@
Download the Google Authenticator application from App Store or Google Play Store and scan this code. Download the Google Authenticator application from App Store or Google Play Store and scan this code.
More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}. More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}.
.row.append-bottom-10 .row.append-bottom-10
.col-md-3 .col-md-4
= raw @qr_code = raw @qr_code
.col-md-9 .col-md-8
.account-well .account-well
%p.prepend-top-0.append-bottom-0 %p.prepend-top-0.append-bottom-0
Can't scan the code? Can't scan the code?
...@@ -50,7 +51,7 @@ ...@@ -50,7 +51,7 @@
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-4
%h4.prepend-top-0 %h4.prepend-top-0
Register Universal Two-Factor (U2F) Device Register Universal Two-Factor (U2F) Device
%p %p
...@@ -59,7 +60,7 @@ ...@@ -59,7 +60,7 @@
As U2F devices are only supported by a few browsers, we require that you set up a As U2F devices are only supported by a few browsers, we require that you set up a
two-factor authentication app before a U2F device. That way you'll always be able to two-factor authentication app before a U2F device. That way you'll always be able to
log in - even when you're using an unsupported browser. log in - even when you're using an unsupported browser.
.col-lg-9 .col-lg-8
- if @u2f_registration.errors.present? - if @u2f_registration.errors.present?
= form_errors(@u2f_registration) = form_errors(@u2f_registration)
= render "u2f/register" = render "u2f/register"
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- notes = commit.notes - notes = commit.notes
- note_count = notes.user.count - note_count = notes.user.count
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits)]
- cache_key.push(commit.status(ref)) if commit.status(ref) - cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do = cache(cache_key, expires_in: 1.day) do
......
- @no_container = true - @no_container = true
- page_title "Metrics for environment", @environment.name - page_title "Metrics for environment", @environment.name
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3') = webpack_bundle_tag 'common_vue'
= page_specific_javascript_bundle_tag('monitoring') = webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'monitoring'
= render "projects/pipelines/head" = render "projects/pipelines/head"
#js-metrics.prometheus-container{ class: container_class, data: { has_metrics: "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } } .prometheus-container{ class: container_class }
.top-area .top-area
.row .row
.col-sm-6 .col-sm-6
...@@ -13,68 +14,8 @@ ...@@ -13,68 +14,8 @@
Environment: Environment:
= link_to @environment.name, environment_path(@environment) = link_to @environment.name, environment_path(@environment)
.prometheus-state #prometheus-graphs{ data: { "settings-path": edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'),
.js-getting-started.hidden "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
.row "additional-metrics": additional_metrics_namespace_project_environment_path(@project.namespace, @project, @environment, format: :json),
.col-md-4.col-md-offset-4.state-svg "has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } }
= render "shared/empty_states/monitoring/getting_started.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Get started with performance monitoring
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.
= link_to help_page_path('administration/monitoring/prometheus/index.md') do
Learn more about performance monitoring
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'), class: 'btn btn-success' do
Configure Prometheus
.js-loading.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= render "shared/empty_states/monitoring/loading.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Waiting for performance data
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to help_page_path('administration/monitoring/prometheus/index.md'), class: 'btn btn-success' do
View documentation
.js-unable-to-connect.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= render "shared/empty_states/monitoring/unable_to_connect.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Unable to connect to Prometheus server
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Ensure connectivity is available from the GitLab server to the
= link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus') do
Prometheus server
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to help_page_path('administration/monitoring/prometheus/index.md'), class:'btn btn-success' do
View documentation
.prometheus-graphs
.row
.col-sm-12
%h4
CPU utilization
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
%h4
Memory usage
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
- cache_key = project_list_cache_key(project) - cache_key = project_list_cache_key(project)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_at) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
%li.project-row{ class: css_class } %li.project-row{ class: css_class }
= cache(cache_key) do = cache(cache_key) do
......
---
title: Supplement Traditional Chinese in Hong Kong translation of Project Page & Repository Page
merge_request: 11995
author: Huang Tao
---
title: Supplement Bulgarian translation of Project Page & Repository Page
merge_request: 12083
author: Lyubomir Vasilev
---
title: Fix inconsistent display of the "Browse files" button in the commit list
merge_request:
author:
---
title: Fix diff of requirements.txt file by not matching newlines as part of package
names
merge_request:
author:
---
title: Fix head pipeline stored in merge request for external pipelines
merge_request: 12478
author:
---
title: Fixed issue boards closed list not showing all closed issues
merge_request:
author:
---
title: Fixed multi-line markdown tooltip buttons in issue edit form
merge_request:
author:
---
title: Ensure participants for issues, merge requests, etc. are calculated correctly
when sending notifications
merge_request:
author:
...@@ -116,6 +116,7 @@ module Gitlab ...@@ -116,6 +116,7 @@ module Gitlab
config.assets.precompile << "vendor/assets/fonts/*" config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.css" config.assets.precompile << "test.css"
config.assets.precompile << "new_nav.css" config.assets.precompile << "new_nav.css"
config.assets.precompile << "new_sidebar.css"
# Version of your assets, change this if you want to expire all your assets # Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0' config.assets.version = '1.0'
......
...@@ -168,6 +168,7 @@ var config = { ...@@ -168,6 +168,7 @@ var config = {
'issue_show', 'issue_show',
'job_details', 'job_details',
'merge_conflicts', 'merge_conflicts',
'monitoring',
'notebook_viewer', 'notebook_viewer',
'pdf_viewer', 'pdf_viewer',
'pipelines', 'pipelines',
......
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured
platform for software development! platform for software development!
<<<<<<< HEAD
We offer four different products for you and your organization: We offer four different products for you and your organization:
=======
We offer four different products for you and your company:
>>>>>>> upstream/master
- **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/), - **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/),
self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com. self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com.
...@@ -15,6 +19,13 @@ self-hosted, fully featured solution of GitLab, available under distinct [subscr ...@@ -15,6 +19,13 @@ self-hosted, fully featured solution of GitLab, available under distinct [subscr
plus premium features available in each version: **Enterprise Edition Starter** plus premium features available in each version: **Enterprise Edition Starter**
(**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in (**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in
**EES** is also available in **EEP**. **EES** is also available in **EEP**.
<<<<<<< HEAD
=======
**Note:** _We are unifying the documentation for CE and EE. To check if certain feature is
available in CE or EE, look for a note right below the page title containing the GitLab
version which introduced that feature._
>>>>>>> upstream/master
---- ----
......
...@@ -194,3 +194,7 @@ bundle exec rake gitlab:check RAILS_ENV=production ...@@ -194,3 +194,7 @@ bundle exec rake gitlab:check RAILS_ENV=production
``` ```
Note: It is recommended to log into the `git` user using `sudo -i -u git` or `sudo su - git`. While the sudo commands provided by gitlabhq work in Ubuntu they do not always work in RHEL. Note: It is recommended to log into the `git` user using `sudo -i -u git` or `sudo su - git`. While the sudo commands provided by gitlabhq work in Ubuntu they do not always work in RHEL.
## GitLab.com
We've also detailed [our architecture of GitLab.com](https://about.gitlab.com/handbook/infrastructure/production-architecture/) but this is probably over the top unless you have millions of users.
...@@ -294,9 +294,9 @@ sudo usermod -aG redis git ...@@ -294,9 +294,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-2-stable gitlab sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-3-stable gitlab
**Note:** You can change `9-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! **Note:** You can change `9-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It ### Configure It
......
...@@ -117,7 +117,7 @@ cd /home/git/gitlab ...@@ -117,7 +117,7 @@ cd /home/git/gitlab
sudo -u git -H git checkout 9-3-stable-ee sudo -u git -H git checkout 9-3-stable-ee
``` ```
### 5. Update gitlab-shell ### 7. Update gitlab-shell
```bash ```bash
cd /home/git/gitlab-shell cd /home/git/gitlab-shell
...@@ -127,7 +127,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) ...@@ -127,7 +127,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
sudo -u git -H bin/compile sudo -u git -H bin/compile
``` ```
### 6. Update gitlab-workhorse ### 8. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from [Go 1.5](https://golang.org/dl) which should already be on your system from
...@@ -143,7 +143,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) ...@@ -143,7 +143,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
sudo -u git -H make sudo -u git -H make
``` ```
### 7. Update Gitaly ### 9. Update Gitaly
If you have not yet set up Gitaly then follow [Gitaly section of the installation If you have not yet set up Gitaly then follow [Gitaly section of the installation
guide](../install/installation.md#install-gitaly). guide](../install/installation.md#install-gitaly).
......
...@@ -4,65 +4,6 @@ Feature: Group Members ...@@ -4,65 +4,6 @@ Feature: Group Members
And "John Doe" is owner of group "Owned" And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest" And "John Doe" is guest of group "Guest"
# Leave
@javascript
Scenario: Owner should be able to remove himself from group if he is not the last owner
Given "Mary Jane" is owner of group "Owned"
When I visit group "Owned" members page
Then I should see user "John Doe" in team list
Then I should see user "Mary Jane" in team list
When I click on the "Remove User From Group" button for "John Doe"
And I visit group "Owned" members page
Then I should not see user "John Doe" in team list
Then I should see user "Mary Jane" in team list
@javascript
Scenario: Owner should not be able to remove himself from group if he is the last owner
Given "Mary Jane" is guest of group "Owned"
When I visit group "Owned" members page
Then I should see user "John Doe" in team list
Then I should see user "Mary Jane" in team list
Then I should not see the "Remove User From Group" button for "John Doe"
@javascript
Scenario: Guest should be able to remove himself from group
Given "Mary Jane" is guest of group "Guest"
When I visit group "Guest" members page
Then I should see user "John Doe" in team list
Then I should see user "Mary Jane" in team list
When I click on the "Remove User From Group" button for "John Doe"
When I visit group "Guest" members page
Then I should not see user "John Doe" in team list
Then I should see user "Mary Jane" in team list
@javascript
Scenario: Guest should be able to remove himself from group even if he is the only user in the group
When I visit group "Guest" members page
Then I should see user "John Doe" in team list
When I click on the "Remove User From Group" button for "John Doe"
When I visit group "Guest" members page
Then I should not see user "John Doe" in team list
# Remove others
Scenario: Owner should be able to remove other users from group
Given "Mary Jane" is owner of group "Owned"
When I visit group "Owned" members page
Then I should see user "John Doe" in team list
Then I should see user "Mary Jane" in team list
When I click on the "Remove User From Group" button for "Mary Jane"
When I visit group "Owned" members page
Then I should see user "John Doe" in team list
Then I should not see user "Mary Jane" in team list
Scenario: Guest should not be able to remove other users from group
Given "Mary Jane" is guest of group "Guest"
When I visit group "Guest" members page
Then I should see user "John Doe" in team list
Then I should see user "Mary Jane" in team list
Then I should not see the "Remove User From Group" button for "Mary Jane"
Scenario: Search member by name Scenario: Search member by name
Given "Mary Jane" is guest of group "Guest" Given "Mary Jane" is guest of group "Guest"
And I visit group "Guest" members page And I visit group "Guest" members page
......
...@@ -108,6 +108,9 @@ module API ...@@ -108,6 +108,9 @@ module API
render_api_error!('invalid state', 400) render_api_error!('invalid state', 400)
end end
MergeRequest.where(source_project: @project, source_branch: ref)
.update_all(head_pipeline_id: pipeline) if pipeline.latest?
present status, with: Entities::CommitStatus present status, with: Entities::CommitStatus
rescue StateMachines::InvalidTransition => e rescue StateMachines::InvalidTransition => e
render_api_error!(e.message, 400) render_api_error!(e.message, 400)
......
...@@ -52,7 +52,7 @@ module Gitlab ...@@ -52,7 +52,7 @@ module Gitlab
# # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"` # # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"`
def link_regex(regex, &url_proc) def link_regex(regex, &url_proc)
highlighted_lines.map!.with_index do |rich_line, i| highlighted_lines.map!.with_index do |rich_line, i|
marker = StringRegexMarker.new(plain_lines[i], rich_line.html_safe) marker = StringRegexMarker.new(plain_lines[i].chomp, rich_line.html_safe)
marker.mark(regex, group: :name) do |text, left:, right:| marker.mark(regex, group: :name) do |text, left:, right:|
url = yield(text) url = yield(text)
......
...@@ -104,9 +104,63 @@ module Gitlab ...@@ -104,9 +104,63 @@ module Gitlab
[] []
end end
# Delegate Repository#find_commits # Returns commits collection
#
# Ex.
# Commit.find_all(
# repo,
# ref: 'master',
# max_count: 10,
# skip: 5,
# order: :date
# )
#
# +options+ is a Hash of optional arguments to git
# :ref is the ref from which to begin (SHA1 or name)
# :max_count is the maximum number of commits to fetch
# :skip is the number of commits to skip
# :order is the commits order and allowed value is :none (default), :date,
# :topo, or any combination of them (in an array). Commit ordering types
# are documented here:
# http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
#
def find_all(repo, options = {}) def find_all(repo, options = {})
repo.find_commits(options) actual_options = options.dup
allowed_options = [:ref, :max_count, :skip, :order]
actual_options.keep_if do |key|
allowed_options.include?(key)
end
default_options = { skip: 0 }
actual_options = default_options.merge(actual_options)
rugged = repo.rugged
walker = Rugged::Walker.new(rugged)
if actual_options[:ref]
walker.push(rugged.rev_parse_oid(actual_options[:ref]))
else
rugged.references.each("refs/heads/*") do |ref|
walker.push(ref.target_id)
end
end
walker.sorting(rugged_sort_type(actual_options[:order]))
commits = []
offset = actual_options[:skip]
limit = actual_options[:max_count]
walker.each(offset: offset, limit: limit) do |commit|
commits.push(decorate(commit))
end
walker.reset
commits
rescue Rugged::OdbError
[]
end end
def decorate(commit, ref = nil) def decorate(commit, ref = nil)
...@@ -131,6 +185,20 @@ module Gitlab ...@@ -131,6 +185,20 @@ module Gitlab
diff.find_similar!(break_rewrites: break_rewrites) diff.find_similar!(break_rewrites: break_rewrites)
diff diff
end end
# Returns the `Rugged` sorting type constant for one or more given
# sort types. Valid keys are `:none`, `:topo`, and `:date`, or an array
# containing more than one of them. `:date` uses a combination of date and
# topological sorting to closer mimic git's native ordering.
def rugged_sort_type(sort_type)
@rugged_sort_types ||= {
none: Rugged::SORT_NONE,
topo: Rugged::SORT_TOPO,
date: Rugged::SORT_DATE | Rugged::SORT_TOPO
}
@rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE)
end
end end
def initialize(raw_commit, head = nil) def initialize(raw_commit, head = nil)
......
...@@ -494,70 +494,6 @@ module Gitlab ...@@ -494,70 +494,6 @@ module Gitlab
end end
end end
# Returns commits collection
#
# Ex.
# repo.find_commits(
# ref: 'master',
# max_count: 10,
# skip: 5,
# order: :date
# )
#
# +options+ is a Hash of optional arguments to git
# :ref is the ref from which to begin (SHA1 or name)
# :contains is the commit contained by the refs from which to begin (SHA1 or name)
# :max_count is the maximum number of commits to fetch
# :skip is the number of commits to skip
# :order is the commits order and allowed value is :none (default), :date,
# :topo, or any combination of them (in an array). Commit ordering types
# are documented here:
# http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
#
def find_commits(options = {})
actual_options = options.dup
allowed_options = [:ref, :max_count, :skip, :contains, :order]
actual_options.keep_if do |key|
allowed_options.include?(key)
end
default_options = { skip: 0 }
actual_options = default_options.merge(actual_options)
walker = Rugged::Walker.new(rugged)
if actual_options[:ref]
walker.push(rugged.rev_parse_oid(actual_options[:ref]))
elsif actual_options[:contains]
branches_contains(actual_options[:contains]).each do |branch|
walker.push(branch.target_id)
end
else
rugged.references.each("refs/heads/*") do |ref|
walker.push(ref.target_id)
end
end
sort_type = rugged_sort_type(actual_options[:order])
walker.sorting(sort_type)
commits = []
offset = actual_options[:skip]
limit = actual_options[:max_count]
walker.each(offset: offset, limit: limit) do |commit|
gitlab_commit = Gitlab::Git::Commit.decorate(commit)
commits.push(gitlab_commit)
end
walker.reset
commits
rescue Rugged::OdbError
[]
end
# Returns branch names collection that contains the special commit(SHA1 # Returns branch names collection that contains the special commit(SHA1
# or name) # or name)
# #
...@@ -613,32 +549,20 @@ module Gitlab ...@@ -613,32 +549,20 @@ module Gitlab
rugged.rev_parse(oid_or_ref_name) rugged.rev_parse(oid_or_ref_name)
end end
# Return hash with submodules info for this repository # Returns url for submodule
# #
# Ex. # Ex.
# { # @repository.submodule_url_for('master', 'rack')
# "current_path/rack" => { # # => git@localhost:rack.git
# "name" => "original_path/rack",
# "id" => "c67be4624545b4263184c4a0e8f887efd0a66320",
# "url" => "git://github.com/chneukirchen/rack.git"
# },
# "encoding" => {
# "id" => ....
# }
# }
# #
def submodules(ref) def submodule_url_for(ref, path)
commit = rev_parse_target(ref) if submodules(ref).any?
return {} unless commit submodule = submodules(ref)[path]
begin if submodule
content = blob_content(commit, ".gitmodules") submodule['url']
rescue InvalidBlobName end
return {}
end end
parser = GitmodulesParser.new(content)
fill_submodule_ids(commit, parser.parse)
end end
# Return total commits count accessible from passed ref # Return total commits count accessible from passed ref
...@@ -976,6 +900,23 @@ module Gitlab ...@@ -976,6 +900,23 @@ module Gitlab
private private
# We are trying to deprecate this method because it does a lot of work
# but it seems to be used only to look up submodule URL's.
# https://gitlab.com/gitlab-org/gitaly/issues/329
def submodules(ref)
commit = rev_parse_target(ref)
return {} unless commit
begin
content = blob_content(commit, ".gitmodules")
rescue InvalidBlobName
return {}
end
parser = GitmodulesParser.new(content)
fill_submodule_ids(commit, parser.parse)
end
def alternate_object_directories def alternate_object_directories
Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).compact Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).compact
end end
...@@ -1228,20 +1169,6 @@ module Gitlab ...@@ -1228,20 +1169,6 @@ module Gitlab
rescue GRPC::BadStatus => e rescue GRPC::BadStatus => e
raise CommandError.new(e) raise CommandError.new(e)
end end
# Returns the `Rugged` sorting type constant for one or more given
# sort types. Valid keys are `:none`, `:topo`, and `:date`, or an array
# containing more than one of them. `:date` uses a combination of date and
# topological sorting to closer mimic git's native ordering.
def rugged_sort_type(sort_type)
@rugged_sort_types ||= {
none: Rugged::SORT_NONE,
topo: Rugged::SORT_TOPO,
date: Rugged::SORT_DATE | Rugged::SORT_TOPO
}
@rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE)
end
end end
end end
end end
...@@ -19,8 +19,12 @@ module Gitlab ...@@ -19,8 +19,12 @@ module Gitlab
# Whether user is allowed, or not, we should update # Whether user is allowed, or not, we should update
# permissions to keep things clean # permissions to keep things clean
if access.allowed? if access.allowed?
<<<<<<< HEAD
access.update_user access.update_user
Users::UpdateService.new(user, last_credential_check_a: Time.now).execute Users::UpdateService.new(user, last_credential_check_a: Time.now).execute
=======
Users::UpdateService.new(user, last_credential_check_at: Time.now).execute
>>>>>>> upstream/master
true true
else else
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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