Commit bfcee4a5 authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch 'master' into issue_14952

parents 946b4519 b6d5fcd4
...@@ -2,7 +2,6 @@ image: "ruby:2.1" ...@@ -2,7 +2,6 @@ image: "ruby:2.1"
services: services:
- mysql:latest - mysql:latest
- postgres:latest
- redis:latest - redis:latest
cache: cache:
...@@ -35,123 +34,79 @@ spec:feature: ...@@ -35,123 +34,79 @@ spec:feature:
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
tags:
- ruby
- mysql
spec:api: spec:api:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api
tags:
- ruby
- mysql
spec:models: spec:models:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
tags:
- ruby
- mysql
spec:lib: spec:lib:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
tags:
- ruby
- mysql
spec:services: spec:services:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
tags:
- ruby
- mysql
spec:other: spec:other:
stage: test stage: test
script: script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
tags:
- ruby
- mysql
spinach:project:half: spinach:project:half:
stage: test stage: test
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
tags:
- ruby
- mysql
spinach:project:rest: spinach:project:rest:
stage: test stage: test
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
tags:
- ruby
- mysql
spinach:other: spinach:other:
stage: test stage: test
script: script:
- RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other
tags:
- ruby
- mysql
teaspoon: teaspoon:
stage: test stage: test
script: script:
- RAILS_ENV=test bundle exec teaspoon - RAILS_ENV=test bundle exec teaspoon
tags:
- ruby
- mysql
rubocop: rubocop:
stage: test stage: test
script: script:
- bundle exec rubocop - bundle exec rubocop
tags:
- ruby
- mysql
scss-lint: scss-lint:
stage: test stage: test
script: script:
- bundle exec rake scss_lint - bundle exec rake scss_lint
tags:
- ruby
brakeman: brakeman:
stage: test stage: test
script: script:
- bundle exec rake brakeman - bundle exec rake brakeman
tags:
- ruby
- mysql
flog: flog:
stage: test stage: test
script: script:
- bundle exec rake flog - bundle exec rake flog
tags:
- ruby
- mysql
flay: flay:
stage: test stage: test
script: script:
- bundle exec rake flay - bundle exec rake flay
tags:
- ruby
- mysql
bundler:audit: bundler:audit:
stage: test stage: test
...@@ -159,9 +114,6 @@ bundler:audit: ...@@ -159,9 +114,6 @@ bundler:audit:
- master - master
script: script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941" - "bundle exec bundle-audit check --update --ignore OSVDB-115941"
tags:
- ruby
- mysql
# Ruby 2.2 jobs # Ruby 2.2 jobs
...@@ -177,9 +129,6 @@ spec:feature:ruby22: ...@@ -177,9 +129,6 @@ spec:feature:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:api:ruby22: spec:api:ruby22:
stage: test stage: test
...@@ -192,9 +141,6 @@ spec:api:ruby22: ...@@ -192,9 +141,6 @@ spec:api:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:models:ruby22: spec:models:ruby22:
stage: test stage: test
...@@ -207,9 +153,6 @@ spec:models:ruby22: ...@@ -207,9 +153,6 @@ spec:models:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:lib:ruby22: spec:lib:ruby22:
stage: test stage: test
...@@ -222,9 +165,6 @@ spec:lib:ruby22: ...@@ -222,9 +165,6 @@ spec:lib:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:services:ruby22: spec:services:ruby22:
stage: test stage: test
...@@ -237,9 +177,6 @@ spec:services:ruby22: ...@@ -237,9 +177,6 @@ spec:services:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spec:other:ruby22: spec:other:ruby22:
stage: test stage: test
...@@ -252,9 +189,6 @@ spec:other:ruby22: ...@@ -252,9 +189,6 @@ spec:other:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spinach:project:half:ruby22: spinach:project:half:ruby22:
stage: test stage: test
...@@ -268,9 +202,6 @@ spinach:project:half:ruby22: ...@@ -268,9 +202,6 @@ spinach:project:half:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spinach:project:rest:ruby22: spinach:project:rest:ruby22:
stage: test stage: test
...@@ -284,9 +215,6 @@ spinach:project:rest:ruby22: ...@@ -284,9 +215,6 @@ spinach:project:rest:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
spinach:other:ruby22: spinach:other:ruby22:
stage: test stage: test
...@@ -300,10 +228,6 @@ spinach:other:ruby22: ...@@ -300,10 +228,6 @@ spinach:other:ruby22:
key: "ruby22" key: "ruby22"
paths: paths:
- vendor - vendor
tags:
- ruby
- mysql
notify:slack: notify:slack:
stage: notifications stage: notifications
......
...@@ -14,6 +14,7 @@ v 8.7.0 (unreleased) ...@@ -14,6 +14,7 @@ v 8.7.0 (unreleased)
- Add links to CI setup documentation from project settings and builds pages - Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu) - Handle nil descriptions in Slack issue messages (Stan Hu)
- Add default scope to projects to exclude projects pending deletion - Add default scope to projects to exclude projects pending deletion
- Ensure empty recipients are rejected in BuildsEmailService
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Gracefully handle notes on deleted commits in merge requests (Stan Hu)
...@@ -22,6 +23,7 @@ v 8.7.0 (unreleased) ...@@ -22,6 +23,7 @@ v 8.7.0 (unreleased)
- Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
- Improved UX of the navigation sidebar - Improved UX of the navigation sidebar
- Build status notifications - Build status notifications
- API: Expose user location (Robert Schilling)
v 8.6.5 (unreleased) v 8.6.5 (unreleased)
- Check permissions when user attempts to import members from another project - Check permissions when user attempts to import members from another project
......
...@@ -177,10 +177,11 @@ class GitLabDropdown ...@@ -177,10 +177,11 @@ class GitLabDropdown
selector = ".dropdown-page-one .dropdown-content a" selector = ".dropdown-page-one .dropdown-content a"
@dropdown.on "click", selector, (e) -> @dropdown.on "click", selector, (e) ->
selected = self.rowClicked $(@) $el = $(@)
selected = self.rowClicked $el
if self.options.clicked if self.options.clicked
self.options.clicked(selected) self.options.clicked(selected, $el, e)
# Finds an element inside wrapper element # Finds an element inside wrapper element
getElement: (selector) -> getElement: (selector) ->
...@@ -360,6 +361,8 @@ class GitLabDropdown ...@@ -360,6 +361,8 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel
else
selectedObject
else else
if !value? if !value?
field.remove() field.remove()
...@@ -375,7 +378,7 @@ class GitLabDropdown ...@@ -375,7 +378,7 @@ class GitLabDropdown
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
if value? if value?
if !field.length if !field.length and fieldName
# Create hidden input for form # Create hidden input for form
input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" input = "<input type='hidden' name='#{fieldName}' value='#{value}' />"
if @options.inputId? if @options.inputId?
...@@ -394,7 +397,7 @@ class GitLabDropdown ...@@ -394,7 +397,7 @@ class GitLabDropdown
selector = ".dropdown-page-one #{selector}" selector = ".dropdown-page-one #{selector}"
# simulate a click on the first link # simulate a click on the first link
$(selector).trigger "click" $(selector, @dropdown).trigger "click"
addArrowKeyEvent: -> addArrowKeyEvent: ->
ARROW_KEY_CODES = [38, 40] ARROW_KEY_CODES = [38, 40]
......
...@@ -26,6 +26,20 @@ ...@@ -26,6 +26,20 @@
$(".selected_issue").bind "change", Issues.checkChanged $(".selected_issue").bind "change", Issues.checkChanged
# Update state filters if present in page
updateStateFilters: ->
stateFilters = $('.issues-state-filters')
newParams = {}
paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
for paramKey in paramKeys
newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
if stateFilters.length
stateFilters.find('a').each ->
initialUrl = $(this).attr 'href'
$(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
# Make sure we trigger ajax request only after user stop typing # Make sure we trigger ajax request only after user stop typing
initSearch: -> initSearch: ->
@timer = null @timer = null
...@@ -54,6 +68,7 @@ ...@@ -54,6 +68,7 @@
# Change url so if user reload a page - search results are saved # Change url so if user reload a page - search results are saved
history.replaceState {page: issuesUrl}, document.title, issuesUrl history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload() Issues.reload()
Issues.updateStateFilters()
dataType: "json" dataType: "json"
checkChanged: -> checkChanged: ->
......
((w) ->
w.gl ?= {}
w.gl.utils ?= {}
w.gl.utils.getUrlParameter = (sParam) ->
sPageURL = decodeURIComponent(window.location.search.substring(1))
sURLVariables = sPageURL.split('&')
sParameterName = undefined
i = 0
while i < sURLVariables.length
sParameterName = sURLVariables[i].split('=')
if sParameterName[0] is sParam
return if sParameterName[1] is undefined then true else sParameterName[1]
i++
# #
# @param {Object} params - url keys and value to merge
# @param {String} url
# #
w.gl.utils.mergeUrlParams = (params, url) ->
newUrl = decodeURIComponent(url)
for paramName, paramValue of params
pattern = new RegExp "\\b(#{paramName}=).*?(&|$)"
if url.search(pattern) >= 0
newUrl = newUrl.replace pattern, "$1#{paramValue}$2"
else
newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
newUrl
) window
...@@ -62,6 +62,8 @@ class @SearchAutocomplete ...@@ -62,6 +62,8 @@ class @SearchAutocomplete
search: search:
fields: ['text'] fields: ['text']
data: @getData.bind(@) data: @getData.bind(@)
selectable: true
clicked: @onClick.bind(@)
getData: (term, callback) -> getData: (term, callback) ->
_this = @ _this = @
...@@ -102,6 +104,8 @@ class @SearchAutocomplete ...@@ -102,6 +104,8 @@ class @SearchAutocomplete
lastCategory = suggestion.category lastCategory = suggestion.category
data.push data.push
id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}"
category: suggestion.category
text: suggestion.label text: suggestion.label
url: suggestion.url url: suggestion.url
...@@ -133,12 +137,19 @@ class @SearchAutocomplete ...@@ -133,12 +137,19 @@ class @SearchAutocomplete
} }
bindEvents: -> bindEvents: ->
$(document).on 'click', @onDocumentClick
@searchInput.on 'keydown', @onSearchInputKeyDown @searchInput.on 'keydown', @onSearchInputKeyDown
@searchInput.on 'keyup', @onSearchInputKeyUp @searchInput.on 'keyup', @onSearchInputKeyUp
@searchInput.on 'click', @onSearchInputClick @searchInput.on 'click', @onSearchInputClick
@searchInput.on 'focus', @onSearchInputFocus @searchInput.on 'focus', @onSearchInputFocus
@searchInput.on 'blur', @onSearchInputBlur @clearInput.on 'click', @onClearInputClick
@clearInput.on 'click', @onRemoveLocationClick
onDocumentClick: (e) =>
# If clicking outside the search box
# And search input is not focused
# And we are not clicking inside a suggestion
if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length
@onSearchInputBlur()
enableAutocomplete: -> enableAutocomplete: ->
# No need to enable anything if user is not logged in # No need to enable anything if user is not logged in
...@@ -181,6 +192,8 @@ class @SearchAutocomplete ...@@ -181,6 +192,8 @@ class @SearchAutocomplete
# We should display the menu only when input is not empty # We should display the menu only when input is not empty
@enableAutocomplete() @enableAutocomplete()
@wrap.toggleClass 'has-value', !!e.target.value
# Avoid falsy value to be returned # Avoid falsy value to be returned
return return
...@@ -189,27 +202,20 @@ class @SearchAutocomplete ...@@ -189,27 +202,20 @@ class @SearchAutocomplete
e.stopImmediatePropagation() e.stopImmediatePropagation()
onSearchInputFocus: => onSearchInputFocus: =>
@isFocused = true
@wrap.addClass('search-active') @wrap.addClass('search-active')
onRemoveLocationClick: (e) => onClearInputClick: (e) =>
e.preventDefault() e.preventDefault()
@removeLocationBadge()
@searchInput.val('').focus() @searchInput.val('').focus()
@skipBlurEvent = true
onSearchInputBlur: (e) => onSearchInputBlur: (e) =>
@skipBlurEvent = false @isFocused = false
@wrap.removeClass('search-active')
# We should wait to make sure we are not clearing the input instead
setTimeout( =>
return if @skipBlurEvent
@wrap.removeClass('search-active') # If input is blank then restore state
if @searchInput.val() is ''
# If input is blank then restore state @restoreOriginalState()
if @searchInput.val() is ''
@restoreOriginalState()
, 150)
addLocationBadge: (item) -> addLocationBadge: (item) ->
category = if item.category? then "#{item.category}: " else '' category = if item.category? then "#{item.category}: " else ''
...@@ -268,3 +274,23 @@ class @SearchAutocomplete ...@@ -268,3 +274,23 @@ class @SearchAutocomplete
<li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
</ul>" </ul>"
@dropdownContent.html(html) @dropdownContent.html(html)
onClick: (item, $el, e) ->
if location.pathname.indexOf(item.url) isnt -1
e.preventDefault()
if not @badgePresent
if item.category is 'Projects'
@projectInputEl.val(item.id)
@addLocationBadge(
value: 'This project'
)
if item.category is 'Groups'
@groupInputEl.val(item.id)
@addLocationBadge(
value: 'This group'
)
$el.removeClass('is-active')
@disableAutocomplete()
@searchInput.val('').focus()
...@@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80); ...@@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80);
$orange-normal: #e75e40; $orange-normal: #e75e40;
$orange-dark: #ce5237; $orange-dark: #ce5237;
$red-light: #f06559; $red-light: #e52c5a;
$red-normal: #e52c5a; $red-normal: #d22852;
$red-dark: #d22852; $red-dark: darken($red-normal, 5%);
$border-white-light: #f1f2f4; $border-white-light: #f1f2f4;
$border-white-normal: #d6dae2; $border-white-normal: #d6dae2;
...@@ -128,9 +128,9 @@ $border-orange-light: #fc6d26; ...@@ -128,9 +128,9 @@ $border-orange-light: #fc6d26;
$border-orange-normal: #ce5237; $border-orange-normal: #ce5237;
$border-orange-dark: #c14e35; $border-orange-dark: #c14e35;
$border-red-light: #f24f41; $border-red-light: #d22852;
$border-red-normal: #d22852; $border-red-normal: #ca264f;
$border-red-dark: #ca264f; $border-red-dark: darken($border-red-normal, 5%);
$help-well-bg: #fafafa; $help-well-bg: #fafafa;
$help-well-border: #e5e5e5; $help-well-border: #e5e5e5;
...@@ -201,14 +201,14 @@ $award-emoji-new-btn-icon-color: #dcdcdc; ...@@ -201,14 +201,14 @@ $award-emoji-new-btn-icon-color: #dcdcdc;
/* /*
* Search Box * Search Box
*/ */
$search-input-border-color: $dropdown-input-focus-border; $search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-focus-shadow-color: $dropdown-input-focus-shadow;
$search-input-width: $dropdown-width; $search-input-width: 244px;
$location-badge-color: #aaa; $location-badge-color: #aaa;
$location-badge-bg: $gray-normal; $location-badge-bg: $gray-normal;
$location-badge-active-bg: #4f91f8;
$location-icon-color: #e7e9ed; $location-icon-color: #e7e9ed;
$location-active-color: $gl-text-color; $location-icon-active-color: #807e7e;
$location-active-bg: $search-input-border-color;
/* /*
* Notes * Notes
......
...@@ -315,7 +315,7 @@ pre.light-well { ...@@ -315,7 +315,7 @@ pre.light-well {
} }
.git-empty { .git-empty {
margin: 0 7px; margin: 0 7px 7px;
h5 { h5 {
color: #5c5d5e; color: #5c5d5e;
...@@ -401,7 +401,7 @@ pre.light-well { ...@@ -401,7 +401,7 @@ pre.light-well {
} }
.commit_short_id { .commit_short_id {
margin-right: 5px; margin: 0 5px;
color: $gl-link-color; color: $gl-link-color;
font-weight: 600; font-weight: 600;
} }
......
...@@ -135,25 +135,25 @@ ...@@ -135,25 +135,25 @@
.location-badge { .location-badge {
@include transition(all .15s); @include transition(all .15s);
background-color: $location-active-bg; background-color: $location-badge-active-bg;
color: $white-light; color: $white-light;
} }
.search-input-wrap { .search-input-wrap {
i { i {
color: $location-active-color; color: $location-icon-active-color;
} }
} }
}
&.has-location-badge { &.has-value {
.search-icon { .search-icon {
display: none; display: none;
} }
.clear-icon { .clear-icon {
cursor: pointer; cursor: pointer;
display: block; display: block;
}
} }
} }
......
.container-fluid .content { .container-fluid {
.ci-status { .ci-status {
padding: 2px 7px; padding: 2px 7px;
margin-right: 5px; margin-right: 5px;
......
...@@ -50,12 +50,15 @@ class BuildsEmailService < Service ...@@ -50,12 +50,15 @@ class BuildsEmailService < Service
def execute(push_data) def execute(push_data)
return unless supported_events.include?(push_data[:object_kind]) return unless supported_events.include?(push_data[:object_kind])
return unless should_build_be_notified?(push_data)
if should_build_be_notified?(push_data) recipients = all_recipients(push_data)
if recipients.any?
BuildEmailWorker.perform_async( BuildEmailWorker.perform_async(
push_data[:build_id], push_data[:build_id],
all_recipients(push_data), recipients,
push_data, push_data
) )
end end
end end
...@@ -84,7 +87,7 @@ class BuildsEmailService < Service ...@@ -84,7 +87,7 @@ class BuildsEmailService < Service
end end
def all_recipients(data) def all_recipients(data)
all_recipients = recipients.split(',') all_recipients = recipients.split(',').compact.reject(&:blank?)
if add_pusher? && data[:user][:email] if add_pusher? && data[:user][:email]
all_recipients << "#{data[:user][:email]}" all_recipients << "#{data[:user][:email]}"
......
...@@ -21,8 +21,8 @@ ...@@ -21,8 +21,8 @@
%a.is-focused.dropdown-menu-empty-link %a.is-focused.dropdown-menu-empty-link
Loading... Loading...
= dropdown_loading = dropdown_loading
%i.search-icon %i.search-icon
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
= hidden_field_tag :group_id, @group.try(:id) = hidden_field_tag :group_id, @group.try(:id)
= hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
= access = access
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil-square-o') = icon('pencil-square-o')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete' do
= icon('trash-o') = icon('trash-o')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text .note-text
......
...@@ -69,6 +69,7 @@ GET /users ...@@ -69,6 +69,7 @@ GET /users
"state": "blocked", "state": "blocked",
"created_at": "2012-05-23T08:01:01Z", "created_at": "2012-05-23T08:01:01Z",
"bio": null, "bio": null,
"location": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
"twitter": "", "twitter": "",
...@@ -126,6 +127,7 @@ Parameters: ...@@ -126,6 +127,7 @@ Parameters:
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"is_admin": false, "is_admin": false,
"bio": null, "bio": null,
"location": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
"twitter": "", "twitter": "",
...@@ -154,6 +156,7 @@ Parameters: ...@@ -154,6 +156,7 @@ Parameters:
"confirmed_at": "2012-05-23T08:00:58Z", "confirmed_at": "2012-05-23T08:00:58Z",
"last_sign_in_at": "2015-03-23T08:00:58Z", "last_sign_in_at": "2015-03-23T08:00:58Z",
"bio": null, "bio": null,
"location": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
"twitter": "", "twitter": "",
...@@ -191,6 +194,7 @@ Parameters: ...@@ -191,6 +194,7 @@ Parameters:
- `extern_uid` (optional) - External UID - `extern_uid` (optional) - External UID
- `provider` (optional) - External provider name - `provider` (optional) - External provider name
- `bio` (optional) - User's biography - `bio` (optional) - User's biography
- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default) - `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false - `can_create_group` (optional) - User can create groups - true or false
- `confirm` (optional) - Require confirmation - true (default) or false - `confirm` (optional) - Require confirmation - true (default) or false
...@@ -218,6 +222,7 @@ Parameters: ...@@ -218,6 +222,7 @@ Parameters:
- `extern_uid` - External UID - `extern_uid` - External UID
- `provider` - External provider name - `provider` - External provider name
- `bio` - User's biography - `bio` - User's biography
- `location` (optional) - User's location
- `admin` (optional) - User is admin - true or false (default) - `admin` (optional) - User is admin - true or false (default)
- `can_create_group` (optional) - User can create groups - true or false - `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default) - `external` (optional) - Flags the user as external - true or false(default)
...@@ -260,6 +265,7 @@ GET /user ...@@ -260,6 +265,7 @@ GET /user
"state": "active", "state": "active",
"created_at": "2012-05-23T08:00:58Z", "created_at": "2012-05-23T08:00:58Z",
"bio": null, "bio": null,
"location": null,
"skype": "", "skype": "",
"linkedin": "", "linkedin": "",
"twitter": "", "twitter": "",
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- [CI setup](ci_setup.md) for testing GitLab - [CI setup](ci_setup.md) for testing GitLab
- [Gotchas](gotchas.md) to avoid - [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md) - [How to dump production data to staging](db_dump.md)
- [Instrumentation](instrumentation.md)
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations - [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [Rake tasks](rake_tasks.md) for development - [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase - [Shell commands](shell_commands.md) in the GitLab codebase
......
# Instrumenting Ruby Code
GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby
code. This can be used to measure the time spent in a specific part of a larger
chunk of code. The resulting data is written to a separate series.
To start measuring a block of Ruby code you should use
`Gitlab::Metrics.measure` and give it a name for the series to store the data
in:
```ruby
Gitlab::Metrics.measure(:user_logins) do
...
end
```
The first argument of this method is the series name and should be plural. This
name will be prefixed with `rails_` or `sidekiq_` depending on whether the code
was run in the Rails application or one of the Sidekiq workers. In the
above example the final series names would be as follows:
- rails_user_logins
- sidekiq_user_logins
Series names should be plural as this keeps the naming style in line with the
other series names.
By default metrics measured using a block contain a single value, "duration",
which contains the number of milliseconds it took to execute the block. Custom
values can be added by passing a Hash as the 2nd argument. Custom tags can be
added by passing a Hash as the 3rd argument. A simple example is as follows:
```ruby
Gitlab::Metrics.measure(:example_series, { number: 10 }, { class: self.class.to_s }) do
...
end
```
...@@ -15,7 +15,7 @@ module API ...@@ -15,7 +15,7 @@ module API
class User < UserBasic class User < UserBasic
expose :created_at expose :created_at
expose :is_admin?, as: :is_admin expose :is_admin?, as: :is_admin
expose :bio, :skype, :linkedin, :twitter, :website_url expose :bio, :location, :skype, :linkedin, :twitter, :website_url
end end
class Identity < Grape::Entity class Identity < Grape::Entity
......
...@@ -58,6 +58,7 @@ module API ...@@ -58,6 +58,7 @@ module API
# extern_uid - External authentication provider UID # extern_uid - External authentication provider UID
# provider - External provider # provider - External provider
# bio - Bio # bio - Bio
# location - Location of the user
# admin - User is admin - true or false (default) # admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false # can_create_group - User can create groups - true or false
# confirm - Require user confirmation - true (default) or false # confirm - Require user confirmation - true (default) or false
...@@ -67,7 +68,7 @@ module API ...@@ -67,7 +68,7 @@ module API
post do post do
authenticated_as_admin! authenticated_as_admin!
required_attributes! [:email, :password, :name, :username] required_attributes! [:email, :password, :name, :username]
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm, :external] attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
admin = attrs.delete(:admin) admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i)) confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
user = User.build_user(attrs) user = User.build_user(attrs)
...@@ -106,6 +107,7 @@ module API ...@@ -106,6 +107,7 @@ module API
# website_url - Website url # website_url - Website url
# projects_limit - Limit projects each user can create # projects_limit - Limit projects each user can create
# bio - Bio # bio - Bio
# location - Location of the user
# admin - User is admin - true or false (default) # admin - User is admin - true or false (default)
# can_create_group - User can create groups - true or false # can_create_group - User can create groups - true or false
# external - Flags the user as external - true or false(default) # external - Flags the user as external - true or false(default)
...@@ -114,7 +116,7 @@ module API ...@@ -114,7 +116,7 @@ module API
put ":id" do put ":id" do
authenticated_as_admin! authenticated_as_admin!
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external] attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
user = User.find(params[:id]) user = User.find(params[:id])
not_found!('User') unless user not_found!('User') unless user
......
...@@ -119,7 +119,7 @@ module Banzai ...@@ -119,7 +119,7 @@ module Banzai
elsif element_node?(node) elsif element_node?(node)
yield_valid_link(node) do |link, text| yield_valid_link(node) do |link, text|
if ref_pattern && link =~ /\A#{ref_pattern}/ if ref_pattern && link =~ /\A#{ref_pattern}\z/
replace_link_node_with_href(node, link) do replace_link_node_with_href(node, link) do
object_link_filter(link, ref_pattern, link_text: text) object_link_filter(link, ref_pattern, link_text: text)
end end
......
...@@ -70,6 +70,32 @@ module Gitlab ...@@ -70,6 +70,32 @@ module Gitlab
value.to_s.gsub('=', '\\=') value.to_s.gsub('=', '\\=')
end end
# Measures the execution time of a block.
#
# Example:
#
# Gitlab::Metrics.measure(:find_by_username_timings) do
# User.find_by_username(some_username)
# end
#
# series - The name of the series to store the data in.
# values - A Hash containing extra values to add to the metric.
# tags - A Hash containing extra tags to add to the metric.
#
# Returns the value yielded by the supplied block.
def self.measure(series, values = {}, tags = {})
return yield unless Transaction.current
start = Time.now.to_f
retval = yield
duration = (Time.now.to_f - start) * 1000.0
values = values.merge(duration: duration)
Transaction.current.add_metric(series, values, tags)
retval
end
# When enabled this should be set before being used as the usual pattern # When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe. # "@foo ||= bar" is _not_ thread-safe.
if enabled? if enabled?
......
require 'rails_helper'
describe 'Filter issues', feature: true do
let!(:project) { create(:project) }
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
before do
project.team << [user, :master]
login_as(user)
end
describe 'Filter issues for assignee from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-assignee-search').click
find('.dropdown-menu-user-link', text: user.username).click
sleep 2
end
context 'assignee', js: true do
it 'should update to current user' do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
it 'should not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
end
end
describe 'Filter issues for milestone from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-milestone-select').click
find('.milestone-filter .dropdown-content a', text: milestone.title).click
sleep 2
end
context 'milestone', js: true do
it 'should update to current milestone' do
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
it 'should not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
end
end
describe 'Filter issues for assignee and label from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-assignee-search').click
find('.dropdown-menu-user-link', text: user.username).click
sleep 2
find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
sleep 2
end
context 'assignee and label', js: true do
it 'should update to current assignee and label' do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
it 'should not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
it 'should not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
end
end
end
...@@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do ...@@ -95,6 +95,14 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
result = reference_pipeline_result("Fixed #{reference}") result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue] expect(result[:references][:issue]).to eq [issue]
end end
it 'does not process links containing issue numbers followed by text' do
href = "#{reference}st"
doc = reference_filter("<a href='#{href}'></a>")
link = doc.css('a').first.attr('href')
expect(link).to eq(href)
end
end end
context 'cross-project reference' do context 'cross-project reference' do
......
...@@ -13,7 +13,7 @@ describe Gitlab::Metrics do ...@@ -13,7 +13,7 @@ describe Gitlab::Metrics do
end end
end end
describe '#submit_metrics' do describe '.submit_metrics' do
it 'prepares and writes the metrics to InfluxDB' do it 'prepares and writes the metrics to InfluxDB' do
connection = double(:connection) connection = double(:connection)
pool = double(:pool) pool = double(:pool)
...@@ -26,7 +26,7 @@ describe Gitlab::Metrics do ...@@ -26,7 +26,7 @@ describe Gitlab::Metrics do
end end
end end
describe '#prepare_metrics' do describe '.prepare_metrics' do
it 'returns a Hash with the keys as Symbols' do it 'returns a Hash with the keys as Symbols' do
metrics = described_class. metrics = described_class.
prepare_metrics([{ 'values' => {}, 'tags' => {} }]) prepare_metrics([{ 'values' => {}, 'tags' => {} }])
...@@ -51,7 +51,7 @@ describe Gitlab::Metrics do ...@@ -51,7 +51,7 @@ describe Gitlab::Metrics do
end end
end end
describe '#escape_value' do describe '.escape_value' do
it 'escapes an equals sign' do it 'escapes an equals sign' do
expect(described_class.escape_value('foo=')).to eq('foo\\=') expect(described_class.escape_value('foo=')).to eq('foo\\=')
end end
...@@ -60,4 +60,45 @@ describe Gitlab::Metrics do ...@@ -60,4 +60,45 @@ describe Gitlab::Metrics do
expect(described_class.escape_value(10)).to eq('10') expect(described_class.escape_value(10)).to eq('10')
end end
end end
describe '.measure' do
context 'without a transaction' do
it 'returns the return value of the block' do
val = Gitlab::Metrics.measure(:foo) { 10 }
expect(val).to eq(10)
end
end
context 'with a transaction' do
let(:transaction) { Gitlab::Metrics::Transaction.new }
before do
allow(Gitlab::Metrics::Transaction).to receive(:current).
and_return(transaction)
end
it 'adds a metric to the current transaction' do
expect(transaction).to receive(:add_metric).
with(:foo, { duration: a_kind_of(Numeric) }, { tag: 'value' })
Gitlab::Metrics.measure(:foo, {}, tag: 'value') { 10 }
end
it 'supports adding of custom values' do
values = { duration: a_kind_of(Numeric), number: 10 }
expect(transaction).to receive(:add_metric).
with(:foo, values, { tag: 'value' })
Gitlab::Metrics.measure(:foo, { number: 10 }, tag: 'value') { 10 }
end
it 'returns the return value of the block' do
val = Gitlab::Metrics.measure(:foo) { 10 }
expect(val).to eq(10)
end
end
end
end end
...@@ -6,18 +6,38 @@ describe BuildsEmailService do ...@@ -6,18 +6,38 @@ describe BuildsEmailService do
let(:service) { BuildsEmailService.new } let(:service) { BuildsEmailService.new }
describe :execute do describe :execute do
it "sends email" do it 'sends email' do
service.recipients = 'test@gitlab.com' service.recipients = 'test@gitlab.com'
data[:build_status] = 'failed' data[:build_status] = 'failed'
expect(BuildEmailWorker).to receive(:perform_async) expect(BuildEmailWorker).to receive(:perform_async)
service.execute(data) service.execute(data)
end end
it "does not sends email with failed build and allowed_failure on" do it 'does not send email with succeeded build and notify_only_broken_builds on' do
expect(service).to receive(:notify_only_broken_builds).and_return(true)
data[:build_status] = 'success'
expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data)
end
it 'does not send email with failed build and build_allow_failure is true' do
data[:build_status] = 'failed' data[:build_status] = 'failed'
data[:build_allow_failure] = true data[:build_allow_failure] = true
expect(BuildEmailWorker).not_to receive(:perform_async) expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data) service.execute(data)
end end
it 'does not send email with unknown build status' do
data[:build_status] = 'foo'
expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data)
end
it 'does not send email when recipients list is empty' do
service.recipients = ' ,, '
data[:build_status] = 'failed'
expect(BuildEmailWorker).not_to receive(:perform_async)
service.execute(data)
end
end end
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