From db19e72027f21e573eb7889791e37ffa14a9a925 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone <annabel.dunstone@gmail.com> Date: Fri, 22 Jul 2016 17:28:39 -0700 Subject: [PATCH] Add route, controller action, and views for MR pipelines --- .../javascripts/merge_request_tabs.js.coffee | 266 ++++++++++++++++++ .../merge_request_widget.js.coffee | 143 ++++++++++ .../projects/merge_requests_controller.rb | 19 +- .../projects/commit/_pipelines_list.haml | 54 ++++ .../projects/merge_requests/_show.html.haml | 6 + .../merge_requests/show/_builds.html.haml | 1 - .../merge_requests/show/_pipelines.html.haml | 1 + .../merge_requests/widget/_show.html.haml | 3 +- config/routes.rb | 1 + 9 files changed, 488 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/merge_request_tabs.js.coffee create mode 100644 app/assets/javascripts/merge_request_widget.js.coffee create mode 100644 app/views/projects/commit/_pipelines_list.haml create mode 100644 app/views/projects/merge_requests/show/_pipelines.html.haml diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee new file mode 100644 index 00000000000..ccdfcf895a3 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -0,0 +1,266 @@ +# MergeRequestTabs +# +# Handles persisting and restoring the current tab selection and lazily-loading +# content on the MergeRequests#show page. +# +#= require jquery.cookie +# +# ### Example Markup +# +# <ul class="nav-links merge-request-tabs"> +# <li class="notes-tab active"> +# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1"> +# Discussion +# </a> +# </li> +# <li class="commits-tab"> +# <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits"> +# Commits +# </a> +# </li> +# <li class="diffs-tab"> +# <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs"> +# Diffs +# </a> +# </li> +# </ul> +# +# <div class="tab-content"> +# <div class="notes tab-pane active" id="notes"> +# Notes Content +# </div> +# <div class="commits tab-pane" id="commits"> +# Commits Content +# </div> +# <div class="diffs tab-pane" id="diffs"> +# Diffs Content +# </div> +# </div> +# +# <div class="mr-loading-status"> +# <div class="loading"> +# Loading Animation +# </div> +# </div> +# +class @MergeRequestTabs + diffsLoaded: false + buildsLoaded: false + commitsLoaded: false + + constructor: (@opts = {}) -> + # Store the `location` object, allowing for easier stubbing in tests + @_location = location + + @bindEvents() + @activateTab(@opts.action) + + bindEvents: -> + $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown + $(document).on 'click', '.js-show-tab', @showTab + + showTab: (event) => + event.preventDefault() + + @activateTab $(event.target).data('action') + + tabShown: (event) => + $target = $(event.target) + action = $target.data('action') + + if action == 'commits' + @loadCommits($target.attr('href')) + @expandView() + else if action == 'diffs' + @loadDiff($target.attr('href')) + if bp? and bp.getBreakpointSize() isnt 'lg' + @shrinkView() + + navBarHeight = $('.navbar-gitlab').outerHeight() + $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight) + else if action == 'builds' + @loadBuilds($target.attr('href')) + @expandView() + else if action == 'pipelines' + @loadPipelines($target.attr('href')) + @expandView() + else + @expandView() + + @setCurrentAction(action) + + scrollToElement: (container) -> + if window.location.hash + navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + + $el = $("#{container} #{window.location.hash}:not(.match)") + $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length + + # Activate a tab based on the current action + activateTab: (action) -> + action = 'notes' if action == 'show' + $(".merge-request-tabs a[data-action='#{action}']").tab('show') + + # Replaces the current Merge Request-specific action in the URL with a new one + # + # If the action is "notes", the URL is reset to the standard + # `MergeRequests#show` route. + # + # Examples: + # + # location.pathname # => "/namespace/project/merge_requests/1" + # setCurrentAction('diffs') + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('notes') + # location.pathname # => "/namespace/project/merge_requests/1" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('commits') + # location.pathname # => "/namespace/project/merge_requests/1/commits" + # + # Returns the new URL String + setCurrentAction: (action) => + # Normalize action, just to be safe + action = 'notes' if action == 'show' + + # Remove a trailing '/commits' or '/diffs' + new_state = @_location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '') + + # Append the new action if we're on a tab other than 'notes' + unless action == 'notes' + new_state += "/#{action}" + + # Ensure parameters and hash come along for the ride + new_state += @_location.search + @_location.hash + + # Replace the current history state with the new one without breaking + # Turbolinks' history. + # + # See https://github.com/rails/turbolinks/issues/363 + history.replaceState {turbolinks: true, url: new_state}, document.title, new_state + + new_state + + loadCommits: (source) -> + return if @commitsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.querySelector("div#commits").innerHTML = data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) + @commitsLoaded = true + @scrollToElement("#commits") + + loadDiff: (source) -> + return if @diffsLoaded + @_get + url: "#{source}.json" + @_location.search + success: (data) => + $('#diffs').html data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) + $('#diffs .js-syntax-highlight').syntaxHighlight() + $('#diffs .diff-file').singleFileDiff() + @expandViewContainer() if @diffViewType() is 'parallel' + @diffsLoaded = true + @scrollToElement("#diffs") + @highlighSelectedLine() + @filesCommentButton = $('.files .diff-file').filesCommentButton() + + $(document) + .off 'click', '.diff-line-num a' + .on 'click', '.diff-line-num a', (e) => + e.preventDefault() + window.location.hash = $(e.currentTarget).attr 'href' + @highlighSelectedLine() + @scrollToElement("#diffs") + + highlighSelectedLine: -> + $('.hll').removeClass 'hll' + locationHash = window.location.hash + + if locationHash isnt '' + hashClassString = ".#{locationHash.replace('#', '')}" + $diffLine = $("#{locationHash}:not(.match)", $('#diffs')) + + if not $diffLine.is 'tr' + $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}") + else + $diffLine = $diffLine.find('td') + + if $diffLine.length + $diffLine.addClass 'hll' + diffLineTop = $diffLine.offset().top + navBarHeight = $('.navbar-gitlab').outerHeight() + + loadBuilds: (source) -> + return if @buildsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.querySelector("div#builds").innerHTML = data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) + @buildsLoaded = true + @scrollToElement("#builds") + + loadPipelines: (source) -> + return if @pipelinesLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.querySelector("div#pipelines").innerHTML = data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#pipelines')) + @pipelinesLoaded = true + @scrollToElement("#pipelines") + + # Show or hide the loading spinner + # + # status - Boolean, true to show, false to hide + toggleLoading: (status) -> + $('.mr-loading-status .loading').toggle(status) + + _get: (options) -> + defaults = { + beforeSend: => @toggleLoading(true) + complete: => @toggleLoading(false) + dataType: 'json' + type: 'GET' + } + + options = $.extend({}, defaults, options) + + $.ajax(options) + + # Returns diff view type + diffViewType: -> + $('.inline-parallel-buttons a.active').data('view-type') + + expandViewContainer: -> + $('.container-fluid').removeClass('container-limited') + + shrinkView: -> + $gutterIcon = $('.js-sidebar-toggle i:visible') + + # Wait until listeners are set + setTimeout( -> + # Only when sidebar is expanded + if $gutterIcon.is('.fa-angle-double-right') + $gutterIcon.closest('a').trigger('click', [true]) + , 0) + + # Expand the issuable sidebar unless the user explicitly collapsed it + expandView: -> + return if $.cookie('collapsed_gutter') == 'true' + + $gutterIcon = $('.js-sidebar-toggle i:visible') + + # Wait until listeners are set + setTimeout( -> + # Only when sidebar is collapsed + if $gutterIcon.is('.fa-angle-double-left') + $gutterIcon.closest('a').trigger('click', [true]) + , 0) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee new file mode 100644 index 00000000000..63a12582ddc --- /dev/null +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -0,0 +1,143 @@ +class @MergeRequestWidget + # Initialize MergeRequestWidget behavior + # + # check_enable - Boolean, whether to check automerge status + # merge_check_url - String, URL to use to check automerge status + # ci_status_url - String, URL to use to check CI status + # + + constructor: (@opts) -> + $('#modal_merge_info').modal(show: false) + @firstCICheck = true + @readyForCICheck = false + @cancel = false + clearInterval @fetchBuildStatusInterval + + @clearEventListeners() + @addEventListeners() + @getCIStatus(false) + @pollCIStatus() + notifyPermissions() + + clearEventListeners: -> + $(document).off 'page:change.merge_request' + + cancelPolling: -> + @cancel = true + + addEventListeners: -> + allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'] + $(document).on 'page:change.merge_request', => + page = $('body').data('page').split(':').last() + if allowedPages.indexOf(page) < 0 + clearInterval @fetchBuildStatusInterval + @cancelPolling() + @clearEventListeners() + + mergeInProgress: (deleteSourceBranch = false)-> + $.ajax + type: 'GET' + url: $('.merge-request').data('url') + success: (data) => + if data.state == "merged" + urlSuffix = if deleteSourceBranch then '?delete_source=true' else '' + + window.location.href = window.location.pathname + urlSuffix + else if data.merge_error + $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>") + else + callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch) + setTimeout(callback, 2000) + dataType: 'json' + + getMergeStatus: -> + $.get @opts.merge_check_url, (data) -> + $('.mr-state-widget').replaceWith(data) + + ciLabelForStatus: (status) -> + switch status + when 'success' + 'passed' + when 'success_with_warnings' + 'passed with warnings' + else + status + + pollCIStatus: -> + @fetchBuildStatusInterval = setInterval ( => + return if not @readyForCICheck + + @getCIStatus(true) + + @readyForCICheck = false + ), 10000 + + getCIStatus: (showNotification) -> + _this = @ + $('.ci-widget-fetching').show() + + $.getJSON @opts.ci_status_url, (data) => + return if @cancel + @readyForCICheck = true + + if data.status is '' + return + + if @firstCICheck || data.status isnt @opts.ci_status and data.status? + @opts.ci_status = data.status + @showCIStatus data.status + if data.coverage + @showCICoverage data.coverage + + # The first check should only update the UI, a notification + # should only be displayed on status changes + if showNotification and not @firstCICheck + status = @ciLabelForStatus(data.status) + + if status is "preparing" + title = @opts.ci_title.preparing + status = status.charAt(0).toUpperCase() + status.slice(1); + message = @opts.ci_message.preparing.replace('{{status}}', status) + else + title = @opts.ci_title.normal + message = @opts.ci_message.normal.replace('{{status}}', status) + + title = title.replace('{{status}}', status) + message = message.replace('{{sha}}', data.sha) + message = message.replace('{{title}}', data.title) + + notify( + title, + message, + @opts.gitlab_icon, + -> + @close() + Turbolinks.visit _this.opts.builds_path + ) + @firstCICheck = false + + showCIStatus: (state) -> + return if not state? + $('.ci_widget').hide() + allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"] + if state in allowed_states + $('.ci_widget.ci-' + state).show() + switch state + when "failed", "canceled", "not_found" + @setMergeButtonClass('btn-danger') + when "running" + @setMergeButtonClass('btn-warning') + when "success", "success_with_warnings" + @setMergeButtonClass('btn-create') + else + $('.ci_widget.ci-error').show() + @setMergeButtonClass('btn-danger') + + showCICoverage: (coverage) -> + text = 'Coverage ' + coverage + '%' + $('.ci_widget:visible .ci-coverage').text(text) + + setMergeButtonClass: (css_class) -> + $('.js-merge-button,.accept-action .dropdown-toggle') + .removeClass('btn-danger btn-warning btn-create') + .addClass(css_class) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 594a61464b9..5c6396fba9f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,15 +7,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :builds, :pipelines, :merge, :merge_check, :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip ] - before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] - before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] + before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] + before_action :define_show_vars, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs] - before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] + before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :pipelines] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -136,6 +136,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def pipelines + respond_to do |format| + format.html do + define_discussion_vars + + render 'show' + end + format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } } + end + end + def new build_merge_request @noteable = @merge_request diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml new file mode 100644 index 00000000000..2c2feced657 --- /dev/null +++ b/app/views/projects/commit/_pipelines_list.haml @@ -0,0 +1,54 @@ +- status = pipeline.status + +%ul.content-list.pipelines + + .table-holder + %table.table.builds + %tbody + %th Status + %th Commit + %th.stage + %th + %th + %tr.commit + %td.commit-link + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do + = ci_status_with_icon(status) + %td + .branch-commit + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do + %span ##{pipeline.id} + - if pipeline.ref + .icon-container + = pipeline.tag? ? icon('tag') : icon('code-fork') + = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name" + .icon-container + = custom_icon("icon_commit") + = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace" + - if pipeline.latest? + %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + - if pipeline.triggered? + %span.label.label-primary triggered + - if pipeline.yaml_errors.present? + %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid + - if pipeline.builds.any?(&:stuck?) + %span.label.label-warning stuck + + %p.commit-title + - if commit = pipeline.commit + = author_avatar(commit, size: 20) + = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message" + - else + Cant find HEAD commit for this branch + + -# - stages_status = pipeline.statuses.latest.stages_status + -# - stages.each do |stage| + -# %td.stage-cell + -# - status = stages_status[stage] + -# - tooltip = "#{stage.titleize}: #{status || 'not found'}" + -# - if status + -# = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + -# = ci_icon_for_status(status) + -# - else + -# .light.has-tooltip{ title: tooltip } + -# \- diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 873ed9b59ee..a78407f26ea 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -53,6 +53,10 @@ Commits %span.badge= @commits_count - if @pipeline + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#pipelines', action: 'pipelines', toggle: 'tab'} do + Pipelines + %span.badge= @statuses.size %li.builds-tab = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do Builds @@ -76,6 +80,8 @@ - # This tab is always loaded via AJAX #builds.builds.tab-pane - # This tab is always loaded via AJAX + #pipelines.pipelines.tab-pane + - # This tab is always loaded via AJAX #diffs.diffs.tab-pane - # This tab is always loaded via AJAX diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml index 81de60f116c..808ef7fed27 100644 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -1,2 +1 @@ = render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true - diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml new file mode 100644 index 00000000000..bcaec137371 --- /dev/null +++ b/app/views/projects/merge_requests/show/_pipelines.html.haml @@ -0,0 +1 @@ += render "projects/commit/pipelines_list", pipeline: @pipeline, link_to_commit: true diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index d9efe81701f..ea618263a4a 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -23,7 +23,8 @@ preparing: "{{status}} build", normal: "Build {{status}}" }, - builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" + builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; if (typeof merge_request_widget !== 'undefined') { diff --git a/config/routes.rb b/config/routes.rb index 21f3585bacd..84a89200111 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -704,6 +704,7 @@ Rails.application.routes.draw do get :commits get :diffs get :builds + get :pipelines get :merge_check post :merge post :cancel_merge_when_build_succeeds -- GitLab