Commit 6d769a8d authored by Robert Speicher's avatar Robert Speicher

Merge branch 'nt/ce-to-ee-thursday' into 'master'

CE upstream: Thursday

See merge request !1497
parents e7098ade b2687717
...@@ -318,6 +318,7 @@ migration paths: ...@@ -318,6 +318,7 @@ migration paths:
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3 - bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- bundle exec rake db:drop db:create db:schema:load db:seed_fu - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_COMMIT_SHA - git checkout $CI_COMMIT_SHA
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- source scripts/prepare_build.sh - source scripts/prepare_build.sh
- bundle exec rake db:migrate - bundle exec rake db:migrate
......
...@@ -82,7 +82,7 @@ If a contributor is no longer actively working on a submitted merge request ...@@ -82,7 +82,7 @@ If a contributor is no longer actively working on a submitted merge request
we can decide that the merge request will be finished by one of our we can decide that the merge request will be finished by one of our
[Merge request coaches][team] or close the merge request. We make this decision [Merge request coaches][team] or close the merge request. We make this decision
based on how important the change is for our product vision. If a Merge request based on how important the change is for our product vision. If a Merge request
coach is going to finish the merge request we assign the coach is going to finish the merge request we assign the
~"coach will finish" label. ~"coach will finish" label.
## Helping others ## Helping others
...@@ -479,8 +479,7 @@ merge request: ...@@ -479,8 +479,7 @@ merge request:
1. [Rails](https://github.com/bbatsov/rails-style-guide) 1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Newlines styleguide][newlines-styleguide] 1. [Newlines styleguide][newlines-styleguide]
1. [Testing](doc/development/testing.md) 1. [Testing](doc/development/testing.md)
1. [JavaScript (ES6)](https://github.com/airbnb/javascript) 1. [JavaScript styleguide][js-styleguide]
1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/es5-deprecated/es5)
1. [SCSS styleguide][scss-styleguide] 1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab 1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security contributors to enhance security
...@@ -549,7 +548,8 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor ...@@ -549,7 +548,8 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[changelog]: doc/development/changelog.md "Generate a changelog entry" [changelog]: doc/development/changelog.md "Generate a changelog entry"
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" [doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide"
[scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/ [UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md [license-finder-doc]: doc/development/licensing.md
......
...@@ -338,6 +338,7 @@ group :test do ...@@ -338,6 +338,7 @@ group :test do
gem 'test_after_commit', '~> 1.1' gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0' gem 'timecop', '~> 0.8.0'
gem 'concurrent-ruby', '~> 1.0.5'
end end
gem 'octokit', '~> 4.6.2' gem 'octokit', '~> 4.6.2'
......
...@@ -136,7 +136,7 @@ GEM ...@@ -136,7 +136,7 @@ GEM
execjs execjs
coffee-script-source (1.10.0) coffee-script-source (1.10.0)
colorize (0.7.7) colorize (0.7.7)
concurrent-ruby (1.0.4) concurrent-ruby (1.0.5)
connection_pool (2.2.1) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
...@@ -905,6 +905,7 @@ DEPENDENCIES ...@@ -905,6 +905,7 @@ DEPENDENCIES
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0) coffee-rails (~> 4.1.0)
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0) connection_pool (~> 2.0)
creole (~> 0.5.0) creole (~> 0.5.0)
d3_rails (~> 3.5.0) d3_rails (~> 3.5.0)
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
$('body').on('click', '.js-toggle-button', function(e) { $('body').on('click', '.js-toggle-button', function(e) {
toggleContainer($(this).closest('.js-toggle-container')); toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.target.tagName.toLowerCase(); const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') { if (targetTag === 'a' || targetTag === 'button') {
e.preventDefault(); e.preventDefault();
} }
......
...@@ -48,7 +48,7 @@ import boardCard from './board_card'; ...@@ -48,7 +48,7 @@ import boardCard from './board_card';
this.list.getIssues(false); this.list.getIssues(false);
} }
if (this.scrollHeight() > this.listHeight()) { if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true; this.showCount = true;
} else { } else {
this.showCount = false; this.showCount = false;
......
...@@ -394,7 +394,7 @@ import FilteredSearchContainer from './container'; ...@@ -394,7 +394,7 @@ import FilteredSearchContainer from './container';
paths.push(`search=${sanitized}`); paths.push(`search=${sanitized}`);
} }
const parameterizedUrl = `?scope=all&utf8=&${paths.join('&')}`; const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
if (this.updateObject) { if (this.updateObject) {
this.updateObject(parameterizedUrl); this.updateObject(parameterizedUrl);
......
import httpStatusCodes from './http_status';
/**
* Polling utility for handling realtime updates.
* Service for vue resouce and method need to be provided as props
*
* @example
* new poll({
* resource: resource,
* method: 'name',
* data: {page: 1, scope: 'all'},
* successCallback: () => {},
* errorCallback: () => {},
* }).makeRequest();
*
* this.service = new BoardsService(endpoint);
* new poll({
* resource: this.service,
* method: 'get',
* data: {page: 1, scope: 'all'},
* successCallback: () => {},
* errorCallback: () => {},
* }).makeRequest();
*
*
* 1. Checks for response and headers before start polling
* 2. Interval is provided by `Poll-Interval` header.
* 3. If `Poll-Interval` is -1, we stop polling
* 4. If HTTP response is 200, we poll.
* 5. If HTTP response is different from 200, we stop polling.
*
*/
export default class Poll {
constructor(options = {}) {
this.options = options;
this.options.data = options.data || {};
this.intervalHeader = 'POLL-INTERVAL';
}
checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers);
const pollInterval = headers[this.intervalHeader];
if (pollInterval > 0 && response.status === httpStatusCodes.OK) {
this.options.successCallback(response);
setTimeout(() => {
this.makeRequest();
}, pollInterval);
} else {
this.options.successCallback(response);
}
}
makeRequest() {
const { resource, method, data, errorCallback } = this.options;
return resource[method](data)
.then(response => this.checkConditions(response))
.catch(error => errorCallback(error));
}
}
...@@ -33,6 +33,10 @@ ...@@ -33,6 +33,10 @@
}; };
Shortcuts.prototype.toggleMarkdownPreview = function(e) { Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
if ($(e.target).hasClass('js-note-text')) {
$('.js-md-preview-button').focus();
}
return $(document).triggerHandler('markdown-preview:toggle', [e]); return $(document).triggerHandler('markdown-preview:toggle', [e]);
}; };
......
...@@ -84,7 +84,6 @@ $warning-message-border: #f0e2bb; ...@@ -84,7 +84,6 @@ $warning-message-border: #f0e2bb;
*/ */
$border-color: #e5e5e5; $border-color: #e5e5e5;
$focus-border-color: #3aabf0; $focus-border-color: #3aabf0;
$sidebar-collapsed-icon-color: #999;
$well-expand-item: #e8f2f7; $well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2; $well-inner-border: #eef0f2;
$well-light-border: #f1f1f1; $well-light-border: #f1f1f1;
......
...@@ -498,7 +498,7 @@ ...@@ -498,7 +498,7 @@
svg { svg {
width: 16px; width: 16px;
height: 16px; height: 16px;
fill: $sidebar-collapsed-icon-color; fill: $issuable-sidebar-color;
} }
&:hover svg { &:hover svg {
......
...@@ -46,6 +46,10 @@ ul.related-merge-requests > li { ...@@ -46,6 +46,10 @@ ul.related-merge-requests > li {
.merge-request-id { .merge-request-id {
flex-shrink: 0; flex-shrink: 0;
} }
.merge-request-info {
margin-left: 5px;
}
} }
.merge-requests-title, .merge-requests-title,
...@@ -58,10 +62,6 @@ ul.related-merge-requests > li { ...@@ -58,10 +62,6 @@ ul.related-merge-requests > li {
display: inline-block; display: inline-block;
} }
.merge-request-info {
margin-left: 5px;
}
.merge-request-status { .merge-request-status {
font-size: 13px; font-size: 13px;
padding: 0 5px; padding: 0 5px;
......
...@@ -64,8 +64,11 @@ class ApplicationController < ActionController::Base ...@@ -64,8 +64,11 @@ class ApplicationController < ActionController::Base
# This filter handles both private tokens and personal access tokens # This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token! def authenticate_user_from_private_token!
token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
return unless token.present?
user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
if user && can?(user, :log_in) if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not # Notice we are passing store false, so the user is not
......
...@@ -6,7 +6,7 @@ class ChatNotificationService < Service ...@@ -6,7 +6,7 @@ class ChatNotificationService < Service
default_value_for :category, 'chat' default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_pipelines boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :webhook, presence: true, url: true, if: :activated? validates :webhook, presence: true, url: true, if: :activated?
...@@ -17,6 +17,7 @@ class ChatNotificationService < Service ...@@ -17,6 +17,7 @@ class ChatNotificationService < Service
if properties.nil? if properties.nil?
self.properties = {} self.properties = {}
self.notify_only_broken_pipelines = true self.notify_only_broken_pipelines = true
self.notify_only_default_branch = true
end end
end end
...@@ -29,6 +30,19 @@ class ChatNotificationService < Service ...@@ -29,6 +30,19 @@ class ChatNotificationService < Service
pipeline wiki_page] pipeline wiki_page]
end end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' },
]
end
def execute(data) def execute(data)
return unless supported_events.include?(data[:object_kind]) return unless supported_events.include?(data[:object_kind])
return unless webhook.present? return unless webhook.present?
...@@ -123,6 +137,17 @@ class ChatNotificationService < Service ...@@ -123,6 +137,17 @@ class ChatNotificationService < Service
end end
def should_pipeline_be_notified?(data) def should_pipeline_be_notified?(data)
notify_for_ref?(data) && notify_for_pipeline?(data)
end
def notify_for_ref?(data)
return true if data[:object_attributes][:tag]
return true unless notify_only_default_branch
data[:object_attributes][:ref] == project.default_branch
end
def notify_for_pipeline?(data)
case data[:object_attributes][:status] case data[:object_attributes][:status]
when 'success' when 'success'
!notify_only_broken_pipelines? !notify_only_broken_pipelines?
......
...@@ -22,19 +22,11 @@ class MattermostService < ChatNotificationService ...@@ -22,19 +22,11 @@ class MattermostService < ChatNotificationService
</ol>' </ol>'
end end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder def default_channel_placeholder
"Channel handle (e.g. town-square)" "Channel handle (e.g. town-square)"
end end
def webhook_placeholder
'http://mattermost.example.com/hooks/…'
end
end end
...@@ -21,19 +21,11 @@ class SlackService < ChatNotificationService ...@@ -21,19 +21,11 @@ class SlackService < ChatNotificationService
</ol>' </ol>'
end end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder def default_channel_placeholder
"Channel name (e.g. general)" "Channel name (e.g. general)"
end end
def webhook_placeholder
'https://hooks.slack.com/services/…'
end
end end
...@@ -344,6 +344,8 @@ class User < ActiveRecord::Base ...@@ -344,6 +344,8 @@ class User < ActiveRecord::Base
end end
def find_by_personal_access_token(token_string) def find_by_personal_access_token(token_string)
return unless token_string
PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
end end
......
...@@ -3,7 +3,7 @@ module Boards ...@@ -3,7 +3,7 @@ module Boards
class ListService < BaseService class ListService < BaseService
def execute def execute
issues = IssuesFinder.new(current_user, filter_params).execute issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list? issues = without_board_labels(issues) unless list
issues = with_list_label(issues) if movable_list? issues = with_list_label(issues) if movable_list?
issues.order_by_position_and_priority issues.order_by_position_and_priority
end end
......
...@@ -46,36 +46,44 @@ ...@@ -46,36 +46,44 @@
%h4 %h4
Features Features
%hr %hr
%p - sign_up = "Sign up"
Sign up %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
= sign_up
%span.light.pull-right %span.light.pull-right
= boolean_to_icon signup_enabled? = boolean_to_icon signup_enabled?
%p - ldap = "LDAP"
LDAP %p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab.config.ldap.enabled = boolean_to_icon Gitlab.config.ldap.enabled
%p - gravatar = "Gravatar"
Gravatar %p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") }
= gravatar
%span.light.pull-right %span.light.pull-right
= boolean_to_icon gravatar_enabled? = boolean_to_icon gravatar_enabled?
%p - omniauth = "OmniAuth"
OmniAuth %p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") }
= omniauth
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab.config.omniauth.enabled = boolean_to_icon Gitlab.config.omniauth.enabled
%p - reply_email = "Reply by email"
Reply by email %p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") }
= reply_email
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled? = boolean_to_icon Gitlab::IncomingEmail.enabled?
%p - elastic = "Elasticsearch"
Elasticsearch %p{ "aria-label" => "#{elastic}: status " + (current_application_settings.elasticsearch_search? ? "on" : "off") }
= elastic
%span.light.pull-right %span.light.pull-right
= boolean_to_icon current_application_settings.elasticsearch_search? = boolean_to_icon current_application_settings.elasticsearch_search?
%p - geo = "Geo"
Geo %p{ "aria-label" => "#{geo}: status " + (Gitlab::Geo.enabled? ? "on" : "off") }
= geo
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab::Geo.enabled? = boolean_to_icon Gitlab::Geo.enabled?
%p - container_reg = "Container Registry"
Container Registry %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
= container_reg
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled = boolean_to_icon Gitlab.config.registry.enabled
......
...@@ -19,12 +19,13 @@ ...@@ -19,12 +19,13 @@
.nav-controls .nav-controls
- if @todos.any?(&:pending?) - if @todos.any?(&:pending?)
= link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do .append-right-default
Mark all as done = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
= icon('spinner spin') Mark all as done
= link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do = icon('spinner spin')
Undo mark all as done = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
= icon('spinner spin') Undo mark all as done
= icon('spinner spin')
.todos-filters .todos-filters
.row-content-block.second-block .row-content-block.second-block
......
...@@ -6,7 +6,9 @@ ...@@ -6,7 +6,9 @@
%h4.prepend-top-0 %h4.prepend-top-0
Syntax highlighting theme Syntax highlighting theme
%p %p
This setting allow you to customize the appearance of the syntax. This setting allows you to customize the appearance of the syntax.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank'
.col-lg-9.syntax-theme .col-lg-9.syntax-theme
- Gitlab::ColorSchemes.each do |scheme| - Gitlab::ColorSchemes.each do |scheme|
= label_tag do = label_tag do
...@@ -20,6 +22,8 @@ ...@@ -20,6 +22,8 @@
Behavior Behavior
%p %p
This setting allows you to customize the behavior of the system layout and default views. This setting allows you to customize the behavior of the system layout and default views.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank'
.col-lg-9 .col-lg-9
.form-group .form-group
= f.label :layout, class: 'label-light' do = f.label :layout, class: 'label-light' do
...@@ -29,13 +33,11 @@ ...@@ -29,13 +33,11 @@
Choose between fixed (max. 1200px) and fluid (100%) application layout. Choose between fixed (max. 1200px) and fluid (100%) application layout.
.form-group .form-group
= f.label :dashboard, class: 'label-light' do = f.label :dashboard, class: 'label-light' do
Default Dashboard Default dashboard
= link_to('(?)', help_page_path('profile/preferences') + '#default-dashboard', target: '_blank')
= f.select :dashboard, dashboard_choices, {}, class: 'form-control' = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
.form-group .form-group
= f.label :project_view, class: 'label-light' do = f.label :project_view, class: 'label-light' do
Project view Project view
= link_to('(?)', help_page_path('profile/preferences') + '#default-project-view', target: '_blank')
= f.select :project_view, project_view_choices, {}, class: 'form-control' = f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block .help-block
Choose what content you want to see on a project's home page. Choose what content you want to see on a project's home page.
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
.form-group .form-group
= f.label :tag_list, "Tags", class: 'label-light' = f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas. %p.help-block Separate tags with commas.
%hr %hr
%fieldset %fieldset
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
%th Last edit %th Last edit
- @services.sort_by(&:title).each do |service| - @services.sort_by(&:title).each do |service|
%tr %tr
%td %td{ "aria-label" => "#{service.title}: status " + (service.activated? ? "on" : "off") }
= boolean_to_icon service.activated? = boolean_to_icon service.activated?
%td %td
= link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
......
---
title: Display all closed issues in “done” board list
merge_request:
author:
---
title: Add `aria-label` for feature status accessibility
merge_request: 9830
author:
---
title: Adds Frontend Styleguide to documentation
merge_request: 9961
author:
---
title: Fix quick submit short-cut on preview tab for comments
merge_request: 10002
author:
---
title: align Mark all as done with other Done buttons on Todos page
merge_request:
author:
---
title: Adds polling utility function for vue resource
merge_request:
author:
---
title: Fixed filtered search not working in IE
merge_request:
author:
---
title: Fixed bug in issue boards which stopped cards being able to be dragged
merge_request:
author:
---
title: Corrected time tracking icon color in the issuable side bar
merge_request:
author:
---
title: Only send chat notifications for the default branch
merge_request:
author:
Rails.application.configure do Rails.application.configure do
# Make sure the middleware is inserted first in middleware chain
config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestBlockerMiddleware')
# Settings specified here will take precedence over those in config/application.rb # Settings specified here will take precedence over those in config/application.rb
# The test environment is used exclusively to run your application's # The test environment is used exclusively to run your application's
......
...@@ -41,10 +41,10 @@ Get details of project's build trigger. ...@@ -41,10 +41,10 @@ Get details of project's build trigger.
GET /projects/:id/triggers/:trigger_id GET /projects/:id/triggers/:trigger_id
``` ```
| Attribute | Type | required | Description | | Attribute | Type | required | Description |
|-----------|---------|----------|--------------------------| |--------------|---------|----------|--------------------------|
| `id` | integer | yes | The ID of a project | | `id` | integer | yes | The ID of a project |
| `token` | string | yes | The `token` of a trigger | | `trigger_id` | integer | yes | The trigger id |
``` ```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5" curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
...@@ -103,6 +103,7 @@ PUT /projects/:id/triggers/:trigger_id ...@@ -103,6 +103,7 @@ PUT /projects/:id/triggers/:trigger_id
| Attribute | Type | required | Description | | Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------| |---------------|---------|----------|--------------------------|
| `id` | integer | yes | The ID of a project |
| `trigger_id` | integer | yes | The trigger id | | `trigger_id` | integer | yes | The trigger id |
| `description` | string | no | The trigger name | | `description` | string | no | The trigger name |
...@@ -133,6 +134,7 @@ POST /projects/:id/triggers/:trigger_id/take_ownership ...@@ -133,6 +134,7 @@ POST /projects/:id/triggers/:trigger_id/take_ownership
| Attribute | Type | required | Description | | Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------| |---------------|---------|----------|--------------------------|
| `id` | integer | yes | The ID of a project |
| `trigger_id` | integer | yes | The trigger id | | `trigger_id` | integer | yes | The trigger id |
``` ```
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations - [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md) - [Testing standards and style guidelines](testing.md)
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements - [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md) - [Frontend guidelines](fe_guide/index.md)
- [SQL guidelines](sql.md) for working with SQL queries - [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers - [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [`Gemfile` guidelines](gemfile.md) - [`Gemfile` guidelines](gemfile.md)
......
# Accessibility
## Resources
[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
are useful for testing for potential accessibility problems in GitLab.
Accessibility best-practices and more in-depth information is available on
[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools.
[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
# Architecture
When you are developing a new feature that requires architectural design, or if
you are changing the fundamental design of an existing feature, make sure it is
discussed with one of the Frontend Architecture Experts.
A Frontend Architect is an expert who makes high-level Frontend design decisions
and decides on technical standards, including coding standards and frameworks.
Architectural decisions should be accessible to everyone, so please document
them in the relevant Merge Request discussion or by updating our documentation
when appropriate.
You can find the Frontend Architecture experts on the [team page][team-page].
## Examples
You can find documentation about the desired architecture for a new feature
built with Vue.js [here][vue-section].
[team-page]: https://about.gitlab.com/team
[vue-section]: vue.md#frontend.html#how-to-build-a-new-feature-with-vue-js
# Design Patterns
## Singletons
When exactly one object is needed for a given task, prefer to define it as a
`class` rather than as an object literal. Prefer also to explicitly restrict
instantiation, unless flexibility is important (e.g. for testing).
```javascript
// bad
const MyThing = {
prop1: 'hello',
method1: () => {}
};
export default MyThing;
// good
class MyThing {
constructor() {
this.prop1 = 'hello';
}
method1() {}
}
export default new MyThing();
// best
export default class MyThing {
constructor() {
if (!this.prototype.singleton) {
this.init();
this.prototype.singleton = this;
}
return this.prototype.singleton;
}
init() {
this.prop1 = 'hello';
}
method1() {}
}
```
## Manipulating the DOM in a JS Class
When writing a class that needs to manipulate the DOM guarantee a container option is provided.
This is useful when we need that class to be instantiated more than once in the same page.
Bad:
```javascript
class Foo {
constructor() {
document.querySelector('.bar');
}
}
new Foo();
```
Good:
```javascript
class Foo {
constructor(opts) {
document.querySelector(`${opts.container} .bar`);
}
}
new Foo({ container: '.my-element' });
```
You can find an example of the above in this [class][container-class-example];
[container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js
# Frontend Development Guidelines
This document describes various guidelines to ensure consistency and quality
across GitLab's frontend team.
## Overview
GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
[Hamlit][hamlit]. Be wary of [the limitations that come with using
Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
modern ECMAScript standards supported through [Babel][babel] and ES module
support through [webpack][webpack].
We also utilize [webpack][webpack] to handle the bundling, minification, and
compression of our assets.
Working with our frontend assets requires Node (v4.3 or greater) and Yarn
(v0.17 or greater). You can find information on how to install these on our
[installation guide][install].
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
### Browser Support
For our currently-supported browsers, see our [requirements][requirements].
---
## [Architecture](architecture.md)
How we go about making fundamental design decisions in GitLab's frontend team
or make changes to our frontend development guidelines.
---
## [Testing](testing.md)
How we write frontend tests, run the GitLab test suite, and debug test related
issues.
---
## [Design Patterns](design_patterns.md)
Common JavaScript design patterns in GitLab's codebase.
---
## [Vue.js Best Practices](vue.md)
Vue specific design patterns and practices.
---
## Style Guides
### [JavaScript Style Guide](style_guide_js.md)
We use eslint to enforce our JavaScript style guides. Our guide is based on
the excellent [Airbnb][airbnb-js-style-guide] style guide with a few small
changes.
### [SCSS Style Guide](style_guide_scss.md)
Our SCSS conventions which are enforced through [scss-lint][scss-lint].
---
## [Performance](performance.md)
Best practices for monitoring and maximizing frontend performance.
---
## [Security](security.md)
Frontend security practices.
---
## [Accessibility](accessibility.md)
Our accessibility standards and resources.
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
[hamlit]: https://github.com/k0kubun/hamlit
[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
[scss]: http://sass-lang.com/
[babel]: https://babeljs.io/
[webpack]: https://webpack.js.org/
[jquery]: https://jquery.com/
[vue]: http://vuejs.org/
[airbnb-js-style-guide]: https://github.com/airbnb/javascript
[scss-lint]: https://github.com/brigade/scss-lint
[install]: ../../install/installation.md#4-node
[requirements]: ../../install/requirements.md#supported-web-browsers
# Performance
## Best Practices
### Realtime Components
When writing code for realtime features we have to keep a couple of things in mind:
1. Do not overload the server with requests.
1. It should feel realtime.
Thus, we must strike a balance between sending requests and the feeling of realtime.
Use the following rules when creating realtime solutions.
1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
Use that as your polling interval. This way it is easy for system administrators to change the
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. 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
`304 Not Modified`. The browser will transform it for you.
## Reducing Asset Footprint
### Page-specific JavaScript
Certain pages may require the use of a third party library, such as [d3][d3] for
the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These
libraries increase the page size significantly, and impact load times due to
bandwidth bottlenecks and the browser needing to parse more JavaScript.
In cases where libraries are only used on a few specific pages, we use
"page-specific JavaScript" to prevent the main `main.js` file from
becoming unnecessarily large.
Steps to split page-specific JavaScript from the main `main.js`:
1. Create a directory for the specific page(s), e.g. `graphs/`.
1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`.
1. Add the new "bundle" file to the list of entry files in `config/webpack.config.js`.
- For example: `graphs: './graphs/graphs_bundle.js',`.
1. Move code reliant on these libraries into the `graphs` directory.
1. In `graphs_bundle.js` add CommonJS `require('./path_to_some_component.js');` statements to load any other files in this directory. Make sure to use relative urls.
1. In the relevant views, add the scripts to the page with the following:
```haml
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('lib_chart')
= page_specific_javascript_bundle_tag('graphs')
```
The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
is separated from the bundle file so it can be cached separately from the bundle
and reused for other pages that also rely on the library. For an example, see
[this Haml file][page-specific-js-example].
### Code Splitting
> *TODO* flesh out this section once webpack is ready for code-splitting
### Minimizing page size
A smaller page size means the page loads faster (especially important on mobile
and poor connections), the page is parsed more quickly by the browser, and less
data is used for users with capped data plans.
General tips:
- Don't add new fonts.
- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF.
- Compress and minify assets wherever possible (For CSS/JS, Sprockets and webpack do this for us).
- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
-------
## Additional Resources
- [WebPage Test][web-page-test] for testing site loading time and size.
- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
- [Profiling with Chrome DevTools][google-devtools-profiling]
- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
[web-page-test]: http://www.webpagetest.org/
[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
[browser-diet]: https://browserdiet.com/
[d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
# Security
### Resources
[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
potential problems and ensuring compliance with security best practices.
<!-- Uncomment these sections when CSP/SRI are implemented.
### Content Security Policy (CSP)
Content Security Policy is a web standard that intends to mitigate certain
forms of Cross-Site Scripting (XSS) as well as data injection.
Content Security Policy rules should be taken into consideration when
implementing new features, especially those that may rely on connection with
external services.
GitLab's CSP is used for the following:
- Blocking plugins like Flash and Silverlight from running at all on our pages.
- Blocking the use of scripts and stylesheets downloaded from external sources.
- Upgrading `http` requests to `https` when possible.
- Preventing `iframe` elements from loading in most contexts.
Some exceptions include:
- Scripts from Google Analytics and Piwik if either is enabled.
- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
We use [the Secure Headers gem][secure_headers] to enable Content
Security Policy headers in the GitLab Rails app.
Some resources on implementing Content Security Policy:
- [MDN Article on CSP][mdn-csp]
- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
### Subresource Integrity (SRI)
Subresource Integrity prevents malicious assets from being provided by a CDN by
guaranteeing that the asset downloaded is identical to the asset the server
is expecting.
The Rails app generates a unique hash of the asset, which is used as the
asset's `integrity` attribute. The browser generates the hash of the asset
on-load and will reject the asset if the hashes do not match.
All CSS and JavaScript assets should use Subresource Integrity.
Some resources on implementing Subresource Integrity:
- [MDN Article on SRI][mdn-sri]
- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
-->
### Including external resources
External fonts, CSS, and JavaScript should never be used with the exception of
Google Analytics and Piwik - and only when the instance has enabled it. Assets
should always be hosted and served locally from the GitLab instance. Embedded
resources via `iframes` should never be used except in certain circumstances
such as with ReCaptcha, which cannot be used without an `iframe`.
### Avoiding inline scripts and styles
In order to protect users from [XSS vulnerabilities][xss], we will disable
inline scripts in the future using Content Security Policy.
While inline scripts can be useful, they're also a security concern. If
user-supplied content is unintentionally left un-sanitized, malicious users can
inject scripts into the web app.
Inline styles should be avoided in almost all cases, they should only be used
when no alternatives can be found. This allows reusability of styles as well as
readability.
[observatory-cli]: https://github.com/mozilla/http-observatory-cli
[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
[secure_headers]: https://github.com/twitter/secureheaders
[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[github-eng-sri]: http://githubengineering.com/subresource-integrity/
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
# Style guides and linting
See the relevant style guides for our guidelines and for information on linting:
## JavaScript
We defer to [Airbnb][airbnb-js-style-guide] on most style-related
conventions and enforce them with eslint.
See [our current .eslintrc][eslintrc] for specific rules and patterns.
### Common
#### ESlint
- **Never** disable eslint rules unless you have a good reason. You may see a lot of legacy files with `/* eslint-disable some-rule, some-other-rule */` at the top, but legacy files are a special case. Any time you develop a new feature or refactor an existing one, you should abide by the eslint rules.
- **Never Ever EVER** disable eslint globally for a file
```javascript
// bad
/* eslint-disable */
// better
/* eslint-disable some-rule, some-other-rule */
// best
// nothing :)
```
- If you do need to disable a rule for a single violation, try to do it as locally as possible
```javascript
// bad
/* eslint-disable no-new */
import Foo from 'foo';
new Foo();
// better
import Foo from 'foo';
// eslint-disable-next-line no-new
new Foo();
```
- When they are needed _always_ place ESlint directive comment blocks on the first line of a script, followed by any global declarations, then a blank newline prior to any imports or code.
```javascript
// bad
/* global Foo */
/* eslint-disable no-new */
import Bar from './bar';
// good
/* eslint-disable no-new */
/* global Foo */
import Bar from './bar';
```
- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
- When declaring multiple globals, always use one `/* global [name] */` line per variable.
```javascript
// bad
/* globals Flash, Cookies, jQuery */
// good
/* global Flash */
/* global Cookies */
/* global jQuery */
```
#### Modules, Imports, and Exports
- Use ES module syntax to import modules
```javascript
// bad
require('foo');
// good
import Foo from 'foo';
// bad
module.exports = Foo;
// good
export default Foo;
```
- Relative paths
Unless you are writing a test, always reference other scripts using relative paths instead of `~`
In **app/assets/javascripts**:
```javascript
// bad
import Foo from '~/foo'
// good
import Foo from '../foo';
```
In **spec/javascripts**:
```javascript
// bad
import Foo from '../../app/assets/javascripts/foo'
// good
import Foo from '~/foo';
```
- Avoid using IIFE. Although we have a lot of examples of files which wrap their contents in IIFEs (immediately-invoked function expressions), this is no longer necessary after the transition from Sprockets to webpack. Do not use them anymore and feel free to remove them when refactoring legacy code.
- Avoid adding to the global namespace.
```javascript
// bad
window.MyClass = class { /* ... */ };
// good
export default class MyClass { /* ... */ }
```
- Side effects are forbidden in any script which contains exports
```javascript
// bad
export default class MyClass { /* ... */ }
document.addEventListener("DOMContentLoaded", function(event) {
new MyClass();
}
```
#### Data Mutation and Pure functions
- Strive to write many small pure functions, and minimize where mutations occur.
```javascript
// bad
const values = {foo: 1};
function impureFunction(items) {
const bar = 1;
items.foo = items.a * bar + 2;
return items.a;
}
const c = impureFunction(values);
// good
var values = {foo: 1};
function pureFunction (foo) {
var bar = 1;
foo = foo * bar + 2;
return foo;
}
var c = pureFunction(values.foo);
```
- Avoid constructors with side-effects
#### Parse Strings into Numbers
- `parseInt()` is preferable over `Number()` or `+`
```javascript
// bad
+'10' // 10
// good
Number('10') // 10
// better
parseInt('10', 10);
```
### Vue.js
#### Basic Rules
- Only include one Vue.js component per file.
- Export components as plain objects:
```javascript
export default {
template: `<h1>I'm a component</h1>
}
```
#### Naming
- **Extensions**: Use `.vue` extension for Vue components.
- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances:
```javascript
// bad
import cardBoard from 'cardBoard';
// good
import CardBoard from 'cardBoard'
// bad
components: {
CardBoard: CardBoard
};
// good
components: {
cardBoard: CardBoard
};
```
- **Props Naming**: Avoid using DOM component prop names.
```javascript
// bad
<component class="btn">
// good
<component cssClass="btn">
```
#### Alignment
- Follow these alignment styles for the template method:
```javascript
// bad
<component v-if="bar"
param="baz" />
// good
<component
v-if="bar"
param="baz"
/>
// if props fit in one line then keep it on the same line
<component bar="bar" />
```
#### Quotes
- Always use double quotes `"` inside templates and single quotes `'` for all other JS.
```javascript
// bad
template: `
<button :class='style'>Button</button>
`
// good
template: `
<button :class="style">Button</button>
`
```
#### Props
- Props should be declared as an object
```javascript
// bad
props: ['foo']
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
```
- Required key should always be provided when declaring a prop
```javascript
// bad
props: {
foo: {
type: String,
}
}
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
```
- Default key should always be provided if the prop is not required:
```javascript
// bad
props: {
foo: {
type: String,
required: false,
}
}
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
// good
props: {
foo: {
type: String,
required: true
}
}
```
#### Data
- `data` method should always be a function
```javascript
// bad
data: {
foo: 'foo'
}
// good
data() {
return {
foo: 'foo'
};
}
```
#### Directives
- Shorthand `@` is preferable over `v-on`
```javascript
// bad
<component v-on:click="eventHandler"/>
// good
<component @click="eventHandler"/>
```
- Shorthand `:` is preferable over `v-bind`
```javascript
// bad
<component v-bind:class="btn"/>
// good
<component :class="btn"/>
```
#### Closing tags
- Prefer self closing component tags
```javascript
// bad
<component></component>
// good
<component />
```
#### Ordering
- Order for a Vue Component:
1. `name`
2. `props`
3. `data`
4. `components`
5. `computedProps`
6. `methods`
7. lifecycle methods
1. `beforeCreate`
2. `created`
3. `beforeMount`
4. `mounted`
5. `beforeUpdate`
6. `updated`
7. `activated`
8. `deactivated`
9. `beforeDestroy`
10. `destroyed`
8. `template`
## SCSS
- [SCSS](style_guide_scss.md)
[airbnb-js-style-guide]: https://github.com/airbnb/javascript
[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
...@@ -35,7 +35,7 @@ between the property and its value. ...@@ -35,7 +35,7 @@ between the property and its value.
```scss ```scss
// Bad // Bad
.container-item { .container-item {
width: 100px; height: 100px; width: 100px; height: 100px;
margin-top: 0; margin-top: 0;
} }
...@@ -63,7 +63,7 @@ between the property and its value. ...@@ -63,7 +63,7 @@ between the property and its value.
} }
``` ```
Note that there is an exception for single-line rulesets, although these are Note that there is an exception for single-line rulesets, although these are
not typically recommended. not typically recommended.
```scss ```scss
...@@ -72,7 +72,7 @@ p { margin: 0; padding: 0; } ...@@ -72,7 +72,7 @@ p { margin: 0; padding: 0; }
### Colors ### Colors
HEX (hexadecimal) colors should use shorthand where possible, and should use HEX (hexadecimal) colors should use shorthand where possible, and should use
lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3` lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3`
vs. `#e3e3e3`. vs. `#e3e3e3`.
...@@ -111,7 +111,7 @@ p { ...@@ -111,7 +111,7 @@ p {
### Semicolons ### Semicolons
Always include semicolons after every property. When the stylesheets are Always include semicolons after every property. When the stylesheets are
minified, the semicolons will be removed automatically. minified, the semicolons will be removed automatically.
```scss ```scss
...@@ -144,7 +144,7 @@ padding: 10px; ...@@ -144,7 +144,7 @@ padding: 10px;
### Zero Units ### Zero Units
Omit length units on zero values, they're unnecessary and not including them Omit length units on zero values, they're unnecessary and not including them
is slightly more performant. is slightly more performant.
```scss ```scss
...@@ -161,36 +161,56 @@ is slightly more performant. ...@@ -161,36 +161,56 @@ is slightly more performant.
### Selectors with a `js-` Prefix ### Selectors with a `js-` Prefix
Do not use any selector prefixed with `js-` for styling purposes. These Do not use any selector prefixed with `js-` for styling purposes. These
selectors are intended for use only with JavaScript to allow for removal or selectors are intended for use only with JavaScript to allow for removal or
renaming without breaking styling. renaming without breaking styling.
### IDs
Don't use ID selectors in CSS.
```scss
// Bad
#my-element {
padding: 0;
}
// Good
.my-element {
padding: 0;
}
```
### Variables
Before adding a new variable for a color or a size, guarantee:
1. There isn't already one
2. There isn't a similar one we can use instead.
## Linting ## Linting
We use [SCSS Lint][scss-lint] to check for style guide conformity. It uses the We use [SCSS Lint][scss-lint] to check for style guide conformity. It uses the
ruleset in `.scss-lint.yml`, which is located in the home directory of the ruleset in `.scss-lint.yml`, which is located in the home directory of the
project. project.
To check if any warnings will be produced by your changes, you can run `rake To check if any warnings will be produced by your changes, you can run `rake
scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to scss_lint` in the GitLab directory. SCSS Lint will also run in GitLab CI to
catch any warnings. catch any warnings.
If the Rake task is throwing warnings you don't understand, SCSS Lint's If the Rake task is throwing warnings you don't understand, SCSS Lint's
documentation includes [a full list of their linters][scss-lint-documentation]. documentation includes [a full list of their linters][scss-lint-documentation].
### Fixing issues ### Fixing issues
If you want to automate changing a large portion of the codebase to conform to If you want to automate changing a large portion of the codebase to conform to
the SCSS style guide, you can use [CSSComb][csscomb]. First install the SCSS style guide, you can use [CSSComb][csscomb]. First install
[Node][node] and [NPM][npm], then run `npm install csscomb -g` to install [Node][node] and [NPM][npm], then run `npm install csscomb -g` to install
CSSComb globally (system-wide). Run it in the GitLab directory with CSSComb globally (system-wide). Run it in the GitLab directory with
`csscomb app/assets/stylesheets` to automatically fix issues with CSS/SCSS. `csscomb app/assets/stylesheets` to automatically fix issues with CSS/SCSS.
Note that this won't fix every problem, but it should fix a majority. Note that this won't fix every problem, but it should fix a majority.
### Ignoring issues ### Ignoring issues
If you want a line or set of lines to be ignored by the linter, you can use If you want a line or set of lines to be ignored by the linter, you can use
`// scss-lint:disable RuleName` ([more info][disabling-linters]): `// scss-lint:disable RuleName` ([more info][disabling-linters]):
```scss ```scss
...@@ -203,8 +223,8 @@ If you want a line or set of lines to be ignored by the linter, you can use ...@@ -203,8 +223,8 @@ If you want a line or set of lines to be ignored by the linter, you can use
``` ```
Make sure a comment is added on the line above the `disable` rule, otherwise the Make sure a comment is added on the line above the `disable` rule, otherwise the
linter will throw a warning. `DisableLinterReason` is enabled to make sure the linter will throw a warning. `DisableLinterReason` is enabled to make sure the
style guide isn't being ignored, and to communicate to others why the style style guide isn't being ignored, and to communicate to others why the style
guide is ignored in this instance. guide is ignored in this instance.
[csscomb]: https://github.com/csscomb/csscomb.js [csscomb]: https://github.com/csscomb/csscomb.js
......
# Frontend Testing
There are two types of tests you'll encounter while developing frontend code
at GitLab. We use Karma and Jasmine for JavaScript unit testing, and RSpec
feature tests with Capybara for integration testing.
Feature tests need to be written for all new features. Regression tests ought
to be written for all bug fixes to prevent them from recurring in the future.
See [the Testing Standards and Style Guidelines](/doc/development/testing.md)
for more information on general testing practices at GitLab.
## Karma test suite
GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test
framework for our JavaScript unit tests. For tests that rely on DOM
manipulation we use fixtures which are pre-compiled from HAML source files and
served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
- `rake karma:fixtures` (re-)generates fixtures
- `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
is sufficient (and saves you some time).
### Live testing and focused testing
While developing locally, it may be helpful to keep karma running so that you
can get instant feedback on as you write tests and modify code. To do this
you can start karma with `npm run karma-start`. It will compile the javascript
assets and run a server at `http://localhost:9876/` where it will automatically
run the tests on any browser which connects to it. You can enter that url on
multiple browsers at once to have it run the tests on each in parallel.
While karma is running, any changes you make will instantly trigger a recompile
and retest of the entire test suite, so you can see instantly if you've broken
a test with your changes. You can use [jasmine focused][jasmine-focus] or
excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the
tests you want while you're working on a specific feature, but make sure to
remove these directives when you commit your code.
## RSpec Feature Integration Tests
Information on setting up and running RSpec integration tests with
[Capybara][capybara] can be found in the
[general testing guide](/doc/development/testing.md).
## Gotchas
### Errors due to use of unsupported JavaScript features
Similar errors will be thrown if you're using JavaScript features not yet
supported by the PhantomJS test runner which is used for both Karma and RSpec
tests. We polyfill some JavaScript objects for older browsers, but some
features are still unavailable:
- Array.from
- Array.first
- Async functions
- Generators
- Array destructuring
- For..Of
- Symbol/Symbol.iterator
- Spread
Until these are polyfilled appropriately, they should not be used. Please
update this list with additional unsupported features.
### RSpec errors due to JavaScript
By default RSpec unit tests will not run JavaScript in the headless browser
and will simply rely on inspecting the HTML generated by rails.
If an integration test depends on JavaScript to run correctly, you need to make
sure the spec is configured to enable JavaScript when the tests are run. If you
don't do this you'll see vague error messages from the spec runner.
To enable a JavaScript driver in an `rspec` test, add `js: true` to the
individual spec or the context block containing multiple specs that need
JavaScript enabled:
```ruby
# For one spec
it 'presents information about abuse report', js: true do
# assertions...
end
describe "Admin::AbuseReports", js: true do
it 'presents information about abuse report' do
# assertions...
end
it 'shows buttons for adding to abuse report' do
# assertions...
end
end
```
### Spinach errors due to missing JavaScript
> **Note:** Since we are discouraging the use of Spinach when writing new
> feature tests, you shouldn't ever need to use this. This information is kept
> available for legacy purposes only.
In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
file for the failing spec, add the `@javascript` flag above the Scenario:
```
@javascript
Scenario: Developer can approve merge request
Given I am a "Shop" developer
And I visit project "Shop" merge requests page
And merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04"
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
```
[capybara]: http://teamcapybara.github.io/capybara/
[jasmine]: https://jasmine.github.io/
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
# Vue
For more complex frontend features, we recommend using Vue.js. It shares
some ideas with React.js as well as Angular.
To get started with Vue, read through [their documentation][vue-docs].
## When to use Vue.js
We recommend using Vue for more complex features. Here are some guidelines for when to use Vue.js:
- If you are starting a new feature or refactoring an old one that highly interacts with the DOM;
- For real time data updates;
- If you are creating a component that will be reused elsewhere;
## When not to use Vue.js
We don't want to refactor all GitLab frontend code into Vue.js, here are some guidelines for
when not to use Vue.js:
- Adding or changing static information;
- Features that highly depend on jQuery will be hard to work with Vue.js
As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions.
## How to build a new feature with Vue.js
**Components, Stores and Services**
In some features implemented with Vue.js, like the [issue board][issue-boards]
or [environments table][environments-table]
you can find a clear separation of concerns:
```
new_feature
├── components
│ └── component.js.es6
│ └── ...
├── store
│ └── new_feature_store.js.es6
├── service
│ └── new_feature_service.js.es6
├── new_feature_bundle.js.es6
```
_For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
**A `*_bundle.js` file**
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
Don't forget to follow [these steps.][page_specific_javascript]
**A folder for Components**
This folder holds all components that are specific of this new feature.
If you need to use or create a component that will probably be used somewhere
else, please refer to `vue_shared/components`.
A good thumb rule to know when you should create a component is to think if
it will be reusable elsewhere.
For example, tables are used in a quite amount of places across GitLab, a table
would be a good fit for a component. On the other hand, a table cell used only
in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
**A folder for the Store**
The Store is a class that allows us to manage the state in a single
source of truth.
The concept we are trying to follow is better explained by Vue documentation
itself, please read this guide: [State Management][state-management]
**A folder for the Service**
The Service is used only to communicate with the server.
It does not store or manipulate any data.
We use [vue-resource][vue-resource-repo] to
communicate with the server.
The [issue boards service][issue-boards-service]
is a good example of this pattern.
## Style guide
Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs)
for best practices while writing your Vue components and templates.
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[vue-resource-repo]: https://github.com/pagekit/vue-resource
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
# Frontend Development Guidelines
This document describes various guidelines to ensure consistency and quality
across GitLab's frontend team.
## Overview
GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
[Hamlit][hamlit]. Be wary of [the limitations that come with using
Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
[ES6 by way of Babel][es6].
The asset pipeline is [Sprockets][sprockets], which handles the concatenation,
minification, and compression of our assets.
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
### Architecture
The Frontend Architect is an expert who makes high-level frontend design choices
and decides on technical standards, including coding standards, and frameworks.
When you are assigned a new feature that requires architectural design,
make sure it is discussed with one of the Frontend Architecture Experts.
This rule also applies if you plan to change the architecture of an existing feature.
These decisions should be accessible to everyone, so please document it on the Merge Request.
You can find the Frontend Architecture experts on the [team page][team-page].
You can find documentation about the desired architecture for a new feature built with Vue.js in [here][vue-section].
### Realtime
When writing code for realtime features we have to keep a couple of things in mind:
1. Do not overload the server with requests.
1. It should feel realtime.
Thus, we must strike a balance between sending requests and the feeling of realtime.
Use the following rules when creating realtime solutions.
1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
Use that as your polling interval. This way it is easy for system administrators to change the
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. 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
`304 Not Modified`. The browser will transform it for you.
### Vue
For more complex frontend features, we recommend using Vue.js. It shares
some ideas with React.js as well as Angular.
To get started with Vue, read through [their documentation][vue-docs].
#### How to build a new feature with Vue.js
**Components, Stores and Services**
In some features implemented with Vue.js, like the [issue board][issue-boards]
or [environments table][environments-table]
you can find a clear separation of concerns:
```
new_feature
├── components
│ └── component.js.es6
│ └── ...
├── store
│ └── new_feature_store.js.es6
├── service
│ └── new_feature_service.js.es6
├── new_feature_bundle.js.es6
```
_For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
**A `*_bundle.js` file**
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
Don't forget to follow [these steps.][page_specific_javascript]
**A folder for Components**
This folder holds all components that are specific of this new feature.
If you need to use or create a component that will probably be used somewhere
else, please refer to `vue_shared/components`.
A good thumb rule to know when you should create a component is to think if
it will be reusable elsewhere.
For example, tables are used in a quite amount of places across GitLab, a table
would be a good fit for a component.
On the other hand, a table cell used only in on table, would not be a good use
of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
**A folder for the Store**
The Store is a class that allows us to manage the state in a single
source of truth.
The concept we are trying to follow is better explained by Vue documentation
itself, please read this guide: [State Management][state-management]
**A folder for the Service**
The Service is used only to communicate with the server.
It does not store or manipulate any data.
We use [vue-resource][vue-resource-repo] to
communicate with the server.
The [issue boards service][issue-boards-service]
is a good example of this pattern.
## Performance
### Resources
- [WebPage Test][web-page-test] for testing site loading time and size.
- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
- [Profiling with Chrome DevTools][google-devtools-profiling]
- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
### Page-specific JavaScript
Certain pages may require the use of a third party library, such as [d3][d3] for
the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These
libraries increase the page size significantly, and impact load times due to
bandwidth bottlenecks and the browser needing to parse more JavaScript.
In cases where libraries are only used on a few specific pages, we use
"page-specific JavaScript" to prevent the main `application.js` file from
becoming unnecessarily large.
Steps to split page-specific JavaScript from the main `application.js`:
1. Create a directory for the specific page(s), e.g. `graphs/`.
1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`.
1. In `graphs_bundle.js` add the line `//= require_tree .`, this adds all other files in the directory to the bundle.
1. Add any necessary libraries to `app/assets/javascripts/lib/`, all files directly descendant from this directory will be precompiled as separate assets, in this case `chart.js` would be added.
1. Add the new "bundle" file to the list of precompiled assets in
`config/application.rb`.
- For example: `config.assets.precompile << "graphs/graphs_bundle.js"`.
1. Move code reliant on these libraries into the `graphs` directory.
1. In the relevant views, add the scripts to the page with the following:
```haml
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/chart.js')
= page_specific_javascript_tag('graphs/graphs_bundle.js')
```
The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
is separated from the bundle file so it can be cached separately from the bundle
and reused for other pages that also rely on the library. For an example, see
[this Haml file][page-specific-js-example].
### Minimizing page size
A smaller page size means the page loads faster (especially important on mobile
and poor connections), the page is parsed more quickly by the browser, and less
data is used for users with capped data plans.
General tips:
- Don't add new fonts.
- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF.
- Compress and minify assets wherever possible (For CSS/JS, Sprockets does this for us).
- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
## Accessibility
### Resources
[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
are useful for testing for potential accessibility problems in GitLab.
Accessibility best-practices and more in-depth information is available on
[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools.
## Security
### Resources
[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
potential problems and ensuring compliance with security best practices.
<!-- Uncomment these sections when CSP/SRI are implemented.
### Content Security Policy (CSP)
Content Security Policy is a web standard that intends to mitigate certain
forms of Cross-Site Scripting (XSS) as well as data injection.
Content Security Policy rules should be taken into consideration when
implementing new features, especially those that may rely on connection with
external services.
GitLab's CSP is used for the following:
- Blocking plugins like Flash and Silverlight from running at all on our pages.
- Blocking the use of scripts and stylesheets downloaded from external sources.
- Upgrading `http` requests to `https` when possible.
- Preventing `iframe` elements from loading in most contexts.
Some exceptions include:
- Scripts from Google Analytics and Piwik if either is enabled.
- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
We use [the Secure Headers gem][secure_headers] to enable Content
Security Policy headers in the GitLab Rails app.
Some resources on implementing Content Security Policy:
- [MDN Article on CSP][mdn-csp]
- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
### Subresource Integrity (SRI)
Subresource Integrity prevents malicious assets from being provided by a CDN by
guaranteeing that the asset downloaded is identical to the asset the server
is expecting.
The Rails app generates a unique hash of the asset, which is used as the
asset's `integrity` attribute. The browser generates the hash of the asset
on-load and will reject the asset if the hashes do not match.
All CSS and JavaScript assets should use Subresource Integrity. For implementation details,
see the documentation for [the Sprockets implementation of SRI][sprockets-sri].
Some resources on implementing Subresource Integrity:
- [MDN Article on SRI][mdn-sri]
- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
-->
### Including external resources
External fonts, CSS, and JavaScript should never be used with the exception of
Google Analytics and Piwik - and only when the instance has enabled it. Assets
should always be hosted and served locally from the GitLab instance. Embedded
resources via `iframes` should never be used except in certain circumstances
such as with ReCaptcha, which cannot be used without an `iframe`.
### Avoiding inline scripts and styles
In order to protect users from [XSS vulnerabilities][xss], we will disable inline scripts in the future using Content Security Policy.
While inline scripts can be useful, they're also a security concern. If
user-supplied content is unintentionally left un-sanitized, malicious users can
inject scripts into the web app.
Inline styles should be avoided in almost all cases, they should only be used
when no alternatives can be found. This allows reusability of styles as well as
readability.
## Style guides and linting
See the relevant style guides for our guidelines and for information on linting:
- [SCSS][scss-style-guide]
- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related
conventions and enforce them with eslint. See [our current .eslintrc][eslintrc]
for specific rules and patterns.
## Testing
Feature tests need to be written for all new features. Regression tests
also need to be written for all bug fixes to prevent them from occurring
again in the future.
See [the Testing Standards and Style Guidelines](testing.md) for more
information.
### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
- `rake karma:fixtures` (re-)generates fixtures
- `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` is sufficient
(and saves you some time).
Please note: Not all of the frontend fixtures are generated. Some are still static
files. These will not be touched by `rake karma:fixtures`.
## Design Patterns
### Singletons
When exactly one object is needed for a given task, prefer to define it as a
`class` rather than as an object literal. Prefer also to explicitly restrict
instantiation, unless flexibility is important (e.g. for testing).
```javascript
// bad
gl.MyThing = {
prop1: 'hello',
method1: () => {}
};
// good
class MyThing {
constructor() {
this.prop1 = 'hello';
}
method1() {}
}
gl.MyThing = new MyThing();
// best
let singleton;
class MyThing {
constructor() {
if (!singleton) {
singleton = this;
singleton.init();
}
return singleton;
}
init() {
this.prop1 = 'hello';
}
method1() {}
}
gl.MyThing = MyThing;
```
### Manipulating the DOM in a JS Class
When writing a class that needs to manipulate the DOM guarantee a container option is provided.
This is useful when we need that class to be instantiated more than once in the same page.
Bad:
```javascript
class Foo {
constructor() {
document.querySelector('.bar');
}
}
new Foo();
```
Good:
```javascript
class Foo {
constructor(opts) {
document.querySelector(`${opts.container} .bar`);
}
}
new Foo({ container: '.my-element' });
```
You can find an example of the above in this [class][container-class-example];
## Supported browsers
For our currently-supported browsers, see our [requirements][requirements].
## Gotchas
### Spec errors due to use of ES6 features in `.js` files
If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
thrown in Karma, Spinach, or Rspec tests but can't reproduce them manually,
you may have included `ES6`-style JavaScript in files that don't have the
`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
you're working in (`git mv <file.js> <file.js.es6>`).
### Spec errors due to use of unsupported JavaScript
Similar errors will be thrown if you're using JavaScript features not yet
supported by our test runner's version of webkit, whether or not you've updated
the file extension. Examples of unsupported JavaScript features are:
- Array.from
- Array.find
- Array.first
- Object.assign
- Async functions
- Generators
- Array destructuring
- For Of
- Symbol/Symbol.iterator
- Spread
Until these are polyfilled or transpiled appropriately, they should not be used.
Please update this list with additional unsupported features or when any of
these are made usable.
### Spec errors due to JavaScript not enabled
If, as a result of a change you've made, a feature now depends on JavaScript to
run correctly, you need to make sure a JavaScript web driver is enabled when
specs are run. If you don't you'll see vague error messages from the spec
runner, and an explosion of vague console errors in the HTML snapshot.
To enable a JavaScript driver in an `rspec` test, add `js: true` to the
individual spec or the context block containing multiple specs that need
JavaScript enabled:
```ruby
# For one spec
it 'presents information about abuse report', js: true do
# assertions...
end
describe "Admin::AbuseReports", js: true do
it 'presents information about abuse report' do
# assertions...
end
it 'shows buttons for adding to abuse report' do
# assertions...
end
end
```
In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
file for the failing spec, add the `@javascript` flag above the Scenario:
```
@javascript
Scenario: Developer can approve merge request
Given I am a "Shop" developer
And I visit project "Shop" merge requests page
And merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04"
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
```
# Frontend Development Guidelines
[rails]: http://rubyonrails.org/ This page has moved [here](fe_guide/index.md).
[haml]: http://haml.info/
[hamlit]: https://github.com/k0kubun/hamlit
[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
[scss]: http://sass-lang.com/
[es6]: https://babeljs.io/
[sprockets]: https://github.com/rails/sprockets
[jquery]: https://jquery.com/
[vue]: http://vuejs.org/
[vue-docs]: http://vuejs.org/guide/index.html
[web-page-test]: http://www.webpagetest.org/
[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
[browser-diet]: https://browserdiet.com/
[d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
[observatory-cli]: https://github.com/mozilla/http-observatory-cli
[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
[secure_headers]: https://github.com/twitter/secureheaders
[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[github-eng-sri]: http://githubengineering.com/subresource-integrity/
[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[scss-style-guide]: scss_styleguide.md
[requirements]: ../install/requirements.md#supported-web-browsers
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[vue-resource-repo]: https://github.com/pagekit/vue-resource
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[airbnb-js-style-guide]: https://github.com/airbnb/javascript
[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
[team-page]: https://about.gitlab.com/team
[vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js
[container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js
...@@ -42,6 +42,20 @@ To run several tests inside one directory: ...@@ -42,6 +42,20 @@ To run several tests inside one directory:
If you want to use [Spring](https://github.com/rails/spring) set If you want to use [Spring](https://github.com/rails/spring) set
`ENABLE_SPRING=1` in your environment. `ENABLE_SPRING=1` in your environment.
## Compile Frontend Assets
You shouldn't ever need to compile frontend assets manually in development, but
if you ever need to test how the assets get compiled in a production
environment you can do so with the following command:
```
RAILS_ENV=production NODE_ENV=production bundle exec rake gitlab:assets:compile
```
This will compile and minify all JavaScript and CSS assets and copy them along
with all other frontend assets (images, fonts, etc) into `/public/assets` where
they can be easily inspected.
## Generate API documentation for project services (e.g. Slack) ## Generate API documentation for project services (e.g. Slack)
``` ```
......
...@@ -34,16 +34,17 @@ GitLab uses [factory_girl] as a test fixture replacement. ...@@ -34,16 +34,17 @@ GitLab uses [factory_girl] as a test fixture replacement.
GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
the command line via `bundle exec karma`. the command line via `bundle exec karma`.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of - JavaScript tests live in `spec/javascripts/`, matching the folder structure
`app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js` has a corresponding of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
`spec/javascripts/behaviors/autosize_spec.js` file. has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
- Haml fixtures required for JavaScript tests live in - Haml fixtures required for JavaScript tests live in
`spec/javascripts/fixtures`. They should contain the bare minimum amount of `spec/javascripts/fixtures`. They should contain the bare minimum amount of
markup necessary for the test. markup necessary for the test.
> **Warning:** Keep in mind that a Rails view may change and > **Warning:** Keep in mind that a Rails view may change and
invalidate your test, but everything will still pass because your fixture invalidate your test, but everything will still pass because your fixture
doesn't reflect the latest view. doesn't reflect the latest view. Because of this we encourage you to
generate fixtures from actual rails views whenever possible.
- Keep in mind that in a CI environment, these tests are run in a headless - Keep in mind that in a CI environment, these tests are run in a headless
browser and you will not have access to certain APIs, such as browser and you will not have access to certain APIs, such as
...@@ -53,6 +54,8 @@ the command line via `bundle exec karma`. ...@@ -53,6 +54,8 @@ the command line via `bundle exec karma`.
[Karma]: https://github.com/karma-runner/karma [Karma]: https://github.com/karma-runner/karma
[Jasmine]: https://github.com/jasmine/jasmine [Jasmine]: https://github.com/jasmine/jasmine
For more information, see the [frontend testing guide](fe_guide/testing.md).
## RSpec ## RSpec
### General Guidelines ### General Guidelines
......
...@@ -109,7 +109,8 @@ Then select 'Internet Site' and press enter to confirm the hostname. ...@@ -109,7 +109,8 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby ## 2. Ruby
**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future. **Note:** The current supported Ruby version is 2.3.x. GitLab 9.0 dropped support
for Ruby 2.1.x.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example, in production, frequently leads to hard to diagnose problems. For example,
......
# Profile Settings # Profile settings
- [Preferences](preferences.md) - [Preferences](../user/profile/preferences.md)
- [Two-factor Authentication (2FA)](two_factor_authentication.md) - [Two-factor Authentication (2FA)](../user/profile/account/two_factor_authentication.md)
# Profile Preferences This document was moved to [another location](../user/profile/preferences.md).
Settings in the **Profile > Preferences** page allow the user to customize
various aspects of the site to their liking.
## Syntax highlighting theme
_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a
list of supported languages visit the rouge website._
Changing this setting allows the user to customize the theme used when viewing
syntax highlighted code on the site.
The default is **White**.
## Behavior
### Default Dashboard
For users who have access to a large number of projects but only keep up with a
select few, the amount of activity on the default Dashboard page can be
overwhelming.
Changing this setting allows the user to redefine what their default dashboard
will be. Setting it to **Starred Projects** will make that Dashboard view the
default when signing in or clicking the application logo in the upper left.
The default is **Your Projects**.
### Default Project view
It allows user to choose what content he or she want to see on project page.
The default is **Readme**.
[rouge]: http://rouge.jneen.net/ "Rouge website"
...@@ -25,9 +25,8 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ...@@ -25,9 +25,8 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 3. Update Ruby ### 3. Update Ruby
We will continue supporting Ruby < 2.3 for the time being but we recommend you NOTE: GitLab 9.0 only supports Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
upgrade to Ruby 2.3 if you're running a source installation, as this is the same sure to upgrade your interpreter if necessary.
version that ships with our Omnibus package.
You can check which version you are running with `ruby -v`. You can check which version you are running with `ruby -v`.
......
# Profile preferences
A user's profile preferences page allows the user to customize various aspects
of GitLab to their liking.
To navigate to your profile's preferences, click your avatar icon in the top
right corner and select **Settings**. From there on, choose the **Preferences**
tab.
## Syntax highlighting theme
>**Note:**
GitLab uses the [rouge Ruby library][rouge] for syntax highlighting. For a
list of supported languages visit the rouge website.
Changing this setting allows you to customize the color theme when viewing any
syntax highlighted code on GitLab.
The default one is **White**, and you can choose among 5 different colors:
- White
- Dark
- Solarized light
- Solarized dark
- Monokai
## Behavior
The following settings allow you to customize the behavior of GitLab's layout
and default views of your dashboard and the projects' landing pages.
### Layout width
GitLab can be set up to use different widths depending on your liking. Choose
between the fixed (max. 1200px) and the fluid (100%) application layout.
### Default dashboard
For users who have access to a large number of projects but only keep up with a
select few, the amount of activity on the default Dashboard page can be
overwhelming. Changing this setting allows you to redefine what your default
dashboard will be.
You have 6 options here that you can use for your default dashboard view:
- Your projects (default)
- Starred projects
- Your projects' activity
- Starred projects' activity
- Your groups
- Your [Todos]
### Default project view
The default project view settings allows you to choose what content you want to
see on a project's landing page.
You can choose between 2 options:
- Show the files and the readme (default)
- Show the project's activity
[rouge]: http://rouge.jneen.net/ "Rouge website"
[todos]: ../../workflow/todos.md
module Gitlab module Gitlab
module EtagCaching module EtagCaching
class Store class Store
EXPIRY_TIME = 10.minutes EXPIRY_TIME = 20.minutes
REDIS_NAMESPACE = 'etag:'.freeze REDIS_NAMESPACE = 'etag:'.freeze
def get(key) def get(key)
......
# rubocop:disable Style/ClassVars
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
# Rack middleware that keeps track of the number of active requests and can block new requests.
module Gitlab
module Testing
class RequestBlockerMiddleware
@@num_active_requests = Concurrent::AtomicFixnum.new(0)
@@block_requests = Concurrent::AtomicBoolean.new(false)
# Returns the number of requests the server is currently processing.
def self.num_active_requests
@@num_active_requests.value
end
# Prevents the server from accepting new requests. Any new requests will return an HTTP
# 503 status.
def self.block_requests!
@@block_requests.value = true
end
# Allows the server to accept requests again.
def self.allow_requests!
@@block_requests.value = false
end
def initialize(app)
@app = app
end
def call(env)
increment_active_requests
if block_requests?
block_request(env)
else
@app.call(env)
end
ensure
decrement_active_requests
end
private
def block_requests?
@@block_requests.true?
end
def block_request(env)
[503, {}, []]
end
def increment_active_requests
@@num_active_requests.increment
end
def decrement_active_requests
@@num_active_requests.decrement
end
end
end
end
...@@ -34,6 +34,7 @@ feature 'Admin updates settings', feature: true do ...@@ -34,6 +34,7 @@ feature 'Admin updates settings', feature: true do
fill_in 'Username', with: 'test_user' fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel' fill_in 'service_push_channel', with: '#test_channel'
page.check('Notify only broken pipelines') page.check('Notify only broken pipelines')
page.check('Notify only default branch')
check_all_events check_all_events
click_on 'Save' click_on 'Save'
......
require 'spec_helper' require 'spec_helper'
feature 'Member autocomplete', feature: true do feature 'Member autocomplete', :js do
include WaitForAjax
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:participant) { create(:user) }
let(:author) { create(:user) } let(:author) { create(:user) }
let(:note) { create(:note, noteable: noteable, project: noteable.project) }
before do before do
allow_any_instance_of(Commit).to receive(:author).and_return(author) note # actually create the note
login_as user login_as(user)
end end
shared_examples "open suggestions" do shared_examples "open suggestions when typing @" do
it 'displays suggestions' do before do
expect(page).to have_selector('.atwho-view', visible: true) page.within('.new-note') do
end find('#note_note').send_keys('@')
it 'suggests author' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(author.username)
end end
end end
it 'suggests participant' do it 'suggests noteable author and note author' do
page.within('.atwho-view', visible: true) do page.within('.atwho-view', visible: true) do
expect(page).to have_content(participant.username) expect(page).to have_content(author.username)
expect(page).to have_content(note.author.username)
end end
end end
end end
context 'adding a new note on a Issue', js: true do context 'adding a new note on a Issue' do
let(:noteable) { create(:issue, author: author, project: project) }
before do before do
issue = create(:issue, author: author, project: project) visit namespace_project_issue_path(project.namespace, project, noteable)
create(:note, note: 'Ultralight Beam', noteable: issue,
project: project, author: participant)
visit_issue(project, issue)
end end
context 'when typing @' do include_examples "open suggestions when typing @"
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end end
context 'adding a new note on a Merge Request ', js: true do context 'adding a new note on a Merge Request' do
let(:noteable) do
create(:merge_request, source_project: project,
target_project: project, author: author)
end
before do before do
merge = create(:merge_request, source_project: project, target_project: project, author: author) visit namespace_project_merge_request_path(project.namespace, project, noteable)
create(:note, note: 'Ultralight Beam', noteable: merge,
project: project, author: participant)
visit_merge_request(project, merge)
end end
context 'when typing @' do include_examples "open suggestions when typing @"
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end end
context 'adding a new note on a Commit ', js: true do context 'adding a new note on a Commit' do
let(:commit) { project.commit } let(:noteable) { project.commit }
let(:note) { create(:note_on_commit, project: project, commit_id: project.commit.id) }
before do before do
allow(commit).to receive(:author).and_return(author) allow_any_instance_of(Commit).to receive(:author).and_return(author)
create(:note_on_commit, author: participant, project: project, commit_id: project.repository.commit.id, note: 'No More Parties in LA')
visit_commit(project, commit)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end
def open_member_suggestions visit namespace_project_commit_path(project.namespace, project, noteable)
page.within('.new-note') do
find('#note_note').send_keys('@')
end end
wait_for_ajax
end
def visit_issue(project, issue)
visit namespace_project_issue_path(project.namespace, project, issue)
end
def visit_merge_request(project, merge)
visit namespace_project_merge_request_path(project.namespace, project, merge)
end
def visit_commit(project, commit) include_examples "open suggestions when typing @"
visit namespace_project_commit_path(project.namespace, project, commit)
end end
end end
...@@ -58,7 +58,7 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper ...@@ -58,7 +58,7 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
}); });
describe('search', () => { describe('search', () => {
const defaultParams = '?scope=all&utf8=&state=opened'; const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
it('should search with a single word', (done) => { it('should search with a single word', (done) => {
input.value = 'searchTerm'; input.value = 'searchTerm';
......
import Vue from 'vue';
import VueResource from 'vue-resource';
import Poll from '~/lib/utils/poll';
Vue.use(VueResource);
class ServiceMock {
constructor(endpoint) {
this.service = Vue.resource(endpoint);
}
fetch() {
return this.service.get();
}
}
describe('Poll', () => {
let callbacks;
beforeEach(() => {
callbacks = {
success: () => {},
error: () => {},
};
spyOn(callbacks, 'success');
spyOn(callbacks, 'error');
});
it('calls the success callback when no header for interval is provided', (done) => {
const successInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), { status: 200 }));
};
Vue.http.interceptors.push(successInterceptor);
new Poll({
resource: new ServiceMock('endpoint'),
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
setTimeout(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
done();
}, 0);
Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
});
it('calls the error callback whe the http request returns an error', (done) => {
const errorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), { status: 500 }));
};
Vue.http.interceptors.push(errorInterceptor);
new Poll({
resource: new ServiceMock('endpoint'),
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
setTimeout(() => {
expect(callbacks.success).not.toHaveBeenCalled();
expect(callbacks.error).toHaveBeenCalled();
done();
}, 0);
Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
});
it('should call the success callback when the interval header is -1', (done) => {
const intervalInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': -1 } }));
};
Vue.http.interceptors.push(intervalInterceptor);
new Poll({
resource: new ServiceMock('endpoint'),
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
setTimeout(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
done();
}, 0);
Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
});
it('starts polling when http status is 200 and interval header is provided', (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();
new Poll({
resource: service,
method: 'fetch',
data: { page: 1 },
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
setTimeout(() => {
expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
done();
}, 5);
Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
});
});
...@@ -3,35 +3,55 @@ import UserCallout from '~/user_callout'; ...@@ -3,35 +3,55 @@ import UserCallout from '~/user_callout';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
describe('UserCallout', () => { describe('UserCallout', function () {
const fixtureName = 'static/user_callout.html.raw'; const fixtureName = 'static/user_callout.html.raw';
preloadFixtures(fixtureName); preloadFixtures(fixtureName);
beforeEach(function () { beforeEach(() => {
loadFixtures(fixtureName); loadFixtures(fixtureName);
Cookies.remove(USER_CALLOUT_COOKIE);
this.userCallout = new UserCallout(); this.userCallout = new UserCallout();
this.closeButton = $('.close-user-callout'); this.closeButton = $('.close-user-callout');
this.userCalloutBtn = $('.user-callout-btn'); this.userCalloutBtn = $('.user-callout-btn');
this.userCalloutContainer = $('.user-callout'); this.userCalloutContainer = $('.user-callout');
Cookies.set(USER_CALLOUT_COOKIE, 'false');
}); });
afterEach(function () { it('does not show when cookie is set not defined', () => {
Cookies.set(USER_CALLOUT_COOKIE, 'false'); expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeUndefined();
expect(this.userCalloutContainer.is(':visible')).toBe(true);
}); });
it('shows when cookie is set to false', function () { it('shows when cookie is set to false', () => {
Cookies.set(USER_CALLOUT_COOKIE, 'false');
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeDefined(); expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeDefined();
expect(this.userCalloutContainer.is(':visible')).toBe(true); expect(this.userCalloutContainer.is(':visible')).toBe(true);
}); });
it('hides when user clicks on the dismiss-icon', function () { it('hides when user clicks on the dismiss-icon', () => {
this.closeButton.click(); this.closeButton.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true'); expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
}); });
it('hides when user clicks on the "check it out" button', function () { it('hides when user clicks on the "check it out" button', () => {
this.userCalloutBtn.click(); this.userCalloutBtn.click();
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);
});
});
...@@ -33,6 +33,7 @@ describe Boards::Issues::ListService, services: true do ...@@ -33,6 +33,7 @@ describe Boards::Issues::ListService, services: true do
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) } let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) } let!(:closed_issue3) { create(:issue, :closed, project: project) }
let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) } let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) }
before do before do
project.team << [user, :developer] project.team << [user, :developer]
...@@ -74,7 +75,7 @@ describe Boards::Issues::ListService, services: true do ...@@ -74,7 +75,7 @@ describe Boards::Issues::ListService, services: true do
issues = described_class.new(project, user, params).execute issues = described_class.new(project, user, params).execute
expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1]
end end
it 'returns opened issues that have label list applied when listing issues from a label list' do it 'returns opened issues that have label list applied when listing issues from a label list' do
......
...@@ -138,7 +138,7 @@ describe Issuable::BulkUpdateService, services: true do ...@@ -138,7 +138,7 @@ describe Issuable::BulkUpdateService, services: true do
let(:labels) { [bug, regression] } let(:labels) { [bug, regression] }
it 'updates the labels of all issues passed to the labels passed' do it 'updates the labels of all issues passed to the labels passed' do
expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id)))
end end
it 'does not update issues not passed in' do it 'does not update issues not passed in' do
......
...@@ -35,7 +35,8 @@ RSpec.configure do |config| ...@@ -35,7 +35,8 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature config.include SearchHelpers, type: :feature
config.include WaitForAjax, type: :feature config.include WaitForRequests, :js
config.include WaitForAjax, :js
config.include StubConfiguration config.include StubConfiguration
config.include EmailHelpers, type: :mailer config.include EmailHelpers, type: :mailer
config.include TestEnv config.include TestEnv
......
...@@ -324,5 +324,24 @@ RSpec.shared_examples 'slack or mattermost notifications' do ...@@ -324,5 +324,24 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it_behaves_like 'call Slack/Mattermost API' it_behaves_like 'call Slack/Mattermost API'
end end
end end
context 'only notify for the default branch' do
context 'when enabled' do
let(:pipeline) do
create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch')
end
before do
chat_service.notify_only_default_branch = true
end
it 'does not call the Slack/Mattermost API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
result = chat_service.execute(data)
expect(result).to be_falsy
end
end
end
end end
end end
module WaitForRequests
extend self
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
def wait_for_requests_complete
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
wait_for('pending AJAX requests complete') do
Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero?
end
ensure
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
# Waits until the passed block returns true
def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01)
wait_until = Time.now + max_wait_time.seconds
loop do
break if yield
if Time.now > wait_until
raise "Condition not met: #{condition_name}"
else
sleep(polling_interval)
end
end
end
end
RSpec.configure do |config|
config.after(:each, :js) do
wait_for_requests_complete
end
end
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