From 6f6119b7389ef7b5e13f2800611d5d7a806e41a5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 10 Nov 2016 15:32:23 +0100 Subject: [PATCH] Support pipelines API Pass `updated_at` to get only incremental changes since last update --- .../projects/pipelines_controller.rb | 16 ++++ app/models/ci/pipeline.rb | 29 +++++-- app/models/commit_status.rb | 11 +-- app/serializers/pipeline_action_entity.rb | 14 ++++ app/serializers/pipeline_artifact_entity.rb | 14 ++++ app/serializers/pipeline_entity.rb | 84 +++++++++++++++++++ app/serializers/pipeline_serializer.rb | 3 + app/serializers/pipeline_stage_entity.rb | 19 +++++ app/serializers/request_aware_entity.rb | 8 ++ .../projects/ci/pipelines/_pipeline.html.haml | 7 +- app/views/projects/commit/_pipeline.html.haml | 2 +- .../projects/commit/_pipelines_list.haml | 2 +- app/views/projects/pipelines/index.html.haml | 3 +- 13 files changed, 190 insertions(+), 22 deletions(-) create mode 100644 app/serializers/pipeline_action_entity.rb create mode 100644 app/serializers/pipeline_artifact_entity.rb create mode 100644 app/serializers/pipeline_entity.rb create mode 100644 app/serializers/pipeline_serializer.rb create mode 100644 app/serializers/pipeline_stage_entity.rb diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 371cc3787fb..0b3503c6848 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -11,6 +11,22 @@ class Projects::PipelinesController < Projects::ApplicationController @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count @pipelines_count = PipelinesFinder.new(project).execute.count + @last_updated = params[:updated_at] + + respond_to do |format| + format.html + format.json do + render json: { + pipelines: PipelineSerializer.new(project: @project). + represent(@pipelines, current_user: current_user, last_updated: @last_updated), + updated_at: Time.now, + count: { + all: @pipelines_count, + running_or_pending: @running_or_pending_count + } + } + end + end end def new diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3fee6c18770..37c2c86e3a3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -98,19 +98,38 @@ module Ci sha[0...8] end - def self.stages - # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries - CommitStatus.where(pipeline: pluck(:id)).stages - end - def self.total_duration where.not(duration: nil).sum(:duration) end + def stages + statuses.group('stage').select(:stage) + .order('max(stage_idx)') + end + + def stages_with_statuses + status_sql = statuses.latest.where('stage=sg.stage').status_sql + + stages_with_statuses = CommitStatus.from(self.stages, :sg). + pluck('sg.stage', status_sql) + + stages_with_statuses.map do |stage| + OpenStruct.new( + name: stage.first, + status: stage.last, + pipeline: self + ) + end + end + def stages_with_latest_statuses statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage) end + def artifacts + builds.latest.with_artifacts_not_expired + end + def project_id project.id end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index d159fc6c5c7..c0b7f44fa9e 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -119,16 +119,7 @@ class CommitStatus < ActiveRecord::Base def self.stages # We group by stage name, but order stages by theirs' index - unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') - end - - def self.stages_status - # We execute subquery for each stage to calculate a stage status - statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) - statuses.inject({}) do |h, k| - h[k.first] = k.last - h - end + unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').select('sg.stage') end def failed_but_allowed? diff --git a/app/serializers/pipeline_action_entity.rb b/app/serializers/pipeline_action_entity.rb new file mode 100644 index 00000000000..d45341f09d2 --- /dev/null +++ b/app/serializers/pipeline_action_entity.rb @@ -0,0 +1,14 @@ +class PipelineActionEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |build| + build.name.humanize + end + + expose :url do |build| + play_namespace_project_build_path( + pipeline.project.namespace, + pipeline.project, + build) + end +end diff --git a/app/serializers/pipeline_artifact_entity.rb b/app/serializers/pipeline_artifact_entity.rb new file mode 100644 index 00000000000..01393dbea2d --- /dev/null +++ b/app/serializers/pipeline_artifact_entity.rb @@ -0,0 +1,14 @@ +class PipelineArtifactEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |build| + build.name + end + + expose :url do |build| + download_namespace_project_build_artifacts_path( + pipeline.project.namespace, + pipeline.project, + build) + end +end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb new file mode 100644 index 00000000000..7afce8fd15b --- /dev/null +++ b/app/serializers/pipeline_entity.rb @@ -0,0 +1,84 @@ +class PipelineEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :user, if: -> (pipeline, opts) { created?(pipeline, opts) }, using: UserEntity + + expose :status + expose :duration + expose :finished_at + expose :stages_with_statuses, as: :stages, if: -> (pipeline, opts) { updated?(pipeline, opts) }, using: PipelineStageEntity + expose :artifacts, if: -> (pipeline, opts) { updated?(pipeline, opts) }, using: PipelineArtifactEntity + expose :manual_actions, if: -> (pipeline, opts) { updated?(pipeline, opts) }, using: PipelineActionEntity + + expose :flags, if: -> (pipeline, opts) { created?(pipeline, opts) } do + expose :latest?, as: :latest + expose :triggered?, as: :triggered + expose :yaml_errors?, as: :yaml_errors do |pipeline| + pipeline.yaml_errors.present? + end + expose :stuck?, as: :stuck do |pipeline| + pipeline.builds.any?(&:stuck?) + end + end + + expose :ref, if: -> (pipeline, opts) { created?(pipeline, opts) } do + expose :name do |pipeline| + pipeline.ref + end + + expose :ref_url do |pipeline| + namespace_project_tree_url( + pipeline.project.namespace, + pipeline.project, + id: pipeline.ref) + end + + expose :tag? + end + + expose :commit, if: -> (pipeline, opts) { created?(pipeline, opts) } do + expose :short_sha + + expose :sha_url do |pipeline| + namespace_project_commit_path( + pipeline.project.namespace, + pipeline.project, + pipeline.sha) + end + + expose :title do |pipeline| + pipeline.commit.try(:title) + end + + expose :author, using: UserEntity do |pipeline| + pipeline.commit.try(:author) + end + end + + expose :retry_url, if: -> (pipeline, opts) { updated?(pipeline, opts) } do |pipeline| + can?(current_user, :update_pipeline, pipeline.project) && + pipeline.retryable? && + retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) + end + + expose :cancel_url, if: -> (pipeline, opts) { updated?(pipeline, opts) } do |pipeline| + can?(current_user, :update_pipeline, pipeline.project) && + pipeline.cancelable? && + cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) + end + + private + + def last_updated(opts) + opts.fetch(:last_updated) + end + + def created?(pipeline, opts) + !last_updated(opts) || pipeline.created_at > last_updated(opts) + end + + def updated?(pipeline, opts) + !last_updated(opts) || pipeline.updated_at > last_updated(opts) + end +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb new file mode 100644 index 00000000000..f7abbec7d45 --- /dev/null +++ b/app/serializers/pipeline_serializer.rb @@ -0,0 +1,3 @@ +class PipelineSerializer < BaseSerializer + entity PipelineEntity +end diff --git a/app/serializers/pipeline_stage_entity.rb b/app/serializers/pipeline_stage_entity.rb new file mode 100644 index 00000000000..230ef8a22da --- /dev/null +++ b/app/serializers/pipeline_stage_entity.rb @@ -0,0 +1,19 @@ +class PipelineStageEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |stage| + stage.name + end + + expose :status do |stage| + stage.status || 'not found' + end + + expose :url do |stage| + namespace_project_pipeline_path( + stage.pipeline.project.namespace, + stage.pipeline.project, + stage.pipeline.id, + anchor: stage.name) + end +end diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb index ff8c1142abc..7a096d9d5a8 100644 --- a/app/serializers/request_aware_entity.rb +++ b/app/serializers/request_aware_entity.rb @@ -8,4 +8,12 @@ module RequestAwareEntity def request @options.fetch(:request) end + + def current_user + @options.fetch(:current_user) + end + + def can?(object, action, subject) + Ability.allowed?(object, action, subject) + end end diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 2a2d24be736..50817f28d78 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -43,9 +43,10 @@ - stages_status = pipeline.statuses.latest.stages_status %td.stage-cell - - stages.each do |stage| - - status = stages_status[stage] - - tooltip = "#{stage.titleize}: #{status || 'not found'}" + - pipeline.statuses.latest.stages_status.each do |stage| + - name = stage.first + - status = stage.last + - tooltip = "#{name.titleize}: #{status || 'not found'}" - if status .stage-container = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index d6916fb7f1a..516893fc6e5 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -66,5 +66,5 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.statuses.relevant.stages.each do |stage| + - pipeline.stages.each do |stage| = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 2dc91a9b762..7f42fde0fea 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -12,4 +12,4 @@ %th Stages %th %th - = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false + = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 6f70b239826..4f9fb699abc 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -37,7 +37,6 @@ %span CI Lint %div.content-list.pipelines{"data-project-id": "#{@project.id}", "data-count": "#{@pipelines_count}"} - - stages = @pipelines.stages - if @pipelines.blank? %div .nothing-here-block No pipelines to show @@ -52,7 +51,7 @@ %th %th.hidden-xs - = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages + = render @pipelines, commit_sha: true, stage: true, allow_retry: true = paginate @pipelines, theme: 'gitlab' - else .vue-pipelines-index -- GitLab