Commit 50fd7e13 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

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

CE Upstream - Friday

Closes gitlab-ce#26595

See merge request !1553
parents adbb6830 abde7fd7
......@@ -26,6 +26,12 @@ export default {
};
},
computed: {
title() {
return 'Deploy to...';
},
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
......@@ -45,8 +51,11 @@ export default {
template: `
<div class="btn-group" role="group">
<button
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body"
data-toggle="dropdown"
:title="title"
:aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
......
......@@ -9,13 +9,21 @@ export default {
},
},
computed: {
title() {
return 'Open';
},
},
template: `
<a
class="btn external_url"
class="btn external-url has-tooltip"
data-container="body"
:href="externalUrl"
target="_blank"
rel="noopener noreferrer"
title="Environment external URL">
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
......
......@@ -11,6 +11,7 @@ import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
import MonitoringButtonComponent from './environment_monitoring';
import CommitComponent from '../../vue_shared/components/commit';
const timeagoInstance = new Timeago();
......@@ -23,6 +24,7 @@ export default {
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
'monitoring-button-component': MonitoringButtonComponent,
},
props: {
......@@ -399,6 +401,14 @@ export default {
return '';
},
monitoringUrl() {
if (this.model && this.model.metrics_path) {
return this.model.metrics_path;
}
return '';
},
/**
* Constructs folder URL based on the current location and the folder id.
*
......@@ -518,13 +528,16 @@ export default {
<external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
<stop-component v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
:service="service"/>
<monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
:monitoring-url="monitoringUrl"/>
<terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
<stop-component v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
:service="service"/>
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
......
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
export default {
props: {
monitoringUrl: {
type: String,
default: '',
required: true,
},
},
computed: {
title() {
return 'Monitoring';
},
},
template: `
<a
class="btn monitoring-url has-tooltip"
data-container="body"
:href="monitoringUrl"
target="_blank"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-area-chart" aria-hidden="true"></i>
</a>
`,
};
......@@ -26,6 +26,12 @@ export default {
};
},
computed: {
title() {
return 'Stop';
},
},
methods: {
onClick() {
if (confirm('Are you sure you want to stop this environment?')) {
......@@ -46,10 +52,12 @@ export default {
template: `
<button type="button"
class="btn stop-env-link"
class="btn stop-env-link has-tooltip"
data-container="body"
@click="onClick"
:disabled="isLoading"
title="Stop Environment">
:title="title"
:aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
......
......@@ -14,12 +14,22 @@ export default {
},
data() {
return { terminalIconSvg };
return {
terminalIconSvg,
};
},
computed: {
title() {
return 'Terminal';
},
},
template: `
<a class="btn terminal-button"
title="Open web terminal"
<a class="btn terminal-button has-tooltip"
data-container="body"
:title="title"
:aria-label="title"
:href="terminalPath">
${terminalIconSvg}
</a>
......
......@@ -6,23 +6,60 @@ var slice = [].slice;
window.GroupsSelect = (function() {
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
const self = _this;
return function(i, select) {
var all_available, skip_groups;
all_available = $(select).data('all-available');
skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({
const $select = $(select);
all_available = $select.data('all-available');
skip_groups = $select.data('skip-groups') || [];
$select.select2({
placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
var options = { all_available: all_available, skip_groups: skip_groups };
return Api.groups(query.term, options, function(groups) {
var data;
data = {
results: groups
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport: function (params) {
$.ajax(params).then((data, status, xhr) => {
const results = data || [];
const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
pagination: {
more,
},
};
}).then(params.success).fail(params.error);
},
data: function (search, page) {
return {
search,
page,
per_page: GroupsSelect.PER_PAGE,
all_available,
skip_groups,
};
},
results: function (data, page) {
if (data.length) return { results: [] };
const results = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
return {
results,
page,
more,
};
return query.callback(data);
});
},
},
initSelection: function(element, callback) {
var id;
......@@ -34,19 +71,23 @@ window.GroupsSelect = (function() {
formatResult: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return _this.formatResult.apply(_this, args);
return self.formatResult.apply(self, args);
},
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return _this.formatSelection.apply(_this, args);
return self.formatSelection.apply(self, args);
},
dropdownCssClass: "ajax-groups-dropdown",
dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
self.dropdown = document.querySelector('.select2-infinite .select2-results');
$select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
......@@ -65,5 +106,12 @@ window.GroupsSelect = (function() {
return group.full_name;
};
GroupsSelect.prototype.forceOverflow = function (e) {
const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight;
this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`;
};
GroupsSelect.PER_PAGE = 20;
return GroupsSelect;
})();
......@@ -231,6 +231,22 @@
return upperCaseHeaders;
};
/**
this will take in the getAllResponseHeaders result and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
w.gl.utils.normalizeCRLFHeaders = (headers) => {
const headersObject = {};
const headersArray = headers.split('\n');
headersArray.forEach((header) => {
const keyValue = header.split(': ');
headersObject[keyValue[0]] = keyValue[1];
});
return w.gl.utils.normalizeHeaders(headersObject);
};
/**
* Parses pagination object string values into numbers.
*
......
......@@ -209,7 +209,7 @@ import Cookies from 'js-cookie';
};
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
......
......@@ -83,6 +83,7 @@ export default {
:class="buttonClass"
:title="title"
:aria-label="title"
data-container="body"
data-placement="top"
:disabled="isLoading">
<i :class="iconClass" aria-hidden="true"/>
......
......@@ -26,7 +26,7 @@
.table.ci-table {
.environments-actions {
min-width: 200px;
min-width: 300px;
}
.environments-commit,
......@@ -358,3 +358,12 @@
stroke: $black;
stroke-width: 1;
}
.environments-actions {
.external-url,
.monitoring-url,
.terminal-button,
.stop-env-link {
width: 38px;
}
}
class Admin::BackgroundJobsController < Admin::ApplicationController
def show
ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
@sidekiq_processes = ps_output.split("\n").grep(/sidekiq/)
@sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/)
@concurrency = Sidekiq.options[:concurrency]
end
end
......@@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController
namespace.add_owner(current_user)
namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
Namespace.find_by_path_or_name(name)
Namespace.find_by_full_path(name)
end
end
end
......@@ -255,7 +255,7 @@ module IssuablesHelper
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template])
params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] }
end
def issuable_todo_button_data(issuable, todo, is_collapsed)
......
......@@ -3,9 +3,9 @@ module SidekiqHelper
(?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+
(?<command>sidekiq.*\])
(?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+
(?<start>.+?)\s+
(?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
\z/x
def parse_sidekiq_ps(line)
......
......@@ -47,7 +47,7 @@ class Blob < SimpleDelegator
end
def ipython_notebook?
text? && language && language.name == 'Jupyter Notebook'
text? && language&.name == 'Jupyter Notebook'
end
def size_within_svg_limits?
......
......@@ -62,7 +62,7 @@ class JiraService < IssueTrackerService
def help
"You need to configure JIRA before enabling this service. For more details
read the
[JIRA service documentation](#{help_page_url('project_services/jira')})."
[JIRA service documentation](#{help_page_url('user/project/integrations/jira')})."
end
def title
......
......@@ -171,6 +171,9 @@ class ProjectTeam
# Lookup only the IDs we need
user_ids = user_ids - access.keys
return access if user_ids.empty?
users_access = project.project_authorizations.
where(user: user_ids).
group(:user_id).
......
......@@ -660,8 +660,10 @@ class User < ActiveRecord::Base
end
def fork_of(project)
links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects)
links = ForkedProjectLink.where(
forked_from_project_id: project,
forked_to_project_id: personal_projects.unscope(:order)
)
if links.any?
links.first.forked_to_project
else
......
......@@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :stop_action?
expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
metrics_namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment)
end
expose :environment_path do |environment|
namespace_project_environment_path(
environment.project.namespace,
......
......@@ -19,7 +19,7 @@ module Issues
if issue.previous_changes.include?('title') ||
issue.previous_changes.include?('description')
todo_service.update_issue(issue, current_user)
todo_service.update_issue(issue, current_user, old_mentioned_users)
end
if issue.previous_changes.include?('milestone_id')
......
......@@ -38,7 +38,7 @@ module MergeRequests
if merge_request.previous_changes.include?('title') ||
merge_request.previous_changes.include?('description')
todo_service.update_merge_request(merge_request, current_user)
todo_service.update_merge_request(merge_request, current_user, old_mentioned_users)
end
if merge_request.previous_changes.include?('target_branch')
......
......@@ -3,11 +3,13 @@ module Notes
def execute(note)
return note unless note.editable?
old_mentioned_users = note.mentioned_users.to_a
note.update_attributes(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
if note.previous_changes.include?('note')
TodoService.new.update_note(note, current_user)
TodoService.new.update_note(note, current_user, old_mentioned_users)
end
note
......
......@@ -19,8 +19,8 @@ class TodoService
#
# * mark all pending todos related to the issue for the current user as done
#
def update_issue(issue, current_user)
update_issuable(issue, current_user)
def update_issue(issue, current_user, skip_users = [])
update_issuable(issue, current_user, skip_users)
end
# When close an issue we should:
......@@ -60,8 +60,8 @@ class TodoService
#
# * create a todo for each mentioned user on merge request
#
def update_merge_request(merge_request, current_user)
update_issuable(merge_request, current_user)
def update_merge_request(merge_request, current_user, skip_users = [])
update_issuable(merge_request, current_user, skip_users)
end
# When close a merge request we should:
......@@ -154,8 +154,8 @@ class TodoService
# * mark all pending todos related to the noteable for the current user as done
# * create a todo for each new user mentioned on note
#
def update_note(note, current_user)
handle_note(note, current_user)
def update_note(note, current_user, skip_users = [])
handle_note(note, current_user, skip_users)
end
# When an emoji is awarded we should:
......@@ -236,11 +236,11 @@ class TodoService
create_mention_todos(issuable.project, issuable, author)
end
def update_issuable(issuable, author)
def update_issuable(issuable, author, skip_users = [])
# Skip toggling a task list item in a description
return if toggling_tasks?(issuable)
create_mention_todos(issuable.project, issuable, author)
create_mention_todos(issuable.project, issuable, author, nil, skip_users)
end
def destroy_issuable(issuable, user)
......@@ -252,7 +252,7 @@ class TodoService
issuable.tasks? && issuable.updated_tasks.any?
end
def handle_note(note, author)
def handle_note(note, author, skip_users = [])
# Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet?
......@@ -260,7 +260,7 @@ class TodoService
target = note.noteable
mark_pending_todos_as_done(target, author)
create_mention_todos(project, target, author, note)
create_mention_todos(project, target, author, note, skip_users)
end
def create_assignment_todo(issuable, author)
......@@ -270,14 +270,14 @@ class TodoService
end
end
def create_mention_todos(project, target, author, note = nil)
def create_mention_todos(project, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users
directly_addressed_users = filter_directly_addressed_users(project, note || target, author)
directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
create_todos(directly_addressed_users, attributes)
# Create Todos for mentioned users
mentioned_users = filter_mentioned_users(project, note || target, author)
mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
end
......@@ -325,13 +325,13 @@ class TodoService
reject_users_without_access(users, project, target).uniq
end
def filter_mentioned_users(project, target, author)
mentioned_users = target.mentioned_users(author)
def filter_mentioned_users(project, target, author, skip_users = [])
mentioned_users = target.mentioned_users(author) - skip_users
filter_todo_users(mentioned_users, project, target)
end
def filter_directly_addressed_users(project, target, author)
directly_addressed_users = target.directly_addressed_users(author)
def filter_directly_addressed_users(project, target, author, skip_users = [])
directly_addressed_users = target.directly_addressed_users(author) - skip_users
filter_todo_users(directly_addressed_users, project, target)
end
......
......@@ -94,7 +94,7 @@ module Users
def build_user_params
if current_user&.is_admin?
user_params = params.slice(*admin_create_params)
user_params[:created_by_id] = current_user.id
user_params[:created_by_id] = current_user&.id
if params[:reset_password]
user_params.merge!(force_random_password: true, password_expires_at: nil)
......
......@@ -34,7 +34,7 @@
Joined #{time_ago_with_tooltip(member.created_at)}
- if member.expires?
·
%span{ class: ('text-warning' if member.expires_soon?) }
%span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) }
Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
......@@ -51,7 +51,7 @@
.controls.member-controls
= render 'shared/members/ee/ldap_tag', can_override: can_override_member, visible: false
- if show_controls && member.source == current_resource
- if user != current_user
- if user != current_user && (can_admin_member || can_override_member)
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
......
---
title: Hide form inputs for group member without editing rights
merge_request: 7816
author:
---
title: Fix linking to new issue with selected template via url parameter
merge_request:
author:
---
title: Create todos only for new mentions
merge_request:
author:
---
title: Add metrics button to environments overview page
merge_request: 10234
author:
---
title: Fix link to Jira service documentation
merge_request:
author:
---
title: Allow users to import GitHub projects to subgroups
merge_request:
author:
---
title: Handle parsing OpenBSD ps output properly to display sidekiq infos on admin->monitoring->background
merge_request: 10303
author: Sebastian Reitenbach
---
title: Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery
merge_request:
author: mhasbini
......@@ -12,10 +12,12 @@ else
user_args[:password] = ENV['GITLAB_ROOT_PASSWORD']
end
user = User.new(user_args)
user.skip_confirmation!
# Only admins can create other admin users in Users::CreateService so to solve
# the chicken-and-egg problem, we pass a non-persisted admin user to the service.
transient_admin = User.new(admin: true)
user = Users::CreateService.new(transient_admin, user_args.merge!(skip_confirmation: true)).execute
if user.save
if user.persisted?
puts "Administrator account created:".color(:green)
puts
puts "login: root".color(:green)
......
## Using Dpl as deployment tool
Dpl (dee-pee-ell) is a deploy tool made for continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI.
# Using Dpl as deployment tool
**We recommend to use Dpl, if you're deploying to any of these of these services: https://github.com/travis-ci/dpl#supported-providers**.
[Dpl](https://github.com/travis-ci/dpl) (dee-pee-ell) is a deploy tool made for
continuous deployment that's developed and used by Travis CI, but can also be
used with GitLab CI.
### Requirements
To use Dpl you need at least Ruby 1.8.7 with ability to install gems.
>**Note:**
We recommend to use Dpl if you're deploying to any of these of these services:
https://github.com/travis-ci/dpl#supported-providers.
## Requirements
To use Dpl you need at least Ruby 1.9.3 with ability to install gems.
## Basic usage
Dpl can be installed on any machine with:
### Basic usage
The Dpl can be installed on any machine with:
```
gem install dpl
```
This allows you to test all commands from your shell, rather than having to test it on a CI server.
This allows you to test all commands from your local terminal, rather than
having to test it on a CI server.
If you don't have Ruby installed you can do it on Debian-compatible Linux with:
```
apt-get update
apt-get install ruby-dev
......@@ -26,9 +36,10 @@ To use it simply define provider and any additional parameters required by the p
For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`.
There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku
```
```yaml
staging:
type: deploy
stage: deploy
script:
- gem install dpl
- dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
```
......@@ -37,14 +48,17 @@ In the above example we use Dpl to deploy `my-app-staging` to Heroku server with
To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers).
### Using Dpl with Docker
## Using Dpl with Docker
When you use GitLab Runner you most likely configured it to use your server's shell commands.
This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner).
It also means that most probably in your Docker container you don't have the Ruby runtime installed.
You will have to install it:
```
```yaml
staging:
type: deploy
stage: deploy
script:
- apt-get update -yq
- apt-get install -y ruby-dev
- gem install dpl
......@@ -53,24 +67,31 @@ staging:
- master
```
The first line `apt-get update -yq` updates the list of available packages, where second `apt-get install -y ruby-dev` install `Ruby` runtime on system.
The first line `apt-get update -yq` updates the list of available packages,
where second `apt-get install -y ruby-dev` installs the Ruby runtime on system.
The above example is valid for all Debian-compatible systems.
### Usage in staging and production
It's pretty common in developer workflow to have staging (development) and production environment.
If we consider above example: we would like to deploy `master` branch to `staging` and `all tags` to `production` environment.
## Usage in staging and production
It's pretty common in the development workflow to have staging (development) and
production environments
Let's consider the following example: we would like to deploy the `master`
branch to `staging` and all tags to the `production` environment.
The final `.gitlab-ci.yml` for that setup would look like this:
```
```yaml
staging:
type: deploy
stage: deploy
script:
- gem install dpl
- dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
only:
- master
production:
type: deploy
stage: deploy
script:
- gem install dpl
- dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY
only:
......@@ -78,21 +99,28 @@ production:
```
We created two deploy jobs that are executed on different events:
1. `staging` is executed for all commits that were pushed to `master` branch,
2. `production` is executed for all pushed tags.
We also use two secure variables:
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
### Storing API keys
In GitLab CI 7.12 a new feature was introduced: Secure Variables.
Secure Variables can added by going to `Project > Variables > Add Variable`.
**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.**
The variables that are defined in the project settings are sent along with the build script to the runner.
The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml.
It is also important that secret's value is hidden in the job log.
## Storing API keys
Secure Variables can added by going to your project's
**Settings ➔ CI/CD Pipelines ➔ Secret variables**. The variables that are defined
in the project settings are sent along with the build script to the Runner.
The secure variables are stored out of the repository. Never store secrets in
your project's `.gitlab-ci.yml`. It is also important that the secret's value
is hidden in the job log.
You access added variable by prefixing it's name with `$` (on non-Windows runners)
or `%` (for Windows Batch runners):
You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners):
1. `$SECRET_VARIABLE` - use it for non-Windows runners
2. `%SECRET_VARIABLE%` - use it for Windows Batch runners
Read more about the [CI variables](../../variables/README.md).
......@@ -17,8 +17,7 @@ polling rate.
A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling.
1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it.
Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
1. Poll on active tabs only. Please use [Visibility](https://github.com/ai/visibilityjs).
1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status
......
......@@ -141,6 +141,7 @@ with GitLab 8.12.
With the new job permissions model, there is now an easy way to access all
dependent source code in a project. That way, we can:
1. Access a project's dependent repositories
1. Access a project's [Git submodules][gitsub]
1. Access private container images
1. Access project's and submodule LFS objects
......@@ -177,6 +178,22 @@ As a user:
access to. As an Administrator, you can verify that by impersonating the user
and retry the failing job in order to verify that everything is correct.
### Dependent repositories
The [Job environment variable][jobenv] `CI_JOB_TOKEN` can be used to
authenticate any clones of dependent repositories. For example:
```
git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/myuser/mydependentrepo
```
It can also be used for system-wide authentication
(only do this in a docker container, it will overwrite ~/.netrc):
```
echo -e "machine gitlab.com\nlogin gitlab-ci-token\npassword ${CI_JOB_TOKEN}" > ~/.netrc
```
### Git submodules
To properly configure submodules with GitLab CI, read the
......@@ -221,3 +238,4 @@ test:
[triggers]: ../../ci/triggers/README.md
[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
[workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
[jobenv]: ../../ci/variables/README.md#predefined-variables-environment-variables
require 'spinach/capybara'
require 'capybara/poltergeist'
require 'capybara-screenshot/spinach'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 40 : 10
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
......@@ -25,5 +24,8 @@ Capybara.ignore_hidden_elements = false
Capybara::Screenshot.prune_strategy = :keep_last_run
Spinach.hooks.before_run do
TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
require 'spinach/capybara'
require 'capybara/rails'
TestEnv.eager_load_driver_server
end
......@@ -36,3 +36,19 @@ Spinach.hooks.before_run do
include FactoryGirl::Syntax::Methods
end
module StdoutReporterWithScenarioLocation
# Override the standard reporter to show filename and line number next to each
# scenario for easy, focused re-runs
def before_scenario_run(scenario, step_definitions = nil)
@max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any?
name = scenario.name
# This number has no significance, it's just to line things up
max_length = @max_step_name_length + 19
out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \
" # #{scenario.feature.filename}:#{scenario.line}"
end
end
Spinach::Reporter::Stdout.prepend(StdoutReporterWithScenarioLocation)
......@@ -13,6 +13,13 @@ describe 'Issues', feature: true do
user2 = create(:user)
project.team << [[@user, user2], :developer]
project.repository.create_file(
@user,
'.gitlab/issue_templates/bug.md',
'this is a test "bug" template',
message: 'added issue template',
branch_name: 'master')
end
describe 'Edit issue' do
......@@ -632,6 +639,16 @@ describe 'Issues', feature: true do
expect(page.find_field("issue_description").value).to match /\n\n$/
end
end
context 'form filled by URL parameters' do
before do
visit new_namespace_project_issue_path(project.namespace, project, issuable_template: 'bug')
end
it 'fills in template' do
expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug')
end
end
end
describe 'new issue by email' do
......
require 'spec_helper'
feature 'Project group links', feature: true, js: true do
feature 'Project group links', :feature, :js do
include Select2Helper
let(:master) { create(:user) }
......@@ -51,4 +51,24 @@ feature 'Project group links', feature: true, js: true do
end
end
end
describe 'the groups dropdown' do
before do
group_two = create(:group)
group.add_owner(master)
group_two.add_owner(master)
visit namespace_project_settings_members_path(project.namespace, project)
execute_script 'GroupsSelect.PER_PAGE = 1;'
open_select2 '#link_group_id'
end
it 'should infinitely scroll' do
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
end
end
end
......@@ -53,6 +53,14 @@ describe SidekiqHelper do
expect(parts).to eq(['17725', '1.0', '12.1', 'Ssl', '19:20:15', 'sidekiq 4.2.1 gitlab-rails [0 of 25 busy]'])
end
it 'parses OpenBSD output' do
# OpenBSD 6.1
line = '49258 0.5 2.3 R/0 Fri10PM ruby23: sidekiq 4.2.7 gitlab [0 of 25 busy] (ruby23)'
parts = helper.parse_sidekiq_ps(line)
expect(parts).to eq(['49258', '0.5', '2.3', 'R/0', 'Fri10PM', 'ruby23: sidekiq 4.2.7 gitlab [0 of 25 busy] (ruby23)'])
end
it 'does fail gracefully on line not matching the format' do
line = '55137 10.0 2.1 S+ 2:30pm something'
parts = helper.parse_sidekiq_ps(line)
......
......@@ -32,7 +32,12 @@ describe('Actions Component', () => {
}).$mount();
});
it('should render a dropdown with the provided actions', () => {
it('should render a dropdown button with icon and title attribute', () => {
expect(component.$el.querySelector('.fa-caret-down')).toBeDefined();
expect(component.$el.querySelector('.dropdown-new').getAttribute('title')).toEqual('Deploy to...');
});
it('should render a dropdown with the provided list of actions', () => {
expect(
component.$el.querySelectorAll('.dropdown-menu li').length,
).toEqual(actionsMock.length);
......
import Vue from 'vue';
import monitoringComp from '~/environments/components/environment_monitoring';
describe('Monitoring Component', () => {
let MonitoringComponent;
beforeEach(() => {
MonitoringComponent = Vue.extend(monitoringComp);
});
it('should render a link to environment monitoring page', () => {
const monitoringUrl = 'https://gitlab.com';
const component = new MonitoringComponent({
propsData: {
monitoringUrl,
},
}).$mount();
expect(component.$el.getAttribute('href')).toEqual(monitoringUrl);
expect(component.$el.querySelector('.fa-area-chart')).toBeDefined();
expect(component.$el.getAttribute('title')).toEqual('Monitoring');
});
});
......@@ -24,7 +24,7 @@ describe('Stop Component', () => {
it('should render a button to stop the environment', () => {
expect(component.$el.tagName).toEqual('BUTTON');
expect(component.$el.getAttribute('title')).toEqual('Stop Environment');
expect(component.$el.getAttribute('title')).toEqual('Stop');
});
it('should call the service when an action is clicked', () => {
......
......@@ -18,7 +18,7 @@ describe('Stop Component', () => {
it('should render a link to open a web terminal with the provided path', () => {
expect(component.$el.tagName).toEqual('A');
expect(component.$el.getAttribute('title')).toEqual('Open web terminal');
expect(component.$el.getAttribute('title')).toEqual('Terminal');
expect(component.$el.getAttribute('href')).toEqual(terminalPath);
});
});
......@@ -108,6 +108,37 @@ require('~/lib/utils/common_utils');
});
});
describe('gl.utils.normalizeCRLFHeaders', () => {
beforeEach(function () {
this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
spyOn(String.prototype, 'split').and.callThrough();
spyOn(gl.utils, 'normalizeHeaders').and.callThrough();
this.normalizeCRLFHeaders = gl.utils.normalizeCRLFHeaders(this.CLRFHeaders);
});
it('should split by newline', function () {
expect(String.prototype.split).toHaveBeenCalledWith('\n');
});
it('should split by colon+space for each header', function () {
expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3);
});
it('should call gl.utils.normalizeHeaders with a parsed headers object', function () {
expect(gl.utils.normalizeHeaders).toHaveBeenCalledWith(jasmine.any(Object));
});
it('should return a normalized headers object', function () {
expect(this.normalizeCRLFHeaders).toEqual({
'A-HEADER': 'a-value',
'ANOTHER-HEADER': 'ANOTHER-VALUE',
'LAST-HEADER': 'last-VALUE',
});
});
});
describe('gl.utils.parseIntPagination', () => {
it('should parse to integers all string values and return pagination object', () => {
const pagination = {
......
......@@ -16,6 +16,26 @@ describe EnvironmentEntity do
expect(subject).to include(:id, :name, :state, :environment_path)
end
context 'metrics disabled' do
before do
allow(environment).to receive(:has_metrics?).and_return(false)
end
it "doesn't expose metrics path" do
expect(subject).not_to include(:metrics_path)
end
end
context 'metrics enabled' do
before do
allow(environment).to receive(:has_metrics?).and_return(true)
end
it 'exposes metrics path' do
expect(subject).to include(:metrics_path)
end
end
context 'with deployment service ready' do
before do
allow(environment).to receive(:deployment_service_ready?).and_return(true)
......
......@@ -13,6 +13,7 @@ describe Issues::UpdateService, services: true do
let(:issue) do
create(:issue, title: 'Old title',
description: "for #{user2.to_reference}",
assignee_id: user3.id,
project: project)
end
......@@ -182,16 +183,24 @@ describe Issues::UpdateService, services: true do
it 'marks pending todos as done' do
expect(todo.reload.done?).to eq true
end
it 'does not create any new todos' do
expect(Todo.count).to eq(1)
end
end
context 'when the description change' do
before do
update_issue(description: 'Also please fix')
update_issue(description: "Also please fix #{user2.to_reference} #{user3.to_reference}")
end
it 'marks todos as done' do
expect(todo.reload.done?).to eq true
end
it 'creates only 1 new todo' do
expect(Todo.count).to eq(2)
end
end
context 'when is reassigned' do
......
......@@ -12,6 +12,7 @@ describe MergeRequests::UpdateService, services: true do
let(:merge_request) do
create(:merge_request, :simple, title: 'Old title',
description: "FYI #{user2.to_reference}",
assignee_id: user3.id,
source_project: project)
end
......@@ -225,16 +226,24 @@ describe MergeRequests::UpdateService, services: true do
it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done
end
it 'does not create any new todos' do
expect(Todo.count).to eq(1)
end
end
context 'when the description change' do
before do
update_merge_request({ description: 'Also please fix' })
update_merge_request({ description: "Also please fix #{user2.to_reference} #{user3.to_reference}" })
end
it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done
end
it 'creates only 1 new todo' do
expect(Todo.count).to eq(2)
end
end
context 'when is reassigned' do
......
......@@ -4,12 +4,14 @@ describe Notes::UpdateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:note) { create(:note, project: project, noteable: issue, author: user, note: 'Old note') }
let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") }
before do
project.team << [user, :master]
project.team << [user2, :developer]
project.team << [user3, :developer]
end
describe '#execute' do
......@@ -23,22 +25,30 @@ describe Notes::UpdateService, services: true do
context 'when the note change' do
before do
update_note({ note: 'New note' })
update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
end
it 'marks todos as done' do
expect(todo.reload).to be_done
end
it 'creates only 1 new todo' do
expect(Todo.count).to eq(2)
end
end
context 'when the note does not change' do
before do
update_note({ note: 'Old note' })
update_note({ note: "Old note #{user2.to_reference}" })
end
it 'keep todos' do
expect(todo.reload).to be_pending
end
it 'does not create any new todos' do
expect(Todo.count).to eq(1)
end
end
end
end
......
This diff is collapsed.
......@@ -61,6 +61,23 @@ describe Users::CreateService, services: true do
)
end
context 'when the current_user is not persisted' do
let(:admin_user) { build(:admin) }
it 'persists the given attributes and sets created_by_id to nil' do
user = service.execute
user.reload
expect(user).to have_attributes(
name: params[:name],
username: params[:username],
email: params[:email],
password: params[:password],
created_by_id: nil
)
end
end
it 'user is not confirmed if skip_confirmation param is not present' do
expect(service.execute).not_to be_confirmed
end
......
# rubocop:disable Style/GlobalVars
require 'capybara/rails'
require 'capybara/rspec'
require 'capybara/poltergeist'
require 'capybara-screenshot/rspec'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 40 : 10
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
......@@ -26,7 +27,10 @@ Capybara.ignore_hidden_elements = true
Capybara::Screenshot.prune_strategy = :keep_last_run
RSpec.configure do |config|
config.before(:suite) do
TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
config.before(:context, :js) do
next if $capybara_server_already_started
TestEnv.eager_load_driver_server
$capybara_server_already_started = true
end
end
......@@ -228,5 +228,19 @@ shared_examples 'a GitHub-ish import controller: POST create' do
post :create, { new_name: test_name, format: :js }
end
end
context 'user has chosen a nested namespace and name for the project' do
let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider).
and_return(double(execute: true))
post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js }
end
end
end
end
......@@ -22,4 +22,12 @@ module Select2Helper
execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');")
end
end
def open_select2(selector)
execute_script("$('#{selector}').select2('open');")
end
def scroll_select2_to_bottom(selector)
evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');"
end
end
......@@ -171,10 +171,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
def eager_load_driver_server
return unless defined?(Capybara)
Capybara.current_session.driver.visit '/'
puts "Starting the Capybara driver server..."
Capybara.current_session.visit '/'
end
private
......
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