Commit 494d0f3f authored by Rémy Coutable's avatar Rémy Coutable

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

CE Upstream - Wednesday

Closes gitlab-ce#29414

See merge request !1537
parents 906a3550 264ffd70
......@@ -15,8 +15,8 @@ variables:
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
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_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
# This hack is needed to make ES not that memory hungry
ES_JAVA_OPTS: "-Xms600m -Xmx600m"
......
......@@ -314,9 +314,12 @@ request is as follows:
organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork
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
1. You don't have to select any approvers
1. Your merge request needs at least 1 approval but feel free to require more.
For instance if you're touching backend and frontend code, it's a good idea
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 description should give a motive for your change and the method you
used to achieve it.
......@@ -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
**approved by a [frontend maintainer][team]**.
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
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
......@@ -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
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[^1]: Specs other than JavaScript specs are considered backend code. Haml
changes are considered backend code if they include Ruby code other than just
pure HTML.
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
......@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0'
gem 'rugged', '~> 0.25.1.1'
# Authentication libraries
gem 'devise', '~> 4.2'
......@@ -65,7 +65,7 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
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
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
......
......@@ -321,9 +321,9 @@ GEM
rouge (~> 2.0)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
gollum-rugged_adapter (0.4.2)
gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.24.0, >= 0.21.3)
rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
......@@ -718,7 +718,7 @@ GEM
rubypants (0.2.0)
rubyzip (1.2.1)
rufus-scheduler (3.1.10)
rugged (0.24.0)
rugged (0.25.1.1)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
......@@ -948,7 +948,7 @@ DEPENDENCIES
gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.8.6)
grape (~> 0.19.0)
......@@ -1032,7 +1032,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
rugged (~> 0.24.0)
rugged (~> 0.25.1.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
......
8.18.0-ee-pre
9.1.0-pre
import spreadString from './spread_string';
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
......@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) {
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
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);
return cp >= tone1 && cp <= tone5;
});
......@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
......@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
spreadString(emojiUnicode).forEach((character) => {
Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
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 @@
// Button does not change visibility. If button has icon - it changes chevron style.
//
// %div.js-toggle-container
// %a.js-toggle-button
// %button.js-toggle-button
// %div.js-toggle-content
//
$('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() {
if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
});
};
import renderNotebook from './notebook';
document.addEventListener('DOMContentLoaded', renderNotebook);
......@@ -50,9 +50,7 @@ export default {
this.showDetail = false;
},
showIssue(e) {
const targetTagName = e.target.tagName.toLowerCase();
if (targetTagName === 'a' || targetTagName === 'button') return;
if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) {
this.showDetail = false;
......
......@@ -84,20 +84,20 @@ import eventHub from '../eventhub';
#{{ issue.id }}
</span>
<a
class="card-assignee has-tooltip"
class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username"
:title="'Assigned to ' + issue.assignee.name"
v-if="issue.assignee"
data-container="body">
<img
class="avatar avatar-inline s20"
class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar"
width="20"
height="20"
:alt="'Avatar for ' + issue.assignee.name" />
</a>
<button
class="label color-label has-tooltip"
class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
......
......@@ -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)
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokens = [];
const tokenIndexes = []; // stores key+value for simple search
let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol;
let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue;
tokenValue = '';
}
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates
if (tokenIndexes.indexOf(tokenIndex) === -1) {
tokenIndexes.push(tokenIndex);
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
}
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
......
......@@ -5,23 +5,37 @@ import httpStatusCodes from './http_status';
* Service for vue resouce and method need to be provided as props
*
* @example
* new poll({
* new Poll({
* resource: resource,
* method: 'name',
* data: {page: 1, scope: 'all'},
* data: {page: 1, scope: 'all'}, // optional
* successCallback: () => {},
* errorCallback: () => {},
* notificationCallback: () => {}, // optional
* }).makeRequest();
*
* this.service = new BoardsService(endpoint);
* new poll({
* resource: this.service,
* method: 'get',
* data: {page: 1, scope: 'all'},
* successCallback: () => {},
* errorCallback: () => {},
* }).makeRequest();
* Usage in pipelines table with visibility lib:
*
* const poll = new Poll({
* resource: this.service,
* method: 'getPipelines',
* data: { page: pageNumber, scope },
* successCallback: this.successCallback,
* errorCallback: this.errorCallback,
* 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
* 2. Interval is provided by `Poll-Interval` header.
......@@ -34,6 +48,8 @@ export default class Poll {
constructor(options = {}) {
this.options = options;
this.options.data = options.data || {};
this.options.notificationCallback = options.notificationCallback ||
function notificationCallback() {};
this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null;
......@@ -42,7 +58,7 @@ export default class Poll {
checkConditions(response) {
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) {
this.timeoutID = setTimeout(() => {
......@@ -54,11 +70,14 @@ export default class Poll {
}
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)
.then(response => this.checkConditions(response))
.catch(error => errorCallback(error));
.then(response => this.checkConditions(response))
.catch(error => errorCallback(error));
}
/**
......@@ -70,4 +89,12 @@ export default class Poll {
this.canPoll = false;
clearTimeout(this.timeoutID);
}
/**
* Restarts polling after it has been stoped
*/
restart() {
this.canPoll = true;
this.makeRequest();
}
}
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_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 {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
this.userCalloutBody = $(userCalloutElementName);
this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
$(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
this.userCalloutBody = $('.user-callout');
this.init();
}
init() {
const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
$template.find('.svg-container').append(this.userCalloutSvg);
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();
$('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
dismissCallout(e) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('close-user-callout')) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
}
}
......
......@@ -3,8 +3,8 @@ const UI_LIMIT = 6;
const SPREAD = '...';
const PREV = 'Prev';
const NEXT = 'Next';
const FIRST = '<< First';
const LAST = 'Last >>';
const FIRST = '« First';
const LAST = 'Last »';
export default {
props: {
......
......@@ -269,8 +269,13 @@
font-size: (14px / $issue-boards-font-size) * 1em;
}
.card-assignee {
margin-right: 5px;
}
.avatar {
margin-left: 0;
margin-right: 0;
}
}
......@@ -345,7 +350,7 @@
border-top: 1px solid $border-color;
}
.issue-boards-sidebar {
.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
&.right-sidebar {
top: 0;
bottom: 0;
......
......@@ -431,6 +431,21 @@
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
@media (max-width: 480px) {
.diff-title {
......
......@@ -60,7 +60,17 @@
}
.modify-merge-commit-link {
padding: 0;
background-color: transparent;
border: 0;
color: $gl-text-color;
&:hover,
&:focus {
text-decoration: underline;
}
}
.merge-param-checkbox {
......
......@@ -410,8 +410,22 @@ ul.notes {
}
.discussion-toggle-button {
padding: 0;
background-color: transparent;
border: 0;
line-height: 20px;
font-size: 13px;
transition: color 0.1s linear;
&:hover {
color: $gl-link-color;
}
&:focus {
text-decoration: underline;
outline: none;
color: $gl-link-color;
}
.fa {
margin-right: 3px;
......
......@@ -310,4 +310,8 @@ module ApplicationHelper
def active_when(condition)
'active' if condition
end
def show_user_callout?
cookies[:user_callout_dismissed] == 'true'
end
end
......@@ -46,6 +46,10 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG'
end
def ipython_notebook?
text? && language && language.name == 'Jupyter Notebook'
end
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
......@@ -63,6 +67,8 @@ class Blob < SimpleDelegator
end
elsif image? || svg?
'image'
elsif ipython_notebook?
'notebook'
elsif text?
'text'
else
......
......@@ -31,7 +31,7 @@ class PrometheusService < MonitoringService
def help
<<-MD.strip_heredoc
Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
and `container_memory_usage_bytes` from the configured Prometheus server.
If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html)
......@@ -74,8 +74,8 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(environment_slug)
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}
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}
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!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
{
success: true,
......
......@@ -4,7 +4,9 @@
- page_title "Projects"
- 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]
= render 'dashboard/projects_head'
......
......@@ -8,7 +8,7 @@
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
.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
= icon("chevron-up")
- 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 @@
.visible-xs-inline
= render_commit_status(commit, ref: ref)
- if commit.description?
%a.text-expander.hidden-xs.js-toggle-button ...
%button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ...
- if commit.description?
%pre.commit-row-description.js-toggle-content
......
.js-toggle-container
.commit-stat-summary
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")
with
%strong.cgreen #{diff_files.sum(&:added_lines)} additions
......
......@@ -48,9 +48,10 @@
- if @project.merge_requests_ff_only_enabled
Fast-forward merge without a merge commit
- else
= link_to "#", class: "modify-merge-commit-link js-toggle-button" do
%button.modify-merge-commit-link.js-toggle-button{ type: "button" }
= icon('edit')
Modify commit message
- unless @project.merge_requests_ff_only_enabled
.js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params,
......
......@@ -76,7 +76,7 @@
Gitea
%div
- 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')
.import_gitlab_project
- 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 @@
- selected_labels = issuable.labels
.block.labels
.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
= selected_labels.size
.title.hide-collapsed
......
......@@ -18,9 +18,9 @@
= event_action_name(event)
%strong
- 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
= 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
%strong
......
......@@ -100,8 +100,8 @@
Snippets
%div{ class: container_class }
- if @user == current_user
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
- if @user == current_user && !show_user_callout?
= render 'shared/user_callout'
.tab-content
#activity.tab-pane
.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
end
collection do
get :folder, path: 'folders/:id'
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end
end
......
......@@ -39,6 +39,7 @@ var config = {
mr_widget_ee: './merge_request_widget/widget_bundle.js',
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js',
......@@ -107,8 +108,9 @@ var config = {
'environments_folder',
'issuable',
'merge_conflicts',
'mr_widget_ee',
'notebook_viewer',
'vue_pipelines',
'mr_widget_ee',
],
minChunks: function(module, count) {
return module.resource && (/vue_shared/).test(module.resource);
......
......@@ -153,8 +153,8 @@ The queries utilized by GitLab are shown in the following table.
| 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 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 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!="POD",environment="$CI_ENVIRONMENT_SLUG"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="$CI_ENVIRONMENT_SLUG"}) * 100` |
## Monitoring CI/CD Environments
......
......@@ -19,7 +19,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
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')
end
......
......@@ -382,7 +382,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
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'
end
......
......@@ -3,7 +3,7 @@ require 'capybara/poltergeist'
require 'capybara-screenshot/spinach'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 40 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
......
......@@ -11,8 +11,8 @@ module Banzai
MergeRequest
end
def find_object(project, id)
project.merge_requests.find_by(iid: id)
def find_object(project, iid)
merge_requests_per_project[project][iid]
end
def url_for_object(mr, project)
......@@ -21,6 +21,31 @@ module Banzai
only_path: context[:only_path])
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)
extras = super
......
......@@ -60,7 +60,7 @@ module Banzai
self.class.references_in(text) do |match, username|
if username == 'all' && !skip_project_check?
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
else
match
......@@ -74,7 +74,7 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
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
# Returns all usernames referenced in the current document.
......
......@@ -125,7 +125,7 @@ module Gitlab
end
puts
puts applies_cleanly_msg(ee_branch)
puts applies_cleanly_msg(ee_branch_found)
end
def check_patch(patch_path)
......@@ -215,7 +215,7 @@ module Gitlab
end
def ee_patch_name
@ee_patch_name ||= patch_name_from_branch(ee_branch)
@ee_patch_name ||= patch_name_from_branch(ee_branch_found)
end
def ee_patch_full_path
......
......@@ -320,7 +320,7 @@ module Gitlab
def log_by_walk(sha, options)
walk_options = {
show: sha,
sort: Rugged::SORT_DATE,
sort: Rugged::SORT_NONE,
limit: options[:limit],
offset: options[:offset]
}
......@@ -387,7 +387,7 @@ module Gitlab
# a detailed list of valid arguments.
def commits_between(from, to)
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_to = sha_from_ref(to)
......@@ -465,7 +465,7 @@ module Gitlab
if actual_options[:order] == :topo
walker.sorting(Rugged::SORT_TOPO)
else
walker.sorting(Rugged::SORT_DATE)
walker.sorting(Rugged::SORT_NONE)
end
commits = []
......@@ -833,23 +833,6 @@ module Gitlab
Rugged::Commit.create(rugged, actual_options)
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 = {
"true" => true,
"false" => false,
......
......@@ -81,6 +81,39 @@ describe Projects::EnvironmentsController do
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
context 'with valid id' do
it 'responds with a status code 200' do
......
......@@ -46,7 +46,7 @@ feature 'Group', feature: true do
describe 'Mattermost team creation' do
before do
allow(Settings.mattermost).to receive_messages(enabled: mattermost_enabled)
stub_mattermost_setting(enabled: mattermost_enabled)
visit new_group_path
end
......
......@@ -296,7 +296,7 @@ feature 'Diff notes resolve', feature: true, js: true do
it 'displays next discussion even if hidden' do
page.all('.note-discussion').each do |discussion|
page.within discussion do
click_link 'Toggle discussion'
click_button 'Toggle discussion'
end
end
......@@ -477,13 +477,13 @@ feature 'Diff notes resolve', feature: true, js: true do
it 'shows resolved icon' do
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')
end
it 'does not allow user to click resolve button' do
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')
end
......
......@@ -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)
expect(textbox).not_to be_visible
click_link "Modify commit message"
click_button "Modify commit message"
expect(textbox).to be_visible
end
......
......@@ -166,6 +166,25 @@ feature 'Environment', :feature do
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
given(:project) { create(:project) }
......
......@@ -7,7 +7,7 @@ feature 'Setup Mattermost slash commands', :feature, :js do
let(:mattermost_enabled) { true }
before do
Settings.mattermost['enabled'] = mattermost_enabled
stub_mattermost_setting(enabled: mattermost_enabled)
project.team << [user, :master]
login_as(user)
visit edit_namespace_project_service_path(project.namespace, project, service)
......
......@@ -7,15 +7,27 @@ describe 'User Callouts', js: true do
before do
login_as(user)
project.team << [user, :master]
project.team << [user, :master]
end
it 'takes you to the profile preferences when the link is clicked' do
it 'takes you to the profile preferences when the link is clicked' do
visit dashboard_projects_path
click_link 'Check it out'
expect(current_path).to eq profile_preferences_path
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
it 'shows up on the user profile' do
visit user_path(user)
......@@ -31,7 +43,7 @@ describe 'User Callouts', js: true do
it 'hides the user callout when click on the dismiss icon' do
visit user_path(user)
within('.user-callout') do
find('.close-user-callout').click
find('.close').click
end
expect(page).not_to have_selector('.user-callout')
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 ListUser */
/* global ListLabel */
/* global listObj */
/* global boardsMockInterceptor */
/* global BoardService */
import Vue from 'vue';
import '~/boards/models/user';
require('~/boards/models/list');
require('~/boards/models/label');
......@@ -130,6 +132,23 @@ describe('Issue card', () => {
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', () => {
triggerEvent('mouseup');
......
......@@ -92,6 +92,20 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
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', () => {
......
......@@ -122,6 +122,14 @@ require('~/filtered_search/filtered_search_tokenizer');
expect(results.lastToken).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', () => {
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';
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);
......@@ -4,7 +4,7 @@ import UserCallout from '~/user_callout';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
describe('UserCallout', function () {
const fixtureName = 'static/user_callout.html.raw';
const fixtureName = 'dashboard/user-callout.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
......@@ -12,26 +12,22 @@ describe('UserCallout', function () {
Cookies.remove(USER_CALLOUT_COOKIE);
this.userCallout = new UserCallout();
this.closeButton = $('.close-user-callout');
this.userCalloutBtn = $('.user-callout-btn');
this.closeButton = $('.js-close-callout.close');
this.userCalloutBtn = $('.js-close-callout:not(.close)');
this.userCalloutContainer = $('.user-callout');
});
it('does not show when cookie is set not defined', () => {
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeUndefined();
expect(this.userCalloutContainer.is(':visible')).toBe(true);
});
it('shows when cookie is set to false', () => {
Cookies.set(USER_CALLOUT_COOKIE, 'false');
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeDefined();
expect(this.userCalloutContainer.is(':visible')).toBe(true);
});
it('hides when user clicks on the dismiss-icon', () => {
it('hides when user clicks on the dismiss-icon', (done) => {
this.closeButton.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
setTimeout(() => {
expect(
document.querySelector('.user-callout'),
).toBeNull();
done();
});
});
it('hides when user clicks on the "check it out" button', () => {
......@@ -39,19 +35,3 @@ describe('UserCallout', function () {
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', () => {
},
}).$mount();
component.changePage({ target: { innerText: 'Last >>' } });
component.changePage({ target: { innerText: 'Last »' } });
expect(changeChanges.one).toEqual(10);
});
......@@ -100,7 +100,7 @@ describe('Pagination component', () => {
},
}).$mount();
component.changePage({ target: { innerText: '<< First' } });
component.changePage({ target: { innerText: '« First' } });
expect(changeChanges.one).toEqual(1);
});
......
......@@ -21,6 +21,19 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
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
it_behaves_like 'a reference containing an element node'
......
......@@ -17,6 +17,19 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
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
let(:reference) { merge.to_reference }
......
......@@ -83,6 +83,14 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(doc.css('a').length).to eq 1
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
doc = reference_filter("Hey #{reference}")
link = doc.css('a').first
......
......@@ -53,6 +53,20 @@ describe Blob do
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
it 'is falsey with image extension' do
git_blob = Gitlab::Git::Blob.new(name: 'image.png')
......@@ -116,6 +130,11 @@ describe Blob do
blob = stubbed_blob
expect(blob.to_partial_path(project)).to eq 'download'
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
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
let!(:service) { described_class.new(user, params) }
before do
Settings.mattermost['enabled'] = true
stub_mattermost_setting(enabled: true)
end
it 'create the chat team with the group' do
......
......@@ -4,7 +4,7 @@ require 'capybara/poltergeist'
require 'capybara-screenshot/rspec'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 40 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
......@@ -27,6 +27,6 @@ Capybara::Screenshot.prune_strategy = :keep_last_run
RSpec.configure do |config|
config.before(:suite) do
TestEnv.warm_asset_cache
TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
end
end
module PrometheusHelpers
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
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
def prometheus_query_url(prometheus_query)
......
......@@ -21,6 +21,10 @@ module StubConfiguration
allow(Gitlab.config.incoming_email).to receive_messages(messages)
end
def stub_mattermost_setting(messages)
allow(Gitlab.config.mattermost).to receive_messages(messages)
end
private
# Modifies stubbed messages to also stub possible predicate versions
......
......@@ -167,17 +167,11 @@ module TestEnv
# Otherwise they'd be created by the first test, often timing out and
# causing a transient test failure
def warm_asset_cache
return if warm_asset_cache?
return unless defined?(Capybara)
Capybara.current_session.driver.visit '/'
end
def warm_asset_cache?
cache = Rails.root.join(*%w(tmp cache assets test))
Dir.exist?(cache) && Dir.entries(cache).length > 2
end
private
def factory_repo_path
......
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