Commit cd0ae854 authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into 'backport-add-epic-sidebar'

# Conflicts:
#   app/assets/javascripts/lib/utils/text_utility.js
parents 421acc9a 2f74b1d3
......@@ -241,7 +241,7 @@ linters:
# Numeric values should not contain unnecessary fractional portions.
UnnecessaryMantissa:
enabled: false
enabled: true
# Do not use parent selector references (&) when they would otherwise
# be unnecessary.
......
......@@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.1.4 (2017-11-14)
### Fixed (4 changes)
- Don't try to create fork network memberships for forks with a missing source. !15366
- Formats bytes to human reabale number in registry table.
- Prevent error when authorizing an admin-created OAauth application without a set owner.
- Prevents position update for image diff notes.
## 10.1.3 (2017-11-10)
- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
......
......@@ -398,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.52.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -274,7 +274,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.51.0)
gitaly-proto (0.52.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -1026,7 +1026,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.51.0)
gitaly-proto (~> 0.52.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
......@@ -1185,4 +1185,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.15.4
1.16.0
app/assets/images/emoji.png

1.16 MB | W: | H:

app/assets/images/emoji.png

1.16 MB | W: | H:

app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/emoji/mrs_claus.png

2.15 KB | W: | H:

app/assets/images/emoji/mrs_claus.png

3.26 KB | W: | H:

app/assets/images/emoji/mrs_claus.png
app/assets/images/emoji/mrs_claus.png
app/assets/images/emoji/mrs_claus.png
app/assets/images/emoji/mrs_claus.png
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/emoji@2x.png

2.84 MB | W: | H:

app/assets/images/emoji@2x.png

2.84 MB | W: | H:

app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -20,15 +20,15 @@ import groupsSelect from './groups_select';
import NamespaceSelect from './namespace_select';
/* global NewCommitForm */
/* global NewBranchForm */
/* global Project */
/* global ProjectAvatar */
import Project from './project';
import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
/* global ProjectFindFile */
/* global ProjectNew */
/* global ProjectShow */
/* global ProjectImport */
import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
/* global Sidebar */
......@@ -378,7 +378,7 @@ import Diff from './diff';
initSettingsPanels();
break;
case 'projects:imports:show':
new ProjectImport();
projectImport();
break;
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
......@@ -604,7 +604,7 @@ import Diff from './diff';
break;
case 'projects':
new Project();
new ProjectAvatar();
projectAvatar();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
......
......@@ -7,6 +7,17 @@ function isFlagEmoji(emojiUnicode) {
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
// Tested on mac OS 10.12.6 and Windows 10 FCU, it renders as two separate characters
const baseFlagCodePoint = 127987; // parseInt('1F3F3', 16)
const rainbowCodePoint = 127752; // parseInt('1F308', 16)
function isRainbowFlagEmoji(emojiUnicode) {
const characters = Array.from(emojiUnicode);
// Length 4 because flags are made of 2 characters which are surrogate pairs
return emojiUnicode.length === 4 &&
characters[0].codePointAt(0) === baseFlagCodePoint &&
characters[1].codePointAt(0) === rainbowCodePoint;
}
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
......@@ -57,9 +68,11 @@ function isPersonZwjEmoji(emojiUnicode) {
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
const isRainbowFlagResult = isRainbowFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
!isFlagResult
(unicodeSupportMap.rainbowFlag && isRainbowFlagResult) ||
(!isFlagResult && !isRainbowFlagResult)
);
}
......@@ -113,6 +126,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe
export {
isEmojiUnicodeSupported as default,
isFlagEmoji,
isRainbowFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
......
import AccessorUtilities from '../../lib/utils/accessor';
const GL_EMOJI_VERSION = '0.2.0';
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
......@@ -13,6 +15,7 @@ const unicodeSupportTestMap = {
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
rainbowFlag: '\u{1F3F3}\u{1F308}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
......@@ -141,23 +144,31 @@ function generateUnicodeSupportMap(testMap) {
}
export default function getUnicodeSupportMap() {
let unicodeSupportMap;
let userAgentFromCache;
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
if (isLocalStorageAvailable) userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
let glEmojiVersionFromCache;
let userAgentFromCache;
if (isLocalStorageAvailable) {
glEmojiVersionFromCache = window.localStorage.getItem('gl-emoji-version');
userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
}
let unicodeSupportMap;
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
if (
!unicodeSupportMap ||
glEmojiVersionFromCache !== GL_EMOJI_VERSION ||
userAgentFromCache !== navigator.userAgent
) {
unicodeSupportMap = generateUnicodeSupportMap(unicodeSupportTestMap);
if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
......
......@@ -309,6 +309,42 @@ export const setParamInURL = (param, value) => {
return search;
};
/**
* Given a string of query parameters creates an object.
*
* @example
* `scope=all&page=2` -> { scope: 'all', page: '2'}
* `scope=all` -> { scope: 'all' }
* ``-> {}
* @param {String} query
* @returns {Object}
*/
export const parseQueryStringIntoObject = (query = '') => {
if (query === '') return {};
return query
.split('&')
.reduce((acc, element) => {
const val = element.split('=');
Object.assign(acc, {
[val[0]]: decodeURIComponent(val[1]),
});
return acc;
}, {});
};
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
* Based on the current location and the string parameters provided
* creates a new entry in the history without reloading the page.
*
* @param {String} param
*/
export const historyPushState = (newUrl) => {
window.history.pushState({}, document.title, newUrl);
};
/**
* Converts permission provided as strings to booleans.
*
......
......@@ -52,3 +52,31 @@ export function bytesToKiB(number) {
export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB);
}
/**
* Utility function that calculates GiB of the given bytes.
* @param {Number} number
* @returns {Number}
*/
export function bytesToGiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB);
}
/**
* Port of rails number_to_human_size
* Formats the bytes in number into a more understandable
* representation (e.g., giving it 1500 yields 1.5 KB).
*
* @param {Number} size
* @returns {String}
*/
export function numberToHumanSize(size) {
if (size < BYTES_IN_KIB) {
return `${size} bytes`;
} else if (size < BYTES_IN_KIB * BYTES_IN_KIB) {
return `${bytesToKiB(size).toFixed(2)} KiB`;
} else if (size < BYTES_IN_KIB * BYTES_IN_KIB * BYTES_IN_KIB) {
return `${bytesToMiB(size).toFixed(2)} MiB`;
}
return `${bytesToGiB(size).toFixed(2)} GiB`;
}
......@@ -60,7 +60,6 @@ export default class Poll {
checkConditions(response) {
const headers = normalizeHeaders(response.headers);
const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => {
this.makeRequest();
......@@ -102,7 +101,12 @@ export default class Poll {
/**
* Restarts polling after it has been stoped
*/
restart() {
restart(options) {
// update data
if (options && options.data) {
this.options.data = options.data;
}
this.canPoll = true;
this.makeRequest();
}
......
......@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) {
}
}
export function redirectTo(url) {
return window.location.assign(url);
}
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
......
......@@ -69,8 +69,6 @@ import './notifications_dropdown';
import './notifications_form';
import './pager';
import './preview_markdown';
import './project';
import './project_avatar';
import './project_find_file';
import './project_import';
import './project_label_subscription';
......
......@@ -5,7 +5,6 @@ export default class Members {
}
addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
......@@ -33,17 +32,6 @@ export default class Members {
});
});
}
// eslint-disable-next-line class-methods-use-this
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
.fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
......
......@@ -2,16 +2,8 @@
export default {
name: 'PipelineNavigationTabs',
props: {
scope: {
type: String,
required: true,
},
count: {
type: Object,
required: true,
},
paths: {
type: Object,
tabs: {
type: Array,
required: true,
},
},
......@@ -23,68 +15,37 @@
// 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined;
},
onTabClick(tab) {
this.$emit('onChangeTab', tab.scope);
},
},
};
</script>
<template>
<ul class="nav-links scrolling-tabs">
<li
class="js-pipelines-tab-all"
:class="{ active: scope === 'all'}">
<a :href="paths.allPath">
All
<span
v-if="shouldRenderBadge(count.all)"
class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-pending"
:class="{ active: scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
<span
v-if="shouldRenderBadge(count.pending)"
class="badge">
{{count.pending}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-running"
:class="{ active: scope === 'running'}">
<a :href="paths.runningPath">
Running
<span
v-if="shouldRenderBadge(count.running)"
class="badge">
{{count.running}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-finished"
:class="{ active: scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
v-for="(tab, i) in tabs"
:key="i"
:class="{
active: tab.isActive,
}"
>
<a
role="button"
@click="onTabClick(tab)"
:class="`js-pipelines-tab-${tab.scope}`"
>
{{ tab.name }}
<span
v-if="shouldRenderBadge(count.finished)"
class="badge">
{{count.finished}}
v-if="shouldRenderBadge(tab.count)"
class="badge"
>
{{tab.count}}
</span>
</a>
</li>
<li
class="js-pipelines-tab-branches"
:class="{ active: scope === 'branches'}">
<a :href="paths.branchesPath">Branches</a>
</li>
<li
class="js-pipelines-tab-tags"
:class="{ active: scope === 'tags'}">
<a :href="paths.tagsPath">Tags</a>
</li>
</ul>
</template>
<script>
import _ from 'underscore';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue';
import navigationControls from './nav_controls.vue';
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
import {
convertPermissionToBoolean,
getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
export default {
props: {
......@@ -41,27 +48,18 @@
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
pendingPath: pipelinesData.pendingPath,
runningPath: pipelinesData.runningPath,
finishedPath: pipelinesData.finishedPath,
branchesPath: pipelinesData.branchesPath,
tagsPath: pipelinesData.tagsPath,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
state: this.store.state,
apiScope: 'all',
pagenum: 1,
scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1',
requestData: {},
};
},
computed: {
canCreatePipelineParsed() {
return convertPermissionToBoolean(this.canCreatePipeline);
},
scope() {
const scope = getParameterByName('scope');
return scope === null ? 'all' : scope;
},
/**
* The empty state should only be rendered when the request is made to fetch all pipelines
......@@ -106,47 +104,113 @@
hasCiEnabled() {
return this.hasCi !== undefined;
},
paths() {
return {
allPath: this.allPath,
pendingPath: this.pendingPath,
finishedPath: this.finishedPath,
runningPath: this.runningPath,
branchesPath: this.branchesPath,
tagsPath: this.tagsPath,
};
tabs() {
const { count } = this.state;
return [
{
name: 'All',
scope: 'all',
count: count.all,
isActive: this.scope === 'all',
},
{
name: 'Pending',
scope: 'pending',
count: count.pending,
isActive: this.scope === 'pending',
},
{
name: 'Running',
scope: 'running',
count: count.running,
isActive: this.scope === 'running',
},
{
name: 'Finished',
scope: 'finished',
count: count.finished,
isActive: this.scope === 'finished',
},
pageParameter() {
return getParameterByName('page') || this.pagenum;
{
name: 'Branches',
scope: 'branches',
isActive: this.scope === 'branches',
},
scopeParameter() {
return getParameterByName('scope') || this.apiScope;
{
name: 'Tags',
scope: 'tags',
isActive: this.scope === 'tags',
},
];
},
},
created() {
this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.pageParameter, scope: this.scopeParameter };
this.requestData = { page: this.page, scope: this.scope };
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
change(pageNumber) {
const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
successCallback(resp) {
return resp.json().then((response) => {
// Because we are polling & the user is interacting verify if the response received
// matches the last request made
if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeCount(response.count);
this.store.storePagination(resp.headers);
this.setCommonData(response.pipelines);
}
});
},
/**
* Handles URL and query parameter changes.
* When the user uses the pagination or the tabs,
* - update URL
* - Make API request to the server with new parameters
* - Update the polling function
* - Update the internal state
*/
updateContent(parameters) {
// stop polling
this.poll.stop();
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
// fetch new data
return this.service.getPipelines(this.requestData)
.then((response) => {
this.isLoading = false;
this.successCallback(response);
// restart polling
this.poll.restart({ data: this.requestData });
})
.catch(() => {
this.isLoading = false;
this.errorCallback();
// restart polling
this.poll.restart();
});
},
onChangeTab(scope) {
this.updateContent({ scope, page: '1' });
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
},
};
</script>
......@@ -154,7 +218,7 @@
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
v-if="!shouldRenderEmptyState">
<div class="fade-left">
<i
class="fa fa-angle-left"
......@@ -167,17 +231,17 @@
aria-hidden="true">
</i>
</div>
<navigation-tabs
:scope="scope"
:count="state.count"
:paths="paths"
:tabs="tabs"
@onChangeTab="onChangeTab"
/>
<navigation-controls
:new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:ciLintPath="ciLintPath"
:ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/>
</div>
......@@ -188,6 +252,7 @@
label="Loading Pipelines"
size="3"
v-if="isLoading"
class="prepend-top-20"
/>
<empty-state
......@@ -221,8 +286,8 @@
<table-pagination
v-if="shouldRenderPagination"
:change="change"
:pageInfo="state.pageInfo"
:change="onChangePage"
:page-info="state.pageInfo"
/>
</div>
</div>
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global ProjectSelect */
import Cookies from 'js-cookie';
(function() {
this.Project = (function() {
function Project() {
export default class Project {
constructor() {
const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone');
const $cloneBtnText = $('a.clone-dropdown-btn span');
......@@ -29,7 +28,7 @@ import Cookies from 'js-cookie';
return $('.clone').text(url);
});
// Ref switcher
this.initRefSwitcher();
Project.initRefSwitcher();
$('.project-refs-select').on('change', function() {
return $(this).parents('form').submit();
});
......@@ -43,23 +42,19 @@ import Cookies from 'js-cookie';
$(this).parents('.no-password-message').remove();
return e.preventDefault();
});
this.projectSelectDropdown();
Project.projectSelectDropdown();
}
Project.prototype.projectSelectDropdown = function() {
static projectSelectDropdown () {
new ProjectSelect();
$('.project-item-select').on('click', (function(_this) {
return function(e) {
return _this.changeProject($(e.currentTarget).val());
};
})(this));
};
Project.prototype.changeProject = function(url) {
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
}
static changeProject(url) {
return window.location = url;
};
}
Project.prototype.initRefSwitcher = function() {
static initRefSwitcher() {
var refListItem = document.createElement('li');
var refLink = document.createElement('a');
......@@ -75,9 +70,9 @@ import Cookies from 'js-cookie';
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref'),
search: term
search: term,
},
dataType: "json"
dataType: 'json',
}).done(function(refs) {
return callback(refs);
});
......@@ -129,11 +124,8 @@ import Cookies from 'js-cookie';
gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
}
},
});
});
};
return Project;
})();
}).call(window);
}
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
(function() {
this.ProjectAvatar = (function() {
function ProjectAvatar() {
$('.js-choose-project-avatar-button').bind('click', function() {
var form;
form = $(this).closest('form');
export default function projectAvatar() {
$('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
const form = $(this).closest('form');
return form.find('.js-project-avatar-input').click();
});
$('.js-project-avatar-input').bind('change', function() {
var filename, form;
form = $(this).closest('form');
filename = $(this).val().replace(/^.*[\\\/]/, '');
$('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
const form = $(this).closest('form');
// eslint-disable-next-line no-useless-escape
const filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find('.js-avatar-filename').text(filename);
});
}
return ProjectAvatar;
})();
}).call(window);
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
import { visitUrl } from './lib/utils/url_utility';
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
setTimeout(function() {
return gl.utils.visitUrl(location.href);
export default function projectImport() {
setTimeout(() => {
visitUrl(location.href);
}, 5000);
}
}
return ProjectImport;
})();
}).call(window);
......@@ -8,6 +8,7 @@
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
import { numberToHumanSize } from '../../lib/utils/number_utils';
export default {
props: {
......@@ -41,6 +42,10 @@
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
formatSize(size) {
return numberToHumanSize(size);
},
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
......@@ -97,7 +102,7 @@
</span>
</td>
<td>
{{item.size}}
{{formatSize(item.size)}}
<template v-if="item.size && item.layers">
&middot;
</template>
......
......@@ -61,7 +61,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
return this.mr.relatedLinks;
return !!this.mr.relatedLinks;
},
shouldRenderDeployments() {
return this.mr.deployments.length;
......
......@@ -34,6 +34,7 @@
@import "framework/modal";
@import "framework/pagination";
@import "framework/panels";
@import "framework/popup";
@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
......
......@@ -7,17 +7,21 @@
width: 100%;
height: 100%;
padding-bottom: 25px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
}
.blank-state {
padding-top: 20px;
padding-bottom: 20px;
.blank-state-row {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
height: 100%;
}
.blank-state-welcome {
text-align: center;
padding: 20px 0 40px;
&.blank-state-welcome {
.blank-state-welcome-title {
font-size: 24px;
}
......@@ -25,11 +29,45 @@
.blank-state-text {
margin-bottom: 0;
}
}
.blank-state-link {
display: block;
color: $gl-text-color;
flex: 0 0 100%;
margin-bottom: 15px;
@media (min-width: $screen-sm-min) {
flex: 0 0 49%;
&:nth-child(odd) {
margin-right: 5px;
}
&:nth-child(even) {
margin-left: 5px;
}
}
.blank-state-icon {
padding-bottom: 20px;
&:hover {
background-color: $gray-light;
text-decoration: none;
color: $gl-text-color;
}
}
.blank-state {
padding: 20px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@media (min-width: $screen-sm-min) {
display: flex;
align-items: center;
padding: 50px 30px;
}
.blank-state-icon {
svg {
display: block;
margin: auto;
......@@ -38,13 +76,17 @@
.blank-state-title {
margin-top: 0;
margin-bottom: 10px;
font-size: 18px;
}
.blank-state-text {
max-width: $container-text-max-width;
margin: 0 auto $gl-padding;
font-size: 14px;
.blank-state-body {
@media (max-width: $screen-xs-max) {
text-align: center;
margin-top: 20px;
}
@media (min-width: $screen-sm-min) {
padding-left: 20px;
}
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -101,13 +101,13 @@
@for $i from 0 through 5 {
.legend-box-#{$i} {
background-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%);
background-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
}
}
@for $i from 1 through 4 {
.legend-box-#{$i + 5} {
background-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
background-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
}
}
}
......@@ -200,13 +200,13 @@
@for $i from 0 through 5 {
td.blame-commit-age-#{$i} {
border-left-color: mix($blame-cyan, $blame-blue, $i / 5.0 * 100%);
border-left-color: mix($blame-cyan, $blame-blue, $i / 5 * 100%);
}
}
@for $i from 1 through 4 {
td.blame-commit-age-#{$i + 5} {
border-left-color: mix($blame-gray, $blame-cyan, $i / 4.0 * 100%);
border-left-color: mix($blame-gray, $blame-cyan, $i / 4 * 100%);
}
}
}
......
......@@ -129,7 +129,7 @@
margin: 5px 2px 5px -8px;
border-radius: $border-radius-default;
svg {
.tanuki-logo {
@media (min-width: $screen-sm-min) {
margin-right: 8px;
}
......
......@@ -180,3 +180,31 @@
display: none;
}
}
@mixin triangle($color, $border-color, $size, $border-size) {
&::before,
&::after {
bottom: 100%;
left: 50%;
border: solid transparent;
content: '';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&::before {
border-color: transparent;
border-bottom-color: $border-color;
border-width: ($size + $border-size);
margin-left: -($size + $border-size);
}
&::after {
border-color: transparent;
border-bottom-color: $color;
border-width: $size;
margin-left: -$size;
}
}
.popup {
@include triangle(
$gray-lighter,
$gray-darker,
$popup-triangle-size,
$popup-triangle-border-size
);
padding: $gl-padding;
background-color: $gray-lighter;
border: 1px solid $gray-darker;
border-radius: $border-radius-default;
box-shadow: 0 5px 8px $popup-box-shadow-color;
position: relative;
}
......@@ -163,7 +163,7 @@ $gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1.0);
$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
......@@ -486,8 +486,8 @@ $callout-success-color: $green-700;
/*
* Commit Page
*/
$commit-max-width-marker-color: rgba(0, 0, 0, 0.0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0.0);
$commit-max-width-marker-color: rgba(0, 0, 0, 0);
$commit-message-text-area-bg: rgba(0, 0, 0, 0);
/*
* Common
......@@ -719,3 +719,10 @@ Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
/*
Popup
*/
$popup-triangle-size: 15px;
$popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
......@@ -173,7 +173,7 @@
.prometheus-graph-overlay {
fill: none;
opacity: 0.0;
opacity: 0;
pointer-events: all;
}
......
......@@ -7,7 +7,7 @@
.diff-file .diff-content {
tr.line_holder:hover > td .line_note_link {
opacity: 1.0;
opacity: 1;
filter: alpha(opacity = 100);
}
}
......
......@@ -125,7 +125,7 @@
color: $white-normal;
}
&:hover {
&:hover:not(.tree-truncated-warning) {
td {
background-color: $row-hover;
border-top: 1px solid $row-hover-border;
......@@ -198,6 +198,11 @@
}
}
.tree-truncated-warning {
color: $orange-600;
background-color: $orange-100;
}
.tree-time-ago {
min-width: 135px;
color: $gl-text-color-secondary;
......
......@@ -92,15 +92,7 @@ module LfsRequest
end
def storage_project
@storage_project ||= begin
result = project
# TODO: Make this go to the fork_network root immeadiatly
# dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
result = result.fork_source while result.forked?
result
end
@storage_project ||= project.lfs_storage_project
end
def objects
......
......@@ -4,7 +4,8 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all]
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
layout 'project'
......@@ -131,6 +132,10 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :update_build, build)
end
def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, build)
end
def build
@build ||= project.builds.find(params[:id])
.present(current_user: current_user)
......
......@@ -36,6 +36,7 @@ class IssuableFinder
iids
label_name
milestone_title
my_reaction_emoji
non_archived
project_id
scope
......
......@@ -4,7 +4,10 @@ module NamespacesHelper
end
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups
groups = current_user.manageable_groups
.joins(:route)
.includes(:route)
.order('routes.path')
users = [current_user.namespace]
unless extra_group.nil? || extra_group.is_a?(Group)
......
module TreeHelper
FILE_LIMIT = 1_000
# Sorts a repository's tree so that folders are before files and renders
# their corresponding partials
#
# contents - A Grit::Tree object for the current tree
# tree - A `Tree` object for the current tree
def render_tree(tree)
# Sort submodules and folders together by name ahead of files
folders, files, submodules = tree.trees, tree.blobs, tree.submodules
tree = ""
tree = ''
items = (folders + submodules).sort_by(&:name) + files
tree << render(partial: "projects/tree/tree_row", collection: items) if items.present?
if items.size > FILE_LIMIT
tree << render(partial: 'projects/tree/truncated_notice_tree_row',
locals: { limit: FILE_LIMIT, total: items.size })
items = items.take(FILE_LIMIT)
end
tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present?
tree.html_safe
end
......
......@@ -192,6 +192,10 @@ module Ci
project.build_timeout
end
def triggered_by?(current_user)
user == current_user
end
# A slugified version of the build ref, suitable for inclusion in URLs and
# domain names. Rules:
#
......
......@@ -66,8 +66,8 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
transition [:success, :failed, :canceled, :skipped] => :running
transition [:created, :skipped] => :pending
transition [:success, :failed, :canceled] => :running
end
event :run do
......
......@@ -255,7 +255,7 @@ module Issuable
participants(user).include?(user)
end
def to_hook_data(user, old_labels: [], old_assignees: [])
def to_hook_data(user, old_labels: [], old_assignees: [], old_total_time_spent: nil)
changes = previous_changes
if old_labels != labels
......@@ -270,6 +270,10 @@ module Issuable
end
end
if old_total_time_spent != total_time_spent
changes[:total_time_spent] = [old_total_time_spent, total_time_spent]
end
Gitlab::HookData::IssuableBuilder.new(self).build(user: user, changes: changes)
end
......
......@@ -18,7 +18,8 @@ class DiffNote < Note
validate :positions_complete
validate :verify_supported
before_validation :set_original_position, :update_position, on: :create
before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code
after_save :keep_around_commits
......
......@@ -6,16 +6,8 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader
def storage_project(project)
if project && project.forked?
storage_project(project.forked_from_project)
else
project
end
end
def project_allowed_access?(project)
projects.exists?(storage_project(project).id)
projects.exists?(project.lfs_storage_project.id)
end
def self.destroy_unreferenced
......
......@@ -704,10 +704,6 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
def github_import?
import_type == 'github'
end
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
......@@ -1047,6 +1043,18 @@ class Project < ActiveRecord::Base
forked_from_project || fork_network&.root_project
end
def lfs_storage_project
@lfs_storage_project ||= begin
result = self
# TODO: Make this go to the fork_network root immeadiatly
# dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
result = result.fork_source while result&.forked?
result || self
end
end
def personal?
!group
end
......
......@@ -25,7 +25,7 @@ class PrometheusService < MonitoringService
end
def description
'Prometheus monitoring'
s_('PrometheusService|Prometheus monitoring')
end
def self.to_param
......@@ -38,8 +38,8 @@ class PrometheusService < MonitoringService
type: 'text',
name: 'api_url',
title: 'API URL',
placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.',
placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'),
required: true
}
]
......
......@@ -21,7 +21,7 @@ class ProjectWiki
end
delegate :empty?, to: :pages
delegate :repository_storage_path, to: :project
delegate :repository_storage_path, :hashed_storage?, to: :project
def path
@project.path + '.wiki'
......
......@@ -921,7 +921,16 @@ class User < ActiveRecord::Base
end
def manageable_namespaces
@manageable_namespaces ||= [namespace] + owned_groups + masters_groups
@manageable_namespaces ||= [namespace] + manageable_groups
end
def manageable_groups
union = Gitlab::SQL::Union.new([owned_groups.select(:id),
masters_groups.select(:id)])
arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql)
owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union))
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end
def namespaces
......
......@@ -10,6 +10,15 @@ module Ci
end
end
rule { protected_ref }.prevent :update_build
condition(:owner_of_job) do
can?(:developer_access) && @subject.triggered_by?(@user)
end
rule { protected_ref }.policy do
prevent :update_build
prevent :erase_build
end
rule { can?(:master_access) | owner_of_job }.enable :erase_build
end
end
......@@ -6,7 +6,7 @@ class BuildDetailsEntity < JobEntity
expose :pipeline, using: PipelineEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :update_build, project) } do |build|
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
end
......
......@@ -172,6 +172,7 @@ class IssuableBaseService < BaseService
old_labels = issuable.labels.to_a
old_mentioned_users = issuable.mentioned_users.to_a
old_assignees = issuable.assignees.to_a
old_total_time_spent = issuable.total_time_spent
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
......@@ -208,7 +209,12 @@ class IssuableBaseService < BaseService
invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update', old_labels: old_labels, old_assignees: old_assignees)
execute_hooks(
issuable,
'update',
old_labels: old_labels,
old_assignees: old_assignees,
old_total_time_spent: old_total_time_spent)
issuable.update_project_counter_caches if update_project_counters
end
......
module Issues
class BaseService < ::IssuableBaseService
def hook_data(issue, action, old_labels: [], old_assignees: [])
hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)
def hook_data(issue, action, old_labels: [], old_assignees: [], old_total_time_spent: nil)
hook_data = issue.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hook_data[:object_attributes][:action] = action
hook_data
......@@ -22,8 +22,8 @@ module Issues
issue, issue.project, current_user, old_assignees)
end
def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [])
issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees)
def execute_hooks(issue, action = 'open', old_labels: [], old_assignees: [], old_total_time_spent: nil)
issue_data = hook_data(issue, action, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
issue.project.execute_hooks(issue_data, hooks_scope)
issue.project.execute_services(issue_data, hooks_scope)
......
......@@ -18,8 +18,8 @@ module MergeRequests
super if changed_title
end
def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [])
hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees)
def hook_data(merge_request, action, old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
hook_data = merge_request.to_hook_data(current_user, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
hook_data[:object_attributes][:action] = action
if old_rev && !Gitlab::Git.blank_ref?(old_rev)
hook_data[:object_attributes][:oldrev] = old_rev
......@@ -28,9 +28,9 @@ module MergeRequests
hook_data
end
def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [])
def execute_hooks(merge_request, action = 'open', old_rev: nil, old_labels: [], old_assignees: [], old_total_time_spent: nil)
if merge_request.project
merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees)
merge_data = hook_data(merge_request, action, old_rev: old_rev, old_labels: old_labels, old_assignees: old_assignees, old_total_time_spent: old_total_time_spent)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
......
......@@ -11,13 +11,11 @@ module Projects
# supported by an importer class (`Gitlab::GithubImport::ParallelImporter`
# for example).
def async?
return false unless has_importer?
!!importer_class.try(:async?)
has_importer? && !!importer_class.try(:async?)
end
def execute
add_repository_to_project unless project.gitlab_project_import?
add_repository_to_project
import_data
......@@ -29,6 +27,14 @@ module Projects
private
def add_repository_to_project
if project.external_import? && !unknown_url?
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
end
# We should skip the repository for a GitHub import or GitLab project import,
# because these importers fetch the project repositories for us.
return if has_importer? && importer_class.try(:imports_repository?)
if unknown_url?
# In this case, we only want to import issues, not a repository.
create_repository
......@@ -44,12 +50,6 @@ module Projects
end
def import_repository
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
# We should return early for a GitHub import because the new GitHub
# importer fetch the project repositories for us.
return if project.github_import?
begin
if project.gitea_import?
fetch_repository
......@@ -88,7 +88,7 @@ module Projects
end
def importer_class
Gitlab::ImportSources.importer(project.import_type)
@importer_class ||= Gitlab::ImportSources.importer(project.import_type)
end
def has_importer?
......
.blank-state
.blank-state-row
= link_to new_project_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_user", size: 50)
= custom_icon("add_new_project", size: 50)
.blank-state-body
%h3.blank-state-title
Add user
Create a project
%p.blank-state-text
Add your team members and others to GitLab.
= link_to new_admin_user_path, class: "btn btn-new" do
New user
Projects are where you store your code, access issues, wiki and other features of GitLab.
.blank-state
- if current_user.can_create_group?
= link_to admin_root_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("configure_server", size: 50)
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Configure GitLab
Create a group
%p.blank-state-text
Make adjustments to how your GitLab instance is set up.
= link_to admin_root_path, class: "btn btn-new" do
Configure
Groups are a great way to organize projects and people.
- if current_user.can_create_group?
= link_to new_admin_user_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
= custom_icon("add_new_user", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group
Add people
%p.blank-state-text
Groups are a great way to organize projects and people.
= link_to new_group_path, class: "btn btn-new" do
New group
Add your team members and others to GitLab.
= link_to admin_root_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("configure_server", size: 50)
.blank-state-body
%h3.blank-state-title
Configure GitLab
%p.blank-state-text
Make adjustments to how your GitLab instance is set up.
- public_project_count = ProjectsFinder.new(current_user: current_user).execute.count
- if current_user.can_create_group?
.blank-state-row
- if current_user.can_create_project?
= link_to new_project_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
= custom_icon("add_new_project", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group for several dependent projects.
Create a project
%p.blank-state-text
Groups are the best way to manage projects and members.
= link_to new_group_path, class: "btn btn-new" do
New group
.blank-state
Projects are where you store your code, access issues, wiki and other features of GitLab.
- else
.blank-state
.blank-state-icon
= custom_icon("add_new_project", size: 50)
.blank-state-body
%h3.blank-state-title
Create a project
%p.blank-state-text
- if current_user.can_create_project?
You don't have access to any projects right now.
You can create up to
%strong= number_with_delimiter(current_user.projects_limit)
= succeed "." do
= "project".pluralize(current_user.projects_limit)
- else
If you are added to a project, it will be displayed here.
- if current_user.can_create_project?
= link_to new_project_path, class: "btn btn-new" do
New project
- if public_project_count > 0
- if current_user.can_create_group?
= link_to new_group_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are the best way to manage projects and members.
- if public_project_count > 0
= link_to trending_explore_projects_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("globe", size: 50)
......@@ -44,5 +46,13 @@
public projects on this server.
Public projects are an easy way to allow
everyone to have read-only access.
= link_to trending_explore_projects_path, class: "btn btn-new" do
Browse projects
= link_to "https://docs.gitlab.com/", class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("lightbulb", size: 50)
.blank-state-body
%h3.blank-state-title
Learn more about GitLab
%p.blank-state-text
Take a look at the documentation to discover all of GitLab's capabilities.
.row.blank-state-parent-container
.blank-state-parent-container
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body
.blank-state.blank-state-welcome
.row
.blank-state-welcome
%h2.blank-state-welcome-title
Welcome to GitLab
%p.blank-state-text
......
......@@ -71,7 +71,7 @@
class: 'js-raw-link-controller has-tooltip controllers-buttons' do
= icon('file-text-o')
- if can?(current_user, :update_build, @project) && @build.erasable?
- if @build.erasable? && can?(current_user, :erase_build, @build)
= link_to erase_project_job_path(@project, @build),
method: :post,
data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' },
......
......@@ -9,12 +9,6 @@
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"all-path" => project_pipelines_path(@project),
"pending-path" => project_pipelines_path(@project, scope: :pending),
"running-path" => project_pipelines_path(@project, scope: :running),
"finished-path" => project_pipelines_path(@project, scope: :finished),
"branches-path" => project_pipelines_path(@project, scope: :branches),
"tags-path" => project_pipelines_path(@project, scope: :tags),
"has-ci" => @repository.gitlab_ci_yml,
"ci-lint-path" => ci_lint_path } }
......
......@@ -4,42 +4,39 @@
.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
.col-lg-3
%h4.prepend-top-0
Metrics
= s_('PrometheusService|Metrics')
%p
Metrics are automatically configured and monitored
based on a library of metrics from popular exporters.
= link_to 'More information', help_page_path('user/project/integrations/prometheus')
= s_('PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters.')
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus')
.col-lg-9
.panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{project_prometheus_active_metrics_path(@project, :json)}" } }
.panel-heading
%h3.panel-title
Monitored
= s_('PrometheusService|Monitored')
%span.badge.js-monitored-count 0
.panel-body
.loading-metrics.text-center.js-loading-metrics
= icon('spinner spin 3x', class: 'metrics-load-spinner')
%p Finding and configuring metrics...
%p
= s_('PrometheusService|Finding and configuring metrics...')
.empty-metrics.text-center.hidden.js-empty-metrics
= custom_icon('icon_empty_metrics')
%p No metrics are being monitored. To start monitoring, deploy to an environment.
= link_to project_environments_path(@project), title: 'View environments', class: 'btn btn-success' do
View environments
%p
= s_('PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment.')
= link_to s_('PrometheusService|View environments'), project_environments_path(@project), class: 'btn btn-success'
%ul.list-unstyled.metrics-list.hidden.js-metrics-list
.panel.panel-default.hidden.js-panel-missing-env-vars
.panel-heading
%h3.panel-title
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
Missing environment variable
= s_('PrometheusService|Missing environment variable')
%span.badge.js-env-var-count 0
.panel-body.hidden
.flash-container
.flash-notice
.flash-text
To set up automatic monitoring, add the environment variable
%code
$CI_ENVIRONMENT_SLUG
to exporter&rsquo;s queries.
= link_to 'More information', help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
= s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list
%tr.tree-truncated-warning
%td{ colspan: '3' }
= icon('exclamation-triangle fw')
%span
Too many items to show. To preserve performance only
%strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)}
items are displayed.
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M30 24a4 4 0 0 0-4 4v22a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V28a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#FC6D26" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18c4.418 0 8 3.582 8 8v22c0 4.418-3.582 8-8 8H30c-4.418 0-8-3.582-8-8V28c0-4.418 3.582-8 8-8z"/><path fill="#6B4FBB" d="M33 30h8c1.105 0 2 .895 2 2s-.895 2-2 2h-8c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm1 5h10c1.105 0 2 .895 2 2s-.895 2-2 2H34c-1.105 0-2-.895-2-2s.895-2 2-2z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36c.198-1.348.737-2.623 1.566-3.705 3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846.815 1.08 1.343 2.345 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1c-.097-.67-.36-1.303-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3-.416.54-.685 1.18-.784 1.853l-.346 2.36c-.288 1.958-1.963 3.41-3.942 3.42l-13.08.053c-1.994.008-3.69-1.455-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268zm-6 0c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268z"/></g></svg>
......@@ -104,7 +104,6 @@
class: 'btn btn-remove prepend-left-10'
- else
= link_to member,
remote: true,
method: :delete,
data: { confirm: remove_member_message(member) },
class: 'btn btn-remove prepend-left-10',
......
......@@ -2,6 +2,8 @@ class UpdateMergeRequestsWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
LOG_TIME_THRESHOLD = 90 # seconds
def perform(project_id, user_id, oldrev, newrev, ref)
project = Project.find_by(id: project_id)
return unless project
......@@ -9,6 +11,20 @@ class UpdateMergeRequestsWorker
user = User.find_by(id: user_id)
return unless user
# TODO: remove this benchmarking when we have rich logging
time = Benchmark.measure do
MergeRequests::RefreshService.new(project, user).execute(oldrev, newrev, ref)
end
args_log = [
"elapsed=#{time.real}",
"project_id=#{project_id}",
"user_id=#{user_id}",
"oldrev=#{oldrev}",
"newrev=#{newrev}",
"ref=#{ref}"
].join(',')
Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD
end
end
---
title: Stop reloading the page when using pagination and tabs - use API calls - in
Pipelines table
merge_request:
author:
type: other
---
title: Add internationalization support for the prometheus integration
merge_request: 33338
author:
type: other
---
title: Reorganize welcome page for new users
merge_request:
author:
type: other
---
title: Fix gitlab:backup rake for hashed storage based repositories
merge_request: 15400
author:
type: fixed
---
title: Fix pipeline status transition for single manual job. This would also fix pipeline
duration becuse it is depending on status transition
merge_request: 15251
author:
type: fixed
---
title: Add total_time_spent to the `changes` hash in issuable Webhook payloads
merge_request: 15381
author:
type: changed
---
title: Remove extra margin from wordmark in header
merge_request:
author:
type: fixed
---
title: Don't use JS to delete memberships from projects and groups
merge_request: 15344
author:
type: fixed
---
title: Make sure a user can add projects to subgroups they have access to
merge_request: 15294
author:
type: fixed
---
title: Prevent error when authorizing an admin-created OAauth application without
a set owner
merge_request:
author:
type: fixed
---
title: Enable UnnecessaryMantissa in scss-lint
merge_request: 15255
author: Takuya Noguchi
type: other
---
title: Fix filter by my reaction is not working
merge_request: 15345
author: Hiroyuki Sato
type: fixed
---
title: Only owner or master can erase jobs
merge_request: 15216
author:
type: changed
---
title: Truncate tree to max 1,000 items and display notice to users
merge_request:
author:
type: performance
---
title: 'Update emojis. Add :gay_pride_flag: and :speech_left:. Remove extraneous comma
in :cartwheel_tone4:'
merge_request:
author:
type: changed
---
title: Add performance logging to UpdateMergeRequestsWorker.
merge_request: 15360
author:
type: performance
......@@ -459,9 +459,9 @@
:versions: []
:when: 2017-09-13 17:31:16.425819400 Z
- - :approve
- gitlab-svgs
- "@gitlab-org/gitlab-svgs"
- :who: Tim Zallmann
:why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs
:why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs
:versions: []
:when: 2017-09-19 14:36:32.795496000 Z
- - :license
......
......@@ -145,7 +145,7 @@
- container_memory_usage_bytes
weight: 1
queries:
- query_range: '(sum(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"})) /1024/1024'
- query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024'
label: Average
unit: MB
- title: "CPU Utilization"
......@@ -154,8 +154,6 @@
- container_cpu_usage_seconds_total
weight: 1
queries:
- query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) * 100'
label: CPU
- query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
label: Average
unit: "%"
\ No newline at end of file
series:
- label: cpu
......@@ -22,6 +22,7 @@ comments: false
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](fe_guide/index.md)
- [Emoji guide](fe_guide/emojis.md)
## Backend guides
......
# Emojis
GitLab supports native unicode emojis and fallsback to image-based emojis selectively
when your platform does not support it.
# How to update Emojis
1. Update the `gemojione` gem
1. Update `fixtures/emojis/index.json` from [Gemojione](https://github.com/jonathanwiesel/gemojione/blob/master/config/index.json).
In the future, we could grab the file directly from the gem.
We should probably make a PR on the Gemojione project to get access to
all emojis after being parsed or just a raw path to the `json` file itself.
1. Ensure [`emoji-unicode-version`](https://www.npmjs.com/package/emoji-unicode-version)
is up to date with the latest version.
1. Run `bundle exec rake gemojione:aliases`
1. Run `bundle exec rake gemojione:digests`
1. Run `bundle exec rake gemojione:sprite`
1. Ensure new sprite sheets generated for 1x and 2x
- `app/assets/images/emoji.png`
- `app/assets/images/emoji@2x.png`
1. Ensure you see new individual images copied into `app/assets/images/emoji/`
1. Ensure you can see the new emojis and their aliases in the GFM Autocomplete
1. Ensure you can see the new emojis and their aliases in the award emoji menu
1. You might need to add new emoji unicode support checks and rules for platforms
that do not support a certain emoji and we need to fallback to an image.
See `app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js`
and `app/assets/javascripts/emoji/support/unicode_support_map.js`
......@@ -29,7 +29,7 @@ Please use the following function inside JS to render an icon :
All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
To upgrade to a new SVG Sprite version run `yarn upgrade https://gitlab.com/gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders.
To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders.
# SVG Illustrations
......
......@@ -4,11 +4,11 @@ GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed
## Automated Testing
In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems and node modules in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition.
There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
There are some limitations with the automated testing, however. CSS, JavaScript, or Ruby libraries which are not included by way of Bundler, NPM, or Yarn (for instance those manually copied into our source tree in the `vendor` directory), must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them.
Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually.
Some gems may not include their license information in their `gemspec` file, and some node modules may not include their license information in their `package.json` file. These won't be detected by License Finder, and will have to be verified manually.
### License Finder commands
......
......@@ -336,6 +336,12 @@ Blocks of code that are EE-specific should be moved to partials as much as
possible to avoid conflicts with big chunks of HAML code that that are not fun
to resolve when you add the indentation in the equation.
### Assets
#### gitlab-svgs
Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs).
---
[Return to Development documentation](README.md)
......@@ -122,6 +122,15 @@ they can be easily inspected.
bundle exec rake services:doc
```
## Updating Emoji Aliases
To update the Emoji aliases file (used for Emoji autocomplete) you must run the
following:
```
bundle exec rake gemojione:aliases
```
## Updating Emoji Digests
To update the Emoji digests file (used for Emoji autocomplete) you must run the
......@@ -131,6 +140,7 @@ following:
bundle exec rake gemojione:digests
```
This will update the file `fixtures/emojis/digests.json` based on the currently
available Emoji.
......
......@@ -321,7 +321,7 @@ Auto DevOps uses [Helm](https://helm.sh/) to deploy your application to Kubernet
You can override the Helm chart used by bundling up a chart into your project
repo or by specifying a project variable:
- **Bundled chart** - If your project has a `./charts` directory with a `Chart.yaml`
- **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml`
file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed.
......
......@@ -197,6 +197,7 @@ instance and project. In addition, all admins can use the admin interface under
|---------------------------------------|-----------------|-------------|----------|--------|
| See commits and jobs | ✓ | ✓ | ✓ | ✓ |
| Retry or cancel job | | ✓ | ✓ | ✓ |
| Erase job artifacts and trace | | ✓ [^7] | ✓ | ✓ |
| Remove project | | | ✓ | ✓ |
| Create project | | | ✓ | ✓ |
| Change project configuration | | | ✓ | ✓ |
......@@ -261,5 +262,6 @@ only.
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one.
[^6]: Only if user is a member of the project.
[^7]: Only if the build was triggered by the user
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
......@@ -13,8 +13,8 @@ integration services must be enabled.
| Name | Query |
| ---- | ----- |
| Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 |
| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 |
| Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 |
| Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 |
## Configuring Prometheus to monitor for Kubernetes node metrics
......
......@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding.
If you set up a GitLab Pages project on GitLab.com,
it will automatically be accessible under a
[subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com).
[subdomain of `namespace.gitlab.io`](introduction.md#gitlab-pages-on-gitlab-com).
The `namespace` is defined by your username on GitLab.com,
or the group name you created this project under.
......
......@@ -62,7 +62,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I should see additional file lines' do
page.within @diff.parent do
page.within @diff.query_scope do
expect(first('.new_line').text).not_to have_content "..."
end
end
......
......@@ -339,6 +339,7 @@
"baguette_bread":"french_bread",
"anguished":"frowning",
"white_frowning_face":"frowning2",
"rainbow_flag":"gay_pride_flag",
"goal_net":"goal",
"hammer_and_pick":"hammer_pick",
"raised_hand_with_fingers_splayed":"hand_splayed",
......@@ -488,6 +489,7 @@
"slightly_smiling_face":"slight_smile",
"sneeze":"sneezing_face",
"speaking_head_in_silhouette":"speaking_head",
"left_speech_bubble":"speech_left",
"sleuth_or_spy":"spy",
"sleuth_or_spy_tone1":"spy_tone1",
"sleuth_or_spy_tone2":"spy_tone2",
......
......@@ -1478,7 +1478,7 @@
},
"cartwheel_tone4": {
"category": "activity",
"moji": "🤸🏾,",
"moji": "🤸🏾",
"description": "person doing cartwheel tone 4",
"unicodeVersion": "9.0",
"digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
......@@ -5375,6 +5375,13 @@
"unicodeVersion": "6.0",
"digest": "180e66f19d9285e02d0a5e859722c608206826e80323942b9938fc49d44973b1"
},
"gay_pride_flag": {
"category": "flags",
"moji": "🏳🌈",
"description": "gay_pride_flag",
"unicodeVersion": "6.0",
"digest": "924e668c559db61b7f4724a661223081c2fc60d55169f3fe1ad6156934d1d37f"
},
"gemini": {
"category": "symbols",
"moji": "♊",
......@@ -7578,7 +7585,7 @@
"moji": "🤶",
"description": "mother christmas",
"unicodeVersion": "9.0",
"digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
"digest": "357d769371305a8584f46d6087a962d647b6af22fab363a44702f38ab7814091"
},
"mrs_claus_tone1": {
"category": "people",
......@@ -10709,6 +10716,13 @@
"unicodeVersion": "6.0",
"digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca"
},
"speech_left": {
"category": "symbols",
"moji": "🗨",
"description": "left speech bubble",
"unicodeVersion": "7.0",
"digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0"
},
"speedboat": {
"category": "travel",
"moji": "🚤",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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