Commit 743ab827 authored by Max Woolf's avatar Max Woolf

Merge branch '329208-project-level-value-stream' into 'master'

Expose default VSA stages on project level

See merge request gitlab-org/gitlab!60925
parents c83695a3 cfbd9169
# frozen_string_literal: true
class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController
respond_to :json
feature_category :planning_analytics
before_action :authorize_read_cycle_analytics!
before_action :only_default_value_stream_is_allowed!
def index
result = list_service.execute
if result.success?
render json: cycle_analytics_configuration(result.payload[:stages])
else
render json: { message: result.message }, status: result.http_status
end
end
private
def only_default_value_stream_is_allowed!
render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME
end
def value_stream
Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)
end
def list_params
{ value_stream: value_stream }
end
def list_service
Analytics::CycleAnalytics::Stages::ListService.new(parent: @project, current_user: current_user, params: list_params)
end
def cycle_analytics_configuration(stages)
stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) }
Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters)
end
end
# frozen_string_literal: true
class Projects::Analytics::CycleAnalytics::ValueStreamsController < Projects::ApplicationController
respond_to :json
feature_category :planning_analytics
before_action :authorize_read_cycle_analytics!
def index
# FOSS users can only see the default value stream
value_streams = [Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)]
render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams)
end
end
...@@ -3,29 +3,19 @@ ...@@ -3,29 +3,19 @@
module Analytics module Analytics
module CycleAnalytics module CycleAnalytics
class StageFinder class StageFinder
NUMBERS_ONLY = /\A\d+\z/.freeze
def initialize(parent:, stage_id:) def initialize(parent:, stage_id:)
@parent = parent @parent = parent
@stage_id = stage_id @stage_id = stage_id
end end
def execute def execute
if in_memory_default_stage? build_in_memory_stage_by_name
build_in_memory_stage_by_name
else
parent.cycle_analytics_stages.find(stage_id)
end
end end
private private
attr_reader :parent, :stage_id attr_reader :parent, :stage_id
def in_memory_default_stage?
!NUMBERS_ONLY.match?(stage_id.to_s)
end
def build_in_memory_stage_by_name def build_in_memory_stage_by_name
parent.cycle_analytics_stages.build(find_in_memory_stage) parent.cycle_analytics_stages.build(find_in_memory_stage)
end end
...@@ -43,3 +33,5 @@ module Analytics ...@@ -43,3 +33,5 @@ module Analytics
end end
end end
end end
Analytics::CycleAnalytics::StageFinder.prepend_mod_with('Analytics::CycleAnalytics::StageFinder')
...@@ -7,10 +7,13 @@ module Analytics ...@@ -7,10 +7,13 @@ module Analytics
validates :project, presence: true validates :project, presence: true
belongs_to :project belongs_to :project
belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', foreign_key: :project_value_stream_id
alias_attribute :parent, :project alias_attribute :parent, :project
alias_attribute :parent_id, :project_id alias_attribute :parent_id, :project_id
alias_attribute :value_stream_id, :project_value_stream_id
delegate :group, to: :project delegate :group, to: :project
validate :validate_project_group_for_label_events, if: -> { start_event_label_based? || end_event_label_based? } validate :validate_project_group_for_label_events, if: -> { start_event_label_based? || end_event_label_based? }
......
# frozen_string_literal: true
class Analytics::CycleAnalytics::ProjectValueStream < ApplicationRecord
belongs_to :project
has_many :stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
validates :project, :name, presence: true
validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :project_id }
def custom?
false
end
def stages
[]
end
def self.build_default_value_stream(project)
new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, project: project)
end
end
...@@ -27,6 +27,7 @@ module Analytics ...@@ -27,6 +27,7 @@ module Analytics
scope :default_stages, -> { where(custom: false) } scope :default_stages, -> { where(custom: false) }
scope :ordered, -> { order(:relative_position, :id) } scope :ordered, -> { order(:relative_position, :id) }
scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered } scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered }
scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) }
end end
def parent=(_) def parent=(_)
......
...@@ -335,7 +335,8 @@ class Project < ApplicationRecord ...@@ -335,7 +335,8 @@ class Project < ApplicationRecord
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project has_many :remote_mirrors, inverse_of: :project
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage' has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :project
has_many :value_streams, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', inverse_of: :project
has_many :external_pull_requests, inverse_of: :project has_many :external_pull_requests, inverse_of: :project
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Analytics module Analytics
module CycleAnalytics module CycleAnalytics
class GroupValueStreamEntity < Grape::Entity class ValueStreamEntity < Grape::Entity
expose :name expose :name
expose :id expose :id
expose :is_custom do |object| expose :is_custom do |object|
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
module Analytics module Analytics
module CycleAnalytics module CycleAnalytics
class GroupValueStreamSerializer < BaseSerializer class ValueStreamSerializer < BaseSerializer
entity ::Analytics::CycleAnalytics::GroupValueStreamEntity entity ::Analytics::CycleAnalytics::ValueStreamEntity
end end
end end
end end
...@@ -26,29 +26,10 @@ module Analytics ...@@ -26,29 +26,10 @@ module Analytics
ServiceResponse.success(payload: { stage: stage }, http_status: http_status) ServiceResponse.success(payload: { stage: stage }, http_status: http_status)
end end
def error(stage)
ServiceResponse.error(message: 'Invalid parameters', payload: { errors: stage.errors }, http_status: :unprocessable_entity)
end
def not_found
ServiceResponse.error(message: 'Stage not found', payload: {}, http_status: :not_found)
end
def forbidden def forbidden
ServiceResponse.error(message: 'Forbidden', payload: {}, http_status: :forbidden) ServiceResponse.error(message: 'Forbidden', payload: {}, http_status: :forbidden)
end end
def persist_default_stages!
persisted_default_stages = parent.cycle_analytics_stages.by_value_stream(value_stream).default_stages
# make sure that we persist default stages only once
stages_to_persist = build_default_stages.select do |new_default_stage|
!persisted_default_stages.find { |s| s.name.eql?(new_default_stage.name) }
end
stages_to_persist.each(&:save!)
end
def build_default_stages def build_default_stages
Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |stage_params| Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |stage_params|
parent.cycle_analytics_stages.build(stage_params.merge(value_stream: value_stream)) parent.cycle_analytics_stages.build(stage_params.merge(value_stream: value_stream))
...@@ -56,9 +37,11 @@ module Analytics ...@@ -56,9 +37,11 @@ module Analytics
end end
def value_stream def value_stream
@value_stream ||= params[:value_stream] || parent.value_streams.safe_find_or_create_by!(name: DEFAULT_VALUE_STREAM_NAME) @value_stream ||= params[:value_stream]
end end
end end
end end
end end
end end
Analytics::CycleAnalytics::Stages::BaseService.prepend_mod_with('Analytics::CycleAnalytics::Stages::BaseService')
...@@ -3,32 +3,25 @@ ...@@ -3,32 +3,25 @@
module Analytics module Analytics
module CycleAnalytics module CycleAnalytics
module Stages module Stages
class ListService < BaseService class ListService < Analytics::CycleAnalytics::Stages::BaseService
extend ::Gitlab::Utils::Override
def execute def execute
return forbidden unless can?(current_user, :read_group_cycle_analytics, parent) return forbidden unless allowed?
success(persisted_stages.presence || build_default_stages) success(build_default_stages)
end end
private private
def success(stages) def allowed?
ServiceResponse.success(payload: { stages: stages }) can?(current_user, :read_cycle_analytics, parent)
end
def persisted_stages
scope = parent.cycle_analytics_stages
scope = scope.by_value_stream(params[:value_stream]) if params[:value_stream]
scope.for_list
end end
override :value_stream def success(stages)
def value_stream ServiceResponse.success(payload: { stages: stages })
@value_stream ||= (params[:value_stream] || parent.value_streams.new(name: DEFAULT_VALUE_STREAM_NAME))
end end
end end
end end
end end
end end
Analytics::CycleAnalytics::Stages::ListService.prepend_mod_with('Analytics::CycleAnalytics::Stages::ListService')
---
title: Create database structure to support project value streams
merge_request: 60925
author:
type: other
...@@ -267,6 +267,15 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -267,6 +267,15 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
get '/cycle_analytics', to: redirect('%{namespace_id}/%{project_id}/-/value_stream_analytics') get '/cycle_analytics', to: redirect('%{namespace_id}/%{project_id}/-/value_stream_analytics')
namespace :analytics do
resource :cycle_analytics, only: :show, path: 'value_stream_analytics'
scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do
resources :value_streams, only: [:index] do
resources :stages, only: [:index]
end
end
end
concerns :clusterable concerns :clusterable
namespace :serverless do namespace :serverless do
......
# frozen_string_literal: true
class CreateProjectValueStreams < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_analytics_ca_project_value_streams_on_project_id_and_name'
def up
create_table_with_constraints :analytics_cycle_analytics_project_value_streams do |t|
t.timestamps_with_timezone
t.references(:project,
null: false,
index: false,
foreign_key: { to_table: :projects, on_delete: :cascade }
)
t.text :name, null: false
t.index [:project_id, :name], unique: true, name: INDEX_NAME
t.text_limit :name, 100
end
end
def down
with_lock_retries do
drop_table :analytics_cycle_analytics_project_value_streams
end
end
end
# frozen_string_literal: true
class AddProjectValueStreamIdToProjectStages < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
INDEX_NAME = 'index_analytics_ca_project_stages_on_value_stream_id'
class ProjectValueStream < ActiveRecord::Base
self.table_name = 'analytics_cycle_analytics_project_stages'
include EachBatch
end
def up
ProjectValueStream.reset_column_information
# The table was never used, there is no user-facing code that modifies the table, it should be empty.
# Since there is no functionality present that depends on this data, it's safe to delete the rows.
ProjectValueStream.each_batch(of: 100) do |relation|
relation.delete_all
end
transaction do
add_reference :analytics_cycle_analytics_project_stages, :project_value_stream, null: false, index: { name: INDEX_NAME }, foreign_key: { on_delete: :cascade, to_table: :analytics_cycle_analytics_project_value_streams }, type: :bigint # rubocop: disable Migration/AddReference, Rails/NotNullColumn
end
end
def down
remove_reference :analytics_cycle_analytics_project_stages, :project_value_stream
end
end
de8bf6c02589bf308914d43e5cd44dae91d3bbabcdaafcebdb96fba0a09b20bc
\ No newline at end of file
2fdcb66e511d8322ea8fc4de66ecce859f8e91b2a9da22336281a1e784d9b4a5
\ No newline at end of file
...@@ -9051,7 +9051,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages ( ...@@ -9051,7 +9051,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages (
end_event_label_id bigint, end_event_label_id bigint,
hidden boolean DEFAULT false NOT NULL, hidden boolean DEFAULT false NOT NULL,
custom boolean DEFAULT true NOT NULL, custom boolean DEFAULT true NOT NULL,
name character varying(255) NOT NULL name character varying(255) NOT NULL,
project_value_stream_id bigint NOT NULL
); );
CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq
...@@ -9063,6 +9064,24 @@ CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq ...@@ -9063,6 +9064,24 @@ CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq
ALTER SEQUENCE analytics_cycle_analytics_project_stages_id_seq OWNED BY analytics_cycle_analytics_project_stages.id; ALTER SEQUENCE analytics_cycle_analytics_project_stages_id_seq OWNED BY analytics_cycle_analytics_project_stages.id;
CREATE TABLE analytics_cycle_analytics_project_value_streams (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
project_id bigint NOT NULL,
name text NOT NULL,
CONSTRAINT check_9b1970a898 CHECK ((char_length(name) <= 100))
);
CREATE SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq OWNED BY analytics_cycle_analytics_project_value_streams.id;
CREATE TABLE analytics_devops_adoption_segment_selections ( CREATE TABLE analytics_devops_adoption_segment_selections (
id bigint NOT NULL, id bigint NOT NULL,
segment_id bigint NOT NULL, segment_id bigint NOT NULL,
...@@ -19369,6 +19388,8 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams ALTER COLUMN id S ...@@ -19369,6 +19388,8 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams ALTER COLUMN id S
ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_stages_id_seq'::regclass); ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_stages_id_seq'::regclass);
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_value_streams_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_segment_selections ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segment_selections_id_seq'::regclass); ALTER TABLE ONLY analytics_devops_adoption_segment_selections ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segment_selections_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass); ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass);
...@@ -20449,6 +20470,9 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams ...@@ -20449,6 +20470,9 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams
ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ADD CONSTRAINT analytics_cycle_analytics_project_stages_pkey PRIMARY KEY (id); ADD CONSTRAINT analytics_cycle_analytics_project_stages_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams
ADD CONSTRAINT analytics_cycle_analytics_project_value_streams_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_devops_adoption_segment_selections ALTER TABLE ONLY analytics_devops_adoption_segment_selections
ADD CONSTRAINT analytics_devops_adoption_segment_selections_pkey PRIMARY KEY (id); ADD CONSTRAINT analytics_devops_adoption_segment_selections_pkey PRIMARY KEY (id);
...@@ -22262,6 +22286,10 @@ CREATE INDEX index_analytics_ca_project_stages_on_relative_position ON analytics ...@@ -22262,6 +22286,10 @@ CREATE INDEX index_analytics_ca_project_stages_on_relative_position ON analytics
CREATE INDEX index_analytics_ca_project_stages_on_start_event_label_id ON analytics_cycle_analytics_project_stages USING btree (start_event_label_id); CREATE INDEX index_analytics_ca_project_stages_on_start_event_label_id ON analytics_cycle_analytics_project_stages USING btree (start_event_label_id);
CREATE INDEX index_analytics_ca_project_stages_on_value_stream_id ON analytics_cycle_analytics_project_stages USING btree (project_value_stream_id);
CREATE UNIQUE INDEX index_analytics_ca_project_value_streams_on_project_id_and_name ON analytics_cycle_analytics_project_value_streams USING btree (project_id, name);
CREATE INDEX index_analytics_cycle_analytics_group_stages_custom_only ON analytics_cycle_analytics_group_stages USING btree (id) WHERE (custom = true); CREATE INDEX index_analytics_cycle_analytics_group_stages_custom_only ON analytics_cycle_analytics_group_stages USING btree (id) WHERE (custom = true);
CREATE UNIQUE INDEX index_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id); CREATE UNIQUE INDEX index_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id);
...@@ -26519,6 +26547,9 @@ ALTER TABLE ONLY namespace_admin_notes ...@@ -26519,6 +26547,9 @@ ALTER TABLE ONLY namespace_admin_notes
ALTER TABLE ONLY web_hook_logs_archived ALTER TABLE ONLY web_hook_logs_archived
ADD CONSTRAINT fk_rails_666826e111 FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_666826e111 FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams
ADD CONSTRAINT fk_rails_669f4ba293 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY jira_imports ALTER TABLE ONLY jira_imports
ADD CONSTRAINT fk_rails_675d38c03b FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE SET NULL; ADD CONSTRAINT fk_rails_675d38c03b FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE SET NULL;
...@@ -26621,6 +26652,9 @@ ALTER TABLE ONLY ci_subscriptions_projects ...@@ -26621,6 +26652,9 @@ ALTER TABLE ONLY ci_subscriptions_projects
ALTER TABLE ONLY terraform_states ALTER TABLE ONLY terraform_states
ADD CONSTRAINT fk_rails_78f54ca485 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_78f54ca485 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ADD CONSTRAINT fk_rails_796a7dbc9c FOREIGN KEY (project_value_stream_id) REFERENCES analytics_cycle_analytics_project_value_streams(id) ON DELETE CASCADE;
ALTER TABLE ONLY software_license_policies ALTER TABLE ONLY software_license_policies
ADD CONSTRAINT fk_rails_7a7a2a92de FOREIGN KEY (software_license_id) REFERENCES software_licenses(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_7a7a2a92de FOREIGN KEY (software_license_id) REFERENCES software_licenses(id) ON DELETE CASCADE;
...@@ -9,7 +9,7 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Groups::Analyt ...@@ -9,7 +9,7 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Groups::Analyt
end end
def index def index
render json: Analytics::CycleAnalytics::GroupValueStreamSerializer.new.represent(value_streams) render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams)
end end
def create def create
...@@ -75,7 +75,7 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Groups::Analyt ...@@ -75,7 +75,7 @@ class Groups::Analytics::CycleAnalytics::ValueStreamsController < Groups::Analyt
end end
def serialize_value_stream(result) def serialize_value_stream(result)
Analytics::CycleAnalytics::GroupValueStreamSerializer.new.represent(result.payload[:value_stream]) Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(result.payload[:value_stream])
end end
def serialize_value_stream_error(result) def serialize_value_stream_error(result)
......
# frozen_string_literal: true
module EE
module Analytics
module CycleAnalytics
module StageFinder
extend ::Gitlab::Utils::Override
NUMBERS_ONLY = /\A\d+\z/.freeze
def initialize(parent:, stage_id:)
@parent = parent
@stage_id = stage_id
end
override :execute
def execute
return super if in_memory_default_stage?
parent.cycle_analytics_stages.find(stage_id)
end
private
attr_reader :parent, :stage_id
def in_memory_default_stage?
!NUMBERS_ONLY.match?(stage_id.to_s)
end
end
end
end
end
...@@ -13,7 +13,7 @@ module Analytics ...@@ -13,7 +13,7 @@ module Analytics
alias_attribute :parent, :group alias_attribute :parent, :group
alias_attribute :parent_id, :group_id alias_attribute :parent_id, :group_id
scope :by_value_stream, -> (value_stream) { where(group_value_stream_id: value_stream.id) } alias_attribute :value_stream_id, :group_value_stream_id
def self.relative_positioning_query_base(stage) def self.relative_positioning_query_base(stage)
where(group_id: stage.group_id) where(group_id: stage.group_id)
......
# frozen_string_literal: true
module EE
module Analytics
module CycleAnalytics
module Stages
module BaseService
extend ::Gitlab::Utils::Override
private
def error(stage)
ServiceResponse.error(message: 'Invalid parameters', payload: { errors: stage.errors }, http_status: :unprocessable_entity)
end
def not_found
ServiceResponse.error(message: 'Stage not found', http_status: :not_found)
end
def persist_default_stages!
persisted_default_stages = parent.cycle_analytics_stages.by_value_stream(value_stream).default_stages
# make sure that we persist default stages only once
stages_to_persist = build_default_stages.select do |new_default_stage|
!persisted_default_stages.find { |s| s.name.eql?(new_default_stage.name) }
end
stages_to_persist.each(&:save!)
end
override :value_stream
def value_stream
@value_stream ||= params[:value_stream] || parent.value_streams.safe_find_or_create_by!(name: ::Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME)
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Analytics
module CycleAnalytics
module Stages
module ListService
extend ::Gitlab::Utils::Override
override :value_stream
def execute
return forbidden unless allowed?
success(persisted_stages.presence || build_default_stages)
end
private
override :allowed?
def allowed?
return super unless parent.is_a?(Group)
can?(current_user, :read_group_cycle_analytics, parent)
end
def persisted_stages
scope = parent.cycle_analytics_stages
scope = scope.by_value_stream(params[:value_stream]) if params[:value_stream]
scope.for_list
end
override :value_stream
def value_stream
@value_stream ||= (params[:value_stream] || parent.value_streams.new(name: ::Analytics::CycleAnalytics::Stages::ListService::DEFAULT_VALUE_STREAM_NAME))
end
end
end
end
end
end
...@@ -15,6 +15,7 @@ RSpec.describe Analytics::CycleAnalytics::GroupStage do ...@@ -15,6 +15,7 @@ RSpec.describe Analytics::CycleAnalytics::GroupStage do
end end
it_behaves_like 'value stream analytics stage' do it_behaves_like 'value stream analytics stage' do
let(:factory) { :cycle_analytics_group_stage }
let(:parent) { create(:group) } let(:parent) { create(:group) }
let(:parent_name) { :group } let(:parent_name) { :group }
end end
......
...@@ -3,15 +3,8 @@ ...@@ -3,15 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::StageEntity do RSpec.describe Analytics::CycleAnalytics::StageEntity do
let(:stage) { build(:cycle_analytics_group_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
subject(:entity_json) { described_class.new(Analytics::CycleAnalytics::StagePresenter.new(stage)).as_json } subject(:entity_json) { described_class.new(Analytics::CycleAnalytics::StagePresenter.new(stage)).as_json }
it 'exposes start and end event descriptions' do
expect(entity_json).to have_key(:start_event_html_description)
expect(entity_json).to have_key(:end_event_html_description)
end
context 'when label based event is given' do context 'when label based event is given' do
let(:label) { create(:group_label, title: 'test label') } let(:label) { create(:group_label, title: 'test label') }
let(:stage) { build(:cycle_analytics_group_stage, group: label.group, start_event_label: label, start_event_identifier: :merge_request_label_added, end_event_identifier: :merge_request_merged) } let(:stage) { build(:cycle_analytics_group_stage, group: label.group, start_event_label: label, start_event_identifier: :merge_request_label_added, end_event_identifier: :merge_request_merged) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let(:params) { { namespace_id: group, project_id: project, value_stream_id: 'default' } }
before do
sign_in(user)
end
describe 'GET index' do
context 'when user is member of the project' do
before do
project.add_developer(user)
end
it 'succeeds' do
get :index, params: params
expect(response).to have_gitlab_http_status(:ok)
end
it 'exposes the default stages' do
get :index, params: params
expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
end
context 'when list service fails' do
it 'renders 403' do
expect_next_instance_of(Analytics::CycleAnalytics::Stages::ListService) do |list_service|
expect(list_service).to receive(:allowed?).and_return(false)
end
get :index, params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
context 'when invalid value stream id is given' do
before do
params[:value_stream_id] = 1
end
it 'renders 404' do
get :index, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user is not member of the project' do
it 'renders 404' do
get :index, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Analytics::CycleAnalytics::ValueStreamsController do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let(:params) { { namespace_id: group, project_id: project } }
before do
sign_in(user)
end
describe 'GET index' do
context 'when user is member of the project' do
before do
project.add_developer(user)
end
it 'succeeds' do
get :index, params: params
expect(response).to have_gitlab_http_status(:ok)
end
it 'exposes the default value stream' do
get :index, params: params
expect(json_response.first['name']).to eq('default')
end
end
context 'when user is not member of the project' do
it 'renders 404' do
get :index, params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
...@@ -6,6 +6,7 @@ FactoryBot.define do ...@@ -6,6 +6,7 @@ FactoryBot.define do
sequence(:name) { |n| "Stage ##{n}" } sequence(:name) { |n| "Stage ##{n}" }
hidden { false } hidden { false }
issue_stage issue_stage
value_stream { association(:cycle_analytics_project_value_stream, project: project) }
trait :issue_stage do trait :issue_stage do
start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated.identifier } start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated.identifier }
......
# frozen_string_literal: true
FactoryBot.define do
factory :cycle_analytics_project_value_stream, class: 'Analytics::CycleAnalytics::ProjectValueStream' do
sequence(:name) { |n| "Value Stream ##{n}" }
project
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::StageFinder do
let(:project) { build(:project) }
let(:stage_id) { { id: Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first } }
subject { described_class.new(parent: project, stage_id: stage_id[:id]).execute }
context 'when looking up in-memory default stage by name exists' do
it { expect(subject).not_to be_persisted }
it { expect(subject.name).to eq(stage_id[:id]) }
end
context 'when in-memory default stage cannot be found' do
before do
stage_id[:id] = 'unknown_default_stage'
end
it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
end
end
...@@ -352,6 +352,7 @@ project: ...@@ -352,6 +352,7 @@ project:
- cluster_project - cluster_project
- creator - creator
- cycle_analytics_stages - cycle_analytics_stages
- value_streams
- group - group
- namespace - namespace
- management_clusters - management_clusters
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20210503105845_add_project_value_stream_id_to_project_stages.rb')
RSpec.describe AddProjectValueStreamIdToProjectStages, schema: 20210503105022 do
let(:stages) { table(:analytics_cycle_analytics_project_stages) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:namespace) { table(:namespaces).create!(name: 'ns1', path: 'nsq1') }
before do
project = projects.create!(name: 'p1', namespace_id: namespace.id)
stages.create!(
project_id: project.id,
created_at: Time.now,
updated_at: Time.now,
start_event_identifier: 1,
end_event_identifier: 2,
name: 'stage 1'
)
stages.create!(
project_id: project.id,
created_at: Time.now,
updated_at: Time.now,
start_event_identifier: 3,
end_event_identifier: 4,
name: 'stage 2'
)
end
it 'deletes the existing rows' do
migrate!
expect(stages.count).to eq(0)
end
end
...@@ -17,6 +17,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do ...@@ -17,6 +17,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do
end end
it_behaves_like 'value stream analytics stage' do it_behaves_like 'value stream analytics stage' do
let(:factory) { :cycle_analytics_project_stage }
let(:parent) { build(:project) } let(:parent) { build(:project) }
let(:parent_name) { :project } let(:parent_name) { :project }
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::ProjectValueStream, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:stages) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(100) }
it 'validates uniqueness of name' do
project = create(:project)
create(:cycle_analytics_project_value_stream, name: 'test', project: project)
value_stream = build(:cycle_analytics_project_value_stream, name: 'test', project: project)
expect(value_stream).to be_invalid
expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')])
end
end
it 'is not custom' do
expect(described_class.new).not_to be_custom
end
describe '.build_default_value_stream' do
it 'builds the default value stream' do
project = build(:project)
value_stream = described_class.build_default_value_stream(project)
expect(value_stream.name).to eq('default')
end
end
end
...@@ -113,7 +113,8 @@ RSpec.describe Project, factory_default: :keep do ...@@ -113,7 +113,8 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:lfs_file_locks) } it { is_expected.to have_many(:lfs_file_locks) }
it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:project_deploy_tokens) }
it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
it { is_expected.to have_many(:cycle_analytics_stages) } it { is_expected.to have_many(:cycle_analytics_stages).inverse_of(:project) }
it { is_expected.to have_many(:value_streams).inverse_of(:project) }
it { is_expected.to have_many(:external_pull_requests) } it { is_expected.to have_many(:external_pull_requests) }
it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:source_pipelines) } it { is_expected.to have_many(:source_pipelines) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::StageEntity do
let(:stage) { build(:cycle_analytics_project_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
subject(:entity_json) { described_class.new(Analytics::CycleAnalytics::StagePresenter.new(stage)).as_json }
it 'exposes start and end event descriptions' do
expect(entity_json).to have_key(:start_event_html_description)
expect(entity_json).to have_key(:end_event_html_description)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::CycleAnalytics::Stages::ListService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:value_stream) { Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(project) }
let(:stages) { subject.payload[:stages] }
subject { described_class.new(parent: project, current_user: user).execute }
before_all do
project.add_reporter(user)
end
it 'returns only the default stages' do
expect(stages.size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size)
end
it 'provides the default stages as non-persisted objects' do
expect(stages.map(&:id)).to all(be_nil)
end
end
...@@ -58,6 +58,19 @@ RSpec.shared_examples 'value stream analytics stage' do ...@@ -58,6 +58,19 @@ RSpec.shared_examples 'value stream analytics stage' do
it { expect(stage).not_to be_valid } it { expect(stage).not_to be_valid }
end end
# rubocop: disable Rails/SaveBang
describe '.by_value_stream' do
it 'finds stages by value stream' do
stage1 = create(factory)
create(factory) # other stage with different value stream
result = described_class.by_value_stream(stage1.value_stream)
expect(result).to eq([stage1])
end
end
# rubocop: enable Rails/SaveBang
end end
describe '#subject_class' do describe '#subject_class' do
......
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