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

Merge remote-tracking branch 'ce/master' into rc/ce-to-ee-wednesday

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 59828154 1e4f64d2
...@@ -15,10 +15,15 @@ variables: ...@@ -15,10 +15,15 @@ variables:
GIT_DEPTH: "20" GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1" PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3" GET_SOURCES_ATTEMPTS: "3"
<<<<<<< HEAD
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-${CI_COMMIT_REF_SLUG}.json KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-${CI_COMMIT_REF_SLUG}.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-${CI_COMMIT_REF_SLUG}.json KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-${CI_COMMIT_REF_SLUG}.json
# This hack is needed to make ES not that memory hungry # This hack is needed to make ES not that memory hungry
ES_JAVA_OPTS: "-Xms600m -Xmx600m" ES_JAVA_OPTS: "-Xms600m -Xmx600m"
=======
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
>>>>>>> ce/master
before_script: before_script:
- source ./scripts/prepare_build.sh - source ./scripts/prepare_build.sh
......
...@@ -314,9 +314,12 @@ request is as follows: ...@@ -314,9 +314,12 @@ request is as follows:
organized commits by [squashing them][git-squash] organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork 1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch 1. Submit a merge request (MR) to the `master` branch
1. Leave the approvals settings as they are: 1. Your merge request needs at least 1 approval but feel free to require more.
1. Your merge request needs at least 1 approval For instance if you're touching backend and frontend code, it's a good idea
1. You don't have to select any approvers to require 2 approvals: 1 from a backend maintainer and 1 from a frontend
maintainer
1. You don't have to select any approvers, but you can if you really want
specific people to approve your merge request
1. The MR title should describe the change you want to make 1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you 1. The MR description should give a motive for your change and the method you
used to achieve it. used to achieve it.
...@@ -376,7 +379,7 @@ There are a few rules to get your merge request accepted: ...@@ -376,7 +379,7 @@ There are a few rules to get your merge request accepted:
1. If your merge request includes only frontend changes [^1], it must be 1. If your merge request includes only frontend changes [^1], it must be
**approved by a [frontend maintainer][team]**. **approved by a [frontend maintainer][team]**.
1. If your merge request includes frontend and backend changes [^1], it must 1. If your merge request includes frontend and backend changes [^1], it must
be approved by a frontend **and** a backend maintainer. be **approved by a [frontend and a backend maintainer][team]**.
1. To lower the amount of merge requests maintainers need to review, you can 1. To lower the amount of merge requests maintainers need to review, you can
ask or assign any [reviewers][team] for a first review. ask or assign any [reviewers][team] for a first review.
1. If you need some guidance (e.g. it's your first merge request), feel free 1. If you need some guidance (e.g. it's your first merge request), feel free
...@@ -556,6 +559,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -556,6 +559,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html [polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[^1]: Specs other than JavaScript specs are considered backend code. Haml [^1]: Please note that specs other than JavaScript specs are considered backend
changes are considered backend code if they include Ruby code other than just code.
pure HTML.
...@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0' ...@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.3.16', group: :mysql gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0' gem 'rugged', '~> 0.25.1.1'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
...@@ -65,7 +65,7 @@ gem 'net-ldap' ...@@ -65,7 +65,7 @@ gem 'net-ldap'
# Git Wiki # Git Wiki
# Required manually in config/initializers/gollum.rb to control load order # Required manually in config/initializers/gollum.rb to control load order
gem 'gollum-lib', '~> 4.2', require: false gem 'gollum-lib', '~> 4.2', require: false
gem 'gollum-rugged_adapter', '~> 0.4.2', require: false gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
# Language detection # Language detection
gem 'github-linguist', '~> 4.7.0', require: 'linguist' gem 'github-linguist', '~> 4.7.0', require: 'linguist'
......
...@@ -321,9 +321,9 @@ GEM ...@@ -321,9 +321,9 @@ GEM
rouge (~> 2.0) rouge (~> 2.0)
sanitize (~> 2.1.0) sanitize (~> 2.1.0)
stringex (~> 2.5.1) stringex (~> 2.5.1)
gollum-rugged_adapter (0.4.2) gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15) mime-types (>= 1.15)
rugged (~> 0.24.0, >= 0.21.3) rugged (~> 0.25)
gon (6.1.0) gon (6.1.0)
actionpack (>= 3.0) actionpack (>= 3.0)
json json
...@@ -718,7 +718,7 @@ GEM ...@@ -718,7 +718,7 @@ GEM
rubypants (0.2.0) rubypants (0.2.0)
rubyzip (1.2.1) rubyzip (1.2.1)
rufus-scheduler (3.1.10) rufus-scheduler (3.1.10)
rugged (0.24.0) rugged (0.25.1.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
...@@ -948,7 +948,7 @@ DEPENDENCIES ...@@ -948,7 +948,7 @@ DEPENDENCIES
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.8.6) google-api-client (~> 0.8.6)
grape (~> 0.19.0) grape (~> 0.19.0)
...@@ -1032,7 +1032,7 @@ DEPENDENCIES ...@@ -1032,7 +1032,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.12.0) rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
rugged (~> 0.24.0) rugged (~> 0.25.1.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0) scss_lint (~> 0.47.0)
......
<<<<<<< HEAD
8.18.0-ee-pre 8.18.0-ee-pre
=======
9.1.0-pre
>>>>>>> ce/master
import spreadString from './spread_string';
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ // On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16) const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16) const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
...@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) { ...@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) {
const tone1 = 127995;// parseInt('1F3FB', 16) const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16) const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) { function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0); const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5; return cp >= tone1 && cp <= tone5;
}); });
...@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) { ...@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing // doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) { function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode); isSkinToneComboEmoji(emojiUnicode);
} }
...@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) ...@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) { function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false; let hasPersonEmoji = false;
let hasZwj = false; let hasZwj = false;
spreadString(emojiUnicode).forEach((character) => { Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0); const cp = character.codePointAt(0);
if (cp === zwj) { if (cp === zwj) {
hasZwj = true; hasZwj = true;
......
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
function knownCharCodeAt(givenString, index) {
const str = `${givenString}`;
const end = str.length;
const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
let idx = index;
while ((surrogatePairs.exec(str)) != null) {
const li = surrogatePairs.lastIndex;
if (li - 2 < idx) {
idx += 1;
} else {
break;
}
}
if (idx >= end || idx < 0) {
return NaN;
}
const code = str.charCodeAt(idx);
let high;
let low;
if (code >= 0xD800 && code <= 0xDBFF) {
high = code;
low = str.charCodeAt(idx + 1);
// Go one further, since one of the "characters" is part of a surrogate pair
return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
return code;
}
// See http://stackoverflow.com/a/38901550/796832
// ES5/PhantomJS compatible version of spreading a string
//
// [...'foo'] -> ['f', 'o', 'o']
// [...'🖐🏿'] -> ['🖐', '🏿']
function spreadString(str) {
const arr = [];
let i = 0;
while (!isNaN(knownCharCodeAt(str, i))) {
const codePoint = knownCharCodeAt(str, i);
arr.push(String.fromCodePoint(codePoint));
i += 1;
}
return arr;
}
export default spreadString;
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
// Button does not change visibility. If button has icon - it changes chevron style. // Button does not change visibility. If button has icon - it changes chevron style.
// //
// %div.js-toggle-container // %div.js-toggle-container
// %a.js-toggle-button // %button.js-toggle-button
// %div.js-toggle-content // %div.js-toggle-content
// //
$('body').on('click', '.js-toggle-button', function(e) { $('body').on('click', '.js-toggle-button', function(e) {
......
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
import NotebookLab from 'vendor/notebooklab';
Vue.use(VueResource);
Vue.use(NotebookLab);
export default () => {
const el = document.getElementById('js-notebook-viewer');
new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
json: {},
};
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="iPython notebook loading">
</i>
</div>
<notebook-lab
v-if="!loading && !error"
:notebook="json"
code-css-class="code white" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst parsing the file.
</span>
</p>
</div>
`,
methods: {
loadFile() {
this.$http.get(el.dataset.endpoint)
.then((res) => {
this.json = res.json();
this.loading = false;
})
.catch((e) => {
if (e.status) {
this.loadError = true;
}
this.error = true;
});
},
},
mounted() {
$('<link>', {
rel: 'stylesheet',
type: 'text/css',
href: gon.katex_css_url,
}).appendTo('head');
if (gon.katex_js_url) {
$.getScript(gon.katex_js_url, () => {
this.loadFile();
});
} else {
this.loadFile();
}
},
});
};
import renderNotebook from './notebook';
document.addEventListener('DOMContentLoaded', renderNotebook);
...@@ -50,9 +50,7 @@ export default { ...@@ -50,9 +50,7 @@ export default {
this.showDetail = false; this.showDetail = false;
}, },
showIssue(e) { showIssue(e) {
const targetTagName = e.target.tagName.toLowerCase(); if (e.target.classList.contains('js-no-trigger')) return;
if (targetTagName === 'a' || targetTagName === 'button') return;
if (this.showDetail) { if (this.showDetail) {
this.showDetail = false; this.showDetail = false;
......
...@@ -84,20 +84,20 @@ import eventHub from '../eventhub'; ...@@ -84,20 +84,20 @@ import eventHub from '../eventhub';
#{{ issue.id }} #{{ issue.id }}
</span> </span>
<a <a
class="card-assignee has-tooltip" class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username" :href="rootPath + issue.assignee.username"
:title="'Assigned to ' + issue.assignee.name" :title="'Assigned to ' + issue.assignee.name"
v-if="issue.assignee" v-if="issue.assignee"
data-container="body"> data-container="body">
<img <img
class="avatar avatar-inline s20" class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar" :src="issue.assignee.avatar"
width="20" width="20"
height="20" height="20"
:alt="'Avatar for ' + issue.assignee.name" /> :alt="'Avatar for ' + issue.assignee.name" />
</a> </a>
<button <button
class="label color-label has-tooltip" class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels" v-for="label in issue.labels"
type="button" type="button"
v-if="showLabel(label)" v-if="showLabel(label)"
......
...@@ -8,21 +8,31 @@ require('./filtered_search_token_keys'); ...@@ -8,21 +8,31 @@ require('./filtered_search_token_keys');
// Values that start with a double quote must end in a double quote (same for single) // Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokens = []; const tokens = [];
const tokenIndexes = []; // stores key+value for simple search
let lastToken = null; let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3; let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol; let tokenSymbol = symbol;
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue; tokenSymbol = tokenValue;
tokenValue = ''; tokenValue = '';
} }
tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates
if (tokenIndexes.indexOf(tokenIndex) === -1) {
tokenIndexes.push(tokenIndex);
tokens.push({ tokens.push({
key, key,
value: tokenValue || '', value: tokenValue || '',
symbol: tokenSymbol || '', symbol: tokenSymbol || '',
}); });
}
return ''; return '';
}).replace(/\s{2,}/g, ' ').trim() || ''; }).replace(/\s{2,}/g, ' ').trim() || '';
......
...@@ -5,23 +5,37 @@ import httpStatusCodes from './http_status'; ...@@ -5,23 +5,37 @@ import httpStatusCodes from './http_status';
* Service for vue resouce and method need to be provided as props * Service for vue resouce and method need to be provided as props
* *
* @example * @example
* new poll({ * new Poll({
* resource: resource, * resource: resource,
* method: 'name', * method: 'name',
* data: {page: 1, scope: 'all'}, * data: {page: 1, scope: 'all'}, // optional
* successCallback: () => {}, * successCallback: () => {},
* errorCallback: () => {}, * errorCallback: () => {},
* notificationCallback: () => {}, // optional
* }).makeRequest(); * }).makeRequest();
* *
* this.service = new BoardsService(endpoint); * Usage in pipelines table with visibility lib:
* new poll({ *
* const poll = new Poll({
* resource: this.service, * resource: this.service,
* method: 'get', * method: 'getPipelines',
* data: {page: 1, scope: 'all'}, * data: { page: pageNumber, scope },
* successCallback: () => {}, * successCallback: this.successCallback,
* errorCallback: () => {}, * errorCallback: this.errorCallback,
* }).makeRequest(); * notificationCallback: this.updateLoading,
* });
*
* if (!Visibility.hidden()) {
* poll.makeRequest();
* }
* *
* Visibility.change(() => {
* if (!Visibility.hidden()) {
* poll.restart();
* } else {
* poll.stop();
* }
* });
* *
* 1. Checks for response and headers before start polling * 1. Checks for response and headers before start polling
* 2. Interval is provided by `Poll-Interval` header. * 2. Interval is provided by `Poll-Interval` header.
...@@ -34,6 +48,8 @@ export default class Poll { ...@@ -34,6 +48,8 @@ export default class Poll {
constructor(options = {}) { constructor(options = {}) {
this.options = options; this.options = options;
this.options.data = options.data || {}; this.options.data = options.data || {};
this.options.notificationCallback = options.notificationCallback ||
function notificationCallback() {};
this.intervalHeader = 'POLL-INTERVAL'; this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null; this.timeoutID = null;
...@@ -42,7 +58,7 @@ export default class Poll { ...@@ -42,7 +58,7 @@ export default class Poll {
checkConditions(response) { checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers); const headers = gl.utils.normalizeHeaders(response.headers);
const pollInterval = headers[this.intervalHeader]; const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => { this.timeoutID = setTimeout(() => {
...@@ -54,7 +70,10 @@ export default class Poll { ...@@ -54,7 +70,10 @@ export default class Poll {
} }
makeRequest() { makeRequest() {
const { resource, method, data, errorCallback } = this.options; const { resource, method, data, errorCallback, notificationCallback } = this.options;
// It's called everytime a new request is made. Useful to update the status.
notificationCallback(true);
return resource[method](data) return resource[method](data)
.then(response => this.checkConditions(response)) .then(response => this.checkConditions(response))
...@@ -70,4 +89,12 @@ export default class Poll { ...@@ -70,4 +89,12 @@ export default class Poll {
this.canPoll = false; this.canPoll = false;
clearTimeout(this.timeoutID); clearTimeout(this.timeoutID);
} }
/**
* Restarts polling after it has been stoped
*/
restart() {
this.canPoll = true;
this.makeRequest();
}
} }
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
const userCalloutElementName = '.user-callout';
const closeButton = '.close-user-callout';
const userCalloutBtn = '.user-callout-btn';
const userCalloutSvgAttrName = 'callout-svg';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
const USER_CALLOUT_TEMPLATE = `
<div class="bordered-box landing content-block">
<button class="btn btn-default close close-user-callout" type="button">
<i class="fa fa-times dismiss-icon"></i>
</button>
<div class="row">
<div class="col-sm-3 col-xs-12 svg-container">
</div>
<div class="col-sm-8 col-xs-12 inner-content">
<h4>
Customize your experience
</h4>
<p>
Change syntax themes, default project pages, and more in preferences.
</p>
<a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
</div>
</div>
</div>`;
export default class UserCallout { export default class UserCallout {
constructor() { constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
this.userCalloutBody = $(userCalloutElementName); this.userCalloutBody = $('.user-callout');
this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
$(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
this.init(); this.init();
} }
init() { init() {
const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
$template.find('.svg-container').append(this.userCalloutSvg); $('.js-close-callout').on('click', e => this.dismissCallout(e));
this.userCalloutBody.append($template);
$template.find(closeButton).on('click', e => this.dismissCallout(e));
$template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
} else {
this.userCalloutBody.remove();
} }
} }
dismissCallout(e) { dismissCallout(e) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget); const $currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('close-user-callout')) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove(); this.userCalloutBody.remove();
} }
} }
......
...@@ -3,8 +3,8 @@ const UI_LIMIT = 6; ...@@ -3,8 +3,8 @@ const UI_LIMIT = 6;
const SPREAD = '...'; const SPREAD = '...';
const PREV = 'Prev'; const PREV = 'Prev';
const NEXT = 'Next'; const NEXT = 'Next';
const FIRST = '<< First'; const FIRST = '« First';
const LAST = 'Last >>'; const LAST = 'Last »';
export default { export default {
props: { props: {
......
...@@ -269,8 +269,13 @@ ...@@ -269,8 +269,13 @@
font-size: (14px / $issue-boards-font-size) * 1em; font-size: (14px / $issue-boards-font-size) * 1em;
} }
.card-assignee {
margin-right: 5px;
}
.avatar { .avatar {
margin-left: 0; margin-left: 0;
margin-right: 0;
} }
} }
...@@ -325,6 +330,7 @@ ...@@ -325,6 +330,7 @@
} }
} }
<<<<<<< HEAD
.boards-title-holder { .boards-title-holder {
padding: 25px 13px $gl-padding; padding: 25px 13px $gl-padding;
...@@ -346,6 +352,9 @@ ...@@ -346,6 +352,9 @@
} }
.issue-boards-sidebar { .issue-boards-sidebar {
=======
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
>>>>>>> ce/master
&.right-sidebar { &.right-sidebar {
top: 0; top: 0;
bottom: 0; bottom: 0;
......
...@@ -431,6 +431,21 @@ ...@@ -431,6 +431,21 @@
border-bottom: none; border-bottom: none;
} }
.diff-stats-summary-toggler {
padding: 0;
background-color: transparent;
border: 0;
color: $gl-link-color;
transition: color 0.1s linear;
&:hover,
&:focus {
outline: none;
text-decoration: underline;
color: $gl-link-hover-color;
}
}
// Mobile // Mobile
@media (max-width: 480px) { @media (max-width: 480px) {
.diff-title { .diff-title {
......
...@@ -60,7 +60,17 @@ ...@@ -60,7 +60,17 @@
} }
.modify-merge-commit-link { .modify-merge-commit-link {
padding: 0;
background-color: transparent;
border: 0;
color: $gl-text-color; color: $gl-text-color;
&:hover,
&:focus {
text-decoration: underline;
}
} }
.merge-param-checkbox { .merge-param-checkbox {
......
...@@ -410,8 +410,22 @@ ul.notes { ...@@ -410,8 +410,22 @@ ul.notes {
} }
.discussion-toggle-button { .discussion-toggle-button {
padding: 0;
background-color: transparent;
border: 0;
line-height: 20px; line-height: 20px;
font-size: 13px; font-size: 13px;
transition: color 0.1s linear;
&:hover {
color: $gl-link-color;
}
&:focus {
text-decoration: underline;
outline: none;
color: $gl-link-color;
}
.fa { .fa {
margin-right: 3px; margin-right: 3px;
......
...@@ -310,4 +310,8 @@ module ApplicationHelper ...@@ -310,4 +310,8 @@ module ApplicationHelper
def active_when(condition) def active_when(condition)
'active' if condition 'active' if condition
end end
def show_user_callout?
cookies[:user_callout_dismissed] == 'true'
end
end end
...@@ -46,6 +46,10 @@ class Blob < SimpleDelegator ...@@ -46,6 +46,10 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG' text? && language && language.name == 'SVG'
end end
def ipython_notebook?
text? && language && language.name == 'Jupyter Notebook'
end
def size_within_svg_limits? def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE size <= MAXIMUM_SVG_SIZE
end end
...@@ -63,6 +67,8 @@ class Blob < SimpleDelegator ...@@ -63,6 +67,8 @@ class Blob < SimpleDelegator
end end
elsif image? || svg? elsif image? || svg?
'image' 'image'
elsif ipython_notebook?
'notebook'
elsif text? elsif text?
'text' 'text'
else else
......
...@@ -74,8 +74,8 @@ class PrometheusService < MonitoringService ...@@ -74,8 +74,8 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(environment_slug) def calculate_reactive_cache(environment_slug)
return unless active? && project && !project.pending_delete? return unless active? && project && !project.pending_delete?
memory_query = %{(sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})) /1024/1024} memory_query = %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}) * 100} cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
{ {
success: true, success: true,
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
- page_title "Projects" - page_title "Projects"
- header_title "Projects", dashboard_projects_path - header_title "Projects", dashboard_projects_path
.user-callout{ 'callout-svg' => custom_icon('icon_customization') } - unless show_user_callout?
= render 'shared/user_callout'
- if @projects.any? || params[:name] - if @projects.any? || params[:name]
= render 'dashboard/projects_head' = render 'dashboard/projects_head'
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } } .discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header .discussion-header
.discussion-actions .discussion-actions
= link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button" }
- if expanded - if expanded
= icon("chevron-up") = icon("chevron-up")
- else - else
......
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('notebook_viewer')
.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
.visible-xs-inline .visible-xs-inline
= render_commit_status(commit, ref: ref) = render_commit_status(commit, ref: ref)
- if commit.description? - if commit.description?
%a.text-expander.hidden-xs.js-toggle-button ... %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ...
- if commit.description? - if commit.description?
%pre.commit-row-description.js-toggle-content %pre.commit-row-description.js-toggle-content
......
.js-toggle-container .js-toggle-container
.commit-stat-summary .commit-stat-summary
Showing Showing
= link_to '#', class: 'js-toggle-button' do %button.diff-stats-summary-toggler.js-toggle-button{ type: "button" }
%strong= pluralize(diff_files.size, "changed file") %strong= pluralize(diff_files.size, "changed file")
with with
%strong.cgreen #{diff_files.sum(&:added_lines)} additions %strong.cgreen #{diff_files.sum(&:added_lines)} additions
......
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/squash_and_merge'), title: 'About this feature', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} = link_to icon('question-circle'), help_page_path('user/project/merge_requests/squash_and_merge'), title: 'About this feature', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'}
.accept-control .accept-control
<<<<<<< HEAD
- if @project.merge_requests_ff_only_enabled - if @project.merge_requests_ff_only_enabled
Fast-forward merge without a merge commit Fast-forward merge without a merge commit
- else - else
...@@ -58,5 +59,16 @@ ...@@ -58,5 +59,16 @@
message_without_description: @merge_request.merge_commit_message, message_without_description: @merge_request.merge_commit_message,
text: @merge_request.merge_commit_message, text: @merge_request.merge_commit_message,
rows: 14, hint: true rows: 14, hint: true
=======
%button.modify-merge-commit-link.js-toggle-button{ type: "button" }
= icon('edit')
Modify commit message
.js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params,
message_with_description: @merge_request.merge_commit_message(include_description: true),
message_without_description: @merge_request.merge_commit_message,
text: @merge_request.merge_commit_message,
rows: 14, hint: true
>>>>>>> ce/master
= hidden_field_tag :merge_when_pipeline_succeeds, "", autocomplete: "off" = hidden_field_tag :merge_when_pipeline_succeeds, "", autocomplete: "off"
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
Gitea Gitea
%div %div
- if git_import_enabled? - if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do %button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL') = icon('git', text: 'Repo by URL')
.import_gitlab_project .import_gitlab_project
- if gitlab_project_import_enabled? - if gitlab_project_import_enabled?
......
.user-callout
.bordered-box.landing.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss customize experience box' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.row
.col-sm-3.col-xs-12.svg-container
= custom_icon('icon_customization')
.col-sm-8.col-xs-12.inner-content
%h4
Customize your experience
%p
Change syntax themes, default project pages, and more in preferences.
= link_to 'Check it out', profile_preferences_path, class: 'btn btn-default js-close-callout'
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
- selected_labels = issuable.labels - selected_labels = issuable.labels
.block.labels .block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
= icon('tags', class: 'hidden', 'aria-hidden': 'true') = icon('tags', 'aria-hidden': 'true')
%span %span
= selected_labels.size = selected_labels.size
.title.hide-collapsed .title.hide-collapsed
......
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
= event_action_name(event) = event_action_name(event)
%strong %strong
- if event.note? - if event.note?
= link_to event.note_target.to_reference, event_note_target_path(event) = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title
- elsif event.target - elsif event.target
= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target] = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
at at
%strong %strong
......
...@@ -97,8 +97,8 @@ ...@@ -97,8 +97,8 @@
Snippets Snippets
%div{ class: container_class } %div{ class: container_class }
- if @user == current_user - if @user == current_user && !show_user_callout?
.user-callout{ 'callout-svg' => custom_icon('icon_customization') } = render 'shared/user_callout'
.tab-content .tab-content
#activity.tab-pane #activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs .row-content-block.calender-block.white.second-block.hidden-xs
......
---
title: Update rugged to 0.25.1.1
merge_request: 10286
author: Elan Ruusamäe
---
title: Resolve "404 when requesting build trace"
merge_request: 9759
author: dosuken123
---
title: Remove duplicated tokens in issuable search bar
merge_request:
author:
---
title: Update toggle buttons to be <button>
merge_request:
author:
---
title: consistent icons in vue and kaminari pagers
merge_request:
author:
---
title: Fix escaped html appearing in milestone page
merge_request: 10224
author:
---
title: Improve Markdown rendering when a lot of merge requests are referenced
merge_request: 10252
author:
---
title: Add tooltip to user's calendar activities
merge_request: 10123
author: Alex Argunov
---
title: Fix after_script processing for Runners APIv4
merge_request: 10185
author:
---
title: Fix environment folder route when special chars present in environment name
merge_request: 10250
author:
---
title: Fix bug that caused jobs that already had been retried to be retried again
when retrying a pipeline
merge_request: 10249
author:
---
title: Optimize labels finder query when searching for a project with a group
merge_request:
author: mhasbini
---
title: Make user mentions case-insensitive
merge_request: 10285
author: blackst0ne
---
title: Simplify search queries for projects and merge requests
merge_request: 10053
author: mhasbini
...@@ -204,7 +204,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -204,7 +204,7 @@ constraints(ProjectUrlConstrainer.new) do
end end
collection do collection do
get :folder, path: 'folders/:id' get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end end
end end
......
...@@ -39,6 +39,7 @@ var config = { ...@@ -39,6 +39,7 @@ var config = {
mr_widget_ee: './merge_request_widget/widget_bundle.js', mr_widget_ee: './merge_request_widget/widget_bundle.js',
monitoring: './monitoring/monitoring_bundle.js', monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js', network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
...@@ -107,7 +108,11 @@ var config = { ...@@ -107,7 +108,11 @@ var config = {
'environments_folder', 'environments_folder',
'issuable', 'issuable',
'merge_conflicts', 'merge_conflicts',
<<<<<<< HEAD
'mr_widget_ee', 'mr_widget_ee',
=======
'notebook_viewer',
>>>>>>> ce/master
'vue_pipelines', 'vue_pipelines',
], ],
minChunks: function(module, count) { minChunks: function(module, count) {
......
...@@ -153,8 +153,8 @@ The queries utilized by GitLab are shown in the following table. ...@@ -153,8 +153,8 @@ The queries utilized by GitLab are shown in the following table.
| Metric | Query | | Metric | Query |
| ------ | ----- | | ------ | ----- |
| Average Memory (MB) | `(sum(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) / count(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"})) /1024/1024` | | Average Memory (MB) | `(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` |
| Average CPU Utilization (%) | `sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) * 100` | | Average CPU Utilization (%) | `sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="$CI_ENVIRONMENT_SLUG"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="$CI_ENVIRONMENT_SLUG"}) * 100` |
## Monitoring CI/CD Environments ## Monitoring CI/CD Environments
......
...@@ -19,7 +19,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps ...@@ -19,7 +19,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('Bitbucket') expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com') expect(page).to have_link('GitLab.com')
expect(page).to have_link('Google Code') expect(page).to have_link('Google Code')
expect(page).to have_link('Repo by URL') expect(page).to have_button('Repo by URL')
expect(page).to have_link('GitLab export') expect(page).to have_link('GitLab export')
end end
......
...@@ -382,7 +382,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -382,7 +382,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end end
step 'I modify merge commit message' do step 'I modify merge commit message' do
find('.modify-merge-commit-link').click click_button "Modify commit message"
fill_in 'commit_message', with: 'wow such merge' fill_in 'commit_message', with: 'wow such merge'
end end
......
...@@ -233,6 +233,7 @@ module API ...@@ -233,6 +233,7 @@ module API
.cancel(merge_request) .cancel(merge_request)
end end
<<<<<<< HEAD
desc 'List issues that will be closed on merge' do desc 'List issues that will be closed on merge' do
success Entities::MRNote success Entities::MRNote
end end
...@@ -291,6 +292,8 @@ module API ...@@ -291,6 +292,8 @@ module API
present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user present merge_request, with: Entities::MergeRequestApprovals, current_user: current_user
end end
=======
>>>>>>> ce/master
desc 'List issues that will be closed on merge' do desc 'List issues that will be closed on merge' do
success Entities::MRNote success Entities::MRNote
end end
......
...@@ -11,8 +11,8 @@ module Banzai ...@@ -11,8 +11,8 @@ module Banzai
MergeRequest MergeRequest
end end
def find_object(project, id) def find_object(project, iid)
project.merge_requests.find_by(iid: id) merge_requests_per_project[project][iid]
end end
def url_for_object(mr, project) def url_for_object(mr, project)
...@@ -21,6 +21,31 @@ module Banzai ...@@ -21,6 +21,31 @@ module Banzai
only_path: context[:only_path]) only_path: context[:only_path])
end end
def project_from_ref(ref)
projects_per_reference[ref || current_project_path]
end
# Returns a Hash containing the merge_requests per Project instance.
def merge_requests_per_project
@merge_requests_per_project ||= begin
hash = Hash.new { |h, k| h[k] = {} }
projects_per_reference.each do |path, project|
merge_request_ids = references_per_project[path]
merge_requests = project.merge_requests
.where(iid: merge_request_ids.to_a)
.includes(target_project: :namespace)
merge_requests.each do |merge_request|
hash[project][merge_request.iid.to_i] = merge_request
end
end
hash
end
end
def object_link_text_extras(object, matches) def object_link_text_extras(object, matches)
extras = super extras = super
......
...@@ -60,7 +60,7 @@ module Banzai ...@@ -60,7 +60,7 @@ module Banzai
self.class.references_in(text) do |match, username| self.class.references_in(text) do |match, username|
if username == 'all' && !skip_project_check? if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content) link_to_all(link_content: link_content)
elsif namespace = namespaces[username] elsif namespace = namespaces[username.downcase]
link_to_namespace(namespace, link_content: link_content) || match link_to_namespace(namespace, link_content: link_content) || match
else else
match match
...@@ -74,7 +74,7 @@ module Banzai ...@@ -74,7 +74,7 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the # The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects. # corresponding Namespace objects.
def namespaces def namespaces
@namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path) @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path).transform_keys(&:downcase)
end end
# Returns all usernames referenced in the current document. # Returns all usernames referenced in the current document.
......
...@@ -125,7 +125,7 @@ module Gitlab ...@@ -125,7 +125,7 @@ module Gitlab
end end
puts puts
puts applies_cleanly_msg(ee_branch) puts applies_cleanly_msg(ee_branch_found)
end end
def check_patch(patch_path) def check_patch(patch_path)
...@@ -215,7 +215,7 @@ module Gitlab ...@@ -215,7 +215,7 @@ module Gitlab
end end
def ee_patch_name def ee_patch_name
@ee_patch_name ||= patch_name_from_branch(ee_branch) @ee_patch_name ||= patch_name_from_branch(ee_branch_found)
end end
def ee_patch_full_path def ee_patch_full_path
......
...@@ -320,7 +320,7 @@ module Gitlab ...@@ -320,7 +320,7 @@ module Gitlab
def log_by_walk(sha, options) def log_by_walk(sha, options)
walk_options = { walk_options = {
show: sha, show: sha,
sort: Rugged::SORT_DATE, sort: Rugged::SORT_NONE,
limit: options[:limit], limit: options[:limit],
offset: options[:offset] offset: options[:offset]
} }
...@@ -382,7 +382,7 @@ module Gitlab ...@@ -382,7 +382,7 @@ module Gitlab
# a detailed list of valid arguments. # a detailed list of valid arguments.
def commits_between(from, to) def commits_between(from, to)
walker = Rugged::Walker.new(rugged) walker = Rugged::Walker.new(rugged)
walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE) walker.sorting(Rugged::SORT_NONE | Rugged::SORT_REVERSE)
sha_from = sha_from_ref(from) sha_from = sha_from_ref(from)
sha_to = sha_from_ref(to) sha_to = sha_from_ref(to)
...@@ -460,7 +460,7 @@ module Gitlab ...@@ -460,7 +460,7 @@ module Gitlab
if actual_options[:order] == :topo if actual_options[:order] == :topo
walker.sorting(Rugged::SORT_TOPO) walker.sorting(Rugged::SORT_TOPO)
else else
walker.sorting(Rugged::SORT_DATE) walker.sorting(Rugged::SORT_NONE)
end end
commits = [] commits = []
...@@ -828,23 +828,6 @@ module Gitlab ...@@ -828,23 +828,6 @@ module Gitlab
Rugged::Commit.create(rugged, actual_options) Rugged::Commit.create(rugged, actual_options)
end end
def commits_since(from_date)
walker = Rugged::Walker.new(rugged)
walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
rugged.references.each("refs/heads/*") do |ref|
walker.push(ref.target_id)
end
commits = []
walker.each do |commit|
break if commit.author[:time].to_date < from_date
commits.push(commit)
end
commits
end
AUTOCRLF_VALUES = { AUTOCRLF_VALUES = {
"true" => true, "true" => true,
"false" => false, "false" => false,
......
...@@ -81,6 +81,39 @@ describe Projects::EnvironmentsController do ...@@ -81,6 +81,39 @@ describe Projects::EnvironmentsController do
end end
end end
describe 'GET folder' do
before do
create(:environment, project: project,
name: 'staging-1.0/review',
state: :available)
end
context 'when using default format' do
it 'responds with HTML' do
get :folder, namespace_id: project.namespace,
project_id: project,
id: 'staging-1.0'
expect(response).to be_ok
expect(response).to render_template 'folder'
end
end
context 'when using JSON format' do
it 'responds with JSON' do
get :folder, namespace_id: project.namespace,
project_id: project,
id: 'staging-1.0',
format: :json
expect(response).to be_ok
expect(response).not_to render_template 'folder'
expect(json_response['environments'][0])
.to include('name' => 'staging-1.0/review')
end
end
end
describe 'GET show' do describe 'GET show' do
context 'with valid id' do context 'with valid id' do
it 'responds with a status code 200' do it 'responds with a status code 200' do
......
...@@ -46,7 +46,7 @@ feature 'Group', feature: true do ...@@ -46,7 +46,7 @@ feature 'Group', feature: true do
describe 'Mattermost team creation' do describe 'Mattermost team creation' do
before do before do
allow(Settings.mattermost).to receive_messages(enabled: mattermost_enabled) stub_mattermost_setting(enabled: mattermost_enabled)
visit new_group_path visit new_group_path
end end
......
...@@ -296,7 +296,7 @@ feature 'Diff notes resolve', feature: true, js: true do ...@@ -296,7 +296,7 @@ feature 'Diff notes resolve', feature: true, js: true do
it 'displays next discussion even if hidden' do it 'displays next discussion even if hidden' do
page.all('.note-discussion').each do |discussion| page.all('.note-discussion').each do |discussion|
page.within discussion do page.within discussion do
click_link 'Toggle discussion' click_button 'Toggle discussion'
end end
end end
...@@ -477,13 +477,13 @@ feature 'Diff notes resolve', feature: true, js: true do ...@@ -477,13 +477,13 @@ feature 'Diff notes resolve', feature: true, js: true do
it 'shows resolved icon' do it 'shows resolved icon' do
expect(page).to have_content '1/1 discussion resolved' expect(page).to have_content '1/1 discussion resolved'
click_link 'Toggle discussion' click_button 'Toggle discussion'
expect(page).to have_selector('.line-resolve-btn.is-active') expect(page).to have_selector('.line-resolve-btn.is-active')
end end
it 'does not allow user to click resolve button' do it 'does not allow user to click resolve button' do
expect(page).to have_selector('.line-resolve-btn.is-disabled') expect(page).to have_selector('.line-resolve-btn.is-disabled')
click_link 'Toggle discussion' click_button 'Toggle discussion'
expect(page).to have_selector('.line-resolve-btn.is-disabled') expect(page).to have_selector('.line-resolve-btn.is-disabled')
end end
......
...@@ -41,7 +41,7 @@ feature 'Clicking toggle commit message link', feature: true, js: true do ...@@ -41,7 +41,7 @@ feature 'Clicking toggle commit message link', feature: true, js: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request) visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(textbox).not_to be_visible expect(textbox).not_to be_visible
click_link "Modify commit message" click_button "Modify commit message"
expect(textbox).to be_visible expect(textbox).to be_visible
end end
......
...@@ -166,6 +166,25 @@ feature 'Environment', :feature do ...@@ -166,6 +166,25 @@ feature 'Environment', :feature do
end end
end end
feature 'environment folders', :js do
context 'when folder name contains special charaters' do
before do
create(:environment, project: project,
name: 'staging-1.0/review',
state: :available)
visit folder_namespace_project_environments_path(project.namespace,
project,
id: 'staging-1.0')
end
it 'renders a correct environment folder' do
expect(page).to have_http_status(:ok)
expect(page).to have_content('Environments / staging-1.0')
end
end
end
feature 'auto-close environment when branch is deleted' do feature 'auto-close environment when branch is deleted' do
given(:project) { create(:project) } given(:project) { create(:project) }
......
...@@ -7,7 +7,7 @@ feature 'Setup Mattermost slash commands', :feature, :js do ...@@ -7,7 +7,7 @@ feature 'Setup Mattermost slash commands', :feature, :js do
let(:mattermost_enabled) { true } let(:mattermost_enabled) { true }
before do before do
Settings.mattermost['enabled'] = mattermost_enabled stub_mattermost_setting(enabled: mattermost_enabled)
project.team << [user, :master] project.team << [user, :master]
login_as(user) login_as(user)
visit edit_namespace_project_service_path(project.namespace, project, service) visit edit_namespace_project_service_path(project.namespace, project, service)
......
...@@ -16,6 +16,18 @@ describe 'User Callouts', js: true do ...@@ -16,6 +16,18 @@ describe 'User Callouts', js: true do
expect(current_path).to eq profile_preferences_path expect(current_path).to eq profile_preferences_path
end end
it 'does not show when cookie is set' do
visit dashboard_projects_path
within('.user-callout') do
find('.close').click
end
visit dashboard_projects_path
expect(page).not_to have_selector('.user-callout')
end
describe 'user callout should appear in two routes' do describe 'user callout should appear in two routes' do
it 'shows up on the user profile' do it 'shows up on the user profile' do
visit user_path(user) visit user_path(user)
...@@ -31,7 +43,7 @@ describe 'User Callouts', js: true do ...@@ -31,7 +43,7 @@ describe 'User Callouts', js: true do
it 'hides the user callout when click on the dismiss icon' do it 'hides the user callout when click on the dismiss icon' do
visit user_path(user) visit user_path(user)
within('.user-callout') do within('.user-callout') do
find('.close-user-callout').click find('.close').click
end end
expect(page).not_to have_selector('.user-callout') expect(page).not_to have_selector('.user-callout')
end end
......
import Vue from 'vue';
import renderNotebook from '~/blob/notebook';
describe('iPython notebook renderer', () => {
preloadFixtures('static/notebook_viewer.html.raw');
beforeEach(() => {
loadFixtures('static/notebook_viewer.html.raw');
});
it('shows loading icon', () => {
renderNotebook();
expect(
document.querySelector('.loading'),
).not.toBeNull();
});
describe('successful response', () => {
const response = (request, next) => {
next(request.respondWith(JSON.stringify({
cells: [{
cell_type: 'markdown',
source: ['# test'],
}, {
cell_type: 'code',
execution_count: 1,
source: [
'def test(str)',
' return str',
],
outputs: [],
}],
}), {
status: 200,
}));
};
beforeEach((done) => {
Vue.http.interceptors.push(response);
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, response,
);
});
it('does not show loading icon', () => {
expect(
document.querySelector('.loading'),
).toBeNull();
});
it('renders the notebook', () => {
expect(
document.querySelector('.md'),
).not.toBeNull();
});
it('renders the markdown cell', () => {
expect(
document.querySelector('h1'),
).not.toBeNull();
expect(
document.querySelector('h1').textContent.trim(),
).toBe('test');
});
it('highlights code', () => {
expect(
document.querySelector('.token'),
).not.toBeNull();
expect(
document.querySelector('.language-python'),
).not.toBeNull();
});
});
describe('error in JSON response', () => {
const response = (request, next) => {
next(request.respondWith('{ "cells": [{"cell_type": "markdown"} }', {
status: 200,
}));
};
beforeEach((done) => {
Vue.http.interceptors.push(response);
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, response,
);
});
it('does not show loading icon', () => {
expect(
document.querySelector('.loading'),
).toBeNull();
});
it('shows error message', () => {
expect(
document.querySelector('.md').textContent.trim(),
).toBe('An error occured whilst parsing the file.');
});
});
describe('error getting file', () => {
const response = (request, next) => {
next(request.respondWith('', {
status: 500,
}));
};
beforeEach((done) => {
Vue.http.interceptors.push(response);
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, response,
);
});
it('does not show loading icon', () => {
expect(
document.querySelector('.loading'),
).toBeNull();
});
it('shows error message', () => {
expect(
document.querySelector('.md').textContent.trim(),
).toBe('An error occured whilst loading the file. Please try again later.');
});
});
});
/* global List */ /* global List */
/* global ListUser */
/* global ListLabel */ /* global ListLabel */
/* global listObj */ /* global listObj */
/* global boardsMockInterceptor */ /* global boardsMockInterceptor */
/* global BoardService */ /* global BoardService */
import Vue from 'vue'; import Vue from 'vue';
import '~/boards/models/user';
require('~/boards/models/list'); require('~/boards/models/list');
require('~/boards/models/label'); require('~/boards/models/label');
...@@ -130,6 +132,23 @@ describe('Issue card', () => { ...@@ -130,6 +132,23 @@ describe('Issue card', () => {
expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({}); expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
}); });
it('does not set detail issue if img is clicked', (done) => {
vm.issue.assignee = new ListUser({
id: 1,
name: 'testing 123',
username: 'test',
avatar: 'test_image',
});
Vue.nextTick(() => {
triggerEvent('mouseup', vm.$el.querySelector('img'));
expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
done();
});
});
it('does not set detail issue if showDetail is false after mouseup', () => { it('does not set detail issue if showDetail is false after mouseup', () => {
triggerEvent('mouseup'); triggerEvent('mouseup');
......
...@@ -92,6 +92,20 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper ...@@ -92,6 +92,20 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
manager.search(); manager.search();
}); });
it('removes duplicated tokens', (done) => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
});
manager.search();
});
}); });
describe('handleInputPlaceholder', () => { describe('handleInputPlaceholder', () => {
......
...@@ -122,6 +122,14 @@ require('~/filtered_search/filtered_search_tokenizer'); ...@@ -122,6 +122,14 @@ require('~/filtered_search/filtered_search_tokenizer');
expect(results.lastToken).toBe('std::includes'); expect(results.lastToken).toBe('std::includes');
expect(results.searchToken).toBe('std::includes'); expect(results.searchToken).toBe('std::includes');
}); });
it('removes duplicated values', () => {
const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo');
expect(results.tokens.length).toBe(1);
expect(results.tokens[0].key).toBe('label');
expect(results.tokens[0].value).toBe('foo');
expect(results.tokens[0].symbol).toBe('~');
});
}); });
}); });
})(); })();
require 'spec_helper'
describe Dashboard::ProjectsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
render_views
before(:all) do
clean_frontend_fixtures('dashboard/')
end
before(:each) do
sign_in(admin)
end
it 'dashboard/user-callout.html.raw' do |example|
rendered = render_template('shared/_user_callout')
store_frontend_fixture(rendered, example.description)
end
private
def render_template(template_file_name)
controller.prepend_view_path(JavaScriptFixturesHelpers::FIXTURE_PATH)
controller.render_to_string(template_file_name, layout: false)
end
end
.file-content#js-notebook-viewer{ data: { endpoint: '/test' } }
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
...@@ -160,4 +160,44 @@ describe('Poll', () => { ...@@ -160,4 +160,44 @@ describe('Poll', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor); Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
}); });
}); });
describe('restart', () => {
it('should restart polling when its called', (done) => {
const pollInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
};
Vue.http.interceptors.push(pollInterceptor);
const service = new ServiceMock('endpoint');
spyOn(service, 'fetch').and.callThrough();
const Polling = new Poll({
resource: service,
method: 'fetch',
data: { page: 1 },
successCallback: () => {
Polling.stop();
setTimeout(() => {
Polling.restart();
}, 0);
},
errorCallback: callbacks.error,
});
spyOn(Polling, 'stop').and.callThrough();
Polling.makeRequest();
setTimeout(() => {
expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(Polling.stop).toHaveBeenCalled();
done();
}, 10);
Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
});
});
}); });
...@@ -78,5 +78,11 @@ import '~/right_sidebar'; ...@@ -78,5 +78,11 @@ import '~/right_sidebar';
expect(todoToggleSpy.calls.count()).toEqual(1); expect(todoToggleSpy.calls.count()).toEqual(1);
}); });
it('should not hide collapsed icons', () => {
[].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
});
});
}); });
}).call(window); }).call(window);
...@@ -4,7 +4,7 @@ import UserCallout from '~/user_callout'; ...@@ -4,7 +4,7 @@ import UserCallout from '~/user_callout';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
describe('UserCallout', function () { describe('UserCallout', function () {
const fixtureName = 'static/user_callout.html.raw'; const fixtureName = 'dashboard/user-callout.html.raw';
preloadFixtures(fixtureName); preloadFixtures(fixtureName);
beforeEach(() => { beforeEach(() => {
...@@ -12,26 +12,22 @@ describe('UserCallout', function () { ...@@ -12,26 +12,22 @@ describe('UserCallout', function () {
Cookies.remove(USER_CALLOUT_COOKIE); Cookies.remove(USER_CALLOUT_COOKIE);
this.userCallout = new UserCallout(); this.userCallout = new UserCallout();
this.closeButton = $('.close-user-callout'); this.closeButton = $('.js-close-callout.close');
this.userCalloutBtn = $('.user-callout-btn'); this.userCalloutBtn = $('.js-close-callout:not(.close)');
this.userCalloutContainer = $('.user-callout'); this.userCalloutContainer = $('.user-callout');
}); });
it('does not show when cookie is set not defined', () => { it('hides when user clicks on the dismiss-icon', (done) => {
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeUndefined(); this.closeButton.click();
expect(this.userCalloutContainer.is(':visible')).toBe(true); expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
});
it('shows when cookie is set to false', () => { setTimeout(() => {
Cookies.set(USER_CALLOUT_COOKIE, 'false'); expect(
document.querySelector('.user-callout'),
).toBeNull();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeDefined(); done();
expect(this.userCalloutContainer.is(':visible')).toBe(true);
}); });
it('hides when user clicks on the dismiss-icon', () => {
this.closeButton.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
}); });
it('hides when user clicks on the "check it out" button', () => { it('hides when user clicks on the "check it out" button', () => {
...@@ -39,19 +35,3 @@ describe('UserCallout', function () { ...@@ -39,19 +35,3 @@ describe('UserCallout', function () {
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
}); });
}); });
describe('UserCallout when cookie is present', function () {
const fixtureName = 'static/user_callout.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
Cookies.set(USER_CALLOUT_COOKIE, 'true');
this.userCallout = new UserCallout();
this.userCalloutContainer = $('.user-callout');
});
it('removes the DOM element', () => {
expect(this.userCalloutContainer.length).toBe(0);
});
});
...@@ -83,7 +83,7 @@ describe('Pagination component', () => { ...@@ -83,7 +83,7 @@ describe('Pagination component', () => {
}, },
}).$mount(); }).$mount();
component.changePage({ target: { innerText: 'Last >>' } }); component.changePage({ target: { innerText: 'Last »' } });
expect(changeChanges.one).toEqual(10); expect(changeChanges.one).toEqual(10);
}); });
...@@ -100,7 +100,7 @@ describe('Pagination component', () => { ...@@ -100,7 +100,7 @@ describe('Pagination component', () => {
}, },
}).$mount(); }).$mount();
component.changePage({ target: { innerText: '<< First' } }); component.changePage({ target: { innerText: '« First' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
}); });
......
...@@ -21,6 +21,19 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -21,6 +21,19 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end end
end end
describe 'performance' do
let(:another_issue) { create(:issue, project: project) }
it 'does not have a N+1 query problem' do
single_reference = "Issue #{issue.to_reference}"
multiple_references = "Issues #{issue.to_reference} and #{another_issue.to_reference}"
control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count
expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count)
end
end
context 'internal reference' do context 'internal reference' do
it_behaves_like 'a reference containing an element node' it_behaves_like 'a reference containing an element node'
......
...@@ -17,6 +17,19 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do ...@@ -17,6 +17,19 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
end end
end end
describe 'performance' do
let(:another_merge) { create(:merge_request, source_project: project, source_branch: 'fix') }
it 'does not have a N+1 query problem' do
single_reference = "Merge request #{merge.to_reference}"
multiple_references = "Merge requests #{merge.to_reference} and #{another_merge.to_reference}"
control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count
expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count)
end
end
context 'internal reference' do context 'internal reference' do
let(:reference) { merge.to_reference } let(:reference) { merge.to_reference }
......
...@@ -83,6 +83,14 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do ...@@ -83,6 +83,14 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
end end
it 'links to a User with different case-sensitivity' do
user = create(:user, username: 'RescueRanger')
doc = reference_filter("Hey #{user.to_reference.upcase}")
expect(doc.css('a').length).to eq 1
expect(doc.css('a').text).to eq(user.to_reference)
end
it 'includes a data-user attribute' do it 'includes a data-user attribute' do
doc = reference_filter("Hey #{reference}") doc = reference_filter("Hey #{reference}")
link = doc.css('a').first link = doc.css('a').first
......
...@@ -53,6 +53,20 @@ describe Blob do ...@@ -53,6 +53,20 @@ describe Blob do
end end
end end
describe '#ipython_notebook?' do
it 'is falsey when language is not Jupyter Notebook' do
git_blob = double(text?: true, language: double(name: 'JSON'))
expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
end
it 'is truthy when language is Jupyter Notebook' do
git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
expect(described_class.decorate(git_blob)).to be_ipython_notebook
end
end
describe '#video?' do describe '#video?' do
it 'is falsey with image extension' do it 'is falsey with image extension' do
git_blob = Gitlab::Git::Blob.new(name: 'image.png') git_blob = Gitlab::Git::Blob.new(name: 'image.png')
...@@ -116,6 +130,11 @@ describe Blob do ...@@ -116,6 +130,11 @@ describe Blob do
blob = stubbed_blob blob = stubbed_blob
expect(blob.to_partial_path(project)).to eq 'download' expect(blob.to_partial_path(project)).to eq 'download'
end end
it 'handles iPython notebooks' do
blob = stubbed_blob(text?: true, ipython_notebook?: true)
expect(blob.to_partial_path(project)).to eq 'notebook'
end
end end
describe '#size_within_svg_limits?' do describe '#size_within_svg_limits?' do
......
require 'spec_helper'
describe Projects::EnvironmentsController, :routing do
let(:project) { create(:empty_project) }
let(:environment) do
create(:environment, project: project,
name: 'staging-1.0/review')
end
let(:environments_route) do
"#{project.namespace.name}/#{project.name}/environments/"
end
describe 'routing environment folders' do
context 'when using JSON format' do
it 'correctly matches environment name and JSON format' do
expect(get_folder('staging-1.0.json'))
.to route_to(*folder_action(id: 'staging-1.0', format: 'json'))
end
end
context 'when using HTML format' do
it 'correctly matches environment name and HTML format' do
expect(get_folder('staging-1.0.html'))
.to route_to(*folder_action(id: 'staging-1.0', format: 'html'))
end
end
context 'when using implicit format' do
it 'correctly matches environment name' do
expect(get_folder('staging-1.0'))
.to route_to(*folder_action(id: 'staging-1.0'))
end
end
end
def get_folder(folder)
get("#{project.namespace.name}/#{project.name}/" \
"environments/folders/#{folder}")
end
def folder_action(**opts)
options = { namespace_id: project.namespace.name,
project_id: project.name }
['projects/environments#folder', options.merge(opts)]
end
end
...@@ -69,7 +69,7 @@ describe Groups::CreateService, '#execute', services: true do ...@@ -69,7 +69,7 @@ describe Groups::CreateService, '#execute', services: true do
let!(:service) { described_class.new(user, params) } let!(:service) { described_class.new(user, params) }
before do before do
Settings.mattermost['enabled'] = true stub_mattermost_setting(enabled: true)
end end
it 'create the chat team with the group' do it 'create the chat team with the group' do
......
module PrometheusHelpers module PrometheusHelpers
def prometheus_memory_query(environment_slug) def prometheus_memory_query(environment_slug)
%{(sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})) /1024/1024} %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
end end
def prometheus_cpu_query(environment_slug) def prometheus_cpu_query(environment_slug)
%{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}) * 100} %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
end end
def prometheus_query_url(prometheus_query) def prometheus_query_url(prometheus_query)
......
...@@ -21,6 +21,10 @@ module StubConfiguration ...@@ -21,6 +21,10 @@ module StubConfiguration
allow(Gitlab.config.incoming_email).to receive_messages(messages) allow(Gitlab.config.incoming_email).to receive_messages(messages)
end end
def stub_mattermost_setting(messages)
allow(Gitlab.config.mattermost).to receive_messages(messages)
end
private private
# Modifies stubbed messages to also stub possible predicate versions # Modifies stubbed messages to also stub possible predicate versions
......
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