diff options
author | James Lopez <james@jameslopez.es> | 2016-06-16 13:04:00 +0200 |
---|---|---|
committer | James Lopez <james@jameslopez.es> | 2016-06-16 13:04:00 +0200 |
commit | 778d72664f386dfee45dab171f137395739958f6 (patch) | |
tree | 7f2c902f4b97ac29aa97e96e877da201130a33b3 /app | |
parent | 452c076a34cc11cc97f4b1c3113e86ce4367e055 (diff) | |
parent | c369cc8bf42a680b2b0fc9721a9a7926dc5426f6 (diff) | |
download | gitlab-ce-778d72664f386dfee45dab171f137395739958f6.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into feature/project-exportfeature/project-export
# Conflicts:
# app/models/ci/pipeline.rb
Diffstat (limited to 'app')
225 files changed, 2819 insertions, 1860 deletions
diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee index 365a062bb81..b06bcf0fcbf 100644 --- a/app/assets/javascripts/LabelManager.js.coffee +++ b/app/assets/javascripts/LabelManager.js.coffee @@ -42,10 +42,10 @@ class @LabelManager $from = @prioritizedLabels if $from.find('li').length is 1 - $from.find('.empty-message').show() + $from.find('.empty-message').removeClass('hidden') if not $target.find('li').length - $target.find('.empty-message').hide() + $target.find('.empty-message').addClass('hidden') $label.detach().appendTo($target) @@ -54,6 +54,9 @@ class @LabelManager if action is 'remove' xhr = $.ajax url: url, type: 'DELETE' + + # Restore empty message + $from.find('.empty-message').removeClass('hidden') unless $from.find('li').length else xhr = @savePrioritySort($label, action) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 69d4c4f5dd3..2f9f6c3ef5b 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -32,10 +32,6 @@ #= require bootstrap/tooltip #= require bootstrap/popover #= require select2 -#= require raphael -#= require g.raphael -#= require g.bar -#= require branch-graph #= require ace/ace #= require ace/ext-searchbox #= require underscore @@ -125,9 +121,10 @@ window.onload = -> setTimeout shiftWindow, 100 $ -> + gl.utils.preventDisabledButtons() bootstrapBreakpoint = bp.getBreakpointSize() - $(".nicescroll").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") + $(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF") # Click a .js-select-on-focus field, select the contents $(".js-select-on-focus").on "focusin", -> @@ -257,3 +254,31 @@ $ -> gl.awardsHandler = new AwardsHandler() checkInitialSidebarSize() new Aside() + + # Sidenav pinning + if $(window).width() < 1440 and $.cookie('pin_nav') is 'true' + $.cookie('pin_nav', 'false') + $('.page-with-sidebar') + .toggleClass('page-sidebar-collapsed page-sidebar-expanded') + .removeClass('page-sidebar-pinned') + $('.navbar-fixed-top').removeClass('header-pinned-nav') + + $(document) + .off 'click', '.js-nav-pin' + .on 'click', '.js-nav-pin', (e) -> + e.preventDefault() + + $(this).toggleClass 'is-active' + + if $.cookie('pin_nav') is 'true' + $.cookie 'pin_nav', 'false' + $('.page-with-sidebar') + .removeClass('page-sidebar-pinned') + .toggleClass('page-sidebar-collapsed page-sidebar-expanded') + $('.navbar-fixed-top') + .removeClass('header-pinned-nav') + .toggleClass('header-collapsed header-expanded') + else + $.cookie 'pin_nav', 'true' + $('.page-with-sidebar').addClass('page-sidebar-pinned') + $('.navbar-fixed-top').addClass('header-pinned-nav') diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 136db8ee14d..030f1564862 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -40,7 +40,7 @@ class @AwardsHandler $menu = $ '.emoji-menu' if $addBtn.hasClass 'js-note-emoji' - $addBtn.parents('.note').find('.js-awards-block').addClass 'current' + $addBtn.closest('.note').find('.js-awards-block').addClass 'current' else $addBtn.closest('.js-awards-block').addClass 'current' diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index f763ba96e33..2d515d7efa2 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -17,6 +17,8 @@ class @CiBuild .off 'resize.build' .on 'resize.build', @hideSidebar + @updateArtifactRemoveDate() + if $('#build-trace').length @getInitialBuildTrace() @initScrollButtonAffix() @@ -103,3 +105,10 @@ class @CiBuild $('.js-build-sidebar') .removeClass 'right-sidebar-collapsed' .addClass 'right-sidebar-expanded' + + updateArtifactRemoveDate: -> + $date = $('.js-artifacts-remove') + + if $date.length + date = $date.text() + $date.text $.timefor(new Date(date), ' ') diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 29ac0f70b30..b560500cce6 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -29,6 +29,7 @@ class Dispatcher new Todos() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() + new DueDateSelect() new GLForm($('.milestone-form')) when 'groups:milestones:new' new ZenMode() @@ -53,9 +54,13 @@ class Dispatcher new Diff() shortcut_handler = new ShortcutsIssuable(true) new ZenMode() + new MergedButtons() + when 'projects:merge_requests:commits', 'projects:merge_requests:builds' + new MergedButtons() when "projects:merge_requests:diffs" new Diff() new ZenMode() + new MergedButtons() when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() Issuable.init() @@ -68,9 +73,7 @@ class Dispatcher new Diff() new ZenMode() shortcut_handler = new ShortcutsNavigation() - when 'projects:commits:show' - shortcut_handler = new ShortcutsNavigation() - when 'projects:activity' + when 'projects:commits:show', 'projects:activity' shortcut_handler = new ShortcutsNavigation() when 'projects:show' shortcut_handler = new ShortcutsNavigation() @@ -96,6 +99,7 @@ class Dispatcher when 'projects:blob:show', 'projects:blame:show' new LineHighlighter() shortcut_handler = new ShortcutsNavigation() + new ShortcutsBlob true when 'projects:labels:new', 'projects:labels:edit' new Labels() when 'projects:labels:index' @@ -129,15 +133,11 @@ class Dispatcher new Project() new ProjectAvatar() switch path[1] - when 'compare' - shortcut_handler = new ShortcutsNavigation() when 'edit' shortcut_handler = new ShortcutsNavigation() new ProjectNew() - when 'new' + when 'new', 'show' new ProjectNew() - when 'show' - new ProjectShow() when 'wikis' new Wikis() shortcut_handler = new ShortcutsNavigation() @@ -146,9 +146,9 @@ class Dispatcher when 'snippets' shortcut_handler = new ShortcutsNavigation() new ZenMode() if path[2] == 'show' - when 'labels', 'graphs' - shortcut_handler = new ShortcutsNavigation() - when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' + when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \ + 'milestones', 'project_members', 'deploy_keys', 'builds', \ + 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() # If we haven't installed a custom shortcut handler, install the default one diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee index 3d009a96d05..d65c018dad5 100644 --- a/app/assets/javascripts/due_date_select.js.coffee +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -1,5 +1,21 @@ class @DueDateSelect constructor: -> + # Milestone edit/new form + $datePicker = $('.datepicker') + + if $datePicker.length + $dueDate = $('#milestone_due_date') + $datePicker.datepicker + dateFormat: 'yy-mm-dd' + onSelect: (dateText, inst) -> + $dueDate.val(dateText) + .datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val())) + + $('.js-clear-due-date').on 'click', (e) -> + e.preventDefault() + $.datepicker._clearDate($datePicker) + + # Issuable sidebar $loading = $('.js-issuable-update .due_date') .find('.block-loading') .hide() @@ -32,7 +48,7 @@ class @DueDateSelect date = new Date value.replace(new RegExp('-', 'g'), ',') mediumDate = $.datepicker.formatDate 'M d, yy', date else - mediumDate = 'None' + mediumDate = 'No due date' data = {} data[abilityName] = {} @@ -50,7 +66,8 @@ class @DueDateSelect $selectbox.hide() $value.css('display', '') - $valueContent.html(mediumDate) + cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value' + $valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>") $sidebarValue.html(mediumDate) if value isnt '' diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee index c2447120033..d0901be1509 100644 --- a/app/assets/javascripts/issuable.js.coffee +++ b/app/assets/javascripts/issuable.js.coffee @@ -56,13 +56,6 @@ issuable_created = false Issuable.filterResults $('.filter-form') $('.js-label-select').trigger('update.label') - toggleLabelFilters: -> - $filteredLabels = $('.filtered-labels') - if $filteredLabels.find('.label-row').length > 0 - $filteredLabels.removeClass('hidden') - else - $filteredLabels.addClass('hidden') - filterResults: (form) => formData = form.serialize() @@ -71,58 +64,16 @@ issuable_created = false issuesUrl = formAction issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}") issuesUrl += formData - $.ajax - type: 'GET' - url: formAction - data: formData - complete: -> - $('.issues-holder, .merge-requests-holder').css('opacity', '1.0') - success: (data) -> - $('.issues-holder, .merge-requests-holder').html(data.html) - # Change url so if user reload a page - search results are saved - history.replaceState {page: issuesUrl}, document.title, issuesUrl - Issuable.reload() - Issuable.updateStateFilters() - $filteredLabels = $('.filtered-labels') - - if typeof Issuable.labelRow is 'function' - $filteredLabels.html(Issuable.labelRow(data)) - Issuable.toggleLabelFilters() - - dataType: "json" - - reload: -> - if Issuable.created - Issuable.initChecks() - - $('#filter_issue_search').val($('#issue_search').val()) + Turbolinks.visit(issuesUrl); initChecks: -> - $('.check_all_issues').on 'click', -> + $('.check_all_issues').off('click').on('click', -> $('.selected_issue').prop('checked', @checked) Issuable.checkChanged() + ) - $('.selected_issue').on 'change', Issuable.checkChanged - - updateStateFilters: -> - stateFilters = $('.issues-state-filters, .dropdown-menu-sort') - newParams = {} - paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search'] - - for paramKey in paramKeys - newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or '' - - if stateFilters.length - stateFilters.find('a').each -> - initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]') - labelNameValues = gl.utils.getParameterValues('label_name[]') - if labelNameValues - labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&') - newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}" - else - newUrl = gl.utils.mergeUrlParams(newParams, initialUrl) - $(this).attr 'href', newUrl + $('.selected_issue').off('change').on('change', Issuable.checkChanged) checkChanged: -> checked_issues = $('.selected_issue:checked') diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index 898506fde32..5b7a4831dfc 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -102,6 +102,10 @@ class @IssuableForm return { results: data } + data: (query) -> + { + search: query + } formatResult: (project) -> project.name_with_namespace formatSelection: (project) -> diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee index 16d023dd391..b454f9389dd 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js.coffee +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -9,6 +9,9 @@ class @IssuableBulkActions @bindEvents() + # Fixes bulk-assign not working when navigating through pages + Issuable.initChecks(); + getElement: (selector) -> @container.find selector @@ -97,13 +100,22 @@ class @IssuableBulkActions $labels = @form.find('.labels-filter input[name="update[label_ids][]"]') $labels.each (k, label) -> - labelIds.push $(label).val() if label + labelIds.push parseInt($(label).val()) if label labelIds ###* - * Just an alias of @getUnmarkedIndeterminedLabels - * @return {Array} Array of labels + * Returns Label IDs that will be removed from issue selection + * @return {Array} Array of labels IDs ### getLabelsToRemove: -> - @getUnmarkedIndeterminedLabels() + result = [] + indeterminatedLabels = @getUnmarkedIndeterminedLabels() + labelsToApply = @getLabelsToApply() + + indeterminatedLabels.map (id) -> + # We need to exclude label IDs that will be applied + # By not doing this will cause issues from selection to not add labels at all + result.push(id) if labelsToApply.indexOf(id) is -1 + + result diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 9ca88f1226e..d350a7c0e7f 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -39,7 +39,7 @@ class @LabelsSelect </a> <% }); %>' ) - labelNoneHTMLTemplate = _.template('<div class="light">None</div>') + labelNoneHTMLTemplate = '<span class="no-value">None</span>' if newLabelField.length @@ -145,7 +145,7 @@ class @LabelsSelect template = labelHTMLTemplate(data) labelCount = data.labels.length else - template = labelNoneHTMLTemplate() + template = labelNoneHTMLTemplate $value .removeAttr('style') .html(template) diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee index 6adac6dac97..f8f0aea427e 100644 --- a/app/assets/javascripts/layout_nav.js.coffee +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -1,14 +1,25 @@ -class @LayoutNav - $ -> - $('.fade-left').addClass('end-scroll') - $('.scrolling-tabs').on 'scroll', (event) -> - $this = $(this) - $el = $(event.target) - currentPosition = $this.scrollLeft() - size = bp.getBreakpointSize() - controlBtnWidth = $('.controls').width() - maxPosition = $this.get(0).scrollWidth - $this.parent().width() - maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length - - $el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0) - $el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) +hideEndFade = ($scrollingTabs) -> + $scrollingTabs.each -> + $this = $(@) + + $this + .find('.fade-right') + .toggleClass('end-scroll', $this.width() is $this.prop('scrollWidth')) + +$ -> + $('.fade-left').addClass('end-scroll') + + hideEndFade($('.scrolling-tabs')) + + $(window) + .off 'resize.nav' + .on 'resize.nav', -> + hideEndFade($('.scrolling-tabs')) + + $('.scrolling-tabs').on 'scroll', (event) -> + $this = $(this) + currentPosition = $this.scrollLeft() + maxPosition = $this.prop('scrollWidth') - $this.outerWidth() + + $this.find('.fade-left').toggleClass('end-scroll', currentPosition is 0) + $this.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) diff --git a/app/assets/javascripts/lib/common_utils.js.coffee b/app/assets/javascripts/lib/common_utils.js.coffee index 0000e99a650..e39dcb2daa9 100644 --- a/app/assets/javascripts/lib/common_utils.js.coffee +++ b/app/assets/javascripts/lib/common_utils.js.coffee @@ -1,5 +1,46 @@ ((w) -> + w.gl or= {} + w.gl.utils or= {} + + w.gl.utils.isInGroupsPage = -> + + return $('body').data('page').split(':')[0] is 'groups' + + + w.gl.utils.isInProjectPage = -> + + return $('body').data('page').split(':')[0] is 'projects' + + + w.gl.utils.getProjectSlug = -> + + return if @isInProjectPage() then $('body').data 'project' else null + + + w.gl.utils.getGroupSlug = -> + + return if @isInGroupsPage() then $('body').data 'group' else null + + + + gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) -> + + $tooltipEl + .tooltip 'destroy' + .attr 'title', newTitle + .tooltip 'fixTitle' + + + gl.utils.preventDisabledButtons = -> + + $('.btn').click (e) -> + if $(this).hasClass 'disabled' + e.preventDefault() + e.stopImmediatePropagation() + return false + + jQuery.timefor = (time, suffix, expiredLabel) -> return '' unless time diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index 9fdc27a9787..dc2590a0355 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -42,9 +42,3 @@ work = -> $(document).on('page:fetch', start) $(document).on('page:change', stop) - -$ -> - # Make logo clickable as part of a workaround for Safari visited - # link behaviour (See !2690). - $('#logo').on 'click', -> - Turbolinks.visit('/') diff --git a/app/assets/javascripts/merged_buttons.js.coffee b/app/assets/javascripts/merged_buttons.js.coffee new file mode 100644 index 00000000000..4929295c10b --- /dev/null +++ b/app/assets/javascripts/merged_buttons.js.coffee @@ -0,0 +1,30 @@ +class @MergedButtons + constructor: -> + @$removeBranchWidget = $('.remove_source_branch_widget') + @$removeBranchProgress = $('.remove_source_branch_in_progress') + @$removeBranchFailed = $('.remove_source_branch_widget.failed') + + @cleanEventListeners() + @initEventListeners() + + cleanEventListeners: -> + $(document).off 'click', '.remove_source_branch' + $(document).off 'ajax:success', '.remove_source_branch' + $(document).off 'ajax:error', '.remove_source_branch' + + initEventListeners: -> + $(document).on 'click', '.remove_source_branch', @removeSourceBranch + $(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess + $(document).on 'ajax:error', '.remove_source_branch', @removeBranchError + + removeSourceBranch: => + @$removeBranchWidget.hide() + @$removeBranchProgress.show() + + removeBranchSuccess: -> + location.reload() + + removeBranchError: -> + @$removeBranchWidget.hide() + @$removeBranchProgress.hide() + @$removeBranchFailed.show() diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 648e1f3bde0..02480f3a025 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -24,14 +24,10 @@ class @MilestoneSelect if issueUpdateURL milestoneLinkTemplate = _.template( - '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"> - <span class="has-tooltip" data-container="body" title="<%= remaining %>"> - <%= _.escape(title) %> - </span> - </a>' + '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>" class="bold has-tooltip" data-container="body" title="<%= remaining %>"><%= _.escape(title) %></a>' ) - milestoneLinkNoneTemplate = '<div class="light">None</div>' + milestoneLinkNoneTemplate = '<span class="no-value">None</span>' collapsedSidebarLabelTemplate = _.template( '<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left"> @@ -116,7 +112,7 @@ class @MilestoneSelect .val() data = {} data[abilityName] = {} - data[abilityName].milestone_id = selected + data[abilityName].milestone_id = if selected? then selected else null $loading .fadeIn() $dropdown.trigger('loading.gl.dropdown') diff --git a/app/assets/javascripts/network/application.js.coffee b/app/assets/javascripts/network/application.js.coffee new file mode 100644 index 00000000000..cb9eead855b --- /dev/null +++ b/app/assets/javascripts/network/application.js.coffee @@ -0,0 +1,20 @@ +# This is a manifest file that'll be compiled into including all the files listed below. +# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +# be included in the compiled file accessible from http://example.com/assets/application.js +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +#= require raphael +#= require g.raphael +#= require g.bar +#= require_tree . + +$ -> + network_graph = new Network({ + url: $(".network-graph").attr('data-url'), + commit_url: $(".network-graph").attr('data-commit-url'), + ref: $(".network-graph").attr('data-ref'), + commit_id: $(".network-graph").attr('data-commit-id') + }) + + new ShortcutsNetwork(network_graph.branch_graph) diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/network/branch-graph.js.coffee index f2fd2a775a4..f2fd2a775a4 100644 --- a/app/assets/javascripts/branch-graph.js.coffee +++ b/app/assets/javascripts/network/branch-graph.js.coffee diff --git a/app/assets/javascripts/network.js.coffee b/app/assets/javascripts/network/network.js.coffee index f4ef07a50a7..f4ef07a50a7 100644 --- a/app/assets/javascripts/network.js.coffee +++ b/app/assets/javascripts/network/network.js.coffee diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ad216910c8d..e2d3241437b 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -115,12 +115,14 @@ class @Notes , @pollingInterval refresh: => - return if @refreshing is true - @refreshing = true if not document.hidden and document.URL.indexOf(@noteable_url) is 0 @getContent() getContent: -> + return if @refreshing + + @refreshing = true + $.ajax url: @notes_url data: "last_fetched_at=" + @last_fetched_at diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee index c9cb0f4bb32..8eb005b0a22 100644 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -43,6 +43,55 @@ class @Sidebar $('.right-sidebar') .hasClass('right-sidebar-collapsed'), { path: '/' }) + $(document) + .off 'click', '.js-issuable-todo' + .on 'click', '.js-issuable-todo', @toggleTodo + + toggleTodo: (e) => + $this = $(e.currentTarget) + $todoLoading = $('.js-issuable-todo-loading') + $btnText = $('.js-issuable-todo-text', $this) + ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST' + ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else '' + + $.ajax( + url: "#{$this.data('url')}#{ajaxUrlExtra}" + type: ajaxType + dataType: 'json' + data: + issuable_id: $this.data('issuable') + issuable_type: $this.data('issuable-type') + beforeSend: => + @beforeTodoSend($this, $todoLoading) + ).done (data) => + @todoUpdateDone(data, $this, $btnText, $todoLoading) + + beforeTodoSend: ($btn, $todoLoading) -> + $btn.disable() + $todoLoading.removeClass 'hidden' + + todoUpdateDone: (data, $btn, $btnText, $todoLoading) -> + $todoPendingCount = $('.todos-pending-count') + $todoPendingCount.text data.count + + $btn.enable() + $todoLoading.addClass 'hidden' + + if data.count is 0 + $todoPendingCount.addClass 'hidden' + else + $todoPendingCount.removeClass 'hidden' + + if data.todo? + $btn + .attr 'aria-label', $btn.data('mark-text') + .attr 'data-id', data.todo.id + $btnText.text $btn.data('mark-text') + else + $btn + .attr 'aria-label', $btn.data('todo-text') + .removeAttr 'data-id' + $btnText.text $btn.data('todo-text') sidebarDropdownLoading: (e) -> $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') @@ -117,5 +166,3 @@ class @Sidebar getBlock: (name) -> @sidebar.find(".block.#{name}") - - diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 5eb915a51ea..421328554b8 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -67,8 +67,12 @@ class @SearchAutocomplete getData: (term, callback) -> _this = @ - # Do not trigger request if input is empty - return if @searchInput.val() is '' + unless term + if contents = @getCategoryContents() + @searchInput.data('glDropdown').filter.options.callback contents + @enableAutocomplete() + + return # Prevent multiple ajax calls return if @loadingSuggestions @@ -122,6 +126,37 @@ class @SearchAutocomplete ).always -> _this.loadingSuggestions = false + + getCategoryContents: -> + + userId = gon.current_user_id + { utils, projectOptions, groupOptions, dashboardOptions } = gl + + if utils.isInGroupsPage() and groupOptions + options = groupOptions[utils.getGroupSlug()] + + else if utils.isInProjectPage() and projectOptions + options = projectOptions[utils.getProjectSlug()] + + else if dashboardOptions + options = dashboardOptions + + { issuesPath, mrPath, name } = options + + items = [ + { header: "#{name}" } + { text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" } + { text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" } + 'separator' + { text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" } + { text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" } + ] + + items.splice 0, 1 unless name + + return items + + serializeState: -> { # Search Criteria @@ -209,6 +244,12 @@ class @SearchAutocomplete @isFocused = true @wrap.addClass('search-active') + @getData() if @getValue() is '' + + + getValue: -> return @searchInput.val() + + onClearInputClick: (e) => e.preventDefault() @searchInput.val('').focus() @@ -229,6 +270,10 @@ class @SearchAutocomplete @locationBadgeEl.text(badgeText).show() @wrap.addClass('has-location-badge') + + hasLocationBadge: -> return @wrap.is '.has-location-badge' + + restoreOriginalState: -> inputs = Object.keys @originalState @@ -257,13 +302,14 @@ class @SearchAutocomplete @getElement("##{input}").val('') + removeLocationBadge: -> - @locationBadgeEl.hide() - # Reset state + @locationBadgeEl.hide() @resetSearchState() - @wrap.removeClass('has-location-badge') + @disableAutocomplete() + disableAutocomplete: -> @searchInput.addClass('disabled') diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index f3d66004138..c03877e9b06 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -1,7 +1,7 @@ class @Shortcuts - constructor: -> + constructor: (skipResetBindings) -> @enabledHelp = [] - Mousetrap.reset() + Mousetrap.reset() if not skipResetBindings Mousetrap.bind('?', @onToggleHelp) Mousetrap.bind('s', Shortcuts.focusSearch) Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview) diff --git a/app/assets/javascripts/shortcuts_blob.coffee b/app/assets/javascripts/shortcuts_blob.coffee new file mode 100644 index 00000000000..6d21e5d1150 --- /dev/null +++ b/app/assets/javascripts/shortcuts_blob.coffee @@ -0,0 +1,10 @@ +#= require shortcuts + +class @ShortcutsBlob extends Shortcuts + constructor: (skipResetBindings) -> + super skipResetBindings + Mousetrap.bind('y', ShortcutsBlob.copyToClipboard) + + @copyToClipboard: -> + clipboardButton = $('.btn-clipboard') + clipboardButton.click() if clipboardButton diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 2ce63c16428..68009e58645 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -3,13 +3,33 @@ expanded = 'page-sidebar-expanded' toggleSidebar = -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") - $('header').toggleClass("header-collapsed header-expanded") + $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded") + + if $.cookie('pin_nav') is 'true' + $('.navbar-fixed-top').toggleClass('header-pinned-nav') + $('.page-with-sidebar').toggleClass('page-sidebar-pinned') setTimeout ( -> - niceScrollBars = $('.nicescroll').niceScroll(); + niceScrollBars = $('.nav-sidebar').niceScroll(); niceScrollBars.updateScrollBar(); ), 300 +$(document) + .off 'click', 'body' + .on 'click', 'body', (e) -> + unless $.cookie('pin_nav') is 'true' + $target = $(e.target) + $nav = $target.closest('.sidebar-wrapper') + pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded') + $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle') + + if $nav.length is 0 and pageExpanded and $toggle.length is 0 + $('.page-with-sidebar') + .toggleClass('page-sidebar-collapsed page-sidebar-expanded') + + $('.navbar-fixed-top') + .toggleClass('header-collapsed header-expanded') + $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) -> e.preventDefault() diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee index f27780dda93..01b28171f72 100644 --- a/app/assets/javascripts/star.js.coffee +++ b/app/assets/javascripts/star.js.coffee @@ -9,9 +9,11 @@ class @Star $this.parent().find('.star-count').text data.star_count if isStarred $starSpan.removeClass('starred').text 'Star' + gl.utils.updateTooltipTitle $this, 'Star project' $starIcon.removeClass('fa-star').addClass 'fa-star-o' else $starSpan.addClass('starred').text 'Unstar' + gl.utils.updateTooltipTitle $this, 'Unstar project' $starIcon.removeClass('fa-star-o').addClass 'fa-star' return diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 88246b0feb8..2548efb2186 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -31,7 +31,7 @@ class @UsersSelect assignTo = (selected) -> data = {} data[abilityName] = {} - data[abilityName].assignee_id = selected + data[abilityName].assignee_id = if selected? then selected else null $loading .fadeIn() $dropdown.trigger('loading.gl.dropdown') @@ -72,7 +72,7 @@ class @UsersSelect assigneeTemplate = _.template( '<% if (username) { %> - <a class="author_link " href="/u/<%= username %>"> + <a class="author_link bold" href="/u/<%= username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%= avatar %>"> <% } %> @@ -82,7 +82,7 @@ class @UsersSelect </span> </a> <% } else { %> - <span class="assign-yourself"> + <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 408d4a68e1e..0a8603b6702 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -8,8 +8,8 @@ */ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { .page-with-sidebar { - - .collapse-nav a { + .toggle-nav-collapse, + .pin-nav-btn { color: $color-light; background: $color; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 63996ea44f6..dca4dbb9f7d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -3,7 +3,7 @@ * */ header { - transition-duration: .3s; + transition: padding $sidebar-transition-duration; &.navbar-empty { height: $header-height; @@ -79,14 +79,9 @@ header { &.header-collapsed { padding: 0 16px; - - .side-nav-toggle { - display: block; - } } .side-nav-toggle { - display: none; position: absolute; left: -10px; margin: 6px 0; @@ -108,9 +103,7 @@ header { .header-content { position: relative; height: $header-height; - padding-right: 40px; padding-left: 30px; - transition-duration: .3s; @media (min-width: $screen-sm-min) { padding-right: 0; @@ -198,18 +191,6 @@ header { } } -.header-collapsed { - margin-left: 0; - - .header-content { - - @media (min-width: $screen-sm-max) { - padding-left: 30px; - transition-duration: .3s; - } - } -} - .tanuki-shape { transition: all 0.8s; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index b34ec16cdba..a12c0bba44a 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -159,7 +159,7 @@ ul.content-list { background-color: $gray-light; border: dotted 1px $gray-dark; margin: 1px 0; - min-height: 30px; + min-height: 52px; } } } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 4de89daeb36..a55918f8711 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -74,6 +74,7 @@ .container-fluid { background-color: $background-color; + margin-bottom: 0; } li { @@ -241,6 +242,12 @@ } } } + + &.adjust { + .nav-text, .nav-controls { + width: auto; + } + } } .layout-nav { @@ -250,7 +257,7 @@ z-index: 11; background: $background-color; border-bottom: 1px solid $border-color; - transition-duration: .3s; + transition: padding $sidebar-transition-duration; text-align: center; .container-fluid { @@ -280,11 +287,10 @@ } .dropdown { - margin-left: 7px; - - @media (max-width: $screen-xs-min) { - margin-left: 0; - } + position: absolute; + top: 7px; + right: 15px; + z-index: 2; li.active { font-weight: bold; @@ -347,6 +353,12 @@ .badge { color: $gl-icon-color; } + + &:hover { + a, i { + color: $black; + } + } } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index b7ec3f70bfb..281c0a0e1e9 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,26 +1,31 @@ .page-with-sidebar { padding-top: $header-height; - transition-duration: .3s; + transition: padding $sidebar-transition-duration; .sidebar-wrapper { position: fixed; top: 0; bottom: 0; - overflow-y: auto; - overflow-x: hidden; left: 0; height: 100%; - transition-duration: .3s; + overflow: hidden; + transition: width $sidebar-transition-duration; } } .sidebar-wrapper { z-index: 1000; background: $background-color; + + .nicescroll-rails-hr { + // TODO: Figure out why nicescroll doesn't hide horizontal bar + display: none!important; + } } .content-wrapper { width: 100%; + transition: padding $sidebar-transition-duration; .container-fluid { background: #fff; @@ -34,22 +39,19 @@ } } -.sidebar-wrapper { - - .sidebar-user { - padding: 15px 22px; - position: fixed; - bottom: 0; - width: $sidebar_width; - overflow: hidden; - transition-duration: .3s; +.sidebar-user { + padding: 15px; + position: absolute; + left: 0; + bottom: 0; + width: $sidebar_width; + overflow: hidden; + font-size: 16px; + line-height: 36px; + transition: width $sidebar-transition-duration, padding $sidebar-transition-duration; - .username { - margin-left: 10px; - width: $sidebar_width - 2 * 10px; - font-size: 16px; - line-height: 34px; - } + @media (min-width: $sidebar-breakpoint) { + bottom: 50px; } } @@ -65,39 +67,46 @@ .nav-sidebar { - margin-top: 22 + $header-height; - margin-bottom: 116px; - transition-duration: .3s; - list-style: none; - overflow: hidden; + position: absolute; + top: 50px; + bottom: 65px; + width: $sidebar_width; + overflow-y: auto; + overflow-x: hidden; + + @media (min-width: $sidebar-breakpoint) { + bottom: 115px; + } &.navbar-collapse { padding: 0 !important; } li { - width: $sidebar_width; - &.separate-item { padding-top: 10px; margin-top: 10px; } + .icon-container { + width: 34px; + display: inline-block; + text-align: center; + } + a { - width: $sidebar_width; - padding: 7px 15px 7px 23px; + padding: 7px 15px 7px 12px; font-size: $gl-font-size; line-height: 24px; display: block; text-decoration: none; font-weight: normal; outline: none; + white-space: nowrap; - &:hover { - text-decoration: none; - } - - &:active, &:focus { + &:hover, + &:active, + &:focus { text-decoration: none; } @@ -109,10 +118,6 @@ svg { margin-right: 13px; } - - &.back-link i { - transition-duration: .3s; - } } } @@ -123,37 +128,50 @@ } } -.sidebar-subnav { - margin-left: 0; - padding-left: 0; - - li { - list-style: none; - } -} - -.collapse-nav a { +.toggle-nav-collapse { width: $sidebar_width; - position: fixed; + position: absolute; top: 0; left: 0; + min-height: 50px; padding: 5px 0; font-size: 18px; - background: transparent; - height: 50px; - text-align: center; - line-height: 40px; + line-height: 30px; +} + +.nav-header-btn { + padding: 10px 5px; + color: inherit; transition-duration: .3s; - outline: none; - &:hover { + &:hover, + &:focus { + color: $white-light; text-decoration: none; } } -.sidebar-wrapper { - &.hidden-nav { - width: 0; +.pin-nav-btn { + display: none; + position: absolute; + left: 0; + bottom: 0; + height: 50px; + width: $sidebar_width; + line-height: 30px; + + @media (min-width: $sidebar-breakpoint) { + display: block; + } + + .fa { + transition: transform .15s; + } + + &.is-active { + .fa { + transform: rotate(90deg); + } } } @@ -162,62 +180,34 @@ .sidebar-wrapper { width: 0; - - .nav-sidebar { - width: 0; - - li { - width: auto; - - a { - span { - display: none; - } - } - } - } - - .collapse-nav a { - width: 0; - - i { - display: none; - } - } - - .sidebar-user { - width: 0; - padding-left: 0; - padding-right: 0; - - .username { - display: none; - } - } } } .page-sidebar-expanded { - - @media (max-width: $screen-sm-max) { - padding-left: 0; - } - .sidebar-wrapper { width: $sidebar_width; + } +} - .nav-sidebar { - width: $sidebar_width; +.page-sidebar-pinned { + .content-wrapper, + .layout-nav { + @media (min-width: $sidebar-breakpoint) { + padding-left: $sidebar_width; } + } +} - .nav-sidebar li a { - width: $sidebar_width; +header.header-pinned-nav { + @media (min-width: $sidebar-breakpoint) { + padding-left: ($sidebar_width + $gl-padding); - &.back-link { - i { - opacity: 0; - } - } + .side-nav-toggle { + display: none; + } + + .header-content { + padding-left: 0; } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 752d8ec8788..acada1f16a0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -6,6 +6,8 @@ $sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 258px; +$sidebar-transition-duration: .15s; +$sidebar-breakpoint: 1440px; /* * UI elements diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index c8c6bbde084..761e33f0df7 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -7,84 +7,111 @@ margin-right: 9px; } -.lists-separator { - margin: 10px 0; - border-color: #ddd; +.commit-header { + padding: 5px 10px; + background-color: $background-color; + border-top: 1px solid #eee; + border-bottom: 1px solid #eee; + font-size: 14px; + + &:first-child { + border-top-width: 0; + } } -.commits-row { - ul { - margin: 0; +.commit-row-title { + line-height: 1; + margin-bottom: 7px; - li.commit { - padding: 8px 0; - } + .notes_count { + float: right; + margin-right: 10px; + } + + .str-truncated { + max-width: 70%; } - .commits-row-date { - font-size: 15px; - line-height: 20px; - margin-bottom: 5px; + .commit-row-message { + color: $gl-dark-link-color; + } + + .text-expander { + display: inline-block; + background: $gray-light; + color: $gl-placeholder-color; + padding: 0 5px; + cursor: pointer; + border: 1px solid $border-gray-dark; + border-radius: $border-radius-default; + margin-left: 5px; + + &:hover { + background-color: darken($gray-light, 10%); + text-decoration: none; + } } } -li.commit { - list-style: none; +.commit-actions { + @media (min-width: $screen-sm-min) { + float: right; + margin-left: $gl-padding; + margin-top: 2px; + font-size: 0; + } - .commit-row-title { - font-size: $list-font-size; - line-height: 20px; - margin-bottom: 2px; + .btn-transparent { + padding-left: 0; + padding-right: 0; + } - .btn-clipboard { - margin-top: -1px; + .btn { + &:not(:first-child) { + margin-left: $gl-padding; } + } +} - .notes_count { - float: right; - margin-right: 10px; - } +.commit-short-id { + font-family: $monospace_font; + font-weight: 600; +} - .commit_short_id { - min-width: 65px; - color: $gl-dark-link-color; - font-family: $monospace_font; - } +.commit { + padding: 10px 0; - .str-truncated { - max-width: 70%; - } + @media (min-width: $screen-sm-min) { + padding-left: 46px; + } - .commit-row-message { - color: $gl-dark-link-color; + &:not(:last-child) { + border-bottom: 1px solid #eee; + } - &:hover { - text-decoration: underline; - } - } + a, + button { + color: $gl-dark-link-color; + vertical-align: baseline; + } - .text-expander { - background: #eee; - color: #555; - padding: 0 5px; - cursor: pointer; - margin-left: 4px; - &:hover { - background-color: #ddd; - } - } + .avatar { + margin-left: -46px; } .item-title { display: inline-block; - max-width: 70%; + + @media (min-width: $screen-sm-min) { + max-width: 70%; + } } .commit-row-description { font-size: 14px; border-left: 1px solid #eee; padding: 10px 15px; - margin: 5px 0 10px 5px; + margin: 10px 0; background: #f9f9f9; display: none; @@ -93,6 +120,7 @@ li.commit { background: inherit; padding: 0; margin: 0; + white-space: pre-wrap; } a { @@ -102,7 +130,7 @@ li.commit { .commit-row-info { color: $gl-gray; - line-height: 24px; + line-height: 1; a { color: $gl-gray; @@ -111,10 +139,6 @@ li.commit { .avatar { margin-right: 8px; } - - .committed_ago { - display: inline-block; - } } &.inline-commit { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss new file mode 100644 index 00000000000..e160d676e35 --- /dev/null +++ b/app/assets/stylesheets/pages/environments.scss @@ -0,0 +1,5 @@ +.environments { + .commit-title { + margin: 0; + } +} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index ec6c099df5b..ac7721cbe15 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -39,3 +39,20 @@ } } } + +.groups-cover-block { + + .container-fluid { + position: relative; + } + + .access-request-button { + @include btn-gray; + position: absolute; + right: 16px; + bottom: 32px; + padding: 3px 10px; + text-transform: none; + background-color: $background-color; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index ea453ce356a..687117233f6 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -34,6 +34,10 @@ color: inherit; } + .issuable-header-text { + margin-top: 7px; + } + .block { @include clearfix; padding: $gl-padding 0; @@ -60,10 +64,6 @@ margin-top: 0; } - .issuable-count { - margin-top: 7px; - } - .gutter-toggle { margin-left: 20px; padding-left: 10px; @@ -145,7 +145,6 @@ .assign-yourself { margin-top: 10px; - font-weight: normal; display: block; } } @@ -158,6 +157,10 @@ font-weight: normal; } + .no-value { + color: $gl-placeholder-color; + } + .sidebar-collapsed-icon { display: none; } @@ -248,11 +251,16 @@ padding-bottom: 0; margin-bottom: 10px; } + + .issuable-header-btn { + display: none; + } } - .issuable-pager { + .issuable-header-btn { background: $gray-normal; border: 1px solid $border-gray-normal; + &:hover { background: $gray-dark; border: 1px solid $border-gray-dark; @@ -263,7 +271,7 @@ } } - a:not(.issuable-pager) { + a { &:hover { color: $md-link-color; text-decoration: none; @@ -322,7 +330,7 @@ margin-left: 5px; a { - color: #8c8c8c; + color: $gl-placeholder-color; } } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index bc65404a741..046c38aba44 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -115,6 +115,13 @@ } } +.draggable-handler { + display: inline-block; + opacity: 0; + transition: opacity .3s; + color: $gray-darkest; +} + .prioritized-labels { margin-bottom: 30px; @@ -122,6 +129,13 @@ display: none; color: $gray-light; } + + li:hover { + .draggable-handler { + display: inline-block; + opacity: 1; + } + } } .other-labels { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index a47f2580aa3..53bff508c72 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -313,3 +313,13 @@ } } } + +.merged-buttons { + .btn { + float: left; + + &:not(:last-child) { + margin-right: 10px; + } + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 0c084118753..35d728aec83 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -139,6 +139,12 @@ ul.notes { @media (min-width: $screen-sm-min) { padding-right: 0; } + + @media (max-width: $screen-xs-min) { + .inline { + display: block; + } + } } .note-emoji-button { @@ -258,7 +264,11 @@ ul.notes { position: absolute; right: 0; top: 0; - + + .note-action-button { + margin-left: 10px; + } + @media (min-width: $screen-sm-min) { position: relative; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bb250904255..c85d23a31f0 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -5,10 +5,12 @@ font-weight: normal; } } + .no-ssh-key-message, .project-limit-message { background-color: #f28d35; margin-bottom: 0; } + .new_project, .edit-project { fieldset.features { @@ -18,13 +20,6 @@ } } -.project-name-holder { - .help-inline { - vertical-align: top; - padding: 7px; - } -} - .project-home-panel { background: $white-light; text-align: left; @@ -229,13 +224,20 @@ right: 16px; bottom: 0; - .btn { - padding: 3px 10px; - background-color: $background-color; + @media (max-width: $screen-lg-min) { + top: 0; } - @media (max-width: 1304px) { - top: 0; + .access-request-button { + position: absolute; + right: 0; + bottom: 61px; + + @media (max-width: $screen-lg-min) { + position: relative; + bottom: 0; + margin-right: 10px; + } } } @@ -286,10 +288,6 @@ color: #555; } -.project_member_row form { - margin: 0; -} - .transfer-project .select2-container { min-width: 200px; } @@ -373,6 +371,7 @@ a.deploy-project-label { .project-import .btn { float: left; + margin-bottom: 10px; margin-right: 10px; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index f16fc7f388f..99c9e81ddb9 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -101,7 +101,7 @@ margin: 0; .commit { - padding: 0; + padding: 0 0 0 55px; .commit-row-title { .commit-row-message { @@ -129,4 +129,6 @@ .tree-controls { float: right; margin-top: 11px; + position: relative; + z-index: 2; } diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 3865b2d61fd..c89678cf2d8 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -35,6 +35,7 @@ class AutocompleteController < ApplicationController project = Project.find_by_id(params[:project_id]) projects = current_user.authorized_projects + projects = projects.search(params[:search]) if params[:search].present? projects = projects.select do |project| current_user.can?(:admin_issue, project) end diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb new file mode 100644 index 00000000000..a24273fad0b --- /dev/null +++ b/app/controllers/concerns/membership_actions.rb @@ -0,0 +1,58 @@ +module MembershipActions + extend ActiveSupport::Concern + include MembersHelper + + def request_access + membershipable.request_access(current_user) + + redirect_to polymorphic_path(membershipable), + notice: 'Your request for access has been queued for review.' + end + + def approve_access_request + @member = membershipable.members.request.find(params[:id]) + + return render_403 unless can?(current_user, action_member_permission(:update, @member), @member) + + @member.accept_request + + redirect_to polymorphic_url([membershipable, :members]) + end + + def leave + @member = membershipable.members.find_by(user_id: current_user) + return render_403 unless @member + + source_type = @member.real_source_type.humanize(capitalize: false) + + if can?(current_user, action_member_permission(:destroy, @member), @member) + notice = + if @member.request? + "Your access request to the #{source_type} has been withdrawn." + else + "You left the \"#{@member.source.human_name}\" #{source_type}." + end + @member.destroy + + redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice + else + if cannot_leave? + alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}." + alert << " Transfer or delete the #{source_type}." + redirect_to polymorphic_url(membershipable), alert: alert + else + render_403 + end + end + end + + protected + + def membershipable + raise NotImplementedError + end + + def cannot_leave? + raise NotImplementedError + end +end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 48dbf656e84..d0f2e2949f0 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,11 +1,13 @@ class Groups::GroupMembersController < Groups::ApplicationController + include MembershipActions + # Authorize - before_action :authorize_admin_group_member!, except: [:index, :leave] + before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] def index @project = @group.projects.find(params[:project_id]) if params[:project_id] @members = @group.group_members - @members = @members.non_invite unless can?(current_user, :admin_group, @group) + @members = @members.non_pending unless can?(current_user, :admin_group, @group) if params[:search].present? users = @group.users.search(params[:search]).to_a @@ -58,25 +60,16 @@ class Groups::GroupMembersController < Groups::ApplicationController end end - def leave - @group_member = @group.group_members.find_by(user_id: current_user) - - if can?(current_user, :destroy_group_member, @group_member) - @group_member.destroy - - redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") - else - if @group.last_owner?(current_user) - redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.") - else - return render_403 - end - end - end - protected def member_params params.require(:group_member).permit(:access_level, :user_id) end + + # MembershipActions concern + alias_method :membershipable, :group + + def cannot_leave? + @group.last_owner?(current_user) + end end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 131a16dad9b..014b9b43ff2 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -42,7 +42,7 @@ class JwtController < ApplicationController end def authenticate_user(login, password) - user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password) + user = Gitlab::Auth.find_with_user_password(login, password) Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login) user end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 18ee55c839a..40d1906a53f 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,12 +1,13 @@ class Profiles::NotificationsController < Profiles::ApplicationController def show - @user = current_user - @group_notifications = current_user.notification_settings.for_groups - @project_notifications = current_user.notification_settings.for_projects + @user = current_user + @group_notifications = current_user.notification_settings.for_groups + @project_notifications = current_user.notification_settings.for_projects + @global_notification_setting = current_user.global_notification_setting end def update - if current_user.update_attributes(user_params) + if current_user.update_attributes(user_params) && update_notification_settings flash[:notice] = "Notification settings saved" else flash[:alert] = "Failed to save new settings" @@ -16,6 +17,18 @@ class Profiles::NotificationsController < Profiles::ApplicationController end def user_params - params.require(:user).permit(:notification_email, :notification_level) + params.require(:user).permit(:notification_email) + end + + def global_notification_setting_params + params.require(:global_notification_setting).permit(:level) + end + + private + + def update_notification_settings + return true unless global_notification_setting_params + + current_user.global_notification_setting.update_attributes(global_notification_setting_params) end end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 832d7deb57d..f11c8321464 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -1,22 +1,18 @@ class Projects::ArtifactsController < Projects::ApplicationController layout 'project' before_action :authorize_read_build! + before_action :authorize_update_build!, only: [:keep] + before_action :validate_artifacts! def download unless artifacts_file.file_storage? return redirect_to artifacts_file.url end - unless artifacts_file.exists? - return render_404 - end - send_file artifacts_file.path, disposition: 'attachment' end def browse - return render_404 unless build.artifacts? - directory = params[:path] ? "#{params[:path]}/" : '' @entry = build.artifacts_metadata_entry(directory) @@ -34,8 +30,17 @@ class Projects::ArtifactsController < Projects::ApplicationController end end + def keep + build.keep_artifacts! + redirect_to namespace_project_build_path(project.namespace, project, build) + end + private + def validate_artifacts! + render_404 unless build.artifacts? + end + def build @build ||= project.builds.find_by!(id: params[:build_id]) end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 14c82826342..ef3051d7519 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -51,7 +51,7 @@ class Projects::BuildsController < Projects::ApplicationController return render_404 end - build = Ci::Build.retry(@build) + build = Ci::Build.retry(@build, current_user) redirect_to build_path(build) end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 20637fa46fe..6751737d15e 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -46,7 +46,7 @@ class Projects::CommitController < Projects::ApplicationController def retry_builds ci_builds.latest.failed.each do |build| if build.retryable? - Ci::Build.retry(build) + Ci::Build.retry(build, current_user) end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb new file mode 100644 index 00000000000..4b433796161 --- /dev/null +++ b/app/controllers/projects/environments_controller.rb @@ -0,0 +1,49 @@ +class Projects::EnvironmentsController < Projects::ApplicationController + layout 'project' + before_action :authorize_read_environment! + before_action :authorize_create_environment!, only: [:new, :create] + before_action :authorize_update_environment!, only: [:destroy] + before_action :environment, only: [:show, :destroy] + + def index + @environments = project.environments + end + + def show + @deployments = environment.deployments.order(id: :desc).page(params[:page]) + end + + def new + @environment = project.environments.new + end + + def create + @environment = project.environments.create(create_params) + + if @environment.persisted? + redirect_to namespace_project_environment_path(project.namespace, project, @environment) + else + render 'new' + end + end + + def destroy + if @environment.destroy + flash[:notice] = 'Environment was successfully removed.' + else + flash[:alert] = 'Failed to remove environment.' + end + + redirect_to namespace_project_environments_path(project.namespace, project) + end + + private + + def create_params + params.require(:environment).permit(:name) + end + + def environment + @environment ||= project.environments.find(params[:id]) + end +end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 348d6cf4d96..f907d63258b 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -43,7 +43,7 @@ class Projects::GitHttpController < Projects::ApplicationController return if project && project.public? && upload_pack? authenticate_or_request_with_http_basic do |login, password| - auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip) + auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip) if auth_result.type == :ci && upload_pack? @ci = true diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 67e7187c10d..851822d805a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -204,10 +204,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.update(merge_error: nil) - if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active? - MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) - .execute(@merge_request) - @status = :merge_when_build_succeeds + if params[:merge_when_build_succeeds].present? + if @merge_request.pipeline && @merge_request.pipeline.active? + MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) + .execute(@merge_request) + @status = :merge_when_build_succeeds + elsif @merge_request.pipeline.success? + # This can be triggered when a user clicks the auto merge button while + # the tests finish at about the same time + MergeWorker.perform_async(@merge_request.id, current_user.id, params) + @status = :success + else + @status = :failed + end else MergeWorker.perform_async(@merge_request.id, current_user.id, params) @status = :success diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index cac440ae53e..127bd1a4318 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -32,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def retry - pipeline.retry_failed + pipeline.retry_failed(current_user) redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index cdea5f0b776..35d067cd029 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,10 +1,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController + include MembershipActions + # Authorize - before_action :authorize_admin_project_member!, except: [:leave, :index] + before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index @project_members = @project.project_members - @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) + @project_members = @project_members.non_pending unless can?(current_user, :admin_project, @project) if params[:search].present? users = @project.users.search(params[:search]).to_a @@ -14,9 +16,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project_members = @project_members.order('access_level DESC') @group = @project.group + if @group @group_members = @group.group_members - @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group) + @group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group) if params[:search].present? users = @group.users.search(params[:search]).to_a @@ -73,26 +76,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController end end - def leave - @project_member = @project.project_members.find_by(user_id: current_user) - - if can?(current_user, :destroy_project_member, @project_member) - @project_member.destroy - - respond_to do |format| - format.html { redirect_to dashboard_projects_path, notice: "You left the project." } - format.js { head :ok } - end - else - if current_user == @project.owner - message = 'You can not leave your own project. Transfer or delete the project.' - redirect_back_or_default(default: { action: 'index' }, options: { alert: message }) - else - render_403 - end - end - end - def apply_import source_project = Project.find(params[:source_project_id]) @@ -112,4 +95,11 @@ class Projects::ProjectMembersController < Projects::ApplicationController def member_params params.require(:project_member).permit(:user_id, :access_level) end + + # MembershipActions concern + alias_method :membershipable, :project + + def cannot_leave? + current_user == @project.owner + end end diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb new file mode 100644 index 00000000000..a51bd5e2b49 --- /dev/null +++ b/app/controllers/projects/todos_controller.rb @@ -0,0 +1,31 @@ +class Projects::TodosController < Projects::ApplicationController + def create + todos = TodoService.new.mark_todo(issuable, current_user) + + render json: { + todo: todos, + count: current_user.todos.pending.count, + } + end + + def update + current_user.todos.find_by_id(params[:id]).update(state: :done) + + render json: { + count: current_user.todos.pending.count, + } + end + + private + + def issuable + @issuable ||= begin + case params[:issuable_type] + when "issue" + @project.issues.find(params[:issuable_id]) + when "merge_request" + @project.merge_requests.find(params[:issuable_id]) + end + end + end +end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 2aa6bed0724..7ec1e73b3be 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController if @page render 'show' elsif file = @project_wiki.find_file(params[:id], params[:version_id]) + response.headers['Content-Security-Policy'] = "default-src 'none'" + response.headers['X-Content-Security-Policy'] = "default-src 'none'" + if file.on_disk? send_file file.on_disk_path, disposition: 'inline' else diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index dae8f7b1447..17aed816cbd 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -40,7 +40,7 @@ class SessionsController < Devise::SessionsController # Handle an "initial setup" state, where there's only one user, it's an admin, # and they require a password change. def check_initial_setup - return unless User.count == 1 + return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one user = User.admins.last diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index ee14ac60fb4..0b7832e6583 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -12,7 +12,7 @@ class NotesFinder when "commit" project.notes.for_commit_id(target_id).non_diff_notes when "issue" - project.issues.find(target_id).notes.inc_author + project.issues.visible_to_user(current_user).find(target_id).notes.inc_author when "merge_request" project.merge_requests.find(target_id).mr_and_commit_notes.inc_author when "snippet", "project_snippet" diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 01cbf91c658..00ff1611039 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -51,7 +51,7 @@ class SnippetsFinder snippets = project.snippets.fresh if current_user - if project.team.member?(current_user.id) || current_user.admin? + if project.team.member?(current_user) || current_user.admin? snippets else snippets.public_and_internal diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 1d88116d7d2..aa47c6c157e 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -36,7 +36,7 @@ class TodosFinder private def action_id? - action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i) + action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i) end def action_id diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index cec2dc753fe..85559fbc5f5 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -116,7 +116,7 @@ module BlobHelper end def blob_text_viewable?(blob) - blob && blob.text? && !blob.lfs_pointer? + blob && blob.text? && !blob.lfs_pointer? && !blob.only_display_raw? end def blob_size(blob) diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index e39548e17e1..3ee3fc74f0c 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -14,4 +14,8 @@ module BranchesHelper ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) end + + def project_branches + options_for_select(@project.repository.branch_names, @project.default_branch) + end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index f742922d926..07a3f452460 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -17,7 +17,15 @@ module ButtonHelper def clipboard_button(data = {}) content_tag :button, icon('clipboard'), - class: 'btn btn-clipboard', + class: "btn", + data: data, + type: :button + end + + def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard') + content_tag :button, + icon('clipboard'), + class: "btn #{css_class}", data: data, type: :button end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 07e5c146844..8e4ae1e6aec 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -38,10 +38,10 @@ module CiStatusHelper icon(icon_name + ' fw') end - def render_commit_status(commit, tooltip_placement: 'auto left') + def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '') project = commit.project path = builds_namespace_project_commit_path(project.namespace, project, commit) - render_status_with_link('commit', commit.status, path, tooltip_placement) + render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass) end def render_pipeline_status(pipeline, tooltip_placement: 'auto left') @@ -57,10 +57,10 @@ module CiStatusHelper private - def render_status_with_link(type, status, path, tooltip_placement) + def render_status_with_link(type, status, path, tooltip_placement, cssclass: '') link_to ci_icon_for_status(status), path, - class: "ci-status-link ci-status-icon-#{status.dasherize}", + class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}", title: "#{type.titleize}: #{ci_label_for_status(status)}", data: { toggle: 'tooltip', placement: tooltip_placement } end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index d328f56c80c..474041eccbb 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -16,6 +16,16 @@ module CommitsHelper commit_person_link(commit, options.merge(source: :committer)) end + def commit_author_avatar(commit, options = {}) + options = options.merge(source: :author) + user = commit.send(options[:source]) + + source_email = clean(commit.send "#{options[:source]}_email".to_sym) + person_email = user.try(:email) || source_email + + image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "") + end + def image_diff_class(diff) if diff.deleted_file "deleted" @@ -102,24 +112,24 @@ module CommitsHelper if current_controller?(:projects, :commits) if @repo.blob_at(commit.id, @path) return link_to( - "Browse File »", + "Browse File", namespace_project_blob_path(project.namespace, project, tree_join(commit.id, @path)), - class: "pull-right" + class: "btn btn-default" ) elsif @path.present? return link_to( - "Browse Directory »", + "Browse Directory", namespace_project_tree_path(project.namespace, project, tree_join(commit.id, @path)), - class: "pull-right" + class: "btn btn-default" ) end end link_to( "Browse Files", namespace_project_tree_path(project.namespace, project, commit), - class: "pull-right" + class: "btn btn-default" ) end @@ -129,7 +139,7 @@ module CommitsHelper tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip if can_collaborate_with_project? - btn_class = "btn btn-grouped btn-close btn-#{btn_class}" unless btn_class.nil? + btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { @@ -141,7 +151,7 @@ module CommitsHelper namespace_key: current_user.namespace.id, continue: continue_params) - btn_class = "btn btn-grouped btn-close" unless btn_class.nil? + btn_class = "btn btn-grouped btn-warning" unless btn_class.nil? link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end @@ -153,7 +163,7 @@ module CommitsHelper tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" if can_collaborate_with_project? - btn_class = "btn btn-default btn-grouped btn-#{btn_class}" unless btn_class.nil? + btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { @@ -187,12 +197,10 @@ module CommitsHelper source_email = clean(commit.send "#{options[:source]}_email".to_sym) person_name = user.try(:name) || source_name - person_email = user.try(:email) || source_email text = if options[:avatar] - avatar = image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]}", width: options[:size], alt: "") - %Q{#{avatar} <span class="commit-#{options[:source]}-name">#{person_name}</span>} + %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>} else person_name end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index cbe47176831..e22dce59d0f 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -135,6 +135,11 @@ module DiffHelper toggle_whitespace_link(url, options) end + def diff_compare_whitespace_link(project, from, to, options) + url = namespace_project_compare_path(project.namespace, project, from, to, params_with_whitespace) + toggle_whitespace_link(url, options) + end + private def hide_whitespace? diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 2ce2d4e694f..5386ddadc62 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -13,10 +13,23 @@ # merge_request_path(merge_request) # module GitlabRoutingHelper + # Project def project_path(project, *args) namespace_project_path(project.namespace, project, *args) end + def project_url(project, *args) + namespace_project_url(project.namespace, project, *args) + end + + def edit_project_path(project, *args) + edit_namespace_project_path(project.namespace, project, *args) + end + + def edit_project_url(project, *args) + edit_namespace_project_url(project.namespace, project, *args) + end + def project_files_path(project, *args) namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref) end @@ -29,6 +42,10 @@ module GitlabRoutingHelper namespace_project_pipelines_path(project.namespace, project, *args) end + def project_environments_path(project, *args) + namespace_project_environments_path(project.namespace, project, *args) + end + def project_builds_path(project, *args) namespace_project_builds_path(project.namespace, project, *args) end @@ -41,10 +58,6 @@ module GitlabRoutingHelper activity_namespace_project_path(project.namespace, project, *args) end - def edit_project_path(project, *args) - edit_namespace_project_path(project.namespace, project, *args) - end - def runners_path(project, *args) namespace_project_runners_path(project.namespace, project, *args) end @@ -65,14 +78,6 @@ module GitlabRoutingHelper namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args) end - def project_url(project, *args) - namespace_project_url(project.namespace, project, *args) - end - - def edit_project_url(project, *args) - edit_namespace_project_url(project.namespace, project, *args) - end - def issue_url(entity, *args) namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args) end @@ -92,4 +97,56 @@ module GitlabRoutingHelper toggle_subscription_namespace_project_merge_request_path(entity.project.namespace, entity.project, entity) end end + + ## Members + def project_members_url(project, *args) + namespace_project_project_members_url(project.namespace, project) + end + + def project_member_path(project_member, *args) + namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) + end + + def request_access_project_members_path(project, *args) + request_access_namespace_project_project_members_path(project.namespace, project) + end + + def leave_project_members_path(project, *args) + leave_namespace_project_project_members_path(project.namespace, project) + end + + def approve_access_request_project_member_path(project_member, *args) + approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) + end + + def resend_invite_project_member_path(project_member, *args) + resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) + end + + # Groups + + ## Members + def group_members_url(group, *args) + group_group_members_url(group, *args) + end + + def group_member_path(group_member, *args) + group_group_member_path(group_member.source, group_member) + end + + def request_access_group_members_path(group, *args) + request_access_group_group_members_path(group) + end + + def leave_group_members_path(group, *args) + leave_group_group_members_path(group) + end + + def approve_access_request_group_member_path(group_member, *args) + approve_access_request_group_group_member_path(group_member.source, group_member) + end + + def resend_invite_group_member_path(group_member, *args) + resend_invite_group_group_member_path(group_member.source, group_member) + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 4cac69c6795..b9211e88473 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,24 +1,4 @@ module GroupsHelper - def remove_user_from_group_message(group, member) - if member.user - "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?" - else - "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?" - end - end - - def leave_group_message(group) - "Are you sure you want to leave \"#{group}\" group?" - end - - def should_user_see_group_roles?(user, group) - if user - user.is_admin? || group.members.exists?(user_id: user.id) - else - false - end - end - def can_change_group_visibility_level?(group) can?(current_user, :change_visibility_level, group) end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 40d8ce8a1d3..8dbc51a689f 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -67,6 +67,12 @@ module IssuablesHelper end end + def has_todo(issuable) + unless current_user.nil? + current_user.todos.find_by(target_id: issuable.id, state: :pending) + end + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb new file mode 100644 index 00000000000..877c77050be --- /dev/null +++ b/app/helpers/members_helper.rb @@ -0,0 +1,39 @@ +module MembersHelper + # Returns a `<action>_<source>_member` association, e.g.: + # - admin_project_member, update_project_member, destroy_project_member + # - admin_group_member, update_group_member, destroy_group_member + def action_member_permission(action, member) + "#{action}_#{member.type.underscore}".to_sym + end + + def remove_member_message(member, user: nil) + user = current_user if defined?(current_user) + + text = 'Are you sure you want to ' + action = + if member.request? + if member.user == user + 'withdraw your access request for' + else + "deny #{member.user.name}'s request to join" + end + elsif member.invite? + "revoke the invitation for #{member.invite_email} to join" + else + "remove #{member.user.name} from" + end + + text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?" + end + + def remove_member_title(member) + text = " from #{member.real_source_type.humanize(capitalize: false)}" + + text.prepend(member.request? ? 'Deny access request' : 'Remove user') + end + + def leave_confirmation_message(member_source) + "Are you sure you want to leave the " \ + "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?" + end +end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 469accf3142..3ff8be5e284 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -12,10 +12,10 @@ module NavHelper end def page_sidebar_class - if nav_menu_collapsed? - "page-sidebar-collapsed" + if pinned_nav? + "page-sidebar-expanded page-sidebar-pinned" else - "page-sidebar-expanded" + "page-sidebar-collapsed" end end @@ -36,7 +36,15 @@ module NavHelper end def nav_header_class - class_name = " with-horizontal-nav" if defined?(nav) && nav + class_name = '' + class_name << " with-horizontal-nav" if defined?(nav) && nav + + if pinned_nav? + class_name << " header-expanded header-pinned-nav" + else + class_name << " header-collapsed" + end + class_name end @@ -47,4 +55,8 @@ module NavHelper def nav_control_class "nav-control" if current_user end + + def pinned_nav? + cookies[:pin_nav] == 'true' + end end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index b8e64b3890a..50c21fc0d49 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -61,4 +61,23 @@ module NotificationsHelper end end end + + def notification_level_radio_buttons + html = "" + + NotificationSetting.levels.each_key do |level| + level = level.to_sym + next if level == :global + + html << content_tag(:div, class: "radio") do + content_tag(:label, { value: level }) do + radio_button_tag(:"global_notification_setting[level]", level, @global_notification_setting.level.to_sym == level) + + content_tag(:div, level.to_s.capitalize, class: "level-title") + + content_tag(:p, notification_description(level)) + end + end + end + + html.html_safe + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5e5d170a9f3..d91e3332e48 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -1,12 +1,4 @@ module ProjectsHelper - def remove_from_project_team_message(project, member) - if member.user - "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?" - else - "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?" - end - end - def link_to_project(project) link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do title = content_tag(:span, project.name, class: 'project-name') @@ -49,7 +41,7 @@ module ProjectsHelper author_html = author_html.html_safe if opts[:name] - link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe + link_to(author_html, user_path(author), class: "author_link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe else title = opts[:title].sub(":name", sanitize(author.name)) link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe @@ -115,14 +107,6 @@ module ProjectsHelper end end - def user_max_access_in_project(user_id, project) - level = project.team.max_member_access(user_id) - - if level - Gitlab::Access.options_with_owner.key(level) - end - end - def license_short_name(project) return 'LICENSE' if project.repository.license_key.nil? @@ -156,6 +140,10 @@ module ProjectsHelper nav_tabs << :container_registry end + if can?(current_user, :read_environment, project) + nav_tabs << :environments + end + if can?(current_user, :admin_project, project) nav_tabs << :settings end @@ -286,10 +274,6 @@ module ProjectsHelper end end - def leave_project_message(project) - "Are you sure you want to leave \"#{project.name}\" project?" - end - def new_readme_path ref = @repository.root_ref if @repository ref ||= 'master' diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index 8142f733e76..b04b0a5114c 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -20,7 +20,6 @@ module TimeHelper end end - def date_from_to(from, to) "#{from.to_s(:short)} - #{to.to_s(:short)}" end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index b4923fbb138..9adf5ef29f7 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -12,6 +12,7 @@ module TodosHelper when Todo::ASSIGNED then 'assigned you' when Todo::MENTIONED then 'mentioned you on' when Todo::BUILD_FAILED then 'The build failed for your' + when Todo::MARKED then 'marked this as a Todo for' end end diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb deleted file mode 100644 index 1c43f95dc8c..00000000000 --- a/app/mailers/emails/groups.rb +++ /dev/null @@ -1,52 +0,0 @@ -module Emails - module Groups - def group_access_granted_email(group_member_id) - @group_member = GroupMember.find(group_member_id) - @group = @group_member.group - - @target_url = group_url(@group) - @current_user = @group_member.user - - mail(to: @group_member.user.notification_email, - subject: subject("Access to group was granted")) - end - - def group_member_invited_email(group_member_id, token) - @group_member = GroupMember.find group_member_id - @group = @group_member.group - @token = token - - @target_url = group_url(@group) - @current_user = @group_member.user - - mail(to: @group_member.invite_email, - subject: "Invitation to join group #{@group.name}") - end - - def group_invite_accepted_email(group_member_id) - @group_member = GroupMember.find group_member_id - return if @group_member.created_by.nil? - - @group = @group_member.group - - @target_url = group_url(@group) - @current_user = @group_member.created_by - - mail(to: @group_member.created_by.notification_email, - subject: subject("Invitation accepted")) - end - - def group_invite_declined_email(group_id, invite_email, access_level, created_by_id) - return if created_by_id.nil? - - @group = Group.find(group_id) - @current_user = @created_by = User.find(created_by_id) - @access_level = access_level - @invite_email = invite_email - - @target_url = group_url(@group) - mail(to: @created_by.notification_email, - subject: subject("Invitation declined")) - end - end -end diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb new file mode 100644 index 00000000000..6dde2e9847d --- /dev/null +++ b/app/mailers/emails/members.rb @@ -0,0 +1,81 @@ +module Emails + module Members + extend ActiveSupport::Concern + include MembersHelper + + included do + helper_method :member_source, :member + end + + def member_access_requested_email(member_source_type, member_id) + @member_source_type = member_source_type + @member_id = member_id + + admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email) + + mail(to: admins, + subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}")) + end + + def member_access_granted_email(member_source_type, member_id) + @member_source_type = member_source_type + @member_id = member_id + + mail(to: member.user.notification_email, + subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted")) + end + + def member_access_denied_email(member_source_type, source_id, user_id) + @member_source_type = member_source_type + @member_source = member_source_class.find(source_id) + requester = User.find(user_id) + + mail(to: requester.notification_email, + subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied")) + end + + def member_invited_email(member_source_type, member_id, token) + @member_source_type = member_source_type + @member_id = member_id + @token = token + + mail(to: member.invite_email, + subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}") + end + + def member_invite_accepted_email(member_source_type, member_id) + @member_source_type = member_source_type + @member_id = member_id + return unless member.created_by + + mail(to: member.created_by.notification_email, + subject: subject('Invitation accepted')) + end + + def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id) + return unless created_by_id + + @member_source_type = member_source_type + @member_source = member_source_class.find(source_id) + @invite_email = invite_email + inviter = User.find(created_by_id) + + mail(to: inviter.notification_email, + subject: subject('Invitation declined')) + end + + def member + @member ||= Member.find(@member_id) + end + + def member_source + @member_source ||= member.source + end + + private + + def member_source_class + @member_source_type.classify.constantize + end + end +end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index fdf1e9f5afc..689fb3e0ffb 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -1,55 +1,5 @@ module Emails module Projects - def project_access_granted_email(project_member_id) - @project_member = ProjectMember.find project_member_id - @project = @project_member.project - - @target_url = namespace_project_url(@project.namespace, @project) - @current_user = @project_member.user - - mail(to: @project_member.user.notification_email, - subject: subject("Access to project was granted")) - end - - def project_member_invited_email(project_member_id, token) - @project_member = ProjectMember.find project_member_id - @project = @project_member.project - @token = token - - @target_url = namespace_project_url(@project.namespace, @project) - @current_user = @project_member.user - - mail(to: @project_member.invite_email, - subject: "Invitation to join project #{@project.name_with_namespace}") - end - - def project_invite_accepted_email(project_member_id) - @project_member = ProjectMember.find project_member_id - return if @project_member.created_by.nil? - - @project = @project_member.project - - @target_url = namespace_project_url(@project.namespace, @project) - @current_user = @project_member.created_by - - mail(to: @project_member.created_by.notification_email, - subject: subject("Invitation accepted")) - end - - def project_invite_declined_email(project_id, invite_email, access_level, created_by_id) - return if created_by_id.nil? - - @project = Project.find(project_id) - @current_user = @created_by = User.find(created_by_id) - @access_level = access_level - @invite_email = invite_email - - @target_url = namespace_project_url(@project.namespace, @project) - - mail(to: @created_by.notification_email, - subject: subject("Invitation declined")) - end - def project_was_moved_email(project_id, user_id, old_path_with_namespace) @current_user = @user = User.find user_id @project = Project.find project_id diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 1c663bdd521..0cc709f68e4 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -6,13 +6,15 @@ class Notify < BaseMailer include Emails::Notes include Emails::Projects include Emails::Profile - include Emails::Groups include Emails::Builds + include Emails::Members add_template_helper MergeRequestsHelper add_template_helper DiffHelper add_template_helper BlobHelper add_template_helper EmailsHelper + add_template_helper MembersHelper + add_template_helper GitlabRoutingHelper def test_email(recipient_email, subject, body) mail(to: recipient_email, diff --git a/app/models/ability.rb b/app/models/ability.rb index 44515550d9e..9c58b956007 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -9,7 +9,6 @@ class Ability when CommitStatus then commit_status_abilities(user, subject) when Project then project_abilities(user, subject) when Issue then issue_abilities(user, subject) - when ExternalIssue then external_issue_abilities(user, subject) when Note then note_abilities(user, subject) when ProjectSnippet then project_snippet_abilities(user, subject) when PersonalSnippet then personal_snippet_abilities(user, subject) @@ -19,6 +18,7 @@ class Ability when GroupMember then group_member_abilities(user, subject) when ProjectMember then project_member_abilities(user, subject) when User then user_abilities + when ExternalIssue, Deployment, Environment then project_abilities(user, subject.project) else [] end.concat(global_abilities(user)) end @@ -187,6 +187,8 @@ class Ability project_report_rules elsif team.guest?(user) project_guest_rules + else + [] end end @@ -228,6 +230,8 @@ class Ability :read_build, :read_container_image, :read_pipeline, + :read_environment, + :read_deployment ] end @@ -246,6 +250,8 @@ class Ability :push_code, :create_container_image, :update_container_image, + :create_environment, + :create_deployment ] end @@ -263,6 +269,8 @@ class Ability @project_master_rules ||= project_dev_rules + [ :push_code_to_protected_branches, :update_project_snippet, + :update_environment, + :update_deployment, :admin_milestone, :admin_project_snippet, :admin_project_member, @@ -273,7 +281,9 @@ class Ability :admin_commit_status, :admin_build, :admin_container_image, - :admin_pipeline + :admin_pipeline, + :admin_environment, + :admin_deployment ] end @@ -317,6 +327,8 @@ class Ability unless project.builds_enabled rules += named_abilities('build') rules += named_abilities('pipeline') + rules += named_abilities('environment') + rules += named_abilities('deployment') end unless project.container_registry_enabled @@ -511,10 +523,6 @@ class Ability end end - def external_issue_abilities(user, subject) - project_abilities(user, subject.project) - end - private def restricted_public_level? @@ -533,7 +541,7 @@ class Ability def filter_confidential_issues_abilities(user, issue, rules) return rules if user.admin? || !issue.confidential? - unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id) + unless issue.author == user || issue.assignee == user || issue.project.team.member?(user, Gitlab::Access::REPORTER) rules.delete(:admin_issue) rules.delete(:read_issue) rules.delete(:update_issue) diff --git a/app/models/blob.rb b/app/models/blob.rb index 0fea6b7f576..4279ea2ce57 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -24,7 +24,7 @@ class Blob < SimpleDelegator end def only_display_raw? - size && size > 5.megabytes + size && truncated? end def svg? diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6a64ca451f7..764d8e4e136 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -11,6 +11,8 @@ module Ci scope :unstarted, ->() { where(runner_id: nil) } scope :ignore_failures, ->() { where(allow_failure: false) } + scope :with_artifacts, ->() { where.not(artifacts_file: nil) } + scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -38,7 +40,7 @@ module Ci new_build.save end - def retry(build) + def retry(build, user = nil) new_build = Ci::Build.new(status: 'pending') new_build.ref = build.ref new_build.tag = build.tag @@ -52,6 +54,7 @@ module Ci new_build.stage = build.stage new_build.stage_idx = build.stage_idx new_build.trigger_request = build.trigger_request + new_build.user = user new_build.save MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build @@ -73,6 +76,17 @@ module Ci build.update_coverage build.execute_hooks end + + after_transition any => [:success] do |build| + if build.environment.present? + service = CreateDeploymentService.new(build.project, build.user, + environment: build.environment, + sha: build.sha, + ref: build.ref, + tag: build.tag) + service.execute(build) + end + end end def retryable? @@ -83,10 +97,6 @@ module Ci !self.pipeline.statuses.latest.include?(self) end - def retry - Ci::Build.retry(self) - end - def depends_on_builds # Get builds of the same type latest_builds = self.pipeline.builds.latest @@ -317,7 +327,7 @@ module Ci end def artifacts? - artifacts_file.exists? + !artifacts_expired? && artifacts_file.exists? end def artifacts_metadata? @@ -328,11 +338,15 @@ module Ci Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path, **options).to_entry end + def erase_artifacts! + remove_artifacts_file! + remove_artifacts_metadata! + end + def erase(opts = {}) return false unless erasable? - remove_artifacts_file! - remove_artifacts_metadata! + erase_artifacts! erase_trace! update_erased!(opts[:erased_by]) end @@ -345,6 +359,25 @@ module Ci !self.erased_at.nil? end + def artifacts_expired? + artifacts_expire_at && artifacts_expire_at < Time.now + end + + def artifacts_expire_in + artifacts_expire_at - Time.now if artifacts_expire_at + end + + def artifacts_expire_in=(value) + self.artifacts_expire_at = + if value + Time.now + ChronicDuration.parse(value) + end + end + + def keep_artifacts! + self.update(artifacts_expire_at: nil) + end + private def erase_trace! @@ -352,7 +385,7 @@ module Ci end def update_erased!(user = nil) - self.update(erased_by: user, erased_at: Time.now) + self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) end def yaml_variables diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d780467034e..13be5b0fa5d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -76,8 +76,10 @@ module Ci builds.running_or_pending.each(&:cancel) end - def retry_failed - builds.latest.failed.select(&:retryable?).each(&:retry) + def retry_failed(user) + builds.latest.failed.select(&:retryable?).each do |build| + Ci::Build.retry(build, user) + end end def latest? @@ -161,6 +163,10 @@ module Ci git_commit_message =~ /(\[ci skip\])/ if git_commit_message end + def environments + builds.where.not(environment: nil).success.pluck(:environment).uniq + end + def notes Note.for_commit_id(sha) end diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb new file mode 100644 index 00000000000..eedd32a729f --- /dev/null +++ b/app/models/concerns/access_requestable.rb @@ -0,0 +1,16 @@ +# == AccessRequestable concern +# +# Contains functionality related to objects that can receive request for access. +# +# Used by Project, and Group. +# +module AccessRequestable + extend ActiveSupport::Concern + + def request_access(user) + members.create( + access_level: Gitlab::Access::DEVELOPER, + user: user, + requested_at: Time.now.utc) + end +end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index aa4b4201250..539c7c31e30 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -5,7 +5,7 @@ module Awardable has_many :award_emoji, as: :awardable, dependent: :destroy if self < Participable - participant :award_emoji + participant :award_emoji_with_associations end end @@ -34,8 +34,12 @@ module Awardable end end + def award_emoji_with_associations + award_emoji.includes(:user) + end + def grouped_awards(with_thumbs: true) - awards = award_emoji.group_by(&:name) + awards = award_emoji_with_associations.group_by(&:name) if with_thumbs awards[AwardEmoji::UPVOTE_NAME] ||= [] diff --git a/app/models/deployment.rb b/app/models/deployment.rb new file mode 100644 index 00000000000..e498ca96e3c --- /dev/null +++ b/app/models/deployment.rb @@ -0,0 +1,29 @@ +class Deployment < ActiveRecord::Base + include InternalId + + belongs_to :project, required: true, validate: true + belongs_to :environment, required: true, validate: true + belongs_to :user + belongs_to :deployable, polymorphic: true + + validates :sha, presence: true + validates :ref, presence: true + + delegate :name, to: :environment, prefix: true + + def commit + project.commit(sha) + end + + def commit_title + commit.try(:title) + end + + def short_sha + Commit.truncate_sha(sha) + end + + def last? + self == environment.last_deployment + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb new file mode 100644 index 00000000000..ac3a571a1f3 --- /dev/null +++ b/app/models/environment.rb @@ -0,0 +1,16 @@ +class Environment < ActiveRecord::Base + belongs_to :project, required: true, validate: true + + has_many :deployments + + validates :name, + presence: true, + uniqueness: { scope: :project_id }, + length: { within: 0..255 }, + format: { with: Gitlab::Regex.environment_name_regex, + message: Gitlab::Regex.environment_name_regex_message } + + def last_deployment + deployments.last + end +end diff --git a/app/models/group.rb b/app/models/group.rb index aec92e335e6..e66e04371b2 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -3,11 +3,18 @@ require 'carrierwave/orm/activerecord' class Group < Namespace include Gitlab::ConfigHelper include Gitlab::VisibilityLevel + include AccessRequestable include Referable has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' alias_method :members, :group_members - has_many :users, through: :group_members + has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members + + has_many :owners, + -> { where(members: { access_level: Gitlab::Access::OWNER }) }, + through: :group_members, + source: :user + has_many :project_group_links, dependent: :destroy has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source @@ -58,6 +65,10 @@ class Group < Namespace "#{self.class.reference_prefix}#{name}" end + def web_url + Gitlab::Routing.url_helpers.group_url(self) + end + def human_name name end @@ -83,10 +94,6 @@ class Group < Namespace end end - def owners - @owners ||= group_members.owners.includes(:user).map(&:user) - end - def add_users(user_ids, access_level, current_user = nil) user_ids.each do |user_id| Member.add_user(self.group_members, user_id, access_level, current_user) diff --git a/app/models/issue.rb b/app/models/issue.rb index 235922710ad..1bdf9c011b2 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -51,10 +51,18 @@ class Issue < ActiveRecord::Base end def self.visible_to_user(user) - return where(confidential: false) if user.blank? + return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank? return all if user.admin? - where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id)) + where(' + issues.confidential IS NULL + OR issues.confidential IS FALSE + OR (issues.confidential = TRUE + AND (issues.author_id = :user_id + OR issues.assignee_id = :user_id + OR issues.project_id IN(:project_ids)))', + user_id: user.id, + project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id)) end def self.reference_prefix diff --git a/app/models/member.rb b/app/models/member.rb index d3060f07fc0..cea6d259760 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -26,20 +26,28 @@ class Member < ActiveRecord::Base allow_nil: true } - scope :invite, -> { where(user_id: nil) } - scope :non_invite, -> { where("user_id IS NOT NULL") } + scope :invite, -> { where.not(invite_token: nil) } + scope :non_invite, -> { where(invite_token: nil) } + scope :request, -> { where.not(requested_at: nil) } + scope :non_request, -> { where(requested_at: nil) } + scope :non_pending, -> { non_request.non_invite } + scope :guests, -> { where(access_level: GUEST) } scope :reporters, -> { where(access_level: REPORTER) } scope :developers, -> { where(access_level: DEVELOPER) } scope :masters, -> { where(access_level: MASTER) } scope :owners, -> { where(access_level: OWNER) } + scope :owners_and_masters, -> { where(access_level: [OWNER, MASTER]) } before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } + after_create :send_invite, if: :invite? - after_create :create_notification_setting, unless: :invite? - after_create :post_create_hook, unless: :invite? - after_update :post_update_hook, unless: :invite? - after_destroy :post_destroy_hook, unless: :invite? + after_create :send_request, if: :request? + after_create :create_notification_setting, unless: :pending? + after_create :post_create_hook, unless: :pending? + after_update :post_update_hook, unless: :pending? + after_destroy :post_destroy_hook, unless: :pending? + after_destroy :post_decline_request, if: :request? delegate :name, :username, :email, to: :user, prefix: true @@ -96,10 +104,31 @@ class Member < ActiveRecord::Base end end + def real_source_type + source_type + end + def invite? self.invite_token.present? end + def request? + requested_at.present? + end + + def pending? + invite? || request? + end + + def accept_request + return false unless request? + + updated = self.update(requested_at: nil) + after_accept_request if updated + + updated + end + def accept_invite!(new_user) return false unless invite? @@ -157,6 +186,10 @@ class Member < ActiveRecord::Base # override in subclass end + def send_request + # override in subclass + end + def post_create_hook system_hook_service.execute_hooks_for(self, :create) end @@ -177,6 +210,14 @@ class Member < ActiveRecord::Base # override in subclass end + def after_accept_request + post_create_hook + end + + def post_decline_request + # override in subclass + end + def system_hook_service SystemHooksService.new end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index f63a0debf1a..363db877968 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -8,9 +8,6 @@ class GroupMember < Member validates_format_of :source_type, with: /\ANamespace\z/ default_scope { where(source_type: SOURCE_TYPE) } - scope :with_group, ->(group) { where(source_id: group.id) } - scope :with_user, ->(user) { where(user_id: user.id) } - def self.access_level_roles Gitlab::Access.options_with_owner end @@ -23,6 +20,11 @@ class GroupMember < Member access_level end + # Because source_type is `Namespace`... + def real_source_type + 'Group' + end + private def send_invite @@ -31,6 +33,12 @@ class GroupMember < Member super end + def send_request + notification_service.new_group_access_request(self) + + super + end + def post_create_hook notification_service.new_group_member(self) @@ -56,4 +64,10 @@ class GroupMember < Member super end + + def post_decline_request + notification_service.decline_group_access_request(self) + + super + end end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 46955b430f3..250ee04fd1d 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -11,8 +11,6 @@ class ProjectMember < Member default_scope { where(source_type: SOURCE_TYPE) } scope :in_project, ->(project) { where(source_id: project.id) } - scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) } - scope :with_user, ->(user) { where(user_id: user.id) } before_destroy :delete_member_todos @@ -84,7 +82,7 @@ class ProjectMember < Member Gitlab::Access.sym_options end - def access_roles + def access_level_roles Gitlab::Access.options end end @@ -113,6 +111,12 @@ class ProjectMember < Member super end + def send_request + notification_service.new_project_access_request(self) + + super + end + def post_create_hook unless owner? event_service.join_project(self.project, self.user) @@ -148,6 +152,12 @@ class ProjectMember < Member super end + def post_decline_request + notification_service.decline_project_access_request(self) + + super + end + def event_service EventCreateService.new end diff --git a/app/models/note.rb b/app/models/note.rb index 585d8c4ad84..4b6748053ff 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -88,22 +88,9 @@ class Note < ActiveRecord::Base table = arel_table pattern = "%#{query}%" - found_notes = joins('LEFT JOIN issues ON issues.id = noteable_id'). - where(table[:note].matches(pattern)) - - if as_user - found_notes.where(' - issues.confidential IS NULL - OR issues.confidential IS FALSE - OR (issues.confidential IS TRUE - AND (issues.author_id = :user_id - OR issues.assignee_id = :user_id - OR issues.project_id IN(:project_ids)))', - user_id: as_user.id, - project_ids: as_user.authorized_projects.select(:id)) - else - found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE') - end + Note.joins('LEFT JOIN issues ON issues.id = noteable_id'). + where(table[:note].matches(pattern)). + merge(Issue.visible_to_user(as_user)) end end @@ -200,6 +187,10 @@ class Note < ActiveRecord::Base award_emoji_supported? && contains_emoji_only? end + def emoji_awardable? + !system? + end + def clear_blank_line_code! self.line_code = nil if self.line_code.blank? end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 17fb15b08df..0ce87968e46 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -7,7 +7,6 @@ class NotificationSetting < ActiveRecord::Base belongs_to :source, polymorphic: true validates :user, presence: true - validates :source, presence: true validates :level, presence: true validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source", diff --git a/app/models/project.rb b/app/models/project.rb index 9dca63f124b..0ede6c6b139 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -5,6 +5,7 @@ class Project < ActiveRecord::Base include Gitlab::ShellAdapter include Gitlab::VisibilityLevel include Gitlab::CurrentSettings + include AccessRequestable include Referable include Sortable include AfterCommitQueue @@ -80,7 +81,7 @@ class Project < ActiveRecord::Base has_one :jira_service, dependent: :destroy has_one :redmine_service, dependent: :destroy has_one :custom_issue_tracker_service, dependent: :destroy - has_one :gitlab_issue_tracker_service, dependent: :destroy + has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project has_one :external_wiki_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" @@ -102,8 +103,9 @@ class Project < ActiveRecord::Base has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy - has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember' - has_many :users, through: :project_members + has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember' + alias_method :members, :project_members + has_many :users, -> { where(members: { requested_at: nil }) }, through: :project_members has_many :deploy_keys_projects, dependent: :destroy has_many :deploy_keys, through: :deploy_keys_projects has_many :users_star_projects, dependent: :destroy @@ -125,6 +127,8 @@ class Project < ActiveRecord::Base has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id + has_many :environments, dependent: :destroy + has_many :deployments, dependent: :destroy accepts_nested_attributes_for :variables, allow_destroy: true @@ -146,7 +150,6 @@ class Project < ActiveRecord::Base message: Gitlab::Regex.project_path_regex_message } validates :issues_enabled, :merge_requests_enabled, :wiki_enabled, inclusion: { in: [true, false] } - validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id @@ -589,10 +592,6 @@ class Project < ActiveRecord::Base update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) end - def can_have_issues_tracker_id? - self.issues_enabled && !self.default_issues_tracker? - end - def build_missing_services services_templates = Service.where(template: true) @@ -685,16 +684,6 @@ class Project < ActiveRecord::Base end end - def project_member_by_name_or_email(name = nil, email = nil) - user = users.find_by('name like ? or email like ?', name, email) - project_members.where(user: user) if user - end - - # Get Team Member record by user id - def project_member_by_id(user_id) - project_members.find_by(user_id: user_id) - end - def name_with_namespace @name_with_namespace ||= begin if namespace @@ -704,6 +693,7 @@ class Project < ActiveRecord::Base end end end + alias_method :human_name, :name_with_namespace def path_with_namespace if namespace diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 1d1780dcfbf..b5c76e4d4fe 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,6 +1,4 @@ class BambooService < CiService - include HTTParty - prop_accessor :bamboo_url, :build_key, :username, :password validates :bamboo_url, presence: true, url: true, if: :activated? @@ -61,18 +59,7 @@ class BambooService < CiService end def build_info(sha) - url = URI.join(bamboo_url, "/rest/api/latest/result?label=#{sha}").to_s - - if username.blank? && password.blank? - @response = HTTParty.get(url, verify: false) - else - url << '&os_authType=basic' - auth = { - username: username, - password: password - } - @response = HTTParty.get(url, verify: false, basic_auth: auth) - end + @response = get_path("rest/api/latest/result?label=#{sha}") end def build_page(sha, ref) @@ -80,11 +67,11 @@ class BambooService < CiService if @response.code != 200 || @response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. - URI.join(bamboo_url, "/browse/#{build_key}").to_s + URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s else # If actual build link is available, go to build result page. result_key = @response['results']['results']['result']['planResultKey']['key'] - URI.join(bamboo_url, "/browse/#{result_key}").to_s + URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s end end @@ -112,8 +99,27 @@ class BambooService < CiService def execute(data) return unless supported_events.include?(data[:object_kind]) - # Bamboo requires a GET and does not take any data. - url = URI.join(bamboo_url, "/updateAndBuild.action?buildKey=#{build_key}").to_s - self.class.get(url, verify: false) + get_path("updateAndBuild.action?buildKey=#{build_key}") + end + + private + + def build_url(path) + URI.join("#{bamboo_url}/", path).to_s + end + + def get_path(path) + url = build_url(path) + + if username.blank? && password.blank? + HTTParty.get(url, verify: false) + else + url << '&os_authType=basic' + HTTParty.get(url, verify: false, + basic_auth: { + username: username, + password: password + }) + end end end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 6ae9b16d3ce..87ecb3b8b86 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -38,9 +38,9 @@ class IssueTrackerService < Service if enabled_in_gitlab_config self.properties = { title: issues_tracker['title'], - project_url: add_issues_tracker_id(issues_tracker['project_url']), - issues_url: add_issues_tracker_id(issues_tracker['issues_url']), - new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url']) + project_url: issues_tracker['project_url'], + issues_url: issues_tracker['issues_url'], + new_issue_url: issues_tracker['new_issue_url'] } else self.properties = {} @@ -83,16 +83,4 @@ class IssueTrackerService < Service def issues_tracker Gitlab.config.issues_tracker[to_param] end - - def add_issues_tracker_id(url) - if self.project - id = self.project.issues_tracker_id - - if id - url = url.gsub(":issues_tracker_id", id) - end - end - - url - end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b0dcb52eba1..a4a967c9bc9 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,6 +1,4 @@ class TeamcityService < CiService - include HTTParty - prop_accessor :teamcity_url, :build_type, :username, :password validates :teamcity_url, presence: true, url: true, if: :activated? @@ -64,15 +62,7 @@ class TeamcityService < CiService end def build_info(sha) - url = URI.join( - teamcity_url, - "/httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}" - ).to_s - auth = { - username: username, - password: password - } - @response = HTTParty.get(url, verify: false, basic_auth: auth) + @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") end def build_page(sha, ref) @@ -81,14 +71,11 @@ class TeamcityService < CiService if @response.code != 200 # If actual build link can't be determined, # send user to build summary page. - URI.join(teamcity_url, "/viewLog.html?buildTypeId=#{build_type}").to_s + build_url("viewLog.html?buildTypeId=#{build_type}") else # If actual build link is available, go to build result page. built_id = @response['build']['id'] - URI.join( - teamcity_url, - "/viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}" - ).to_s + build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") end end @@ -123,8 +110,8 @@ class TeamcityService < CiService branch = Gitlab::Git.ref_name(data[:ref]) - self.class.post( - URI.join(teamcity_url, '/httpAuth/app/rest/buildQueue').to_s, + HTTParty.post( + build_url('httpAuth/app/rest/buildQueue'), body: "<build branchName=\"#{branch}\">"\ "<buildType id=\"#{build_type}\"/>"\ '</build>', @@ -132,4 +119,18 @@ class TeamcityService < CiService basic_auth: auth ) end + + private + + def build_url(path) + URI.join("#{teamcity_url}/", path).to_s + end + + def get_path(path) + HTTParty.get(build_url(path), verify: false, + basic_auth: { + username: username, + password: password + }) + end end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 70a8bbaba65..73e736820af 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -21,23 +21,13 @@ class ProjectTeam end end - def find(user_id) - user = project.users.find_by(id: user_id) - - if group - user ||= group.users.find_by(id: user_id) - end - - user - end - def find_member(user_id) - member = project.project_members.find_by(user_id: user_id) + member = project.members.non_request.find_by(user_id: user_id) # If user is not in project members # we should check for group membership if group && !member - member = group.group_members.find_by(user_id: user_id) + member = group.members.non_request.find_by(user_id: user_id) end member @@ -61,13 +51,10 @@ class ProjectTeam ProjectMember.truncate_team(project) end - def users - members - end - def members @members ||= fetch_members end + alias_method :users, :members def guests @guests ||= fetch_members(:guests) @@ -131,8 +118,14 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MASTER end - def member?(user_id) - !!find_member(user_id) + def member?(user, min_member_access = nil) + member = !!find_member(user.id) + + if min_member_access + member && max_member_access(user.id) >= min_member_access + else + member + end end def human_max_access(user_id) @@ -144,7 +137,7 @@ class ProjectTeam def max_member_access(user_id) access = [] - project.project_members.each do |member| + project.members.non_request.each do |member| if member.user_id == user_id access << member.access_field if member.access_field break @@ -152,7 +145,7 @@ class ProjectTeam end if group - group.group_members.each do |member| + group.members.non_request.each do |member| if member.user_id == user_id access << member.access_field if member.access_field break @@ -167,6 +160,7 @@ class ProjectTeam access.compact.max end + private def max_invited_level(user_id) project.project_group_links.map do |group_link| @@ -183,17 +177,15 @@ class ProjectTeam end.compact.max end - private - def fetch_members(level = nil) - project_members = project.project_members - group_members = group ? group.group_members : [] + project_members = project.members.non_request + group_members = group ? group.members.non_request : [] invited_members = [] if project.invited_groups.any? && project.allowed_to_share_with_group? project.project_group_links.each do |group_link| invited_group = group_link.group - im = invited_group.group_members + im = invited_group.members.non_request if level int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] diff --git a/app/models/repository.rb b/app/models/repository.rb index 1ab163510bf..e5b277cb198 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -446,7 +446,7 @@ class Repository def blob_at(sha, path) unless Gitlab::Git.blank_ref?(sha) - Gitlab::Git::Blob.find(self, sha, path) + Blob.decorate(Gitlab::Git::Blob.find(self, sha, path)) end end diff --git a/app/models/service.rb b/app/models/service.rb index bf352397509..40d39933ad8 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -18,7 +18,7 @@ class Service < ActiveRecord::Base after_commit :reset_updated_properties after_commit :cache_project_has_external_issue_tracker - belongs_to :project + belongs_to :project, inverse_of: :services has_one :service_hook validates :project_id, presence: true, unless: Proc.new { |service| service.template? } diff --git a/app/models/todo.rb b/app/models/todo.rb index 3a091373329..2792fa9b9a8 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base ASSIGNED = 1 MENTIONED = 2 BUILD_FAILED = 3 + MARKED = 4 belongs_to :author, class_name: "User" belongs_to :note diff --git a/app/models/user.rb b/app/models/user.rb index e0987e07e1f..8d0427da5ab 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -10,6 +10,8 @@ class User < ActiveRecord::Base include CaseSensitivity include TokenAuthenticatable + DEFAULT_NOTIFICATION_LEVEL = :participating + add_authentication_token_field :authentication_token default_value_for :admin, false @@ -54,8 +56,7 @@ class User < ActiveRecord::Base # Groups has_many :members, dependent: :destroy - has_many :project_members, source: 'ProjectMember' - has_many :group_members, source: 'GroupMember' + has_many :group_members, dependent: :destroy, source: 'GroupMember' has_many :groups, through: :group_members has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group @@ -63,13 +64,13 @@ class User < ActiveRecord::Base # Projects has_many :groups_projects, through: :groups, source: :projects has_many :personal_projects, through: :namespace, source: :projects + has_many :project_members, dependent: :destroy, class_name: 'ProjectMember' has_many :projects, through: :project_members has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :users_star_projects, dependent: :destroy has_many :starred_projects, through: :users_star_projects, source: :project has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet" - has_many :project_members, dependent: :destroy, class_name: 'ProjectMember' has_many :issues, dependent: :destroy, foreign_key: :author_id has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id @@ -99,7 +100,6 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - validates :notification_level, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :unique_email, if: ->(user) { user.email_changed? } @@ -133,13 +133,6 @@ class User < ActiveRecord::Base # Note: When adding an option, it MUST go on the end of the array. enum project_view: [:readme, :activity, :files] - # Notification level - # Note: When adding an option, it MUST go on the end of the array. - # - # TODO: Add '_prefix: :notification' to enum when update to Rails 5. https://github.com/rails/rails/pull/19813 - # Because user.notification_disabled? is much better than user.disabled? - enum notification_level: [:disabled, :participating, :watch, :global, :mention] - alias_attribute :private_token, :authentication_token delegate :path, to: :namespace, allow_nil: true, prefix: true @@ -411,8 +404,8 @@ class User < ActiveRecord::Base end # Returns projects user is authorized to access. - def authorized_projects - Project.where("projects.id IN (#{projects_union.to_sql})") + def authorized_projects(min_access_level = nil) + Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})") end def viewable_starred_projects @@ -800,6 +793,17 @@ class User < ActiveRecord::Base notification_settings.find_or_initialize_by(source: source) end + # Lazy load global notification setting + # Initializes User setting with Participating level if setting not persisted + def global_notification_setting + return @global_notification_setting if defined?(@global_notification_setting) + + @global_notification_setting = notification_settings.find_or_initialize_by(source: nil) + @global_notification_setting.update_attributes(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted? + + @global_notification_setting + end + def assigned_open_merge_request_count(force: false) Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do assigned_merge_requests.opened.count @@ -819,11 +823,19 @@ class User < ActiveRecord::Base private - def projects_union - Gitlab::SQL::Union.new([personal_projects.select(:id), - groups_projects.select(:id), - projects.select(:id), - groups.joins(:shared_projects).select(:project_id)]) + def projects_union(min_access_level = nil) + relations = [personal_projects.select(:id), + groups_projects.select(:id), + projects.select(:id), + groups.joins(:shared_projects).select(:project_id)] + + + if min_access_level + scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } } + relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) } + end + + Gitlab::SQL::Union.new(relations) end def ci_projects_union diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 64bcdac5c65..3a74ae094e8 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -29,7 +29,8 @@ module Ci :options, :allow_failure, :stage, - :stage_idx) + :stage_idx, + :environment) build_attrs.merge!(ref: @pipeline.ref, tag: @pipeline.tag, diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 4ff268a6f06..f0ed09a629a 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -7,15 +7,19 @@ module Ci builds = if current_runner.shared? - # don't run projects which have not enables shared runners - builds.joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }) + builds. + # don't run projects which have not enabled shared runners + joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true }). + + # this returns builds that are ordered by number of running builds + # we prefer projects that don't use shared runners at all + joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id"). + order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') else - # do run projects which are only assigned to this runner - builds.where(project: current_runner.projects.where(builds_enabled: true)) + # do run projects which are only assigned to this runner (FIFO) + builds.where(project: current_runner.projects.where(builds_enabled: true)).order('created_at ASC') end - builds = builds.order('created_at ASC') - build = builds.find do |build| build.can_be_served?(current_runner) end @@ -35,5 +39,12 @@ module Ci rescue StateMachines::InvalidTransition nil end + + private + + def running_builds_for_shared_runners + Ci::Build.running.where(runner: Ci::Runner.shared). + group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds') + end end end diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb new file mode 100644 index 00000000000..efeb9df9527 --- /dev/null +++ b/app/services/create_deployment_service.rb @@ -0,0 +1,18 @@ +require_relative 'base_service' + +class CreateDeploymentService < BaseService + def execute(deployable = nil) + environment = project.environments.find_or_create_by( + name: params[:environment] + ) + + project.deployments.create( + environment: environment, + ref: params[:ref], + tag: params[:tag], + sha: params[:sha], + user: current_user, + deployable: deployable + ) + end +end diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index 8f5c3393dfc..d7a0c25a044 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -3,7 +3,7 @@ class GitHooksService def execute(user, repo_path, oldrev, newrev, ref) @repo_path = repo_path - @user = Gitlab::ShellEnv.gl_id(user) + @user = Gitlab::GlId.gl_id(user) @oldrev = oldrev @newrev = newrev @ref = ref diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 91ca82ed3b7..f804ac171c4 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -173,16 +173,26 @@ class NotificationService end end + # Project access request + def new_project_access_request(project_member) + mailer.member_access_requested_email(project_member.real_source_type, project_member.id).deliver_later + end + + def decline_project_access_request(project_member) + mailer.member_access_denied_email(project_member.real_source_type, project_member.project.id, project_member.user.id).deliver_later + end + def invite_project_member(project_member, token) - mailer.project_member_invited_email(project_member.id, token).deliver_later + mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later end def accept_project_invite(project_member) - mailer.project_invite_accepted_email(project_member.id).deliver_later + mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later end def decline_project_invite(project_member) - mailer.project_invite_declined_email( + mailer.member_invite_declined_email( + project_member.real_source_type, project_member.project.id, project_member.invite_email, project_member.access_level, @@ -191,23 +201,33 @@ class NotificationService end def new_project_member(project_member) - mailer.project_access_granted_email(project_member.id).deliver_later + mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later end def update_project_member(project_member) - mailer.project_access_granted_email(project_member.id).deliver_later + mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later + end + + # Group access request + def new_group_access_request(group_member) + mailer.member_access_requested_email(group_member.real_source_type, group_member.id).deliver_later + end + + def decline_group_access_request(group_member) + mailer.member_access_denied_email(group_member.real_source_type, group_member.group.id, group_member.user.id).deliver_later end def invite_group_member(group_member, token) - mailer.group_member_invited_email(group_member.id, token).deliver_later + mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later end def accept_group_invite(group_member) - mailer.group_invite_accepted_email(group_member.id).deliver_later + mailer.member_invite_accepted_email(group_member.id).deliver_later end def decline_group_invite(group_member) - mailer.group_invite_declined_email( + mailer.member_invite_declined_email( + group_member.real_source_type, group_member.group.id, group_member.invite_email, group_member.access_level, @@ -216,11 +236,11 @@ class NotificationService end def new_group_member(group_member) - mailer.group_access_granted_email(group_member.id).deliver_later + mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later end def update_group_member(group_member) - mailer.group_access_granted_email(group_member.id).deliver_later + mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later end def project_was_moved(project, old_path_with_namespace) @@ -279,10 +299,11 @@ class NotificationService end def users_with_global_level_watch(ids) - User.where( - id: ids, - notification_level: NotificationSetting.levels[:watch] - ).pluck(:id) + NotificationSetting.where( + user_id: ids, + source_type: nil, + level: NotificationSetting.levels[:watch] + ).pluck(:user_id) end # Build a list of users based on project notifcation settings @@ -352,7 +373,9 @@ class NotificationService users = users.reject(&:blocked?) users.reject do |user| - next user.notification_level == level unless project + global_notification_setting = user.global_notification_setting + + next global_notification_setting.level == level unless project setting = user.notification_settings_for(project) @@ -361,13 +384,13 @@ class NotificationService end # reject users who globally set mention notification and has no setting per project/group - next user.notification_level == level unless setting + next global_notification_setting.level == level unless setting # reject users who set mention notification in project next true if setting.level == level # reject users who have mention level in project and disabled in global settings - setting.global? && user.notification_level == level + setting.global? && global_notification_setting.level == level end end @@ -456,7 +479,6 @@ class NotificationService def build_recipients(target, project, current_user, action: nil, previous_assignee: nil) recipients = target.participants(current_user) - recipients = add_project_watchers(recipients, project) recipients = reject_mention_users(recipients, project) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 8e03ff8ddde..e1f9ea64dc4 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -139,10 +139,16 @@ class TodoService pending_todos(user, attributes).update_all(state: :done) end + # When user marks an issue as todo + def mark_todo(issuable, current_user) + attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED) + create_todos(current_user, attributes) + end + private def create_todos(users, attributes) - Array(users).each do |user| + Array(users).map do |user| next if pending_todos(user, attributes).exists? Todo.create(attributes.merge(user_id: user.id)) end diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml new file mode 100644 index 00000000000..d78682532ed --- /dev/null +++ b/app/views/admin/background_jobs/_head.html.haml @@ -0,0 +1,14 @@ +.nav-links.sub-nav + %ul{ class: (container_class) } + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index de5bc050cf0..654d261aa99 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -1,46 +1,50 @@ +- @no_container = true - page_title "Background Jobs" -%h3.page-title Background Jobs -%p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing += render 'admin/background_jobs/head' -%hr +%div{ class: (container_class) } + %h3.page-title Background Jobs + %p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing -.panel.panel-default - .panel-heading Sidekiq running processes - .panel-body - - if @sidekiq_processes.empty? - %h4.cred - %i.fa.fa-exclamation-triangle - There are no running sidekiq processes. Please restart GitLab - - else - .table-holder - %table.table - %thead - %th USER - %th PID - %th CPU - %th MEM - %th STATE - %th START - %th COMMAND - %tbody - - @sidekiq_processes.each do |process| - - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/) - - data = process.strip.split(' ') - %tr - %td= gitlab_config.user - - 5.times do - %td= data.shift - %td= data.join(' ') + %hr - .clearfix - %p - %i.fa.fa-exclamation-circle - If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'. - %p - %i.fa.fa-exclamation-circle - If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab. + .panel.panel-default + .panel-heading Sidekiq running processes + .panel-body + - if @sidekiq_processes.empty? + %h4.cred + %i.fa.fa-exclamation-triangle + There are no running sidekiq processes. Please restart GitLab + - else + .table-holder + %table.table + %thead + %th USER + %th PID + %th CPU + %th MEM + %th STATE + %th START + %th COMMAND + %tbody + - @sidekiq_processes.each do |process| + - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/) + - data = process.strip.split(' ') + %tr + %td= gitlab_config.user + - 5.times do + %td= data.shift + %td= data.join(' ') + .clearfix + %p + %i.fa.fa-exclamation-circle + If '[25 of 25 busy]' is shown, restart GitLab with 'sudo service gitlab reload'. + %p + %i.fa.fa-exclamation-circle + If more than one sidekiq process is listed, stop GitLab, kill the remaining sidekiq processes (sudo pkill -u #{gitlab_config.user} -f sidekiq) and restart GitLab. -.panel.panel-default - %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"} + + .panel.panel-default + %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"} diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index d74cf8598e8..efd5b12cfeb 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -1,49 +1,54 @@ -.top-area - %ul.nav-links - %li{class: ('active' if @scope.nil?)} - = link_to admin_builds_path do - All - %span.badge.js-totalbuilds-count= @all_builds.count(:id) - - %li{class: ('active' if @scope == 'running')} - = link_to admin_builds_path(scope: :running) do - Running - %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id)) - - %li{class: ('active' if @scope == 'finished')} - = link_to admin_builds_path(scope: :finished) do - Finished - %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id)) - - .nav-controls - - if @all_builds.running_or_pending.any? - = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - -.row-content-block.second-block - #{(@scope || 'all').capitalize} builds - -%ul.content-list - - if @builds.blank? - %li - .nothing-here-block No builds to show - - else - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Project - %th Commit - %th Ref - %th Runner - %th Name - %th Tags - %th Duration - %th Finished at - %th - - - @builds.each do |build| - = render "admin/builds/build", build: build - - = paginate @builds, theme: 'gitlab' +- @no_container = true += render "admin/dashboard/head" + +%div{ class: (container_class) } + + .top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to admin_builds_path do + All + %span.badge.js-totalbuilds-count= @all_builds.count(:id) + + %li{class: ('active' if @scope == 'running')} + = link_to admin_builds_path(scope: :running) do + Running + %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id)) + + %li{class: ('active' if @scope == 'finished')} + = link_to admin_builds_path(scope: :finished) do + Finished + %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id)) + + .nav-controls + - if @all_builds.running_or_pending.any? + = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + + .row-content-block.second-block + #{(@scope || 'all').capitalize} builds + + %ul.content-list + - if @builds.blank? + %li + .nothing-here-block No builds to show + - else + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Project + %th Commit + %th Ref + %th Runner + %th Name + %th Tags + %th Duration + %th Finished at + %th + + - @builds.each do |build| + = render "admin/builds/build", build: build + + = paginate @builds, theme: 'gitlab' diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml new file mode 100644 index 00000000000..7b3f88c24df --- /dev/null +++ b/app/views/admin/dashboard/_head.html.haml @@ -0,0 +1,22 @@ +.nav-links.sub-nav + %ul{ class: (container_class) } + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Overview + = nav_link(controller: [:admin, :projects]) do + = link_to admin_namespaces_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'builds#index' do + = link_to admin_builds_path, title: 'Builds' do + %span + Builds diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 6dd2fef395d..4682016a886 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,155 +1,159 @@ -.admin-dashboard.prepend-top-default - .row - .col-md-4 - %h4 Statistics - %hr - %p - Forks - %span.light.pull-right - = number_with_delimiter(ForkedProjectLink.count) - %p - Issues - %span.light.pull-right - = number_with_delimiter(Issue.count) - %p - Merge Requests - %span.light.pull-right - = number_with_delimiter(MergeRequest.count) - %p - Notes - %span.light.pull-right - = number_with_delimiter(Note.count) - %p - Snippets - %span.light.pull-right - = number_with_delimiter(Snippet.count) - %p - SSH Keys - %span.light.pull-right - = number_with_delimiter(Key.count) - %p - Milestones - %span.light.pull-right - = number_with_delimiter(Milestone.count) - %p - Active Users - %span.light.pull-right - = number_with_delimiter(User.active.count) - .col-md-4 - %h4 - Features - %hr - %p - Sign up - %span.light.pull-right - = boolean_to_icon signup_enabled? - %p - LDAP - %span.light.pull-right - = boolean_to_icon Gitlab.config.ldap.enabled - %p - Gravatar - %span.light.pull-right - = boolean_to_icon gravatar_enabled? - %p - OmniAuth - %span.light.pull-right - = boolean_to_icon Gitlab.config.omniauth.enabled - %p - Reply by email - %span.light.pull-right - = boolean_to_icon Gitlab::IncomingEmail.enabled? - .col-md-4 - %h4 - Components - - if current_application_settings.version_check_enabled - .pull-right - = version_status_badge +- @no_container = true += render "admin/dashboard/head" - %hr - %p - GitLab - %span.pull-right - = Gitlab::VERSION - %p - GitLab Shell - %span.pull-right - = Gitlab::Shell.new.version - %p - GitLab API - %span.pull-right - = API::API::version - %p - Git - %span.pull-right - = Gitlab::Git.version - %p - Ruby - %span.pull-right - #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} - - %p - Rails - %span.pull-right - #{Rails::VERSION::STRING} - - %p - = Gitlab::Database.adapter_name - %span.pull-right - = Gitlab::Database.version - %hr - .row - .col-sm-4 - .light-well - %h4 Projects - .data - = link_to admin_namespaces_projects_path do - %h1= number_with_delimiter(Project.count) - %hr - = link_to('New Project', new_project_path, class: "btn btn-new") - .col-sm-4 - .light-well - %h4 Users - .data - = link_to admin_users_path do - %h1= number_with_delimiter(User.count) - %hr - = link_to 'New User', new_admin_user_path, class: "btn btn-new" - .col-sm-4 - .light-well - %h4 Groups - .data - = link_to admin_groups_path do - %h1= number_with_delimiter(Group.count) - %hr - = link_to 'New Group', new_admin_group_path, class: "btn btn-new" - - .row.prepend-top-10 - .col-md-4 - %h4 Latest projects - %hr - - @projects.each do |project| +%div{ class: (container_class) } + .admin-dashboard.prepend-top-default + .row + .col-md-4 + %h4 Statistics + %hr %p - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated' + Forks %span.light.pull-right - #{time_ago_with_tooltip(project.created_at)} - - .col-md-4 - %h4 Latest users - %hr - - @users.each do |user| + = number_with_delimiter(ForkedProjectLink.count) %p - = link_to [:admin, user], class: 'str-truncated' do - = user.name + Issues %span.light.pull-right - #{time_ago_with_tooltip(user.created_at)} - - .col-md-4 - %h4 Latest groups - %hr - - @groups.each do |group| + = number_with_delimiter(Issue.count) + %p + Merge Requests + %span.light.pull-right + = number_with_delimiter(MergeRequest.count) + %p + Notes + %span.light.pull-right + = number_with_delimiter(Note.count) + %p + Snippets + %span.light.pull-right + = number_with_delimiter(Snippet.count) + %p + SSH Keys + %span.light.pull-right + = number_with_delimiter(Key.count) + %p + Milestones + %span.light.pull-right + = number_with_delimiter(Milestone.count) + %p + Active Users + %span.light.pull-right + = number_with_delimiter(User.active.count) + .col-md-4 + %h4 + Features + %hr + %p + Sign up + %span.light.pull-right + = boolean_to_icon signup_enabled? %p - = link_to [:admin, group], class: 'str-truncated' do - = group.name + LDAP %span.light.pull-right - #{time_ago_with_tooltip(group.created_at)} + = boolean_to_icon Gitlab.config.ldap.enabled + %p + Gravatar + %span.light.pull-right + = boolean_to_icon gravatar_enabled? + %p + OmniAuth + %span.light.pull-right + = boolean_to_icon Gitlab.config.omniauth.enabled + %p + Reply by email + %span.light.pull-right + = boolean_to_icon Gitlab::IncomingEmail.enabled? + .col-md-4 + %h4 + Components + - if current_application_settings.version_check_enabled + .pull-right + = version_status_badge + + %hr + %p + GitLab + %span.pull-right + = Gitlab::VERSION + %p + GitLab Shell + %span.pull-right + = Gitlab::Shell.new.version + %p + GitLab API + %span.pull-right + = API::API::version + %p + Git + %span.pull-right + = Gitlab::Git.version + %p + Ruby + %span.pull-right + #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} + + %p + Rails + %span.pull-right + #{Rails::VERSION::STRING} + + %p + = Gitlab::Database.adapter_name + %span.pull-right + = Gitlab::Database.version + %hr + .row + .col-sm-4 + .light-well + %h4 Projects + .data + = link_to admin_namespaces_projects_path do + %h1= number_with_delimiter(Project.count) + %hr + = link_to('New Project', new_project_path, class: "btn btn-new") + .col-sm-4 + .light-well + %h4 Users + .data + = link_to admin_users_path do + %h1= number_with_delimiter(User.count) + %hr + = link_to 'New User', new_admin_user_path, class: "btn btn-new" + .col-sm-4 + .light-well + %h4 Groups + .data + = link_to admin_groups_path do + %h1= number_with_delimiter(Group.count) + %hr + = link_to 'New Group', new_admin_group_path, class: "btn btn-new" + + .row.prepend-top-10 + .col-md-4 + %h4 Latest projects + %hr + - @projects.each do |project| + %p + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated' + %span.light.pull-right + #{time_ago_with_tooltip(project.created_at)} + + .col-md-4 + %h4 Latest users + %hr + - @users.each do |user| + %p + = link_to [:admin, user], class: 'str-truncated' do + = user.name + %span.light.pull-right + #{time_ago_with_tooltip(user.created_at)} + + .col-md-4 + %h4 Latest groups + %hr + - @groups.each do |group| + %p + = link_to [:admin, group], class: 'str-truncated' do + = group.name + %span.light.pull-right + #{time_ago_with_tooltip(group.created_at)} diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 775072a7441..4f1996ef7ab 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,41 +1,45 @@ +- @no_container = true - page_title "Groups" -%h3.page-title - Groups (#{number_with_delimiter(@groups.total_count)}) += render "admin/dashboard/head" -%p.light - Group allows you to keep projects organized. - Use groups for uniting related projects. +%div{ class: (container_class) } + %h3.page-title + Groups (#{number_with_delimiter(@groups.total_count)}) -.top-area - .nav-search - = form_tag admin_groups_path, method: :get, class: 'form-inline' do - = hidden_field_tag :sort, @sort - = text_field_tag :name, params[:name], class: "form-control" - = button_tag "Search", class: "btn submit btn-primary" + %p.light + Group allows you to keep projects organized. + Use groups for uniting related projects. - .nav-controls - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to admin_groups_path(sort: sort_value_recently_created) do + .top-area + .nav-search + = form_tag admin_groups_path, method: :get, class: 'form-inline' do + = hidden_field_tag :sort, @sort + = text_field_tag :name, params[:name], class: "form-control" + = button_tag "Search", class: "btn submit btn-primary" + + .nav-controls + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else = sort_title_recently_created - = link_to admin_groups_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to admin_groups_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to admin_groups_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - = link_to 'New Group', new_admin_group_path, class: "btn btn-new" + %b.caret + %ul.dropdown-menu + %li + = link_to admin_groups_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to admin_groups_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to admin_groups_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to admin_groups_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + = link_to 'New Group', new_admin_group_path, class: "btn btn-new" -%ul.content-list - - @groups.each do |group| - = render 'group', group: group + %ul.content-list + - @groups.each do |group| + = render 'group', group: group -= paginate @groups, theme: "gitlab" + = paginate @groups, theme: "gitlab" diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index f309e80a39a..5b8a0262ea0 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -109,7 +109,7 @@ %span.pull-right.light = member.human_access - if can?(current_user, :destroy_group_member, member) - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(@group, member), data: { confirm: remove_member_message(member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index c2313986a7f..7b8407f9152 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -1,49 +1,52 @@ +- @no_container = true - page_title "Health Check" += render 'admin/background_jobs/head' -%h3.page-title - Health Check -.bs-callout.clearfix - .pull-left - %p - Access token is - %code#health-check-token= current_application_settings.health_check_access_token - = button_to reset_health_check_token_admin_application_settings_path, - method: :put, class: 'btn btn-default', - data: { confirm: 'Are you sure you want to reset the health check token?' } do - = icon('refresh') - Reset health check access token -%p.light - Health information can be retrieved as plain text, JSON, or XML using: - %ul - %li - %code= health_check_url(token: current_application_settings.health_check_access_token) - %li - %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json) - %li - %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) +%div{ class: (container_class) } + %h3.page-title + Health Check + .bs-callout.clearfix + .pull-left + %p + Access token is + %code#health-check-token= current_application_settings.health_check_access_token + = button_to reset_health_check_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset the health check token?' } do + = icon('refresh') + Reset health check access token + %p.light + Health information can be retrieved as plain text, JSON, or XML using: + %ul + %li + %code= health_check_url(token: current_application_settings.health_check_access_token) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) -%p.light - You can also ask for the status of specific services: - %ul - %li - %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache) - %li - %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database) - %li - %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations) + %p.light + You can also ask for the status of specific services: + %ul + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations) -%hr -.panel.panel-default - .panel-heading - Current Status: - - if @errors.blank? - = icon('circle', class: 'cgreen') - Healthy - - else - = icon('warning', class: 'cred') - Unhealthy - .panel-body - - if @errors.blank? - No Health Problems Detected - - else - = @errors + %hr + .panel.panel-default + .panel-heading + Current Status: + - if @errors.blank? + = icon('circle', class: 'cgreen') + Healthy + - else + = icon('warning', class: 'cred') + Unhealthy + .panel-body + - if @errors.blank? + No Health Problems Detected + - else + = @errors diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 698feb571ac..5ddc3b9ea85 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -1,28 +1,32 @@ +- @no_container = true - page_title "Logs" - loggers = [Gitlab::GitLogger, Gitlab::AppLogger, Gitlab::ProductionLogger, Gitlab::SidekiqLogger, Gitlab::RepositoryCheckLogger] -%ul.nav-links.log-tabs - - loggers.each do |klass| - %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } - = link_to klass::file_name, "##{klass::file_name_noext}", - 'data-toggle' => 'tab' -.row-content-block - To prevent performance issues admin logs output the last 2000 lines -.tab-content - - loggers.each do |klass| - .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), - id: klass::file_name_noext } - .file-holder#README - .file-title - %i.fa.fa-file - = klass::file_name - .pull-right - = link_to '#', class: 'log-bottom' do - %i.fa.fa-arrow-down - Scroll down - .file-content.logs - %ol - - klass.read_latest.each do |line| - %li - %p= line += render 'admin/background_jobs/head' + +%div{ class: (container_class) } + %ul.nav-links.log-tabs + - loggers.each do |klass| + %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } + = link_to klass::file_name, "##{klass::file_name_noext}", + 'data-toggle' => 'tab' + .row-content-block + To prevent performance issues admin logs output the last 2000 lines + .tab-content + - loggers.each do |klass| + .tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), + id: klass::file_name_noext } + .file-holder#README + .file-title + %i.fa.fa-file + = klass::file_name + .pull-right + = link_to '#', class: 'log-bottom' do + %i.fa.fa-arrow-down + Scroll down + .file-content.logs + %ol + - klass.read_latest.each do |line| + %li + %p= line diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index aa07afa0d62..4822cb693c2 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,94 +1,97 @@ +- @no_container = true - page_title "Projects" = render 'shared/show_aside' += render "admin/dashboard/head" -.row.prepend-top-default - %aside.col-md-3 - .panel.admin-filter - = form_tag admin_namespaces_projects_path, method: :get, class: '' do - .form-group - = label_tag :name, 'Name:' - = text_field_tag :name, params[:name], class: "form-control" +%div{ class: (container_class) } + .row.prepend-top-default + %aside.col-md-3 + .panel.admin-filter + = form_tag admin_namespaces_projects_path, method: :get, class: '' do + .form-group + = label_tag :name, 'Name:' + = text_field_tag :name, params[:name], class: "form-control" - .form-group - = label_tag :namespace_id, "Namespace" - = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large' + .form-group + = label_tag :namespace_id, "Namespace" + = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large' - .form-group - %strong Activity - .checkbox - = label_tag :with_push do - = check_box_tag :with_push, 1, params[:with_push] - %span Projects with push events - .checkbox - = label_tag :abandoned do - = check_box_tag :abandoned, 1, params[:abandoned] - %span No activity over 6 month - .checkbox - = label_tag :with_archived do - = check_box_tag :with_archived, 1, params[:with_archived] - %span Show archived projects + .form-group + %strong Activity + .checkbox + = label_tag :with_push do + = check_box_tag :with_push, 1, params[:with_push] + %span Projects with push events + .checkbox + = label_tag :abandoned do + = check_box_tag :abandoned, 1, params[:abandoned] + %span No activity over 6 month + .checkbox + = label_tag :with_archived do + = check_box_tag :with_archived, 1, params[:with_archived] + %span Show archived projects - %fieldset - %strong Visibility level: - .visibility-levels - - Project.visibility_levels.each do |label, level| - .checkbox - %label - = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s) - %span.descr - = visibility_level_icon(level) - = label - %fieldset - %strong Problems - .checkbox - = label_tag :last_repository_check_failed do - = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] - %span Last repository check failed + %fieldset + %strong Visibility level: + .visibility-levels + - Project.visibility_levels.each do |label, level| + .checkbox + %label + = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s) + %span.descr + = visibility_level_icon(level) + = label + %fieldset + %strong Problems + .checkbox + = label_tag :last_repository_check_failed do + = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] + %span Last repository check failed - = hidden_field_tag :sort, params[:sort] - = button_tag "Search", class: "btn submit btn-primary" - = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" + = hidden_field_tag :sort, params[:sort] + = button_tag "Search", class: "btn submit btn-primary" + = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" - %section.col-md-9 - .panel.panel-default - .panel-heading - Projects (#{@projects.total_count}) - .controls - .dropdown.inline - %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do + %section.col-md-9 + .panel.panel-default + .panel-heading + Projects (#{@projects.total_count}) + .controls + .dropdown.inline + %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else = sort_title_recently_created - = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do - = sort_title_largest_repo - = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success" - %ul.well-list - - @projects.each do |project| - %li - .list-item-name - %span{ class: visibility_level_color(project.visibility_level) } - = visibility_level_icon(project.visibility_level) - = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] - .pull-right - - if project.archived - %span.label.label-warning archived - %span.label.label-gray - = repository_size(project) - = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" - = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove" - - if @projects.blank? - .nothing-here-block 0 projects matches - = paginate @projects, theme: "gitlab" + %b.caret + %ul.dropdown-menu + %li + = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do + = sort_title_largest_repo + = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success" + %ul.well-list + - @projects.each do |project| + %li + .list-item-name + %span{ class: visibility_level_color(project.visibility_level) } + = visibility_level_icon(project.visibility_level) + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] + .pull-right + - if project.archived + %span.label.label-warning archived + %span.label.label-gray + = repository_size(project) + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" + = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove" + - if @projects.blank? + .nothing-here-block 0 projects matches + = paginate @projects, theme: "gitlab" diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 73986d21bcf..9e55a562e18 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -142,7 +142,7 @@ %i.fa.fa-pencil-square-o %ul.well-list - @group_members.each do |member| - = render 'groups/group_members/group_member', member: member, show_controls: false + = render 'shared/members/member', member: member, show_controls: false .panel-footer = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab' @@ -172,7 +172,7 @@ %span.light Owner - else %span.light= project_member.human_access - = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do + = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_member_message(project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do %i.fa.fa-times .panel-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml index dbecb7bbfd6..b0a709a568a 100644 --- a/app/views/admin/users/groups.html.haml +++ b/app/views/admin/users/groups.html.haml @@ -13,7 +13,7 @@ .pull-right %span.light= group_member.human_access - unless group_member.owner? - = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-times.fa-inverse - else .nothing-here-block This user has no groups. diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index d6743081c8e..d0a696da64b 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,107 +1,110 @@ +- @no_container = true - page_title "Users" = render 'shared/show_aside' += render "admin/dashboard/head" -.admin-filter - %ul.nav-links - %li{class: "#{'active' unless params[:filter]}"} - = link_to admin_users_path do - Active - %small.badge= number_with_delimiter(User.active.count) - %li{class: "#{'active' if params[:filter] == "admins"}"} - = link_to admin_users_path(filter: "admins") do - Admins - %small.badge= number_with_delimiter(User.admins.count) - %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} - = link_to admin_users_path(filter: 'two_factor_enabled') do - 2FA Enabled - %small.badge= number_with_delimiter(User.with_two_factor.count) - %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} - = link_to admin_users_path(filter: 'two_factor_disabled') do - 2FA Disabled - %small.badge= number_with_delimiter(User.without_two_factor.count) - %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"} - = link_to admin_users_path(filter: 'external') do - External - %small.badge= number_with_delimiter(User.external.count) - %li{class: "#{'active' if params[:filter] == "blocked"}"} - = link_to admin_users_path(filter: "blocked") do - Blocked - %small.badge= number_with_delimiter(User.blocked.count) - %li{class: "#{'active' if params[:filter] == "wop"}"} - = link_to admin_users_path(filter: "wop") do - Without projects - %small.badge= number_with_delimiter(User.without_projects.count) +%div{ class: (container_class) } + .admin-filter + %ul.nav-links + %li{class: "#{'active' unless params[:filter]}"} + = link_to admin_users_path do + Active + %small.badge= number_with_delimiter(User.active.count) + %li{class: "#{'active' if params[:filter] == "admins"}"} + = link_to admin_users_path(filter: "admins") do + Admins + %small.badge= number_with_delimiter(User.admins.count) + %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} + = link_to admin_users_path(filter: 'two_factor_enabled') do + 2FA Enabled + %small.badge= number_with_delimiter(User.with_two_factor.count) + %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} + = link_to admin_users_path(filter: 'two_factor_disabled') do + 2FA Disabled + %small.badge= number_with_delimiter(User.without_two_factor.count) + %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"} + = link_to admin_users_path(filter: 'external') do + External + %small.badge= number_with_delimiter(User.external.count) + %li{class: "#{'active' if params[:filter] == "blocked"}"} + = link_to admin_users_path(filter: "blocked") do + Blocked + %small.badge= number_with_delimiter(User.blocked.count) + %li{class: "#{'active' if params[:filter] == "wop"}"} + = link_to admin_users_path(filter: "wop") do + Without projects + %small.badge= number_with_delimiter(User.without_projects.count) - .row-content-block.second-block - .pull-right - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_name - %b.caret - %ul.dropdown-menu - %li - = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + .row-content-block.second-block + .pull-right + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do - = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do - = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do - = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do - = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do - = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do - = sort_title_oldest_updated + %b.caret + %ul.dropdown-menu + %li + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do + = sort_title_oldest_updated - = link_to 'New User', new_admin_user_path, class: "btn btn-new" - = form_tag admin_users_path, method: :get, class: 'form-inline' do - .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false - = hidden_field_tag "filter", params[:filter] - = button_tag class: 'btn btn-primary' do - %i.fa.fa-search + = link_to 'New User', new_admin_user_path, class: "btn btn-new" + = form_tag admin_users_path, method: :get, class: 'form-inline' do + .form-group + = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false + = hidden_field_tag "filter", params[:filter] + = button_tag class: 'btn btn-primary' do + %i.fa.fa-search -.panel.panel-default - %ul.well-list - - @users.each do |user| - %li - .list-item-name - - if user.blocked? - = icon("lock", class: "cred") - - else - = icon("user", class: "cgreen") - = link_to user.name, [:admin, user] - - if user.admin? - %strong.cred (Admin) - - if user.external? - %strong.cred (External) - - if user == current_user - %span.cred It's you! - .pull-right - %span.light - %i.fa.fa-envelope - = mail_to user.email, user.email, class: 'light' - + .panel.panel-default + %ul.well-list + - @users.each do |user| + %li + .list-item-name + - if user.blocked? + = icon("lock", class: "cred") + - else + = icon("user", class: "cgreen") + = link_to user.name, [:admin, user] + - if user.admin? + %strong.cred (Admin) + - if user.external? + %strong.cred (External) + - if user == current_user + %span.cred It's you! .pull-right - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs' - - unless user == current_user - - if user.ldap_blocked? - = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do - %i.fa.fa-lock - Unblock - - elsif user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success' - - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning' - - if user.access_locked? - = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } - - if user.can_be_removed? - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' -= paginate @users, theme: "gitlab" + %span.light + %i.fa.fa-envelope + = mail_to user.email, user.email, class: 'light' + + .pull-right + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs' + - unless user == current_user + - if user.ldap_blocked? + = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do + %i.fa.fa-lock + Unblock + - elsif user.blocked? + = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success' + - else + = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning' + - if user.access_locked? + = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } + - if user.can_be_removed? + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' + = paginate @users, theme: "gitlab" diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index b655b2a15f5..84b9ceb23b3 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -38,6 +38,5 @@ %span.light= member.human_access - if member.respond_to? :project - = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do + = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do %i.fa.fa-times - diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 83c0c6da21b..0404d0728ea 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" xml.id issues_dashboard_url - xml.updated @issues.first.created_at.xmlschema if @issues.any? + xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? - xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? + xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml deleted file mode 100644 index 6bb542e658d..00000000000 --- a/app/views/groups/group_members/_group_member.html.haml +++ /dev/null @@ -1,57 +0,0 @@ -- user = member.user -- return unless user || member.invite? -- show_roles = local_assigns.fetch(:show_roles, true) - -%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} - %span{class: ("list-item-name" if show_controls)} - - if member.user - = image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' - %strong - = link_to user.name, user_path(user) - %span.cgray= user.username - - if user == current_user - %span.label.label-success It's you - - if user.blocked? - %label.label.label-danger - %strong Blocked - - else - = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' - %strong - = member.invite_email - %span.cgray - invited - - if member.created_by - by - = link_to member.created_by.name, user_path(member.created_by) - = time_ago_with_tooltip(member.created_at) - - - if show_controls && can?(current_user, :admin_group_member, @group) - = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do - Resend invite - - - if show_roles && should_user_see_group_roles?(current_user, @group) - %span.pull-right - %strong.member-access-level= member.human_access - - if show_controls - - if can?(current_user, :update_group_member, member) - = button_tag class: "btn-xs btn btn-grouped inline js-toggle-button", - title: 'Edit access level', type: 'button' do - = icon('pencil') - - - if can?(current_user, :destroy_group_member, member) - - - if current_user == user - = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - = icon("sign-out") - Leave - - else - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - = icon('trash') - - .edit-member.hide.js-toggle-content - %br - = form_for [@group, member], remote: true do |f| - .prepend-top-10 - = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control' - .prepend-top-10 - = f.submit 'Save', class: 'btn btn-save btn-sm' diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 0eb6bbd4420..a36531e095a 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -6,12 +6,13 @@ .panel-heading Add new user to group .panel-body - - if should_user_see_group_roles?(current_user, @group) - %p.light - Members of group have access to all group projects. + %p.light + Members of group have access to all group projects. .new-group-member-holder = render "new_group_member" + = render 'shared/members/requests', membership_source: @group, members: @members.request + .panel.panel-default .panel-heading %strong #{@group.name} @@ -25,9 +26,8 @@ = button_tag class: 'btn', title: 'Search' do = icon("search") %ul.content-list - - @members.each do |member| - = render 'groups/group_members/group_member', member: member, show_controls: true - = paginate @members, theme: 'gitlab' + = render partial: 'shared/members/member', collection: @members.non_request, as: :member + = paginate @members.non_request, theme: 'gitlab' :javascript $('form.member-search-form').on('submit', function(event) { diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml index df726e2b2b9..da71de4cd1e 100644 --- a/app/views/groups/group_members/update.js.haml +++ b/app/views/groups/group_members/update.js.haml @@ -1,2 +1,2 @@ :plain - $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}'); + $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}'); diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index c19671295af..b1628040325 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: issues_group_url, rel: "alternate", type: "text/html" xml.id issues_group_url - xml.updated @issues.first.created_at.xmlschema if @issues.any? + xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? - xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? + xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 85635bc4616..62ebd69485c 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -19,6 +19,9 @@ .cover-desc.description = markdown(@group.description, pipeline: :description) + - if current_user + = render 'shared/members/access_request_buttons', source: @group + %div{ class: container_class } .top-area %ul.nav-links diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder index 68a2d19e58d..96831874144 100644 --- a/app/views/issues/_issue.atom.builder +++ b/app/views/issues/_issue.atom.builder @@ -5,10 +5,28 @@ xml.entry do xml.updated issue.created_at.xmlschema xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) - xml.author do |author| + xml.author do xml.name issue.author_name xml.email issue.author_email end xml.summary issue.title + xml.description issue.description if issue.description + xml.milestone issue.milestone.title if issue.milestone + xml.due_date issue.due_date if issue.due_date + + unless issue.labels.empty? + xml.labels do + issue.labels.each do |label| + xml.label label.name + end + end + end + + if issue.assignee + xml.assignee do + xml.name issue.assignee.name + xml.email issue.assignee.email + end + end end diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml index e4fab897377..8c140a5943e 100644 --- a/app/views/layouts/_collapse_button.html.haml +++ b/app/views/layouts/_collapse_button.html.haml @@ -1 +1,3 @@ -= link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open/Close" += link_to '#', class: 'nav-header-btn text-center toggle-nav-collapse', title: "Open/Close" do + %span.sr-only Toggle navigation + = icon('bars') diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index f89e8582792..199ab3c38c3 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,6 +1,6 @@ -.page-with-sidebar.page-sidebar-collapsed{ class: "#{page_gutter_class}" } +.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } - + = render partial: 'layouts/collapse_button' - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" - elsif current_user @@ -8,13 +8,14 @@ - else = render 'layouts/nav/explore' - .collapse-nav - = render partial: 'layouts/collapse_button' - if current_user = link_to current_user, class: 'sidebar-user', title: "Profile", data: {user: current_user.username} do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username + = link_to '#', class: "nav-header-btn text-center pin-nav-btn #{'is-active' if pinned_nav?} js-nav-pin", title: 'Pin/Unpin navigation' do + %span.sr-only Toggle navigation pinning + = icon('thumb-tack') - if defined?(nav) && nav .layout-nav .container-fluid diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index b49207fc315..245b9c3b4d4 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -36,6 +36,31 @@ - else = hidden_field_tag :search_code, true + :javascript + gl.projectOptions = gl.projectOptions || {}; + gl.projectOptions["#{j(@project.path)}"] = { + issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}", + mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}", + name: "#{j(@project.name)}" + }; + + - if @group and @group.path + :javascript + gl.groupOptions = gl.groupOptions || {}; + gl.groupOptions["#{j(@group.path)}"] = { + name: "#{j(@group.name)}", + issuesPath: "#{issues_group_path(j(@group.path))}", + mrPath: "#{merge_requests_group_path(j(@group.path))}" + }; + + + :javascript + gl.dashboardOptions = { + issuesPath: "#{issues_dashboard_url}", + mrPath: "#{merge_requests_dashboard_url}" + }; + + - if @snippet || @snippets = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index 6591c52bdbd..87064cc9b3f 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,5 +1,5 @@ - page_title "Admin Area" - header_title "Admin Area", admin_root_path -- sidebar "admin" +- nav "admin" = render template: "layouts/application" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 2b86b289bbe..33cedaaf2ee 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head" - %body{class: "#{user_application_theme}", 'data-page' => body_data_page} + %body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}} = Gon::Base.render_data -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index ad30a367fc5..40a2c81eebd 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,4 +1,4 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab.header-collapsed{ class: nav_header_class } +%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } %div{ class: fluid_layout ? "container-fluid" : "container-fluid" } .header-content %button.side-nav-toggle{type: 'button'} @@ -27,9 +27,8 @@ %li = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('bell fw') - - unless todos_pending_count == 0 - %span.badge.todos-pending-count - = todos_pending_count + %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } + = todos_pending_count - if current_user.can_create_project? %li = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do @@ -51,7 +50,7 @@ %h1.title= title .header-logo - #logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = brand_header_logo = yield :header_content diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index f292730fe45..54aa34bee0b 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -1,107 +1,64 @@ -%ul.nav.nav-sidebar - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview' do - = icon('dashboard fw') +%ul.nav-links.scrolling-tabs + .fade-left + = nav_link(controller: %w(dashboard admin projects users groups builds), html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do %span Overview - = nav_link(controller: [:admin, :projects]) do - = link_to admin_namespaces_projects_path, title: 'Projects' do - = icon('cube fw') + = nav_link(controller: %w(background_jobs logs health_check)) do + = link_to admin_background_jobs_path, title: 'Monitoring' do %span - Projects - = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - = icon('user fw') - %span - Users - = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - = icon('group fw') - %span - Groups + Monitoring = nav_link(controller: :deploy_keys) do = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - = icon('key fw') %span Deploy Keys = nav_link path: ['runners#index', 'runners#show'] do = link_to admin_runners_path, title: 'Runners' do - = icon('cog fw') %span Runners - %span.count= number_with_delimiter(Ci::Runner.count(:all)) - = nav_link path: 'builds#index' do - = link_to admin_builds_path, title: 'Builds' do - = icon('link fw') - %span - Builds - %span.count= number_with_delimiter(Ci::Build.count(:all)) - = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - = icon('file-text fw') - %span - Logs - = nav_link(controller: :health_check) do - = link_to admin_health_check_path, title: 'Health Check' do - = icon('medkit fw') - %span - Health Check = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path, title: 'Messages' do - = icon('bullhorn fw') %span Messages = nav_link(controller: :hooks) do = link_to admin_hooks_path, title: 'Hooks' do - = icon('external-link fw') %span Hooks - = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - = icon('cog fw') - %span - Background Jobs + = nav_link(controller: :appearances) do = link_to admin_appearances_path, title: 'Appearances' do - = icon('image') %span Appearance = nav_link(controller: :applications) do = link_to admin_applications_path, title: 'Applications' do - = icon('cloud fw') %span Applications = nav_link(controller: :services) do = link_to admin_application_settings_services_path, title: 'Service Templates' do - = icon('copy fw') %span Service Templates = nav_link(controller: :labels) do = link_to admin_labels_path, title: 'Labels' do - = icon('tags fw') %span Labels = nav_link(controller: :abuse_reports) do = link_to admin_abuse_reports_path, title: "Abuse Reports" do - = icon('exclamation-circle fw') %span Abuse Reports - %span.count= number_with_delimiter(AbuseReport.count(:all)) + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - if askimet_enabled? = nav_link(controller: :spam_logs) do = link_to admin_spam_logs_path, title: "Spam Logs" do - = icon('exclamation-triangle fw') %span Spam Logs - %span.count= number_with_delimiter(SpamLog.count(:all)) = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do = link_to admin_application_settings_path, title: 'Settings' do - = icon('cogs fw') %span Settings + .fade-right diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 18cae5bf87f..52e41b1a857 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,54 +1,64 @@ %ul.nav.nav-sidebar = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do - = navbar_icon('project') + .icon-container + = navbar_icon('project') %span Projects = nav_link(controller: :todos) do = link_to dashboard_todos_path, title: 'Todos' do - = icon('bell fw') + .icon-container + = icon('bell fw') %span Todos %span.count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do - = navbar_icon('activity') + .icon-container + = navbar_icon('activity') %span Activity = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do - = navbar_icon('group') + .icon-container + = navbar_icon('group') %span Groups = nav_link(controller: 'dashboard/milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do - = navbar_icon('milestones') + .icon-container + = navbar_icon('milestones') %span Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do - = navbar_icon('issues') + .icon-container + = navbar_icon('issues') %span Issues %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do - = navbar_icon('mr') + .icon-container + = navbar_icon('mr') %span Merge Requests %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do - = icon('clipboard fw') + .icon-container + = icon('clipboard fw') %span Snippets = nav_link(controller: :help) do = link_to help_path, title: 'Help' do - = icon('question-circle fw') + .icon-container + = icon('question-circle fw') %span Help = nav_link(html_options: {class: profile_tab_class}) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do - = icon('user fw') + .icon-container + = icon('user fw') %span Profile Settings diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index 0b2673f1a82..dac46648b9f 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -14,7 +14,3 @@ %li = link_to edit_group_path(@group) do Edit Group - %li - = link_to leave_group_group_members_path(@group), - data: { confirm: leave_group_message(@group.name) }, method: :delete, title: 'Leave group' do - Leave Group diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 53d1fcc30a6..a851cae4b56 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,23 +1,26 @@ - if current_user .controls - - access = user_max_access_in_project(current_user.id, @project) - - can_edit = can?(current_user, :admin_project, @project) .dropdown.project-settings-dropdown %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} = icon('cog') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - = render 'layouts/nav/project_settings' - %li.divider - - if can_edit - %li - = link_to edit_project_path(@project) do - Edit Project - - if access - %li - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), - data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do - Leave Project + - access = @project.team.max_member_access(current_user.id) + - can_edit = can?(current_user, :admin_project, @project) + + = render 'layouts/nav/project_settings', access: access, can_edit: can_edit + + - if can_edit || access + %li.divider + - if can_edit + %li + = link_to edit_project_path(@project) do + Edit Project + - if access + %li + = link_to polymorphic_path([:leave, @project, :members]), + data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do + Leave Project %div{ class: nav_control_class } %ul.nav-links.scrolling-tabs @@ -39,7 +42,7 @@ Code - if project_nav_tab? :pipelines - = nav_link(controller: :pipelines) do + = nav_link(controller: [:pipelines, :builds, :environments]) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span Pipelines diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 885e78d38c6..13d32bd1354 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -3,43 +3,43 @@ = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do %span Members - -- if @project.allowed_to_share_with_group? - = nav_link(controller: :group_links) do - = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do - %span - Groups -= nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do - %span - Deploy Keys -= nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do - %span - Webhooks -= nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do - %span - Services -= nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do - %span - Protected Branches - -- if @project.builds_enabled? - = nav_link(controller: :runners) do - = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do +- if access && can_edit + - if @project.allowed_to_share_with_group? + = nav_link(controller: :group_links) do + = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do + %span + Groups + = nav_link(controller: :deploy_keys) do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do %span - Runners - = nav_link(controller: :variables) do - = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do + Deploy Keys + = nav_link(controller: :hooks) do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do %span - Variables - = nav_link(controller: :triggers) do - = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do + Webhooks + = nav_link(controller: :services) do + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do %span - Triggers - = nav_link(controller: :badges) do - = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do + Services + = nav_link(controller: :protected_branches) do + = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do %span - Badges + Protected Branches + + - if @project.builds_enabled? + = nav_link(controller: :runners) do + = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do + %span + Runners + = nav_link(controller: :variables) do + = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do + %span + Variables + = nav_link(controller: :triggers) do + = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do + %span + Triggers + = nav_link(controller: :badges) do + = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do + %span + Badges diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml deleted file mode 100644 index f1916d624b6..00000000000 --- a/app/views/notify/group_access_granted_email.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -%p - = "You have been granted #{@group_member.human_access} access to group" - = link_to group_url(@group) do - = @group.name diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb deleted file mode 100644 index ef9617bfc16..00000000000 --- a/app/views/notify/group_access_granted_email.text.erb +++ /dev/null @@ -1,4 +0,0 @@ - -You have been granted <%= @group_member.human_access %> access to group <%= @group.name %> - -<%= url_for(group_url(@group)) %> diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml deleted file mode 100644 index 55efad384a7..00000000000 --- a/app/views/notify/group_invite_accepted_email.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%p - #{@group_member.invite_email}, now known as - #{link_to @group_member.user.name, user_url(@group_member.user)}, - has accepted your invitation to join group - #{link_to @group.name, group_url(@group)}. - diff --git a/app/views/notify/group_invite_accepted_email.text.erb b/app/views/notify/group_invite_accepted_email.text.erb deleted file mode 100644 index f8b70f7a5a6..00000000000 --- a/app/views/notify/group_invite_accepted_email.text.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>. - -<%= group_url(@group) %> diff --git a/app/views/notify/group_invite_declined_email.html.haml b/app/views/notify/group_invite_declined_email.html.haml deleted file mode 100644 index f9525d84fac..00000000000 --- a/app/views/notify/group_invite_declined_email.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%p - #{@invite_email} - has declined your invitation to join group - #{link_to @group.name, group_url(@group)}. - diff --git a/app/views/notify/group_invite_declined_email.text.erb b/app/views/notify/group_invite_declined_email.text.erb deleted file mode 100644 index 6c19a288d15..00000000000 --- a/app/views/notify/group_invite_declined_email.text.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= @invite_email %> has declined your invitation to join group <%= @group.name %>. - -<%= group_url(@group) %> diff --git a/app/views/notify/group_member_invited_email.html.haml b/app/views/notify/group_member_invited_email.html.haml deleted file mode 100644 index 163e88bfea3..00000000000 --- a/app/views/notify/group_member_invited_email.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%p - You have been invited - - if inviter = @group_member.created_by - by - = link_to inviter.name, user_url(inviter) - to join group - = link_to @group.name, group_url(@group) - as #{@group_member.human_access}. - -%p - = link_to 'Accept invitation', invite_url(@token) - or - = link_to 'decline', decline_invite_url(@token) - diff --git a/app/views/notify/group_member_invited_email.text.erb b/app/views/notify/group_member_invited_email.text.erb deleted file mode 100644 index 28ce4819b14..00000000000 --- a/app/views/notify/group_member_invited_email.text.erb +++ /dev/null @@ -1,4 +0,0 @@ -You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>. - -Accept invitation: <%= invite_url(@token) %> -Decline invitation: <%= decline_invite_url(@token) %> diff --git a/app/views/notify/member_access_denied_email.html.haml b/app/views/notify/member_access_denied_email.html.haml new file mode 100644 index 00000000000..71c9c50071a --- /dev/null +++ b/app/views/notify/member_access_denied_email.html.haml @@ -0,0 +1,4 @@ +%p + Your request to join the + #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular} + has been denied. diff --git a/app/views/notify/member_access_denied_email.text.erb b/app/views/notify/member_access_denied_email.text.erb new file mode 100644 index 00000000000..87f2ef817ee --- /dev/null +++ b/app/views/notify/member_access_denied_email.text.erb @@ -0,0 +1,3 @@ +Your request to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> has been denied. + +<%= member_source.web_url %> diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml new file mode 100644 index 00000000000..18dec806539 --- /dev/null +++ b/app/views/notify/member_access_granted_email.html.haml @@ -0,0 +1,3 @@ +%p + You have been granted #{member.human_access} access to the + #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}. diff --git a/app/views/notify/member_access_granted_email.text.erb b/app/views/notify/member_access_granted_email.text.erb new file mode 100644 index 00000000000..a9fb3a589a5 --- /dev/null +++ b/app/views/notify/member_access_granted_email.text.erb @@ -0,0 +1,3 @@ +You have been granted <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>. + +<%= member_source.web_url %> diff --git a/app/views/notify/member_access_requested_email.html.haml b/app/views/notify/member_access_requested_email.html.haml new file mode 100644 index 00000000000..76f1f08a0cb --- /dev/null +++ b/app/views/notify/member_access_requested_email.html.haml @@ -0,0 +1,3 @@ +%p + #{link_to member.user.name, member.user} requested #{member.human_access} + access to the #{link_to member_source.human_name, polymorphic_url([member_source, :members])} #{member_source.model_name.singular}. diff --git a/app/views/notify/member_access_requested_email.text.erb b/app/views/notify/member_access_requested_email.text.erb new file mode 100644 index 00000000000..9c5ee0eaf26 --- /dev/null +++ b/app/views/notify/member_access_requested_email.text.erb @@ -0,0 +1,3 @@ +<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>. + +<%= polymorphic_url([member_source, :members]) %> diff --git a/app/views/notify/member_invite_accepted_email.html.haml b/app/views/notify/member_invite_accepted_email.html.haml new file mode 100644 index 00000000000..2d1d40881eb --- /dev/null +++ b/app/views/notify/member_invite_accepted_email.html.haml @@ -0,0 +1,5 @@ +%p + #{member.invite_email}, now known as + #{link_to member.user.name, user_url(member.user)}, + has accepted your invitation to join the + #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}. diff --git a/app/views/notify/member_invite_accepted_email.text.erb b/app/views/notify/member_invite_accepted_email.text.erb new file mode 100644 index 00000000000..cef87101427 --- /dev/null +++ b/app/views/notify/member_invite_accepted_email.text.erb @@ -0,0 +1,3 @@ +<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>. + +<%= member_source.web_url %> diff --git a/app/views/notify/member_invite_declined_email.html.haml b/app/views/notify/member_invite_declined_email.html.haml new file mode 100644 index 00000000000..aa1b373d1a6 --- /dev/null +++ b/app/views/notify/member_invite_declined_email.html.haml @@ -0,0 +1,4 @@ +%p + #{@invite_email} + has declined your invitation to join the + #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}. diff --git a/app/views/notify/member_invite_declined_email.text.erb b/app/views/notify/member_invite_declined_email.text.erb new file mode 100644 index 00000000000..8bc305910c4 --- /dev/null +++ b/app/views/notify/member_invite_declined_email.text.erb @@ -0,0 +1,3 @@ +<%= @invite_email %> has declined your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>. + +<%= member_source.web_url %> diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml new file mode 100644 index 00000000000..b8b75da3f2f --- /dev/null +++ b/app/views/notify/member_invited_email.html.haml @@ -0,0 +1,13 @@ +%p + You have been invited + - if member.created_by + by + = link_to member.created_by.name, user_url(member.created_by) + to join the + = link_to member_source.human_name, member_source.web_url + #{member_source.model_name.singular} as #{member.human_access}. + +%p + = link_to 'Accept invitation', invite_url(@token) + or + = link_to 'decline', decline_invite_url(@token) diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb new file mode 100644 index 00000000000..0a6393355be --- /dev/null +++ b/app/views/notify/member_invited_email.text.erb @@ -0,0 +1,4 @@ +You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>. + +Accept invitation: <%= invite_url(@token) %> +Decline invitation: <%= decline_invite_url(@token) %> diff --git a/app/views/notify/project_access_granted_email.html.haml b/app/views/notify/project_access_granted_email.html.haml deleted file mode 100644 index dfc30a2d360..00000000000 --- a/app/views/notify/project_access_granted_email.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%p - = "You have been granted #{@project_member.human_access} access to project" -%p - = link_to namespace_project_url(@project.namespace, @project) do - = @project.name_with_namespace diff --git a/app/views/notify/project_access_granted_email.text.erb b/app/views/notify/project_access_granted_email.text.erb deleted file mode 100644 index 68eb1611ba7..00000000000 --- a/app/views/notify/project_access_granted_email.text.erb +++ /dev/null @@ -1,4 +0,0 @@ - -You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %> - -<%= url_for(namespace_project_url(@project.namespace, @project)) %> diff --git a/app/views/notify/project_invite_accepted_email.html.haml b/app/views/notify/project_invite_accepted_email.html.haml deleted file mode 100644 index 7e58d30b10a..00000000000 --- a/app/views/notify/project_invite_accepted_email.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%p - #{@project_member.invite_email}, now known as - #{link_to @project_member.user.name, user_url(@project_member.user)}, - has accepted your invitation to join project - #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}. - diff --git a/app/views/notify/project_invite_accepted_email.text.erb b/app/views/notify/project_invite_accepted_email.text.erb deleted file mode 100644 index fcbe752114d..00000000000 --- a/app/views/notify/project_invite_accepted_email.text.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>. - -<%= namespace_project_url(@project.namespace, @project) %> diff --git a/app/views/notify/project_invite_declined_email.html.haml b/app/views/notify/project_invite_declined_email.html.haml deleted file mode 100644 index c2d7e6f6e3a..00000000000 --- a/app/views/notify/project_invite_declined_email.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%p - #{@invite_email} - has declined your invitation to join project - #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}. - diff --git a/app/views/notify/project_invite_declined_email.text.erb b/app/views/notify/project_invite_declined_email.text.erb deleted file mode 100644 index 484687fa51c..00000000000 --- a/app/views/notify/project_invite_declined_email.text.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>. - -<%= namespace_project_url(@project.namespace, @project) %> diff --git a/app/views/notify/project_member_invited_email.html.haml b/app/views/notify/project_member_invited_email.html.haml deleted file mode 100644 index 79eb89616de..00000000000 --- a/app/views/notify/project_member_invited_email.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%p - You have been invited - - if inviter = @project_member.created_by - by - = link_to inviter.name, user_url(inviter) - to join project - = link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project) - as #{@project_member.human_access}. - -%p - = link_to 'Accept invitation', invite_url(@token) - or - = link_to 'decline', decline_invite_url(@token) diff --git a/app/views/notify/project_member_invited_email.text.erb b/app/views/notify/project_member_invited_email.text.erb deleted file mode 100644 index e0706272115..00000000000 --- a/app/views/notify/project_member_invited_email.text.erb +++ /dev/null @@ -1,4 +0,0 @@ -You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>. - -Accept invitation: <%= invite_url(@token) %> -Decline invitation: <%= decline_invite_url(@token) %> diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml index 89ae7ffda2b..f0cf82afe83 100644 --- a/app/views/profiles/notifications/_group_settings.html.haml +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -1,7 +1,7 @@ %li.notification-list-item %span.notification.fa.fa-holder.append-right-5 - if setting.global? - = notification_icon(current_user.notification_level) + = notification_icon(current_user.global_notification_setting.level) - else = notification_icon(setting.level) diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml index 17c097154da..e0fad555c09 100644 --- a/app/views/profiles/notifications/_project_settings.html.haml +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -1,7 +1,7 @@ %li.notification-list-item %span.notification.fa.fa-holder.append-right-5 - if setting.global? - = notification_icon(current_user.notification_level) + = notification_icon(current_user.global_notification_setting.level) - else = notification_icon(setting.level) diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 7696f112bb3..f2659ac14b5 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -26,33 +26,7 @@ = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" .form-group = f.label :notification_level, class: 'label-light' - .radio - = f.label :notification_level, value: :disabled do - = f.radio_button :notification_level, :disabled - .level-title - Disabled - %p You will not get any notifications via email - - .radio - = f.label :notification_level, value: :mention do - = f.radio_button :notification_level, :mention - .level-title - On Mention - %p You will receive notifications only for comments in which you were @mentioned - - .radio - = f.label :notification_level, value: :participating do - = f.radio_button :notification_level, :participating - .level-title - Participating - %p You will only receive notifications from related resources (e.g. from your commits or assigned issues) - - .radio - = f.label :notification_level, value: :watch do - = f.radio_button :notification_level, :watch - .level-title - Watch - %p You will receive notifications for any activity + = notification_level_radio_buttons .prepend-top-default = f.submit 'Update settings', class: "btn btn-create" diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index ce76cb73c9c..593be2617c1 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -51,9 +51,9 @@ %p Use a hardware device to add the second factor of authentication. %p - As U2F devices are only supported by a few browsers, it's recommended that you set up a - two-factor authentication app as well as a U2F device so you'll always be able to log in - using an unsupported browser. + As U2F devices are only supported by a few browsers, we require that you set up a + two-factor authentication app before a U2F device. That way you'll always be able to + log in - even when you're using an unsupported browser. .col-lg-9 %p - if @registration_key_handles.present? diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index f5bc1b4e409..2b19ee93eea 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -29,10 +29,13 @@ .project-clone-holder = render "shared/clone_panel" - .project-repo-buttons.btn-group.project-right-buttons - = render "projects/buttons/download" - = render 'projects/buttons/dropdown' - = render 'projects/buttons/notifications' + .project-repo-buttons.project-right-buttons + - if current_user + = render 'shared/members/access_request_buttons', source: @project + .btn-group + = render "projects/buttons/download" + = render 'projects/buttons/dropdown' + = render 'projects/buttons/notifications' :javascript new Star(); diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 5d931389dfb..cab21f0cf19 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -11,19 +11,33 @@ %p.build-detail-row #{@build.coverage}% - - if can?(current_user, :read_build, @project) && @build.artifacts? + - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block{ class: ("block-first" if !@build.coverage) } .title Build artifacts - .btn-group.btn-group-justified{ role: :group } - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do - Download + - if @build.artifacts_expired? + %p.build-detail-row + The artifacts were removed + #{time_ago_with_tooltip(@build.artifacts_expire_at)} + - elsif @build.artifacts_expire_at + %p.build-detail-row + The artifacts will be removed in + %span.js-artifacts-remove= @build.artifacts_expire_at - - if @build.artifacts_metadata? - = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do - Browse + - if @build.artifacts? + .btn-group.btn-group-justified{ role: :group } + - if @build.artifacts_expire_at + = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do + Keep - .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && @build.artifacts?)) } + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do + Download + + - if @build.artifacts_metadata? + = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do + Browse + + .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) } .title Build details - if @build.retryable? diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index 3b97dc9328f..a7a97181096 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,7 +1,7 @@ - if @notification_setting = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| = f.hidden_field :level - .dropdown + .dropdown.hidden-sm %button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } } = icon('bell') = notification_title(@notification_setting.level) diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 02dbb2985a4..71cf5582a4c 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,5 +1,5 @@ - if current_user - = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: "Star project" do + = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do - if current_user.starred?(@project) = icon('star fw') %span.starred Unstar diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index a0ffa065067..b8d8758fd2b 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -60,7 +60,7 @@ %li = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do = icon("download") - %span #{build.name} + %span Download '#{build.name}' artifacts - if can?(current_user, :update_pipeline, @project) - if pipeline.retryable? diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 44ef1fdbbe3..d9b800a4ded 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -17,7 +17,7 @@ .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 - = select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch" + = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch" - if can?(current_user, :push_code, @project) .js-create-merge-request-container .checkbox diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 367027182b6..a959b34a539 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -9,26 +9,30 @@ = cache(cache_key) do %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } + = commit_author_avatar(commit, size: 36) .commit-row-title %span.item-title = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" + %span.commit-row-message.visible-xs-inline + · + = commit.short_id + - if commit.status + = render_commit_status(commit, cssclass: 'visible-xs-inline') - if commit.description? - %a.text-expander.js-toggle-button ... + %a.text-expander.hidden-xs.js-toggle-button ... - .pull-right + .commit-actions.hidden-xs - if commit.status - = render_commit_status(commit) - = clipboard_button(clipboard_text: commit.id) - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" + = render_commit_status(commit, cssclass: 'btn btn-transparent') + = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent') + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" + = link_to_browse_code(project, commit) - if commit.description? - .commit-row-description.js-toggle-content - %pre - = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) + %pre.commit-row-description.js-toggle-content + = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) .commit-row-info - by - = commit_author_link(commit, avatar: true, size: 24) - .committed_ago - #{time_ago_with_tooltip(commit.committed_date)} - = link_to_browse_code(project, commit) + = commit_author_link(commit, avatar: false, size: 24) + authored + #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 7283a78a64e..dd12eae8f7e 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -4,18 +4,11 @@ - commits, hidden = limited_commits(@commits) - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| - .row.commits-row - .col-md-2.hidden-xs.hidden-sm - %h5.commits-row-date - %i.fa.fa-calendar - %span= day.strftime('%d %b, %Y') - .light - = pluralize(commits.count, 'commit') - .col-md-10.col-sm-12 - %ul.content-list - = render commits, project: project - %hr.lists-separator + %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}" + %li.commits-row + %ul.list-unstyled.commit-list + = render commits, project: project - if hidden > 0 - .alert.alert-warning + %li.alert.alert-warning #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index a72e8ba73ad..c8aa849c217 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,6 +1,6 @@ .scrolling-tabs-container - %ul.nav-links.sub-nav.scrolling-tabs - %div{ class: (container_class) } + .nav-links.sub-nav.scrolling-tabs + %ul{ class: (container_class) } .fade-left = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do = link_to project_files_path(@project) do diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 76ba0bea36d..51ca4eb903e 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -23,21 +23,18 @@ Create Merge Request .control - = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do - = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - + = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do + = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false } - if current_user && current_user.private_token .control = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do = icon("rss") - - %ul.breadcrumb.repo-breadcrumb = commits_breadcrumbs %div{id: dom_id(@project)} - #commits-list.content_list= render "commits", project: @project - .clear + %ol#commits-list.list-unstyled.content_list + = render "commits", project: @project = spinner :javascript diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml index 4e9f936539b..f35faa6afb5 100644 --- a/app/views/projects/container_registry/_tag.html.haml +++ b/app/views/projects/container_registry/_tag.html.haml @@ -9,11 +9,19 @@ - else \- %td - = number_to_human_size(tag.total_size) - · - = pluralize(tag.layers.size, "layer") + - if tag.total_size + = number_to_human_size(tag.total_size) + · + = pluralize(tag.layers.size, "layer") + - else + .light + \- %td - = time_ago_in_words(tag.created_at) + - if tag.created_at + = time_ago_in_words(tag.created_at) + - else + .light + \- - if can?(current_user, :update_container_image, @project) %td.content .controls.hidden-xs.pull-right diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml new file mode 100644 index 00000000000..0f9d9512d88 --- /dev/null +++ b/app/views/projects/deployments/_commit.html.haml @@ -0,0 +1,12 @@ +%div.branch-commit + - if deployment.ref + = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace" + · + = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace" + + %p.commit-title + %span + - if commit_title = deployment.commit_title + = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message" + - else + Cant find HEAD commit for this branch diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml new file mode 100644 index 00000000000..d08dd92f1f6 --- /dev/null +++ b/app/views/projects/deployments/_deployment.html.haml @@ -0,0 +1,23 @@ +%tr.deployment + %td + %strong= "##{deployment.iid}" + + %td + = render 'projects/deployments/commit', deployment: deployment + + %td + - if deployment.deployable + = link_to namespace_project_build_path(@project.namespace, @project, deployment.deployable) do + = "#{deployment.deployable.name} (##{deployment.deployable.id})" + + %td + #{time_ago_with_tooltip(deployment.created_at)} + + %td + - if can?(current_user, :create_deployment, deployment) && deployment.deployable + .pull-right + = link_to retry_namespace_project_build_path(@project.namespace, @project, deployment.deployable), method: :post, class: 'btn btn-build' do + - if deployment.last? + Retry + - else + Rollback diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index d9c4b410d32..f18bc8c41b3 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -11,6 +11,8 @@ = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') - elsif current_controller?(:merge_requests) = diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs') + - elsif current_controller?(:compare) + = diff_compare_whitespace_link(@project, params[:from], params[:to], class: 'hidden-xs') .btn-group = inline_diff_btn = parallel_diff_btn @@ -24,6 +26,7 @@ - diff_commit = commit_for_diff(diff_file) - blob = project.repository.blob_for_diff(diff_commit, diff_file) - next unless blob + - blob.load_all_data!(project.repository) unless blob.only_display_raw? = render 'projects/diffs/file', i: index, project: project, diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index e5983c58039..2395ea3c275 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -49,6 +49,8 @@ = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else = render "projects/diffs/text_file", diff_file: diff_file, index: i + - elsif blob.only_display_raw? + .nothing-here-block This file is too large to display. - elsif blob.image? - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file) = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml new file mode 100644 index 00000000000..eafa246d05f --- /dev/null +++ b/app/views/projects/environments/_environment.html.haml @@ -0,0 +1,17 @@ +- last_deployment = environment.last_deployment + +%tr.environment + %td + %strong + = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) + + %td + - if last_deployment + = render 'projects/deployments/commit', deployment: last_deployment + - else + %p.commit-title + No deployments yet + + %td + - if last_deployment + #{time_ago_with_tooltip(last_deployment.created_at)} diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml new file mode 100644 index 00000000000..c07f4bd510c --- /dev/null +++ b/app/views/projects/environments/_form.html.haml @@ -0,0 +1,7 @@ += form_for @environment, url: namespace_project_environments_path(@project.namespace, @project), html: { class: 'col-lg-9' } do |f| + = form_errors(@environment) + .form-group + = f.label :name, 'Name', class: 'label-light' + = f.text_field :name, required: true, class: 'form-control' + = f.submit 'Create environment', class: 'btn btn-create' + = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/environments/_header_title.html.haml b/app/views/projects/environments/_header_title.html.haml new file mode 100644 index 00000000000..e056fccad5d --- /dev/null +++ b/app/views/projects/environments/_header_title.html.haml @@ -0,0 +1 @@ +- header_title project_title(@project, "Environments", project_environments_path(@project)) diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml new file mode 100644 index 00000000000..ae9e77e7d89 --- /dev/null +++ b/app/views/projects/environments/index.html.haml @@ -0,0 +1,23 @@ +- @no_container = true +- page_title "Environments" += render "projects/pipelines/head" + +%div{ class: (container_class) } + - if can?(current_user, :create_environment, @project) + .top-area + .nav-controls + = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do + New environment + + - if @environments.blank? + %ul.content-list.environments + %li.nothing-here-block + No environments to show + - else + .table-holder + %table.table.environments + %tbody + %th Environment + %th Last deployment + %th Date + = render @environments diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml new file mode 100644 index 00000000000..54465828ba9 --- /dev/null +++ b/app/views/projects/environments/new.html.haml @@ -0,0 +1,9 @@ +- page_title 'New Environment' + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + New Environment + %p Environments allow you to track deployments of your application + + = render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml new file mode 100644 index 00000000000..069b77b5adf --- /dev/null +++ b/app/views/projects/environments/show.html.haml @@ -0,0 +1,33 @@ +- @no_container = true +- page_title "Environments" += render "projects/pipelines/head" + +%div{ class: (container_class) } + .top-area + .col-md-9 + %h3.page-title= @environment.name.titleize + + .col-md-3 + .nav-controls + - if can?(current_user, :update_environment, @environment) + = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete + + - if @deployments.blank? + %ul.content-list.environments + %li.nothing-here-block + No deployments for + %strong= @environment.name + - else + .table-holder + %table.table.environments + %thead + %tr + %th ID + %th Commit + %th Build + %th Date + %th + + = render @deployments + + = paginate @deployments, theme: 'gitlab' diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 166dae248b6..403adb7426b 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -1,5 +1,5 @@ -%ul.nav-links.sub-nav - %div{ class: (container_class) } +.nav-links.sub-nav + %ul{ class: (container_class) } - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) = nav_link(controller: :issues) do = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 75f36579b11..d8075371853 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -24,8 +24,6 @@ MERGED - elsif merge_request.closed? CLOSED - %li - = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} - if @closed_by_merge_requests.present? %li = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index b9bb6fe559d..c6fc499a7b8 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -6,7 +6,7 @@ %li - sha = @project.repository.find_branch(branch).target - pipeline = @project.pipeline(sha, branch) if sha - - if ci_copipelinemmit + - if pipeline %span.related-branch-ci-status = render_pipeline_status(pipeline) %span.related-branch-info diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 7ad7c9c87e8..36957560de0 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_issues_url(@project.namespace, @project) - xml.updated @issues.first.created_at.xmlschema if @issues.any? + xml.updated @issues.first.created_at.xmlschema if @issues.reorder(nil).any? - xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? + xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? end diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 6e1baa46b05..aa4d69550ec 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -4,9 +4,10 @@ = render "projects/issues/head" %div{ class: (container_class) } - .top-area + .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. + Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. + .nav-controls - if can?(current_user, :admin_label, @project) = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do @@ -19,10 +20,9 @@ .prioritized-labels{ class: ('hide' if hide) } %h5 Prioritized Labels %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } + %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet - if @prioritized_labels.present? = render @prioritized_labels - - else - %p.empty-message No prioritized labels yet .other-labels - if can?(current_user, :admin_label, @project) %h5{ class: ('hide' if hide) } Other Labels diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index a8f09f855d4..0b05785430b 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -2,4 +2,5 @@ = icon("sort-amount-desc") Most recent commits displayed first -= render "projects/commits/commits", project: @merge_request.project +%ol#commits-list.list-unstyled + = render "projects/commits/commits", project: @merge_request.project diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index ec4beae9727..19b5d0ff066 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -6,46 +6,29 @@ - if @merge_request.merge_event by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)} #{time_ago_with_tooltip(@merge_request.merge_event.created_at)} - %div - - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true') + - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true') + %p + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + The source branch has been removed. + = render 'projects/merge_requests/widget/merged_buttons' + - elsif @merge_request.can_remove_source_branch?(current_user) + .remove_source_branch_widget %p The changes were merged into #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - The source branch has been removed. - = render 'projects/merge_requests/widget/merged_buttons' - - elsif @merge_request.can_remove_source_branch?(current_user) - .remove_source_branch_widget - %p - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - You can remove the source branch now. - = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true - .remove_source_branch_widget.failed.hide - %p - Failed to remove source branch '#{@merge_request.source_branch}'. - - .remove_source_branch_in_progress.hide - %p - = icon('spinner spin') - Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded. - - :javascript - $('.remove_source_branch').on('click', function() { - $('.remove_source_branch_widget').hide(); - $('.remove_source_branch_in_progress').show(); - }); - - $(".remove_source_branch").on("ajax:success", function (e, data, status, xhr) { - location.reload(); - }); + You can remove the source branch now. + = render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true + .remove_source_branch_widget.failed.hide + %p + Failed to remove source branch '#{@merge_request.source_branch}'. - $(".remove_source_branch").on("ajax:error", function (e, data, status, xhr) { - $('.remove_source_branch_widget').hide(); - $('.remove_source_branch_in_progress').hide(); - $('.remove_source_branch_widget.failed').show(); - }); - - else + .remove_source_branch_in_progress.hide %p - The changes were merged into - #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. - = render 'projects/merge_requests/widget/merged_buttons' + = icon('spinner spin') + Removing source branch '#{@merge_request.source_branch}'. Please wait, this page will be automatically reloaded. + - else + %p + The changes were merged into + #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}. + = render 'projects/merge_requests/widget/merged_buttons' diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index 56167509af9..d836a253507 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -3,9 +3,9 @@ - mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked? - if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked - .btn-group + .clearfix.merged-buttons - if can_remove_source_branch - = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do = icon('trash-o') Remove Source Branch - if mr_can_be_reverted diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index f5e2b927da8..cbf1ba04170 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -19,6 +19,7 @@ = f.label :due_date, "Due Date", class: "control-label" .col-sm-10 = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" + %a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date .form-actions - if @milestone.new_record? @@ -27,10 +28,3 @@ -else = f.submit 'Save changes', class: "btn-save btn" = link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel" - - -:javascript - $(".datepicker").datepicker({ - dateFormat: "yy-mm-dd", - onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } - }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index bf9baaea889..e4ab064eda8 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,4 +1,5 @@ - page_title "Network", @ref +- page_specific_javascripts asset_path("network/application.js") = render "projects/commits/head" = render "head" %div{ class: (container_class) } @@ -14,14 +15,5 @@ = check_box_tag :filter_ref, 1, @options[:filter_ref] %span Begin with the selected commit - .network-graph + .network-graph{ data: { url: '#{escape_javascript(@url)}', commit_url: '#{escape_javascript(@commit_url)}', ref: '#{escape_javascript(@ref)}', commit_id: '#{escape_javascript(@commit.id)}' } } = spinner nil, true - -:javascript - network_graph = new Network({ - url: "#{escape_javascript(@url)}", - commit_url: "#{escape_javascript(@commit_url)}", - ref: "#{escape_javascript(@ref)}", - commit_id: '#{@commit.id}' - }) - new ShortcutsNetwork(network_graph.branch_graph) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index f9ac16b32f3..7e8b8f83467 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -11,26 +11,22 @@ .project-edit-content = form_for @project, html: { class: 'new_project form-horizontal js-requires-input' } do |f| - .form-group.project-name-holder + .form-group = f.label :path, class: 'control-label' do - Project path + Project owner .col-sm-10 - .input-group - - if current_user.can_select_namespace? - .input-group-addon - = root_url - = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} - .input-group-addon - \/ - - else - .input-group-addon - #{root_url}#{current_user.username}/ - = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true - + = f.select :namespace_id, namespaces_options(:current_user), {}, {class: 'select2 js-select-namespace', tabindex: 1} + - if current_user.can_create_group? .help-block Want to house several dependent projects under the same namespace? = link_to "Create a group", new_group_path + + .form-group + = f.label :path, class: 'control-label' do + Project name + .col-sm-10 + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true - if import_sources_enabled? .project-import.js-toggle-container diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index f278d4e0538..d65faf86d4e 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,15 +1,19 @@ -%ul.nav-links.sub-nav - %div{ class: (container_class) } +.nav-links.sub-nav + %ul{ class: (container_class) } - if project_nav_tab? :pipelines = nav_link(controller: :pipelines) do = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do %span Pipelines - %span.badge.count.ci_counter= number_with_delimiter(@project.pipelines.running_or_pending.count) - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do %span Builds - %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) + + - if project_nav_tab? :environments + = nav_link(controller: %w(environments)) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index 6671ee2c6d6..cb6136c215a 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -6,11 +6,14 @@ (#{members.count}) - if can?(current_user, :admin_group_member, @group) .controls - = link_to group_group_members_path(@group), class: 'btn' do - Manage group members + = link_to 'Manage group members', + group_group_members_path(@group), + class: 'btn' %ul.content-list - - members.limit(20).each do |member| - = render 'groups/group_members/group_member', member: member, show_controls: false - - if members.count > 20 + = render partial: 'shared/members/member', + collection: members.limit(20), + as: :member, + locals: { show_controls: false } + - if members.size > 20 %li and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)} diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index f0f3bb3c177..82892a33358 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -9,7 +9,7 @@ .form-group = f.label :access_level, "Project Access", class: 'control-label' .col-sm-10 - = select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2" + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2" .help-block Read more about role permissions %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml deleted file mode 100644 index 268f140d7db..00000000000 --- a/app/views/projects/project_members/_project_member.html.haml +++ /dev/null @@ -1,55 +0,0 @@ -- user = member.user -- return unless user || member.invite? - -%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)} - %span.list-item-name - - if member.user - = image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' - %strong - = link_to user.name, user_path(user) - %span.cgray= user.username - - if user == current_user - %span.label.label-success It's you - - if user.blocked? - %label.label.label-danger - %strong Blocked - - else - = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' - %strong - = member.invite_email - %span.cgray - invited - - if member.created_by - by - = link_to member.created_by.name, user_path(member.created_by) - = time_ago_with_tooltip(member.created_at) - - - if can?(current_user, :admin_project_member, @project) - = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do - Resend invite - - - if can?(current_user, :admin_project_member, @project) - .pull-right - %strong= member.human_access - - if can?(current_user, :update_project_member, member) - = button_tag class: "btn-xs btn-grouped inline btn js-toggle-button", - title: 'Edit access level', type: 'button' do - = icon('pencil') - - - if can?(current_user, :destroy_project_member, member) - - - if current_user == user - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do - = icon("sign-out") - Leave - - else - = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do - = icon('trash') - - .edit-member.hide.js-toggle-content - %br - = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f| - .prepend-top-10 - = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control' - .prepend-top-10 - = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml index ae13f8428f0..952844acefc 100644 --- a/app/views/projects/project_members/_shared_group_members.html.haml +++ b/app/views/projects/project_members/_shared_group_members.html.haml @@ -14,8 +14,10 @@ %i.fa.fa-pencil-square-o Edit group members %ul.content-list - - shared_group.group_members.order('access_level DESC').limit(20).each do |member| - = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false + = render partial: 'shared/members/member', + collection: shared_group.group_members.order(access_level: :desc).limit(20), + as: :member, + locals: { show_controls: false, show_roles: false } - if shared_group_users_count > 20 %li and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)} diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index e8dce30425f..03207614258 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -11,8 +11,7 @@ = button_tag class: 'btn', title: 'Search' do = icon("search") %ul.content-list - - members.each do |project_member| - = render 'project_member', member: project_member + = render partial: 'shared/members/member', collection: members, as: :member :javascript $('form.member-search-form').on('submit', function (event) { diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 15dc064e7ea..357ccccaf1d 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -13,7 +13,9 @@ Users with access to this project are listed below. = render "new_project_member" - = render "team", members: @project_members + = render 'shared/members/requests', membership_source: @project, members: @project_members.request + + = render 'team', members: @project_members.non_request - if @group = render "group_members", members: @group_members diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 2fb3a41d541..45f8ef89060 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,2 +1,2 @@ :plain - $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}'); + $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 478c04318c6..77676454b57 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,5 +1,7 @@ %span.label-row - if can?(current_user, :admin_label, @project) + .draggable-handler + = icon('bars') .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), dom_id: dom_id(label) } } %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index e74fc36c797..ca3178395c1 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -1,4 +1,4 @@ -- if @merge_requests.any? +- if @merge_requests.reorder(nil).any? - @merge_requests.group_by(&:target_project).each do |group| .panel.panel-default.panel-small - project = group[0] diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index a25365a94f2..1ad95351005 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -9,7 +9,7 @@ = link_to edit_group_path(group), class: "btn" do = icon('cogs') - = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn", title: 'Leave this group' do + = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do = icon('sign-out') .stats diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 17e2a7e9290..c30bdb0ae91 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -35,13 +35,13 @@ .clearfix .error-alert -- if issuable.is_a?(Issue) && !issuable.project.private? +- if issuable.is_a?(Issue) .form-group .col-sm-offset-2.col-sm-10 .checkbox = f.label :confidential do = f.check_box :confidential - This issue is confidential and should only be visible to team members + This issue is confidential and should only be visible to team members with at least Reporter access. - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - has_due_date = issuable.has_attribute?(:due_date) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index fb906de829a..210c9b9aab5 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,9 +1,21 @@ +- todo = has_todo(issuable) %aside.right-sidebar{ class: sidebar_gutter_collapsed_class } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header - %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'} + - if current_user + %span.issuable-header-text.hide-collapsed.pull-left + Todo + %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } } = sidebar_gutter_toggle_icon + - if current_user + %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", id: (todo.id unless todo.nil?), issuable: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project) } } + %span.js-issuable-todo-text + - if todo.nil? + Add Todo + - else + Mark Done + = icon('spin spinner', class: 'hidden js-issuable-todo-loading') = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| .block.assignee @@ -17,20 +29,21 @@ = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.hide-collapsed + .value.hide-collapsed - if issuable.assignee - = link_to_member(@project, issuable.assignee, size: 32) do + = link_to_member(@project, issuable.assignee, size: 32, extra_class: 'bold') do - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee) %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' } = icon('exclamation-triangle') %span.username = issuable.assignee.to_reference - else - %span.assign-yourself + %span.assign-yourself.no-value No assignee - if can_edit_issuable + \- %a.js-assign-yourself{ href: '#' } - \- assign yourself + assign yourself .selectbox.hide-collapsed = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' @@ -50,13 +63,11 @@ = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.hide-collapsed + .value.hide-collapsed - if issuable.milestone - = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1}} - = issuable.milestone.title + = link_to issuable.milestone.title, namespace_project_milestone_path(@project.namespace, @project, issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 } - else - .light None + %span.no-value None .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil @@ -73,14 +84,14 @@ = icon('spinner spin', class: 'block-loading') - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.hide-collapsed + .value.hide-collapsed %span.value-content - if issuable.due_date - = issuable.due_date.to_s(:medium) + %span.bold= issuable.due_date.to_s(:medium) - else - None + %span.no-value No due date - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - %span.light.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) } + %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) } \- %a.js-remove-due-date{ href: "#", role: "button" } remove due date @@ -112,7 +123,7 @@ - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else - .light None + %span.no-value None .selectbox.hide-collapsed - issuable.labels_array.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml new file mode 100644 index 00000000000..ed0a6ebcf84 --- /dev/null +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -0,0 +1,12 @@ +- member = source.members.find_by(user_id: current_user.id) + +- if member + - if member.request? + = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]), + method: :delete, + data: { confirm: remove_member_message(member) }, + class: 'btn access-request-button hidden-xs' +- else + = link_to 'Request Access', polymorphic_path([:request_access, source, :members]), + method: :post, + class: 'btn access-request-button hidden-xs' diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml new file mode 100644 index 00000000000..0191814849a --- /dev/null +++ b/app/views/shared/members/_member.html.haml @@ -0,0 +1,78 @@ +- default_show_roles = can?(current_user, action_member_permission(:update, member), member) || can?(current_user, action_member_permission(:destroy, member), member) +- show_roles = local_assigns.fetch(:show_roles, default_show_roles) +- show_controls = local_assigns.fetch(:show_controls, true) +- user = member.user + +%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) } + %span{ class: ("list-item-name" if show_controls) } + - if user + = image_tag avatar_icon(user, 24), class: "avatar s24", alt: '' + %strong + = link_to user.name, user_path(user) + %span.cgray= user.username + + - if user == current_user + %span.label.label-success It's you + + - if user.blocked? + %label.label.label-danger + %strong Blocked + + - if member.request? + %span.cgray + – Requested + = time_ago_with_tooltip(member.requested_at) + - else + = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: '' + %strong= member.invite_email + %span.cgray + – Invited + - if member.created_by + by + = link_to member.created_by.name, user_path(member.created_by) + = time_ago_with_tooltip(member.created_at) + + - if show_controls && can?(current_user, action_member_permission(:admin, member), member.source) + = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), + method: :post, + class: 'btn-xs btn' + + - if show_roles + %span.pull-right + %strong= member.human_access + - if show_controls + - if can?(current_user, action_member_permission(:update, member), member) + = button_tag icon('pencil'), + type: 'button', + class: 'btn-xs btn btn-grouped inline js-toggle-button', + title: 'Edit access level' + + - if member.request? + + = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), + method: :post, + class: 'btn-xs btn btn-success', + title: 'Grant access' + + - if can?(current_user, action_member_permission(:destroy, member), member) + + - if current_user == user + = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), + method: :delete, + data: { confirm: leave_confirmation_message(member.source) }, + class: 'btn-xs btn btn-remove' + - else + = link_to icon('trash'), member, + remote: true, + method: :delete, + data: { confirm: remove_member_message(member) }, + class: 'btn-xs btn btn-remove', + title: remove_member_title(member) + + .edit-member.hide.js-toggle-content + %br + = form_for member, remote: true do |f| + .prepend-top-10 + = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control' + .prepend-top-10 + = f.submit 'Save', class: 'btn btn-save btn-sm' diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml new file mode 100644 index 00000000000..b5963876034 --- /dev/null +++ b/app/views/shared/members/_requests.html.haml @@ -0,0 +1,8 @@ +- if members.any? + .panel.panel-default + .panel-heading + %strong= membership_source.name + access requests + %small= "(#{members.size})" + %ul.content-list + = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml index c29d8ee6737..9c193f901e2 100644 --- a/app/views/shared/milestones/_merge_requests_tab.haml +++ b/app/views/shared/milestones/_merge_requests_tab.haml @@ -3,10 +3,10 @@ .row.prepend-top-default .col-md-3 - = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned') + = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned', show_counter: true) .col-md-3 - = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing') + = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing', show_counter: true) .col-md-3 - = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed') + = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed', show_counter: true) .col-md-3 - = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true) + = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true, show_counter: true) diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index 46af591fc43..cbb8dfb7829 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -4,11 +4,18 @@ %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). %script#js-register-u2f-setup{ type: "text/template" } - .row.append-bottom-10 - .col-md-3 - %a#js-setup-u2f-device.btn.btn-info{ href: 'javascript:void(0)' } Setup New U2F Device - .col-md-9 - %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. + - if current_user.two_factor_otp_enabled? + .row.append-bottom-10 + .col-md-3 + %button#js-setup-u2f-device.btn.btn-info Setup New U2F Device + .col-md-9 + %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. + - else + .row.append-bottom-10 + .col-md-3 + %button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup New U2F Device + .col-md-9 + %p.text-warning You need to register a two-factor authentication app before you can set up a U2F device. %script#js-register-u2f-in-progress{ type: "text/template" } %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb new file mode 100644 index 00000000000..c64ea108d52 --- /dev/null +++ b/app/workers/expire_build_artifacts_worker.rb @@ -0,0 +1,13 @@ +class ExpireBuildArtifactsWorker + include Sidekiq::Worker + + def perform + Rails.logger.info 'Cleaning old build artifacts' + + builds = Ci::Build.with_expired_artifacts + builds.find_each(batch_size: 50).each do |build| + Rails.logger.debug "Removing artifacts build #{build.id}..." + build.erase_artifacts! + end + end +end diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb index ca594e77e7c..6828013b377 100644 --- a/app/workers/stuck_ci_builds_worker.rb +++ b/app/workers/stuck_ci_builds_worker.rb @@ -6,7 +6,7 @@ class StuckCiBuildsWorker def perform Rails.logger.info 'Cleaning stuck builds' - builds = Ci::Build.running_or_pending.where('updated_at < ?', BUILD_STUCK_TIMEOUT.ago) + builds = Ci::Build.joins(:project).running_or_pending.where('ci_builds.updated_at < ?', BUILD_STUCK_TIMEOUT.ago) builds.find_each(batch_size: 50).each do |build| Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}" build.drop |