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
/.yarn-cache
/.byebug_history
/Vagrantfile
/app/assets/javascripts/locale/**/app.js
/backups/*
/config/aws.yml
/config/database.yml
......
......@@ -199,6 +199,7 @@ setup-test-env:
script:
- node --version
- yarn install --pure-lockfile --cache-folder .yarn-cache
- bundle exec rake gettext:po_to_json
- bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
......@@ -419,6 +420,7 @@ gitlab:assets:compile:
NO_COMPRESSION: "true"
script:
- yarn install --pure-lockfile --production --cache-folder .yarn-cache
- bundle exec rake gettext:po_to_json
- bundle exec rake gitlab:assets:compile
artifacts:
name: webpack-report
......@@ -436,6 +438,7 @@ karma:
BABEL_ENV: "coverage"
CHROME_LOG_FILE: "chrome_debug.log"
script:
- bundle exec rake gettext:po_to_json
- bundle exec rake karma
coverage: '/^Statements *: (\d+\.\d+%)/'
artifacts:
......@@ -459,6 +462,7 @@ codeclimate:
script:
- docker pull codeclimate/codeclimate
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json
- sed -i.bak 's/\({"body":"\)[^"]*\("}\)/\1\2/g' codeclimate.json
artifacts:
paths: [codeclimate.json]
......@@ -533,3 +537,9 @@ cache gems:
only:
- master@gitlab-org/gitlab-ce
- 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
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.24.0'
gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
......
......@@ -395,7 +395,7 @@ GEM
temple (~> 0.7.6)
thor
tilt
hashdiff (0.3.2)
hashdiff (0.3.4)
hashie (3.5.5)
hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0)
......@@ -491,7 +491,7 @@ GEM
mimemagic (0.3.0)
mini_portile2 (2.1.0)
minitest (5.7.0)
mmap2 (2.2.6)
mmap2 (2.2.7)
mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1)
......@@ -919,7 +919,7 @@ GEM
vmstat (2.3.0)
warden (1.2.6)
rack (>= 1.0)
webmock (1.24.6)
webmock (2.3.2)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
......@@ -1158,7 +1158,7 @@ DEPENDENCIES
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.3.0)
webmock (~> 1.24.0)
webmock (~> 2.3.2)
webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1)
......
......@@ -2,11 +2,7 @@
/* global Flash */
import Cookies from 'js-cookie';
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';
import * as Emoji from './emoji';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
......@@ -17,8 +13,6 @@ const requestAnimationFrame = window.requestAnimationFrame ||
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
let categoryMap = null;
const categoryLabelMap = {
activity: 'Activity',
people: 'People',
......@@ -30,26 +24,6 @@ const categoryLabelMap = {
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 = {}) {
return `
<h5 class="emoji-menu-title">
......@@ -59,7 +33,7 @@ function renderCategory(name, emojiList, opts = {}) {
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${glEmojiTag(emojiName, {
${Emoji.glEmojiTag(emojiName, {
sprite: true,
})}
</button>
......@@ -72,7 +46,6 @@ function renderCategory(name, emojiList, opts = {}) {
export default class AwardsHandler {
constructor() {
this.eventListeners = [];
this.aliases = emojiAliases;
// If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
const $menu = $('.emoji-menu');
......@@ -81,8 +54,6 @@ export default class AwardsHandler {
this.createEmojiMenu();
});
}
// Prebuild the categoryMap
categoryMap = categoryMap || buildCategoryMap();
});
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
e.stopPropagation();
......@@ -168,7 +139,7 @@ export default class AwardsHandler {
this.isCreatingEmojiMenu = true;
// Render the first category
categoryMap = categoryMap || buildCategoryMap();
const categoryMap = Emoji.getEmojiCategoryMap();
const categoryNameKey = Object.keys(categoryMap)[0];
const emojisInCategory = categoryMap[categoryNameKey];
const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory);
......@@ -208,7 +179,7 @@ export default class AwardsHandler {
}
this.isAddingRemainingEmojiMenuCategories = true;
categoryMap = categoryMap || buildCategoryMap();
const categoryMap = Emoji.getEmojiCategoryMap();
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
......@@ -262,14 +233,8 @@ export default class AwardsHandler {
return $menu.css(css);
}
addAward(
votesBlock,
awardUrl,
emoji,
checkMutuality,
callback,
) {
const normalizedEmoji = this.normalizeEmojiName(emoji);
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
......@@ -279,16 +244,12 @@ export default class AwardsHandler {
$('.js-add-award.is-active').removeClass('is-active');
}
addAwardToEmojiBar(
votesBlock,
emoji,
checkForMutuality,
) {
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
if (checkForMutuality || checkForMutuality === null) {
this.checkMutuality(votesBlock, emoji);
}
this.addEmojiToFrequentlyUsedList(emoji);
const normalizedEmoji = this.normalizeEmojiName(emoji);
const normalizedEmoji = Emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
if ($emojiButton.length > 0) {
if (this.isActive($emojiButton)) {
......@@ -413,7 +374,7 @@ export default class AwardsHandler {
createAwardButtonForVotesBlock(votesBlock, emojiName) {
const buttonHtml = `
<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>
</button>
`;
......@@ -478,12 +439,8 @@ export default class AwardsHandler {
return $('body, html').animate(options, 200);
}
normalizeEmojiName(emoji) {
return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji;
}
addEmojiToFrequentlyUsedList(emoji) {
if (isEmojiNameValid(emoji)) {
if (Emoji.isEmojiNameValid(emoji)) {
this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
}
......@@ -493,7 +450,7 @@ export default class AwardsHandler {
return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => isEmojiNameValid(inputName),
inputName => Emoji.isEmojiNameValid(inputName),
);
return this.frequentlyUsedEmojis;
......@@ -535,21 +492,11 @@ export default class AwardsHandler {
}
}
findMatchingEmojiElements(term) {
const safeTerm = term.toLowerCase();
const namesMatchingAlias = [];
Object.keys(emojiAliases).forEach((alias) => {
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}"]`)),
$([]),
);
findMatchingEmojiElements(query) {
const emojiMatches = Emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
return $matchingElements.closest('li').clone();
}
......
import installCustomElements from 'document-register-element';
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map';
import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported';
import { emojiImageTag, emojiFallbackImageSrc } from '../emoji';
import isEmojiUnicodeSupported from '../emoji/support';
installCustomElements(window);
const generatedUnicodeSupportMap = getUnicodeSupportMap();
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() {
export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
......@@ -90,7 +25,7 @@ function installGlEmojiElement() {
if (
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
......@@ -100,7 +35,7 @@ function installGlEmojiElement() {
} else if (hasImageFallback) {
this.innerHTML = emojiImageTag(name, fallbackSrc);
} else {
const src = assembleFallbackImageSrc(name);
const src = emojiFallbackImageSrc(name);
this.innerHTML = emojiImageTag(name, src);
}
}
......@@ -110,9 +45,3 @@ function installGlEmojiElement() {
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 './bind_in_out';
import './details_behavior';
import { installGlEmojiElement } from './gl_emoji';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
import './requires_input';
import './toggler_behavior';
......
......@@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
e.preventDefault();
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')) {
$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
}
export {
isEmojiUnicodeSupported,
isEmojiUnicodeSupported as default,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
......
......@@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) {
return resultMap;
}
function getUnicodeSupportMap() {
export default function getUnicodeSupportMap() {
let unicodeSupportMap;
let userAgentFromCache;
......@@ -165,8 +165,3 @@ function getUnicodeSupportMap() {
return unicodeSupportMap;
}
export {
getUnicodeSupportMap,
generateUnicodeSupportMap,
};
......@@ -2,6 +2,7 @@
import AjaxFilter from '~/droplab/plugins/ajax_filter';
import './filtered_search_dropdown';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class DropdownUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, tokenKeys, filter) {
......@@ -32,8 +33,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
}
hideCurrentUser() {
const currenUserItem = this.dropdown.querySelector('.js-current-user');
currenUserItem.classList.add('hidden');
addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden');
}
itemClicked(e) {
......
......@@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
import RecentSearchesService from './services/recent_searches_service';
import eventHub from './event_hub';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
class FilteredSearchManager {
constructor(page) {
......@@ -231,11 +232,7 @@ class FilteredSearchManager {
}
addInputContainerFocus() {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-box');
if (inputContainer) {
inputContainer.classList.add('focus');
}
addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus');
}
removeInputContainerFocus(e) {
......
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from '~/behaviors/gl_emoji';
import glRegexp from '~/lib/utils/regexp';
import AjaxCache from '~/lib/utils/ajax_cache';
import { validEmojiNames, glEmojiTag } from './emoji';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
function sanitize(str) {
return str.replace(/<(?:.|\n)*?>/gm, '');
......@@ -375,7 +373,7 @@ class GfmAutoComplete {
if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
this.loadData($input, at, validEmojiNames);
} else {
AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true)
.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) {
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
if (blockTag != null) {
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
insertText = this.blockTagText(text, textArea, blockTag, selected);
} else {
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.removeEventListener('DOMContentLoaded', onLoad, false);
return new PrometheusGraph();
}, false);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs',
components: {
'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 {
const cachedNoteBodyText = $noteBodyText.html();
// 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.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 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 TargetBranchDropdown from './components/target_branch_dropdown';
document.addEventListener('DOMContentLoaded', () => {
const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput);
Vue.use(Translate);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input');
const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : '';
new IntervalPatternInputComponent({
propsData: {
initialCronInterval,
return new Vue({
el: intervalPatternMount,
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');
gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
......
......@@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
$purple-600: #6e49cb;
$purple-650: #5c35ae;
$purple-700: #4a2192;
$purple-800: #2c0a5c;
$purple-900: #380d75;
......@@ -103,6 +105,7 @@ $well-light-text-color: #5b6169;
*/
$gl-font-size: 14px;
$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-disabled: rgba(0, 0, 0, .35);
$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 @@
}
}
.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,
.y-axis path,
.label-x-axis-line,
......@@ -355,6 +338,7 @@
.text-metric {
font-weight: 600;
font-size: 14px;
}
.selected-metric-line {
......@@ -364,20 +348,15 @@
.deployment-line {
stroke: $black;
stroke-width: 2;
stroke-width: 1;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
}
.text-metric-bold {
font-weight: 600;
}
.prometheus-state {
margin-top: 10px;
display: none;
.state-button-section {
margin-top: 10px;
......@@ -392,3 +371,59 @@
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 {
}
.more-actions {
display: inline;
display: inline-block;
.tooltip {
white-space: nowrap;
......
......@@ -385,6 +385,7 @@ a.deploy-project-label {
}
.breadcrumb.repo-breadcrumb {
flex: 1;
padding: 0;
background: transparent;
border: none;
......
......@@ -70,7 +70,8 @@
}
.file-finder {
width: 50%;
max-width: 500px;
width: 100%;
.file-finder-input {
width: 95%;
......
......@@ -113,7 +113,7 @@ module Network
opts[:ref] = @commit.id if @filter_ref
@repo.find_commits(opts)
Gitlab::Git::Commit.find_all(@repo.raw_repository, opts)
end
def commits_sort_by_ref
......
......@@ -705,7 +705,7 @@ class Project < ActiveRecord::Base
end
def last_activity_date
last_activity_at || updated_at
last_repository_updated_at || last_activity_at || updated_at
end
def project_id
......
......@@ -612,22 +612,6 @@ class Repository
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)
sha = last_commit_id_for_path(sha, path)
commit(sha)
......
......@@ -3,7 +3,7 @@ module Boards
class ListService < BaseService
def 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.order_by_position_and_priority
end
......@@ -21,7 +21,15 @@ module Boards
end
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
def filter_params
......
......@@ -11,7 +11,7 @@ class NotificationRecipientService
def build_recipients(target, current_user, action:, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
recipients = participants(target, current_user)
recipients = add_project_watchers(recipients)
recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients)
......@@ -86,12 +86,7 @@ class NotificationRecipientService
mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
# Add all users participating in the thread (author, assignee, comment authors)
recipients =
if target.respond_to?(:participants)
target.participants(note.author)
else
mentioned_users
end
recipients = participants(target, note.author) || mentioned_users
unless note.for_personal_snippet?
# Merge project watchers
......@@ -123,6 +118,14 @@ class NotificationRecipientService
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
def add_custom_notifications(recipients, action)
user_ids = []
......
- page_title "Edit", @application.name, "Applications"
- @content_class = "limit-container-width" unless fluid_layout
%h3.page-title Edit application
= render 'form', application: @application
- page_title "Applications"
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
......@@ -10,7 +11,7 @@
and applications that you've authorized to use your account.
- else
Manage applications that you've authorized to use your account.
.col-lg-9
.col-lg-8
- if user_oauth_applications?
%h5.prepend-top-0
Add new application
......
- page_title @application.name, "Applications"
- @content_class = "limit-container-width" unless fluid_layout
%h3.page-title
Application: #{@application.name}
......
......@@ -32,6 +32,7 @@
- if show_new_nav?
= stylesheet_link_tag "new_nav", media: "all"
= stylesheet_link_tag "new_sidebar", media: "all"
= Gon::Base.render_data
......
.page-with-sidebar{ class: page_gutter_class }
- if defined?(nav) && nav
.layout-nav
.container-fluid
= render "layouts/nav/#{nav}"
- if content_for?(:sub_nav)
= yield :sub_nav
.content-wrapper{ class: layout_nav_class }
.page-with-sidebar{ class: "#{('page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar)} #{page_gutter_class}" }
- if show_new_nav?
- if defined?(nav) && nav
= render "layouts/nav/#{nav}"
- else
- if defined?(nav) && nav
.layout-nav
.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
= render "layouts/broadcast"
= render "layouts/flash"
......
- page_title "Admin Area"
- 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"
- page_title @group.name
- page_description @group.description unless page_description
- 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"
.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"
- header_title "User Settings", profile_path unless header_title
- sidebar "dashboard"
- nav "profile"
- if show_new_nav?
- nav "new_profile_sidebar"
- @new_sidebar = true
- else
- nav "profile"
= render template: "layouts/application"
- page_title @project.name_with_namespace
- page_description @project.description unless page_description
- 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
- project = @target_project || @project
......
- page_title "Account"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
- if current_user.ldap_user?
......@@ -6,13 +7,13 @@
Some options are unavailable for LDAP accounts
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Private Tokens
%p
Keep these tokens secret, anyone with access to them can interact with
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: '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 @@
%hr
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Two-Factor Authentication
%p
Increase your account's security by enabling Two-Factor Authentication (2FA).
.col-lg-9
.col-lg-8
%p
Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'}
- if current_user.two_factor_enabled?
......@@ -43,12 +44,12 @@
%hr
- if button_based_providers.any?
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Social sign-in
%p
Activate signin with one of the following services
.col-lg-9
.col-lg-8
%label.label-light
Connected Accounts
%p Click on icon to activate signin with one of the following services
......@@ -69,12 +70,12 @@
%hr
- if current_user.can_change_username?
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0.warning-title
Change username
%p
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-group
= f.label :username, "Path", class: "label-light"
......@@ -93,10 +94,10 @@
- if signup_enabled?
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0.danger-title
Remove account
.col-lg-9
.col-lg-8
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p
Deleting an account has the following effects:
......
- page_title "Authentication log"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h3.prepend-top-0
= page_title
%p
This is a security log of important events involving your account.
.col-lg-9
.col-lg-8
= render 'event_table', events: @events
- page_title 'Chat'
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
You can see your Chat accounts.
.col-lg-9
.col-lg-8
%h5 Active chat names (#{@chat_names.size})
- if @chat_names.present?
......
- page_title "Emails"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
Control emails linked to your account
.col-lg-9
.col-lg-8
%h4.prepend-top-0
Add email address
= form_for 'email', url: profile_emails_path do |f|
......
- page_title "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
SSH keys allow you to establish a secure connection between your computer and GitLab.
.col-lg-9
.col-lg-8
%h5.prepend-top-0
Add an SSH key
%p.profile-settings-content
......
- page_title @key.title, "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
= render "key_details"
- page_title "Notifications"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
%div
......@@ -10,14 +11,14 @@
= hidden_field_tag :notification_type, 'global'
.row
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4
= page_title
%p
You can specify notification level per group or per project.
%p
By default, all projects and groups will use the global notifications setting.
.col-lg-9
.col-lg-8
%h5
Global notification settings
......
- page_title "Password"
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
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
Change your password
- unless @user.password_automatically_set?
......
- page_title "Personal Access Tokens"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= page_title
%p
......@@ -11,7 +12,7 @@
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.
.col-lg-9
.col-lg-8
- if flash[:personal_access_token]
.created-personal-access-token-container
......
- page_title 'Preferences'
- @content_class = "limit-container-width" unless fluid_layout
= 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|
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Syntax highlighting theme
%p
This setting allows you to customize the appearance of the syntax.
= succeed '.' do
= 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|
= label_tag do
.preview= image_tag "#{scheme.css_class}-scheme-preview.png"
......@@ -36,14 +37,14 @@
New
.col-sm-12
%hr
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Behavior
%p
This setting allows you to customize the behavior of the system layout and default views.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank'
.col-lg-9
.col-lg-8
.form-group
= f.label :layout, class: 'label-light' do
Layout width
......
- @content_class = "limit-container-width" unless fluid_layout
= 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|
= form_errors(@user)
.row
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Public Avatar
%p
......@@ -16,7 +17,7 @@
You can upload an avatar here
- if gravatar_enabled?
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
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
......@@ -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'
%hr
.row
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Main settings
%p
This information will appear on your profile.
- if current_user.ldap_user?
Some options are unavailable for LDAP accounts
.col-lg-9
.col-lg-8
.row
= f.text_field :name, required: true, wrapper: { class: 'col-md-9' },
help: 'Enter your name, so people you know can recognize you.'
......
- page_title 'Two-Factor Authentication', 'Account'
- header_title "Two-Factor Authentication", profile_two_factor_auth_path
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
- if inject_u2f_api?
......@@ -7,12 +8,12 @@
= page_specific_javascript_bundle_tag('u2f')
.row.prepend-top-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0
Register Two-Factor Authentication App
%p
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?
= 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
......@@ -20,9 +21,9 @@
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'))}.
.row.append-bottom-10
.col-md-3
.col-md-4
= raw @qr_code
.col-md-9
.col-md-8
.account-well
%p.prepend-top-0.append-bottom-0
Can't scan the code?
......@@ -50,7 +51,7 @@
.row.prepend-top-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0
Register Universal Two-Factor (U2F) Device
%p
......@@ -59,7 +60,7 @@
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
log in - even when you're using an unsupported browser.
.col-lg-9
.col-lg-8
- if @u2f_registration.errors.present?
= form_errors(@u2f_registration)
= render "u2f/register"
......
......@@ -5,7 +5,7 @@
- notes = commit.notes
- 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(cache_key, expires_in: 1.day) do
......
- @no_container = true
- page_title "Metrics for environment", @environment.name
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('monitoring')
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'monitoring'
= 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
.row
.col-sm-6
......@@ -13,68 +14,8 @@
Environment:
= link_to @environment.name, environment_path(@environment)
.prometheus-state
.js-getting-started.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= 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{ data: { "settings-path": edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"additional-metrics": additional_metrics_namespace_project_environment_path(@project.namespace, @project, @environment, format: :json),
"has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } }
.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 @@
- 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
- 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 }
= 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
config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.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
config.assets.version = '1.0'
......
......@@ -168,6 +168,7 @@ var config = {
'issue_show',
'job_details',
'merge_conflicts',
'monitoring',
'notebook_viewer',
'pdf_viewer',
'pipelines',
......
......@@ -3,7 +3,11 @@
Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured
platform for software development!
<<<<<<< HEAD
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/),
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
plus premium features available in each version: **Enterprise Edition Starter**
(**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in
**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
```
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
### Clone the Source
# 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
......
......@@ -117,7 +117,7 @@ cd /home/git/gitlab
sudo -u git -H git checkout 9-3-stable-ee
```
### 5. Update gitlab-shell
### 7. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
......@@ -127,7 +127,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
sudo -u git -H bin/compile
```
### 6. Update gitlab-workhorse
### 8. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[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)
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
guide](../install/installation.md#install-gitaly).
......
......@@ -4,65 +4,6 @@ Feature: Group Members
And "John Doe" is owner of group "Owned"
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
Given "Mary Jane" is guest of group "Guest"
And I visit group "Guest" members page
......
......@@ -108,6 +108,9 @@ module API
render_api_error!('invalid state', 400)
end
MergeRequest.where(source_project: @project, source_branch: ref)
.update_all(head_pipeline_id: pipeline) if pipeline.latest?
present status, with: Entities::CommitStatus
rescue StateMachines::InvalidTransition => e
render_api_error!(e.message, 400)
......
......@@ -52,7 +52,7 @@ module Gitlab
# # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"`
def link_regex(regex, &url_proc)
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:|
url = yield(text)
......
......@@ -104,9 +104,63 @@ module Gitlab
[]
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 = {})
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
def decorate(commit, ref = nil)
......@@ -131,6 +185,20 @@ module Gitlab
diff.find_similar!(break_rewrites: break_rewrites)
diff
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
def initialize(raw_commit, head = nil)
......
......@@ -494,70 +494,6 @@ module Gitlab
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
# or name)
#
......@@ -613,32 +549,20 @@ module Gitlab
rugged.rev_parse(oid_or_ref_name)
end
# Return hash with submodules info for this repository
# Returns url for submodule
#
# Ex.
# {
# "current_path/rack" => {
# "name" => "original_path/rack",
# "id" => "c67be4624545b4263184c4a0e8f887efd0a66320",
# "url" => "git://github.com/chneukirchen/rack.git"
# },
# "encoding" => {
# "id" => ....
# }
# }
# @repository.submodule_url_for('master', 'rack')
# # => git@localhost:rack.git
#
def submodules(ref)
commit = rev_parse_target(ref)
return {} unless commit
def submodule_url_for(ref, path)
if submodules(ref).any?
submodule = submodules(ref)[path]
begin
content = blob_content(commit, ".gitmodules")
rescue InvalidBlobName
return {}
if submodule
submodule['url']
end
end
parser = GitmodulesParser.new(content)
fill_submodule_ids(commit, parser.parse)
end
# Return total commits count accessible from passed ref
......@@ -976,6 +900,23 @@ module Gitlab
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
Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).compact
end
......@@ -1228,20 +1169,6 @@ module Gitlab
rescue GRPC::BadStatus => e
raise CommandError.new(e)
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
......@@ -19,8 +19,12 @@ module Gitlab
# Whether user is allowed, or not, we should update
# permissions to keep things clean
if access.allowed?
<<<<<<< HEAD
access.update_user
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
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