Commit 8c911468 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'zj-real-time-pipelines' into 'master'

Real time pipeline show action

Closes #25226

See merge request !10777
parents 1186dcab c17e6a6c
...@@ -8,6 +8,8 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -8,6 +8,8 @@ class Projects::PipelinesController < Projects::ApplicationController
wrap_parameters Ci::Pipeline wrap_parameters Ci::Pipeline
POLLING_INTERVAL = 10_000
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder @pipelines = PipelinesFinder
...@@ -31,7 +33,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -31,7 +33,7 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000) Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
...@@ -57,15 +59,25 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -57,15 +59,25 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipeline = Ci::CreatePipelineService @pipeline = Ci::CreatePipelineService
.new(project, current_user, create_params) .new(project, current_user, create_params)
.execute(ignore_skip_ci: true, save_on_errors: false) .execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted?
if @pipeline.persisted?
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
else
render 'new' render 'new'
return
end end
redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline)
end end
def show def show
respond_to do |format|
format.html
format.json do
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent(@pipeline, grouped: true)
end
end
end end
def builds def builds
......
module Ci
##
# This domain model is a representation of a group of jobs that are related
# to each other, like `rspec 0 1`, `rspec 0 2`.
#
# It is not persisted in the database.
#
class Group
include StaticModel
attr_reader :stage, :name, :jobs
delegate :size, to: :jobs
def initialize(stage, name:, jobs:)
@stage = stage
@name = name
@jobs = jobs
end
def status
@status ||= commit_statuses.status
end
def detailed_status(current_user)
if jobs.one?
jobs.first.detailed_status(current_user)
else
Gitlab::Ci::Status::Group::Factory
.new(self, current_user).fabricate!
end
end
private
def commit_statuses
@commit_statuses ||= CommitStatus.where(id: jobs.map(&:id))
end
end
end
...@@ -15,6 +15,14 @@ module Ci ...@@ -15,6 +15,14 @@ module Ci
@warnings = warnings @warnings = warnings
end end
def groups
@groups ||= statuses.ordered.latest
.sort_by(&:sortable_name).group_by(&:group_name)
.map do |group_name, grouped_statuses|
Ci::Group.new(self, name: group_name, jobs: grouped_statuses)
end
end
def to_param def to_param
name name
end end
......
class JobGroupEntity < Grape::Entity
include RequestAwareEntity
expose :name
expose :size
expose :detailed_status, as: :status, with: StatusEntity
expose :jobs, with: BuildEntity
private
alias_method :group, :object
def detailed_status
group.detailed_status(request.user)
end
end
...@@ -7,9 +7,11 @@ class StageEntity < Grape::Entity ...@@ -7,9 +7,11 @@ class StageEntity < Grape::Entity
"#{stage.name}: #{detailed_status.label}" "#{stage.name}: #{detailed_status.label}"
end end
expose :detailed_status, expose :groups,
as: :status, if: -> (_, opts) { opts[:grouped] },
with: StatusEntity with: JobGroupEntity
expose :detailed_status, as: :status, with: StatusEntity
expose :path do |stage| expose :path do |stage|
namespace_project_pipeline_path( namespace_project_pipeline_path(
......
...@@ -12,4 +12,11 @@ class StatusEntity < Grape::Entity ...@@ -12,4 +12,11 @@ class StatusEntity < Grape::Entity
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico")) ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end end
expose :action, if: -> (status, _) { status.has_action? } do
expose :action_icon, as: :icon
expose :action_title, as: :title
expose :action_path, as: :path
expose :action_method, as: :method
end
end end
---
title: Pipeline view updates in near real time
merge_request: 10777
author:
module Gitlab
module Ci
module Status
module Group
module Common
def has_details?
false
end
def details_path
nil
end
def has_action?
false
end
end
end
end
end
end
module Gitlab
module Ci
module Status
module Group
class Factory < Status::Factory
def self.common_helpers
Status::Group::Common
end
end
end
end
end
end
...@@ -36,7 +36,11 @@ module Gitlab ...@@ -36,7 +36,11 @@ module Gitlab
Gitlab::EtagCaching::Router::Route.new( Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines\.json\z), %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines\.json\z),
'project_pipelines' 'project_pipelines'
) ),
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS})).*/pipelines/\d+\.json\z),
'project_pipeline'
),
].freeze ].freeze
def self.match(env) def self.match(env)
......
require 'spec_helper' require 'spec_helper'
describe Projects::PipelinesController do describe Projects::PipelinesController do
include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
...@@ -24,6 +26,7 @@ describe Projects::PipelinesController do ...@@ -24,6 +26,7 @@ describe Projects::PipelinesController do
it 'returns JSON with serialized pipelines' do it 'returns JSON with serialized pipelines' do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(response).to match_response_schema('pipeline')
expect(json_response).to include('pipelines') expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 4 expect(json_response['pipelines'].count).to eq 4
...@@ -34,6 +37,34 @@ describe Projects::PipelinesController do ...@@ -34,6 +37,34 @@ describe Projects::PipelinesController do
end end
end end
describe 'GET show JSON' do
let!(:pipeline) { create(:ci_pipeline_with_one_job, project: project) }
it 'returns the pipeline' do
get_pipeline_json
expect(response).to have_http_status(:ok)
expect(json_response).not_to be_an(Array)
expect(json_response['id']).to be(pipeline.id)
expect(json_response['details']).to have_key 'stages'
end
context 'when the pipeline has multiple jobs' do
it 'does not perform N + 1 queries' do
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
create(:ci_build, pipeline: pipeline)
# The plus 2 is needed to group and sort
expect { get_pipeline_json }.not_to exceed_query_limit(control_count + 2)
end
end
def get_pipeline_json
get :show, namespace_id: project.namespace, project_id: project, id: pipeline, format: :json
end
end
describe 'GET stages.json' do describe 'GET stages.json' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
......
This diff is collapsed.
require 'spec_helper'
describe Gitlab::Ci::Status::Group::Common do
subject do
Gitlab::Ci::Status::Core.new(double, double)
.extend(described_class)
end
it 'does not have action' do
expect(subject).not_to have_action
end
it 'has details' do
expect(subject).not_to have_details
end
it 'has no details_path' do
expect(subject.details_path).to be_falsy
end
end
require 'spec_helper'
describe Gitlab::Ci::Status::Group::Factory do
it 'inherits from the core factory' do
expect(described_class)
.to be < Gitlab::Ci::Status::Factory
end
it 'exposes group helpers' do
expect(described_class.common_helpers)
.to eq Gitlab::Ci::Status::Group::Common
end
end
require 'spec_helper'
describe Ci::Group, models: true do
subject do
described_class.new('test', name: 'rspec', jobs: jobs)
end
let!(:jobs) { build_list(:ci_build, 1, :success) }
it { is_expected.to include_module(StaticModel) }
it { is_expected.to respond_to(:stage) }
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:jobs) }
it { is_expected.to respond_to(:status) }
describe '#size' do
it 'returns the number of statuses in the group' do
expect(subject.size).to eq(1)
end
end
describe '#detailed_status' do
context 'when there is only one item in the group' do
it 'calls the status from the object itself' do
expect(jobs.first).to receive(:detailed_status)
expect(subject.detailed_status(double(:user)))
end
end
context 'when there are more than one commit status in the group' do
let(:jobs) do
[create(:ci_build, :failed),
create(:ci_build, :success)]
end
it 'fabricates a new detailed status object' do
expect(subject.detailed_status(double(:user)))
.to be_a(Gitlab::Ci::Status::Failed)
end
end
end
end
...@@ -28,6 +28,35 @@ describe Ci::Stage, models: true do ...@@ -28,6 +28,35 @@ describe Ci::Stage, models: true do
end end
end end
describe '#groups' do
before do
create_job(:ci_build, name: 'rspec 0 2')
create_job(:ci_build, name: 'rspec 0 1')
create_job(:ci_build, name: 'spinach 0 1')
create_job(:commit_status, name: 'aaaaa')
end
it 'returns an array of three groups' do
expect(stage.groups).to be_a Array
expect(stage.groups).to all(be_a Ci::Group)
expect(stage.groups.size).to eq 3
end
it 'returns groups with correctly ordered statuses' do
expect(stage.groups.first.jobs.map(&:name))
.to eq ['aaaaa']
expect(stage.groups.second.jobs.map(&:name))
.to eq ['rspec 0 1', 'rspec 0 2']
expect(stage.groups.third.jobs.map(&:name))
.to eq ['spinach 0 1']
end
it 'returns groups with correct names' do
expect(stage.groups.map(&:name))
.to eq %w[aaaaa rspec spinach]
end
end
describe '#statuses_count' do describe '#statuses_count' do
before do before do
create_job(:ci_build) create_job(:ci_build)
...@@ -223,7 +252,7 @@ describe Ci::Stage, models: true do ...@@ -223,7 +252,7 @@ describe Ci::Stage, models: true do
end end
end end
def create_job(type, status: 'success', stage: stage_name) def create_job(type, status: 'success', stage: stage_name, **opts)
create(type, pipeline: pipeline, stage: stage, status: status) create(type, pipeline: pipeline, stage: stage, status: status, **opts)
end end
end end
...@@ -47,5 +47,13 @@ describe StageEntity do ...@@ -47,5 +47,13 @@ describe StageEntity do
it 'contains stage title' do it 'contains stage title' do
expect(subject[:title]).to eq 'test: passed' expect(subject[:title]).to eq 'test: passed'
end end
context 'when the jobs should be grouped' do
let(:entity) { described_class.new(stage, request: request, grouped: true) }
it 'exposes the group key' do
expect(subject).to include :groups
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment