summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG3
-rw-r--r--app/assets/javascripts/api.js.coffee14
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee270
-rw-r--r--app/assets/javascripts/issue_status_select.js.coffee11
-rw-r--r--app/assets/javascripts/labels_select.js.coffee92
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee60
-rw-r--r--app/assets/javascripts/notes.js.coffee31
-rw-r--r--app/assets/javascripts/projects_list.js.coffee6
-rw-r--r--app/assets/javascripts/users_select.js.coffee75
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss244
-rw-r--r--app/assets/stylesheets/framework/filters.scss1
-rw-r--r--app/assets/stylesheets/framework/typography.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss12
-rw-r--r--app/assets/stylesheets/pages/labels.scss22
-rw-r--r--app/assets/stylesheets/pages/snippets.scss8
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb4
-rw-r--r--app/controllers/explore/projects_controller.rb6
-rw-r--r--app/helpers/dropdowns_helper.rb100
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/user.rb7
-rw-r--r--app/views/help/ui.html.haml227
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/notes/_edit_form.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml91
-rw-r--r--app/views/shared/snippets/_blob.html.haml2
-rw-r--r--config/application.rb18
-rw-r--r--config/initializers/session_store.rb5
-rw-r--r--config/initializers/sidekiq.rb17
-rw-r--r--config/mail_room.yml9
-rw-r--r--features/steps/dashboard/issues.rb17
-rw-r--r--features/steps/dashboard/merge_requests.rb17
-rw-r--r--features/steps/project/issues/filter_labels.rb5
-rw-r--r--features/steps/project/issues/issues.rb11
-rw-r--r--lib/gitlab/exclusive_lease.rb41
-rw-r--r--lib/gitlab/redis_config.rb30
-rw-r--r--lib/gitlab/user_access.rb2
-rw-r--r--lib/tasks/cache.rake6
-rw-r--r--public/logo.svg35
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb7
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb7
-rw-r--r--spec/lib/gitlab/exclusive_lease_spec.rb21
43 files changed, 1428 insertions, 128 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 9474ca50b24..aaffa51898d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -14,6 +14,8 @@ v 8.6.0 (unreleased)
- Strip leading and trailing spaces in URL validator (evuez)
- Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez)
- Return empty array instead of 404 when commit has no statuses in commit status API
+ - Decrease the font size and the padding of the `.anchor` icons used in the README (Roberto Dip)
+ - Rewrite logo to simplify SVG code (Sean Lang)
- Add support for cross-project label references
- Update documentation to reflect Guest role not being enforced on internal projects
- Allow search for logged out users
@@ -24,6 +26,7 @@ v 8.6.0 (unreleased)
- Show labels in dashboard and group milestone views
- Add main language of a project in the list of projects (Tiago Botelho)
- Add ability to show archived projects on dashboard, explore and group pages
+ - Fix pagination for filtered dashboard and explore pages
v 8.5.5
- Ensure removing a project removes associated Todo entries.
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 3e0fdb3f795..2ddf8612db3 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -4,6 +4,7 @@
namespaces_path: "/api/:version/namespaces.json"
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
+ labels_path: "/api/:version/projects/:id/labels"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -61,6 +62,19 @@
).done (projects) ->
callback(projects)
+ newLabel: (project_id, data, callback) ->
+ url = Api.buildUrl(Api.labels_path)
+ url = url.replace(':id', project_id)
+
+ data.private_token = gon.api_token
+ $.ajax(
+ url: url
+ type: "POST"
+ data: data
+ dataType: "json"
+ ).done (label) ->
+ callback(label)
+
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
new file mode 100644
index 00000000000..8e1449bc59c
--- /dev/null
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -0,0 +1,270 @@
+class GitLabDropdownFilter
+ BLUR_KEYCODES = [27, 40]
+
+ constructor: (@dropdown, @options) ->
+ @input = @dropdown.find(".dropdown-input .dropdown-input-field")
+
+ # Key events
+ timeout = ""
+ @input.on "keyup", (e) =>
+ if e.keyCode is 13 && @input.val() isnt ""
+ if @options.enterCallback
+ @options.enterCallback()
+ return
+
+ clearTimeout timeout
+ timeout = setTimeout =>
+ blur_field = @shouldBlur e.keyCode
+ search_text = @input.val()
+
+ if blur_field
+ @input.blur()
+
+ if @options.remote
+ @options.query search_text, (data) =>
+ @options.callback(data)
+ else
+ @filter search_text
+ , 250
+
+ shouldBlur: (keyCode) ->
+ return BLUR_KEYCODES.indexOf(keyCode) >= 0
+
+ filter: (search_text) ->
+ data = @options.data()
+ results = data
+
+ if search_text isnt ""
+ results = fuzzaldrinPlus.filter(data, search_text,
+ key: @options.keys
+ )
+
+ @options.callback results
+
+class GitLabDropdownRemote
+ constructor: (@dataEndpoint, @options) ->
+
+ execute: ->
+ if typeof @dataEndpoint is "string"
+ @fetchData()
+ else if typeof @dataEndpoint is "function"
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data by calling the data funcfion
+ @dataEndpoint "", (data) =>
+ if @options.success
+ @options.success(data)
+
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data through ajax if the data is a string
+ fetchData: ->
+ $.ajax(
+ url: @dataEndpoint,
+ dataType: @options.dataType,
+ beforeSend: =>
+ if @options.beforeSend
+ @options.beforeSend()
+ success: (data) =>
+ if @options.success
+ @options.success(data)
+ )
+
+class GitLabDropdown
+ LOADING_CLASS = "is-loading"
+ PAGE_TWO_CLASS = "is-page-two"
+ ACTIVE_CLASS = "is-active"
+
+ constructor: (@el, @options) ->
+ self = @
+ @dropdown = $(@el).parent()
+ search_fields = if @options.search then @options.search.fields else [];
+
+ if @options.data
+ # Remote data
+ @remote = new GitLabDropdownRemote @options.data, {
+ dataType: @options.dataType,
+ beforeSend: @toggleLoading.bind(@)
+ success: (data) =>
+ @fullData = data
+
+ @parseData @fullData
+ }
+
+ # Init filiterable
+ if @options.filterable
+ @filter = new GitLabDropdownFilter @dropdown,
+ remote: @options.filterRemote
+ query: @options.data
+ keys: @options.search.fields
+ data: =>
+ return @fullData
+ callback: (data) =>
+ @parseData data
+ enterCallback: =>
+ @selectFirstRow()
+
+ # Event listeners
+ @dropdown.on "shown.bs.dropdown", @opened
+ @dropdown.on "hidden.bs.dropdown", @hidden
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
+ e.preventDefault()
+ e.stopPropagation()
+
+ @togglePage()
+
+ if @options.selectable
+ selector = ".dropdown-content a"
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content a"
+
+ @dropdown.on "click", selector, (e) ->
+ self.rowClicked $(@)
+
+ if self.options.clicked
+ self.options.clicked()
+
+ toggleLoading: ->
+ $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
+
+ togglePage: ->
+ menu = $('.dropdown-menu', @dropdown)
+
+ if menu.hasClass(PAGE_TWO_CLASS)
+ if @remote
+ @remote.execute()
+
+ menu.toggleClass PAGE_TWO_CLASS
+
+ parseData: (data) ->
+ @renderedData = data
+
+ # Render each row
+ html = $.map data, (obj) =>
+ return @renderItem(obj)
+
+ if @options.filterable and data.length is 0
+ # render no matching results
+ html = [@noResults()]
+
+ # Render the full menu
+ full_html = @renderMenu(html.join(""))
+
+ @appendMenu(full_html)
+
+ opened: =>
+ contentHtml = $('.dropdown-content', @dropdown).html()
+ if @remote && contentHtml is ""
+ @remote.execute()
+
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").focus()
+
+ hidden: =>
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").blur().val("")
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
+
+
+ # Render the full menu
+ renderMenu: (html) ->
+ menu_html = ""
+
+ if @options.renderMenu
+ menu_html = @options.renderMenu(html)
+ else
+ menu_html = "<ul>#{html}</ul>"
+
+ return menu_html
+
+ # Append the menu into the dropdown
+ appendMenu: (html) ->
+ selector = '.dropdown-content'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content"
+
+ $(selector, @dropdown).html html
+
+ # Render the row
+ renderItem: (data) ->
+ html = ""
+
+ return "<li class='divider'></li>" if data is "divider"
+
+ if @options.renderRow
+ # Call the render function
+ html = @options.renderRow(data)
+ else
+ selected = if @options.isSelected then @options.isSelected(data) else false
+ url = if @options.url then @options.url(data) else "#"
+ text = if @options.text then @options.text(data) else ""
+ cssClass = "";
+
+ if selected
+ cssClass = "is-active"
+
+ html = "<li>"
+ html += "<a href='#{url}' class='#{cssClass}'>"
+ html += text
+ html += "</a>"
+ html += "</li>"
+
+ return html
+
+ noResults: ->
+ html = "<li>"
+ html += "<a href='#' class='is-focused'>"
+ html += "No matching results."
+ html += "</a>"
+ html += "</li>"
+
+ rowClicked: (el) ->
+ fieldName = @options.fieldName
+ field = @dropdown.parent().find("input[name='#{fieldName}']")
+
+ if el.hasClass(ACTIVE_CLASS)
+ field.remove()
+ else
+ fieldName = @options.fieldName
+ selectedIndex = el.parent().index()
+ if @renderedData
+ selectedObject = @renderedData[selectedIndex]
+ value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+
+ if @options.multiSelect
+ oldValue = field.val()
+ if oldValue
+ value = "#{oldValue},#{value}"
+ else
+ @dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
+ field.remove()
+
+ # Toggle active class for the tick mark
+ el.toggleClass "is-active"
+
+ if value
+ if !field.length
+ # Create hidden input for form
+ input = "<input type='hidden' name='#{fieldName}' />"
+ @dropdown.before input
+
+ @dropdown.parent().find("input[name='#{fieldName}']").val value
+
+ selectFirstRow: ->
+ selector = '.dropdown-content li:first-child a'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content li:first-child a"
+
+ # similute a click on the first link
+ $(selector).trigger "click"
+
+$.fn.glDropdown = (opts) ->
+ return @.each ->
+ new GitLabDropdown @, opts
diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee
new file mode 100644
index 00000000000..c5740f27ddd
--- /dev/null
+++ b/app/assets/javascripts/issue_status_select.js.coffee
@@ -0,0 +1,11 @@
+class @IssueStatusSelect
+ constructor: ->
+ $('.js-issue-status').each (i, el) ->
+ fieldName = $(el).data("field-name")
+
+ $(el).glDropdown(
+ selectable: true
+ fieldName: fieldName
+ id: (obj, el) ->
+ $(el).data("id")
+ )
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
new file mode 100644
index 00000000000..5ade2cb66cb
--- /dev/null
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -0,0 +1,92 @@
+class @LabelsSelect
+ constructor: ->
+ $('.js-label-select').each (i, dropdown) ->
+ projectId = $(dropdown).data('project-id')
+ labelUrl = $(dropdown).data("labels")
+ selectedLabel = $(dropdown).data('selected')
+ if selectedLabel
+ selectedLabel = selectedLabel.split(",")
+ newLabelField = $('#new_label_name')
+ newColorField = $('#new_label_color')
+ showNo = $(dropdown).data('show-no')
+ showAny = $(dropdown).data('show-any')
+
+ if newLabelField.length
+ $('.suggest-colors-dropdown a').on "click", (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+ newColorField.val $(this).data("color")
+ $('.js-dropdown-label-color-preview')
+ .css 'background-color', $(this).data("color")
+ .addClass 'is-active'
+
+ $('.js-new-label-btn').on "click", (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+
+ if newLabelField.val() isnt "" && newColorField.val() isnt ""
+ $('.js-new-label-btn').disable()
+
+ # Create new label with API
+ Api.newLabel projectId, {
+ name: newLabelField.val()
+ color: newColorField.val()
+ }, (label) ->
+ $('.js-new-label-btn').enable()
+ $('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
+
+ $(dropdown).glDropdown(
+ data: (term, callback) ->
+ # We have to fetch the JS version of the labels list because there is no
+ # public facing JSON url for labels
+ $.ajax(
+ url: labelUrl
+ ).done (data) ->
+ html = $(data)
+ data = []
+ html.find('.label-row a').each ->
+ data.push(
+ title: $(@).text().trim()
+ )
+
+ if showNo
+ data.unshift(
+ id: "0"
+ title: 'No label'
+ )
+
+ if showAny
+ data.unshift(
+ title: 'Any label'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, "divider"
+
+ callback data
+ renderRow: (label) ->
+ if $.isArray(selectedLabel)
+ selected = ""
+ $.each selectedLabel, (i, selectedLbl) ->
+ selectedLbl = selectedLbl.trim()
+ if selected is "" && label.title is selectedLbl
+ selected = "is-active"
+ else
+ selected = if label.title is selectedLabel then "is-active" else ""
+
+ "<li>
+ <a href='#' class='#{selected}'>
+ #{label.title}
+ </a>
+ </li>"
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ fieldName: $(dropdown).data('field-name')
+ id: (label) ->
+ label.title
+ clicked: ->
+ if $(dropdown).hasClass "js-filter-submit"
+ $(dropdown).parents('form').submit()
+ )
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
new file mode 100644
index 00000000000..5e884454a65
--- /dev/null
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -0,0 +1,60 @@
+class @MilestoneSelect
+ constructor: ->
+ $('.js-milestone-select').each (i, dropdown) ->
+ projectId = $(dropdown).data('project-id')
+ milestonesUrl = $(dropdown).data('milestones')
+ selectedMilestone = $(dropdown).data('selected')
+ showNo = $(dropdown).data('show-no')
+ showAny = $(dropdown).data('show-any')
+ useId = $(dropdown).data('use-id')
+
+ $(dropdown).glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: milestonesUrl
+ ).done (data) ->
+ html = $(data)
+ data = []
+ html.find('.milestone strong a').each ->
+ link = $(@).attr("href").split("/")
+ data.push(
+ id: link[link.length - 1]
+ title: $(@).text().trim()
+ )
+
+ if showNo
+ data.unshift(
+ id: "0"
+ title: 'No Milestone'
+ )
+
+ if showAny
+ data.unshift(
+ title: 'Any Milestone'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, "divider"
+
+ callback(data)
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ fieldName: $(dropdown).data('field-name')
+ text: (milestone) ->
+ milestone.title
+ id: (milestone) ->
+ if !useId
+ if milestone.title isnt "Any milestone"
+ milestone.title
+ else
+ ""
+ else
+ milestone.id
+ isSelected: (milestone) ->
+ milestone.title is selectedMilestone
+ clicked: ->
+ if $(dropdown).hasClass "js-filter-submit"
+ $(dropdown).parents('form').submit()
+ )
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index c95ead22e6c..863a4edfad7 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -31,7 +31,7 @@ class @Notes
$(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
# change note in UI after update
- $(document).on "ajax:success", "form.edit_note", @updateNote
+ $(document).on "ajax:success", "form.edit-note", @updateNote
# Edit note link
$(document).on "click", ".js-note-edit", @showEditForm
@@ -72,7 +72,7 @@ class @Notes
cleanBinding: ->
$(document).off "ajax:success", ".js-main-target-form"
$(document).off "ajax:success", ".js-discussion-note-form"
- $(document).off "ajax:success", "form.edit_note"
+ $(document).off "ajax:success", "form.edit-note"
$(document).off "click", ".js-note-edit"
$(document).off "click", ".note-edit-cancel"
$(document).off "click", ".js-note-delete"
@@ -347,22 +347,26 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").hide()
note.find(".note-header").hide()
- base_form = note.find(".note-edit-form")
- form = base_form.clone().insertAfter(base_form)
- form.addClass('current-note-edit-form gfm-form')
- form.find('.div-dropzone').remove()
+ form = note.find(".note-edit-form")
+ isNewForm = form.is(':not(.gfm-form)')
+ if isNewForm
+ form.addClass('gfm-form')
+ form.addClass('current-note-edit-form')
+ form.show()
# Show the attachment delete link
note.find(".js-note-attachment-delete").show()
# Setup markdown form
- GitLab.GfmAutoComplete.setup()
- new DropzoneInput(form)
+ if isNewForm
+ GitLab.GfmAutoComplete.setup()
+ new DropzoneInput(form)
- form.show()
textarea = form.find("textarea")
textarea.focus()
- autosize(textarea)
+
+ if isNewForm
+ autosize(textarea)
# HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?).
# The textarea has the correct value, Chrome just won't show it unless we
@@ -371,7 +375,8 @@ class @Notes
textarea.val ""
textarea.val value
- disableButtonIfEmptyField textarea, form.find(".js-comment-button")
+ if isNewForm
+ disableButtonIfEmptyField textarea, form.find(".js-comment-button")
###
Called in response to clicking the edit note link
@@ -383,7 +388,9 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-body > .note-text").show()
note.find(".note-header").show()
- note.find(".current-note-edit-form").remove()
+ note.find(".current-note-edit-form")
+ .removeClass("current-note-edit-form")
+ .hide()
###
Called in response to deleting a note of any kind.
diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee
index ed5206368ce..e4c4bf3b273 100644
--- a/app/assets/javascripts/projects_list.js.coffee
+++ b/app/assets/javascripts/projects_list.js.coffee
@@ -2,6 +2,7 @@
init: ->
$(".projects-list-filter").off('keyup')
this.initSearch()
+ this.initPagination()
initSearch: ->
@timer = null
@@ -29,3 +30,8 @@
# Change url so if user reload a page - search results are saved
history.replaceState {page: project_filter_url}, document.title, project_filter_url
dataType: "json"
+
+ initPagination: ->
+ $('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
+ $('.projects-list-holder').replaceWith(data.html)
+ )
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 9467011799f..987c6f4b8d2 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -3,6 +3,81 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
+ $('.js-user-search').each (i, dropdown) =>
+ @projectId = $(dropdown).data('project-id')
+ @showCurrentUser = $(dropdown).data('current-user')
+ showNullUser = $(dropdown).data('null-user')
+ showAnyUser = $(dropdown).data('any-user')
+ firstUser = $(dropdown).data('first-user')
+ selectedId = $(dropdown).data('selected')
+
+ $(dropdown).glDropdown(
+ data: (term, callback) =>
+ @users term, (users) =>
+ if term.length is 0
+ showDivider = 0
+
+ if firstUser
+ # Move current user to the front of the list
+ for obj, index in users
+ if obj.username == firstUser
+ users.splice(index, 1)
+ users.unshift(obj)
+ break
+
+ if showNullUser
+ showDivider += 1
+ users.unshift(
+ name: 'Unassigned',
+ id: 0
+ )
+
+ if showAnyUser
+ showDivider += 1
+ name = showAnyUser
+ name = 'Any User' if name == true
+ anyUser = {
+ name: name,
+ id: null
+ }
+ users.unshift(anyUser)
+
+ if showDivider
+ users.splice(showDivider, 0, "divider")
+
+ # Send the data back
+ callback users
+ filterable: true
+ filterRemote: true
+ search:
+ fields: ['name', 'username']
+ selectable: true
+ fieldName: $(dropdown).data('field-name')
+ clicked: ->
+ if $(dropdown).hasClass "js-filter-submit"
+ $(dropdown).parents('form').submit()
+ renderRow: (user) ->
+ username = if user.username then "@#{user.username}" else ""
+ avatar = if user.avatar_url then user.avatar_url else false
+ selected = if user.id is selectedId then "is-active" else ""
+ img = ""
+
+ if avatar
+ img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
+
+ "<li>
+ <a href='#' class='dropdown-menu-user-link #{selected}'>
+ #{img}
+ <strong class='dropdown-menu-user-full-name'>
+ #{user.name}
+ </strong>
+ <span class='dropdown-menu-user-username'>
+ #{username}
+ </span>
+ </a>
+ </li>"
+ )
+
$('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id')
@groupId = $(select).data('group-id')
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index d7e4153ddc0..e6609ac7108 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -28,6 +28,10 @@
border-bottom: 1px solid $border-color;
color: $gl-gray;
+ a {
+ color: $md-link-color;
+ }
+
&.oneline-block {
line-height: 42px;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 3dc524ccca4..009d621fc74 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -17,6 +17,47 @@
.dropdown-menu {
display: block;
}
+
+ .dropdown-menu-toggle {
+ border-color: $dropdown-toggle-hover-border-color;
+
+ .fa {
+ color: $dropdown-toggle-hover-icon-color;
+ }
+ }
+}
+
+.dropdown-menu-toggle {
+ position: relative;
+ width: 160px;
+ padding: 6px 20px 6px 10px;
+ background-color: $dropdown-toggle-bg;
+ color: $dropdown-toggle-color;
+ font-size: 15px;
+ text-align: left;
+ border: 1px solid $dropdown-toggle-border-color;
+ border-radius: 2px;
+ outline: 0;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+
+ .fa {
+ position: absolute;
+ top: 50%;
+ right: 6px;
+ margin-top: -4px;
+ color: $dropdown-toggle-icon-color;
+ font-size: 10px;
+ }
+
+ &:hover, {
+ border-color: $dropdown-toggle-hover-border-color;
+
+ .fa {
+ color: $dropdown-toggle-hover-icon-color;
+ }
+ }
}
.dropdown-menu {
@@ -24,7 +65,7 @@
position: absolute;
top: 100%;
left: 0;
- z-index: 9999;
+ z-index: 9;
width: 240px;
margin-top: 2px;
margin-bottom: 0;
@@ -36,6 +77,21 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
+ &.is-loading {
+ .dropdown-content {
+ display: none;
+ }
+
+ .dropdown-loading {
+ display: block;
+ }
+ }
+
+ ul {
+ margin: 0;
+ padding: 0;
+ }
+
li {
text-align: left;
list-style: none;
@@ -61,13 +117,70 @@
white-space: nowrap;
overflow: hidden;
- &:hover {
+ &:hover,
+ &:focus,
+ &.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
+ outline: 0;
}
}
}
+.dropdown-menu-paging {
+ .dropdown-page-two,
+ .dropdown-menu-back {
+ display: none;
+ }
+
+ &.is-page-two {
+ .dropdown-page-one {
+ display: none;
+ }
+
+ .dropdown-page-two,
+ .dropdown-menu-back {
+ display: block;
+ }
+ }
+}
+
+.dropdown-menu-user {
+ .avatar {
+ float: left;
+ width: 30px;
+ height: 30px;
+ margin: 0 10px 0 0;
+ }
+}
+
+.dropdown-menu-user-link {
+ padding-top: 7px;
+ padding-bottom: 7px;
+}
+
+.dropdown-menu-user-full-name {
+ display: block;
+ margin-bottom: 2px;
+ font-weight: 600;
+ line-height: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.dropdown-menu-user-username {
+ display: block;
+ line-height: 1;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.dropdown-select {
+ width: 280px;
+}
+
.dropdown-menu-align-right {
left: auto;
right: 0;
@@ -101,3 +214,130 @@
font-size: 13px;
line-height: 22px;
}
+
+.dropdown-title {
+ position: relative;
+ margin-bottom: 10px;
+ padding-left: 30px;
+ padding-right: 30px;
+ padding-bottom: 10px;
+ font-weight: 600;
+ line-height: 1;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-bottom: 1px solid $dropdown-divider-color;
+ overflow: hidden;
+}
+
+.dropdown-title-button {
+ position: absolute;
+ top: -1px;
+ padding: 0;
+ color: $dropdown-title-btn-color;
+ font-size: 14px;
+ border: 0;
+ background: none;
+ outline: 0;
+
+ &:hover {
+ color: darken($dropdown-title-btn-color, 15%);
+ }
+}
+
+.dropdown-menu-close {
+ right: 0;
+}
+
+.dropdown-menu-back {
+ left: 0;
+}
+
+.dropdown-input {
+ position: relative;
+ margin-bottom: 10px;
+
+ .fa {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ color: #C7C7C7;
+ font-size: 12px;
+ pointer-events: none;
+ }
+}
+
+.dropdown-input-field {
+ width: 100%;
+ padding: 0 7px;
+ color: $dropdown-input-color;
+ line-height: 30px;
+ border: 1px solid $dropdown-divider-color;
+ border-radius: 2px;
+ outline: 0;
+
+ &:focus {
+ color: $dropdown-link-color;
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $dropdown-input-focus-shadow;
+
+ + .fa {
+ color: $dropdown-link-color;
+ }
+ }
+
+ &:hover {
+ + .fa {
+ color: $dropdown-link-color;
+ }
+ }
+}
+
+.dropdown-content {
+ max-height: 200px;
+ overflow-y: scroll;
+}
+
+.dropdown-footer {
+ padding-top: 10px;
+ margin-top: 10px;
+ font-size: 13px;
+ border-top: 1px solid $dropdown-divider-color;
+}
+
+.dropdown-footer-list {
+ font-size: 14px;
+
+ a {
+ padding-left: 10px;
+ }
+}
+
+.dropdown-loading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ z-index: 9;
+ background-color: $dropdown-loading-bg;
+ font-size: 28px;
+
+ .fa {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -14px;
+ margin-left: -14px;
+ }
+}
+
+.dropdown-menu-labels {
+ .label {
+ position: relative;
+ width: 30px;
+ margin-right: 5px;
+ text-indent: -99999px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index eab41628677..c431e2b0df3 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,5 +1,6 @@
.filter-item {
margin-right: 6px;
+ vertical-align: top;
}
@media (min-width: 800px) {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 48570abff49..9381cb3281c 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -149,13 +149,13 @@
}
&:hover > a.anchor {
- $size: 16px;
+ $size: 14px;
position: absolute;
right: 100%;
top: 50%;
- margin-top: -$size/2;
- margin-right: 0px;
- padding-right: 20px;
+ margin-top: -11px;
+ margin-right: 0;
+ padding-right: 15px;
display: inline-block;
width: $size;
height: $size;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index cc84a5ff932..6561b3de7c1 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -138,3 +138,15 @@ $dropdown-shadow-color: rgba(#000, .1);
$dropdown-divider-color: rgba(#000, .1);
$dropdown-header-color: #959494;
$dropdown-caret-color: #54565B;
+$dropdown-title-btn-color: #BFBFBF;
+$dropdown-input-color: #C7C7C7;
+$dropdown-input-focus-border: rgb(58, 171, 240);
+$dropdown-input-focus-shadow: rgba(#000, .2);
+$dropdown-loading-bg: rgba(#fff, .6);
+
+$dropdown-toggle-bg: #fff;
+$dropdown-toggle-color: #626262;
+$dropdown-toggle-border-color: #EAEAEA;
+$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
+$dropdown-toggle-icon-color: #C4C4C4;
+$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 1c78aafdb87..5ec0966194c 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -7,6 +7,28 @@
display: inline-block;
margin-right: 10px;
}
+
+ &.suggest-colors-dropdown {
+ margin-bottom: 5px;
+
+ a {
+ @include border-radius(0);
+ width: 36.7px;
+ margin-right: 0;
+ margin-bottom: -5px;
+ }
+ }
+}
+
+.dropdown-label-color-preview {
+ display: none;
+ margin-top: 5px;
+ width: 100%;
+ height: 25px;
+
+ &.is-active {
+ display: block;
+ }
}
.label-row {
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 7d414ae003d..639d639d5b0 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -28,3 +28,11 @@
border: 1px solid;
line-height: 32px;
}
+
+.markdown-snippet-copy {
+ position: fixed;
+ top: -10px;
+ left: -10px;
+ max-height: 0;
+ max-width: 0;
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index fb74919ea23..1f55b18e0b1 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -246,6 +246,8 @@ class ApplicationController < ActionController::Base
def ldap_security_check
if current_user && current_user.requires_ldap_check?
+ return unless current_user.try_obtain_ldap_lease
+
unless Gitlab::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index fc51c3241af..0e8b63872ca 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
@last_push = current_user.recent_push
@@ -32,7 +32,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
@last_push = current_user.recent_push
@groups = []
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 5b811db3068..8271ca87436 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -8,7 +8,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
@@ -23,7 +23,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
@projects = TrendingProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects)
- @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
@@ -39,7 +39,7 @@ class Explore::ProjectsController < Explore::ApplicationController
@projects = ProjectsFinder.new.execute(current_user)
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
- @projects = @projects.page(params[:page]).per(PER_PAGE) if params[:filter_projects].blank?
+ @projects = @projects.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
new file mode 100644
index 00000000000..74f326e0b83
--- /dev/null
+++ b/app/helpers/dropdowns_helper.rb
@@ -0,0 +1,100 @@
+module DropdownsHelper
+ def dropdown_tag(toggle_text, options: {}, &block)
+ content_tag :div, class: "dropdown" do
+ data_attr = { toggle: "dropdown" }
+
+ if options.has_key?(:data)
+ data_attr = options[:data].merge(data_attr)
+ end
+
+ dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
+
+ dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
+ output = ""
+
+ if options.has_key?(:title)
+ output << dropdown_title(options[:title])
+ end
+
+ if options.has_key?(:filter)
+ output << dropdown_filter(options[:placeholder])
+ end
+
+ output << content_tag(:div, class: "dropdown-content") do
+ capture(&block) if block && !options.has_key?(:footer_content)
+ end
+
+ if block && options.has_key?(:footer_content)
+ output << content_tag(:div, class: "dropdown-footer") do
+ capture(&block)
+ end
+ end
+
+ output << dropdown_loading
+
+ output.html_safe
+ end
+
+ dropdown_output.html_safe
+ end
+ end
+
+ def dropdown_toggle(toggle_text, data_attr, options)
+ content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
+ output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
+ output << icon('chevron-down')
+ output.html_safe
+ end
+ end
+
+ def dropdown_title(title, back: false)
+ content_tag :div, class: "dropdown-title" do
+ title_output = ""
+
+ if back
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
+ icon('arrow-left')
+ end
+ end
+
+ title_output << content_tag(:span, title)
+
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
+ icon('times')
+ end
+
+ title_output.html_safe
+ end
+ end
+
+ def dropdown_filter(placeholder)
+ content_tag :div, class: "dropdown-input" do
+ filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
+ filter_output << icon('search')
+
+ filter_output.html_safe
+ end
+ end
+
+ def dropdown_content(&block)
+ content_tag(:div, class: "dropdown-content") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_footer(&block)
+ content_tag(:div, class: "dropdown-footer") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_loading
+ content_tag :div, class: "dropdown-loading" do
+ icon('spinner spin')
+ end
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 426464dee81..65829bec77a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -528,11 +528,11 @@ class Project < ActiveRecord::Base
end
def ci_services
- services.select { |service| service.category == :ci }
+ services.where(category: :ci)
end
def ci_service
- @ci_service ||= ci_services.find(&:activated?)
+ @ci_service ||= ci_services.reorder(nil).find_by(active: true)
end
def jira_tracker?
diff --git a/app/models/user.rb b/app/models/user.rb
index 3098d49d58a..505a547d8ec 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -612,6 +612,13 @@ class User < ActiveRecord::Base
end
end
+ def try_obtain_ldap_lease
+ # After obtaining this lease LDAP checks will be blocked for 600 seconds
+ # (10 minutes) for this user.
+ lease = Gitlab::ExclusiveLease.new("user_ldap_check:#{id}", timeout: 600)
+ lease.try_obtain
+ end
+
def solo_owned_groups
@solo_owned_groups ||= owned_groups.select do |group|
group.owners == [self]
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index a2c0a858930..d084559abc3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -19,6 +19,8 @@
%li
= link_to 'Buttons', '#buttons'
%li
+ = link_to 'Dropdowns', '#dropdowns'
+ %li
= link_to 'Panels', '#panels'
%li
= link_to 'Alerts', '#alerts'
@@ -180,9 +182,9 @@
.nav-controls
= text_field_tag 'sample', nil, class: 'form-control'
.dropdown
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
%span Sort by name
- %b.caret
+ = icon('chevron-down')
%ul.dropdown-menu
%li
%a Sort by date
@@ -212,6 +214,227 @@
%button.btn.btn-danger{:type => "button"} Danger
%button.btn.btn-link{:type => "button"} Link
+ %h2#dropdowns Dropdowns
+
+ .example
+ .clearfix
+ .dropdown.inline.pull-left
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown.inline.pull-right
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-selectable
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li.divider
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown-footer
+ %strong Tip:
+ If an author is not a member of this project, you can still filter by his name while using the search field.
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown loading
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.is-active{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li.divider
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ %li
+ %a{href: "#"}
+ Dropdown Option
+ .dropdown-footer
+ %strong Tip:
+ If an author is not a member of this project, you can still filter by his name while using the search field.
+ .dropdown-loading
+ = icon('spinner spin')
+
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown user
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
+ .dropdown-title
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.dropdown-menu-user-link.is-active{href: "#"}
+ = link_to_member_avatar(current_user, size: 30)
+ %strong.dropdown-menu-user-full-name
+ = current_user.name
+ .dropdown-menu-user-username
+ = current_user.to_reference
+
+ .example
+ %div
+ .dropdown.inline
+ %button.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Dropdown page 2
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
+ .dropdown-page-one
+ .dropdown-title
+ %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ = icon('arrow-left')
+ %span Dropdown Title
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ %ul
+ %li
+ %a.dropdown-menu-user-link.is-active{href: "#"}
+ = link_to_member_avatar(current_user, size: 30)
+ %strong.dropdown-menu-user-full-name
+ = current_user.name
+ .dropdown-menu-user-username
+ = current_user.to_reference
+ .dropdown-page-two
+ .dropdown-title
+ %button.dropdown-title-button.dropdown-menu-back{aria: {label: "Go back"}}
+ = icon('arrow-left')
+ %span Create label
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Name new label"}
+ .dropdown-content
+ %button.btn.btn-primary
+ Create
+
+ .example
+ %div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ .dropdown-loading
+ = icon('spinner spin')
+ :javascript
+ $('#js-project-dropdown').glDropdown({
+ data: function (term, callback) {
+ Api.projects(term, "last_activity_at", function (data) {
+ callback(data);
+ });
+ },
+ text: function (project) {
+ return project.name_with_namespace || project.name;
+ },
+ selectable: true,
+ fieldName: "author_id",
+ filterable: true,
+ search: {
+ fields: ['name_with_namespace']
+ },
+ id: function (data) {
+ return data.id;
+ },
+ isSelected: function (data) {
+ return data.id === 2;
+ }
+ })
+
+ .example
+ %div
+ = dropdown_tag("Projects", options: { title: "Go to project", filter: true, placeholder: "Filter projects" })
+
%h2#panels Panels
.row
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 14ee2263b7d..6a60cfeff76 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,4 @@
- unless @project.empty_repo?
- if can? current_user, :download_code, @project
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
= icon('download')
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index b5f076088c7..13e624764d9 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,5 +1,5 @@
.note-edit-form
- = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, class: 'js-quick-submit' do |f|
+ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f|
= note_target_fields(note)
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index e55159d996b..c3fbba2ba54 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -7,22 +7,77 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- = users_select_tag(:author_id, selected: params[:author_id],
- placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ - if params[:author_id]
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag("Author", options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author",
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } })
.filter-item.inline
- = users_select_tag(:assignee_id, selected: params[:assignee_id],
- placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
+ - if params[:assignee_id]
+ = hidden_field_tag(:assignee_id, params[:assignee_id])
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: "Search assignee", data: { any_user: "Any Author", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
.filter-item.inline.milestone-filter
- = select_tag('milestone_title', projects_milestones_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Milestone'})
+ - if params[:milestone_title]
+ = hidden_field_tag(:milestone_title, params[:milestone_title])
+ = dropdown_tag("Milestone", options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do
+ - if @project
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_milestone, @project
+ %li
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+ Create new
+ %li
+ = link_to namespace_project_milestones_path(@project.namespace, @project) do
+ - if can? current_user, :admin_milestone, @project
+ Manage milestones
+ - else
+ View milestones
.filter-item.inline.labels-filter
- = select_tag('label_name', projects_labels_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Label'})
+ - if params[:label_name]
+ = hidden_field_tag(:label_name, params[:label_name])
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}}
+ %span.dropdown-toggle-text
+ Label
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ = dropdown_title("Filter by label")
+ = dropdown_filter("Search labels")
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project) do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
+ - if can? current_user, :admin_label, @project
+ .dropdown-page-two
+ = dropdown_title("Create new label", back: true)
+ = dropdown_content do
+ %input#new_label_color{type: "hidden"}
+ %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
+ .dropdown-label-color-preview.js-dropdown-label-color-preview
+ .suggest-colors.suggest-colors-dropdown
+ - suggested_colors.each do |color|
+ = link_to '#', style: "background-color: #{color}", data: { color: color } do
+ &nbsp
+ %button.btn.btn-primary.js-new-label-btn{type: "button"}
+ Create
+ = dropdown_loading
+ .dropdown-loading
+ = icon('spinner spin')
.pull-right
= render 'shared/sort_dropdown'
@@ -31,11 +86,18 @@
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline
- = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" })
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ %ul
+ %li
+ %a{href: "#", data: {id: "reopen"}} Open
+ %li
+ %a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
- = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
- = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
@@ -47,6 +109,9 @@
:javascript
new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index e0e41fc4bea..773ce8ac240 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,5 +1,7 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
+ %textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
+ = @snippet.data
.file-content.wiki
= render_markup(@snippet.file_name, @snippet.data)
- else
diff --git a/config/application.rb b/config/application.rb
index 7fd75ebe69e..d8d1e7b4679 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -4,6 +4,7 @@ require 'rails/all'
require 'devise'
I18n.config.enforce_available_locales = false
Bundler.require(:default, Rails.env)
+require_relative '../lib/gitlab/redis_config'
module Gitlab
REDIS_CACHE_NAMESPACE = 'cache:gitlab'
@@ -67,22 +68,7 @@ module Gitlab
end
end
- # Use Redis caching across all environments
- redis_config_file = Rails.root.join('config', 'resque.yml')
-
- redis_url_string = if File.exists?(redis_config_file)
- YAML.load_file(redis_config_file)[Rails.env]
- else
- "redis://localhost:6379"
- end
-
- # Redis::Store does not handle Unix sockets well, so let's do it for them
- redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(redis_url_string)
- redis_uri = URI.parse(redis_url_string)
- if redis_uri.scheme == 'unix'
- redis_config_hash[:path] = redis_uri.path
- end
-
+ redis_config_hash = Gitlab::RedisConfig.redis_store_options
redis_config_hash[:namespace] = REDIS_CACHE_NAMESPACE
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
config.cache_store = :redis_store, redis_config_hash
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 0fc725842ba..3da5d46be92 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -13,9 +13,12 @@ end
if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
+ redis_config = Gitlab::RedisConfig.redis_store_options
+ redis_config[:namespace] = 'session:gitlab'
+
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
- servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store
+ servers: redis_config,
key: '_gitlab_session',
secure: Gitlab.config.gitlab.https,
httponly: true,
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index dcf6ce74d96..cc83137745a 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,16 +1,9 @@
-# Custom Redis configuration
-config_file = Rails.root.join('config', 'resque.yml')
-
-resque_url = if File.exists?(config_file)
- YAML.load_file(config_file)[Rails.env]
- else
- "redis://localhost:6379"
- end
+SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab'
Sidekiq.configure_server do |config|
config.redis = {
- url: resque_url,
- namespace: 'resque:gitlab'
+ url: Gitlab::RedisConfig.url,
+ namespace: SIDEKIQ_REDIS_NAMESPACE
}
config.server_middleware do |chain|
@@ -36,7 +29,7 @@ end
Sidekiq.configure_client do |config|
config.redis = {
- url: resque_url,
- namespace: 'resque:gitlab'
+ url: Gitlab::RedisConfig.url,
+ namespace: SIDEKIQ_REDIS_NAMESPACE
}
end
diff --git a/config/mail_room.yml b/config/mail_room.yml
index f266a70ee0d..aed55f74eab 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -2,6 +2,7 @@
<%
require "yaml"
require "json"
+require_relative "lib/gitlab/redis_config"
rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
@@ -17,13 +18,7 @@ if File.exists?(config_file)
config['mailbox'] = "inbox" if config['mailbox'].nil?
if config['enabled'] && config['address'] && config['address'].include?('%{key}')
- redis_config_file = "config/resque.yml"
- redis_url =
- if File.exists?(redis_config_file)
- YAML.load_file(redis_config_file)[rails_env]
- else
- "redis://localhost:6379"
- end
+ redis_url = Gitlab::RedisConfig.new(rails_env).url
%>
-
:host: <%= config['host'].to_json %>
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index cbe54e2dc79..d723300f485 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -36,13 +36,22 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- select2(current_user.id, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ execute_script('$("#assignee_id").val("")')
+ execute_script('$(".js-user-search").first().click()')
+ sleep 1
+ execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
+ sleep 1
end
step 'I click "All" link' do
- select2(nil, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ execute_script('$(".js-user-search").first().click()')
+ sleep 1
+ execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
+ sleep 1
+ execute_script('$(".js-user-search").eq(1).click()')
+ sleep 1
+ execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
+ sleep 1
end
def should_see(issue)
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 28c8c6b6015..7fc0e444e86 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -40,13 +40,22 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- select2(current_user.id, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ execute_script('$("#assignee_id").val("")')
+ execute_script('$(".js-user-search").first().click()')
+ sleep 0.5
+ execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
+ sleep 2
end
step 'I click "All" link' do
- select2(nil, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ execute_script('$(".js-user-search").first().click()')
+ sleep 0.5
+ execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
+ sleep 2
+ execute_script('$(".js-user-search").eq(1).click()')
+ sleep 0.5
+ execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
+ sleep 2
end
def should_see(merge_request)
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
index 50bb32429b9..6d50501a722 100644
--- a/features/steps/project/issues/filter_labels.rb
+++ b/features/steps/project/issues/filter_labels.rb
@@ -29,7 +29,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end
step 'I click link "bug"' do
- select2('bug', from: "#label_name")
+ page.find('.js-label-select').click
+ sleep 0.5
+ execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
+ sleep 2
end
step 'I click link "feature"' do
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index d9842ccf95e..383ab4e14da 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -27,7 +27,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click link "Closed"' do
- click_link "Closed"
+ find('.issues-state-filters a', text: "Closed").click
end
step 'I click button "Unsubscribe"' do
@@ -63,14 +63,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click "author" dropdown' do
- first('#s2id_author_id').click
+ page.find('.js-author-search').click
+ sleep 1
end
step 'I see current user as the first user' do
- expect(page).to have_selector('.user-result', visible: true, count: 3)
- users = page.all('.user-name')
+ expect(page).to have_selector('.dropdown-content', visible: true)
+ users = page.all('.dropdown-menu-author .dropdown-content li a')
expect(users[0].text).to eq 'Any Author'
- expect(users[1].text).to eq current_user.name
+ expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}"
end
step 'I submit new issue "500 error on profile"' do
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
new file mode 100644
index 00000000000..2ef50286b1d
--- /dev/null
+++ b/lib/gitlab/exclusive_lease.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ # This class implements an 'exclusive lease'. We call it a 'lease'
+ # because it has a set expiry time. We call it 'exclusive' because only
+ # one caller may obtain a lease for a given key at a time. The
+ # implementation is intended to work across GitLab processes and across
+ # servers. It is a 'cheap' alternative to using SQL queries and updates:
+ # you do not need to change the SQL schema to start using
+ # ExclusiveLease.
+ #
+ # It is important to choose the timeout wisely. If the timeout is very
+ # high (1 hour) then the throughput of your operation gets very low (at
+ # most once an hour). If the timeout is lower than how long your
+ # operation may take then you cannot count on exclusivity. For example,
+ # if the timeout is 10 seconds and you do an operation which may take 20
+ # seconds then two overlapping operations may hold a lease for the same
+ # key at the same time.
+ #
+ class ExclusiveLease
+ def initialize(key, timeout:)
+ @key, @timeout = key, timeout
+ end
+
+ # Try to obtain the lease. Return true on success,
+ # false if the lease is already taken.
+ def try_obtain
+ # Performing a single SET is atomic
+ !!redis.set(redis_key, '1', nx: true, ex: @timeout)
+ end
+
+ private
+
+ def redis
+ # Maybe someday we want to use a connection pool...
+ @redis ||= Redis.new(url: Gitlab::RedisConfig.url)
+ end
+
+ def redis_key
+ "gitlab:exclusive_lease:#{@key}"
+ end
+ end
+end
diff --git a/lib/gitlab/redis_config.rb b/lib/gitlab/redis_config.rb
new file mode 100644
index 00000000000..4949c6db539
--- /dev/null
+++ b/lib/gitlab/redis_config.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ class RedisConfig
+ attr_reader :url
+
+ def self.url
+ new.url
+ end
+
+ def self.redis_store_options
+ url = new.url
+ redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(url)
+ # Redis::Store does not handle Unix sockets well, so let's do it for them
+ redis_uri = URI.parse(url)
+ if redis_uri.scheme == 'unix'
+ redis_config_hash[:path] = redis_uri.path
+ end
+ redis_config_hash
+ end
+
+ def initialize(rails_env=nil)
+ rails_env ||= Rails.env
+ config_file = File.expand_path('../../../config/resque.yml', __FILE__)
+
+ @url = "redis://localhost:6379"
+ if File.exists?(config_file)
+ @url =YAML.load_file(config_file)[rails_env]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 4885baf9526..d1b42c1f9b9 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -3,7 +3,7 @@ module Gitlab
def self.allowed?(user)
return false if user.blocked?
- if user.requires_ldap_check?
+ if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
end
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index f221afcf73a..51e746ef923 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -4,16 +4,16 @@ namespace :cache do
desc "GitLab | Clear redis cache"
task :clear => :environment do
- redis_store = Rails.cache.instance_variable_get(:@data)
+ redis = Redis.new(url: Gitlab::RedisConfig.url)
cursor = REDIS_SCAN_START_STOP
loop do
- cursor, keys = redis_store.scan(
+ cursor, keys = redis.scan(
cursor,
match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*",
count: CLEAR_BATCH_SIZE
)
- redis_store.del(*keys) if keys.any?
+ redis.del(*keys) if keys.any?
break if cursor == REDIS_SCAN_START_STOP
end
diff --git a/public/logo.svg b/public/logo.svg
index c09785cb96f..fc4553137f7 100644
--- a/public/logo.svg
+++ b/public/logo.svg
@@ -1,26 +1,9 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="210px" height="210px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
- <!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
- <title>Slice 1</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
- <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)">
- <g id="Page-1" sketch:type="MSShapeGroup">
- <g id="Fill-1-+-Group-24">
- <g id="Group-24">
- <g id="Group">
- <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329"></path>
- <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26"></path>
- <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326"></path>
- <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329"></path>
- <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26"></path>
- <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326"></path>
- <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329"></path>
- </g>
- </g>
- </g>
- </g>
- </g>
- </g>
-</svg> \ No newline at end of file
+<svg width="210" height="210" viewBox="0 0 210 210" xmlns="http://www.w3.org/2000/svg">
+ <path d="M105.0614 203.655l38.64-118.921h-77.28l38.64 118.921z" fill="#e24329"/>
+ <path d="M105.0614 203.6548l-38.64-118.921h-54.153l92.793 118.921z" fill="#fc6d26"/>
+ <path d="M12.2685 84.7341l-11.742 36.139c-1.071 3.296.102 6.907 2.906 8.944l101.629 73.838-92.793-118.921z" fill="#fca326"/>
+ <path d="M12.2685 84.7342h54.153l-23.273-71.625c-1.197-3.686-6.411-3.685-7.608 0l-23.272 71.625z" fill="#e24329"/>
+ <path d="M105.0614 203.6548l38.64-118.921h54.153l-92.793 118.921z" fill="#fc6d26"/>
+ <path d="M197.8544 84.7341l11.742 36.139c1.071 3.296-.102 6.907-2.906 8.944l-101.629 73.838 92.793-118.921z" fill="#fca326"/>
+ <path d="M197.8544 84.7342h-54.153l23.273-71.625c1.197-3.686 6.411-3.685 7.608 0l23.272 71.625z" fill="#e24329"/>
+</svg>
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 591866b40d4..f6e33f651c4 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Issue filtering by Milestone', feature: true do
- include Select2Helper
-
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -31,6 +29,9 @@ feature 'Issue filtering by Milestone', feature: true do
end
def filter_by_milestone(title)
- select2(title, from: '#milestone_title')
+ find(".js-milestone-select").click
+ sleep 0.5
+ find(".milestone-filter a", text: title).click
+ sleep 1
end
end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index f70214e1122..1b2fd1bab10 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
- include Select2Helper
-
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -31,6 +29,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
end
def filter_by_milestone(title)
- select2(title, from: '#milestone_title')
+ find(".js-milestone-select").click
+ sleep 0.5
+ find(".milestone-filter a", text: title).click
+ sleep 1
end
end
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
new file mode 100644
index 00000000000..fbdb7ea34ac
--- /dev/null
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::ExclusiveLease do
+ it 'cannot obtain twice before the lease has expired' do
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
+ expect(lease.try_obtain).to eq(true)
+ expect(lease.try_obtain).to eq(false)
+ end
+
+ it 'can obtain after the lease has expired' do
+ timeout = 1
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
+ lease.try_obtain # start the lease
+ sleep(2 * timeout) # lease should have expired now
+ expect(lease.try_obtain).to eq(true)
+ end
+
+ def unique_key
+ SecureRandom.hex(10)
+ end
+end