diff --git a/app/controllers/groups/work_items_controller.rb b/app/controllers/groups/work_items_controller.rb new file mode 100644 index 00000000000..d1e15c81471 --- /dev/null +++ b/app/controllers/groups/work_items_controller.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Groups + class WorkItemsController < Groups::ApplicationController + feature_category :team_planning + + def index + not_found unless Feature.enabled?(:namespace_level_work_items, group) + end + end +end diff --git a/app/views/groups/work_items/index.html.haml b/app/views/groups/work_items/index.html.haml new file mode 100644 index 00000000000..9aa90b913a2 --- /dev/null +++ b/app/views/groups/work_items/index.html.haml @@ -0,0 +1 @@ +- page_title s_('WorkItem|Work items') diff --git a/config/feature_flags/development/namespace_level_work_items.yml b/config/feature_flags/development/namespace_level_work_items.yml new file mode 100644 index 00000000000..794e56cf425 --- /dev/null +++ b/config/feature_flags/development/namespace_level_work_items.yml @@ -0,0 +1,8 @@ +--- +name: namespace_level_work_items +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127124 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/419186 +milestone: '16.3' +type: development +group: group::project management +default_enabled: false diff --git a/config/routes/group.rb b/config/routes/group.rb index 9b346867f78..16371fca89e 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -160,6 +160,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do end resources :achievements, only: [:index, :new, :edit] + + resources :work_items, only: [:index] end scope( diff --git a/db/post_migrate/20230718020825_swap_events_target_id_to_bigint_for_gitlab_dot_com.rb b/db/post_migrate/20230718020825_swap_events_target_id_to_bigint_for_gitlab_dot_com.rb new file mode 100644 index 00000000000..03e95e39649 --- /dev/null +++ b/db/post_migrate/20230718020825_swap_events_target_id_to_bigint_for_gitlab_dot_com.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +class SwapEventsTargetIdToBigintForGitlabDotCom < Gitlab::Database::Migration[2.1] + include Gitlab::Database::MigrationHelpers::ConvertToBigint + + disable_ddl_transaction! + + TABLE_NAME = 'events' + + def up + return unless should_run? + + swap + end + + def down + return unless should_run? + + swap + end + + def swap + # This will replace the existing index_events_on_target_type_and_target_id_and_fingerprint + add_concurrent_index TABLE_NAME, [:target_type, :target_id_convert_to_bigint, :fingerprint], + name: :index_events_on_target_type_and_target_id_bigint_fingerprint, + unique: true + + with_lock_retries(raise_on_exhaustion: true) do + execute "LOCK TABLE #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE" + + execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN target_id TO target_id_tmp" + execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN target_id_convert_to_bigint TO target_id" + execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN target_id_tmp TO target_id_convert_to_bigint" + + function_name = Gitlab::Database::UnidirectionalCopyTrigger + .on_table(TABLE_NAME, connection: connection) + .name(:target_id, :target_id_convert_to_bigint) + execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL" + + execute 'DROP INDEX IF EXISTS index_events_on_target_type_and_target_id_and_fingerprint' + rename_index TABLE_NAME, 'index_events_on_target_type_and_target_id_bigint_fingerprint', + 'index_events_on_target_type_and_target_id_and_fingerprint' + end + end + + def should_run? + com_or_dev_or_test_but_not_jh? + end +end diff --git a/db/schema_migrations/20230718020825 b/db/schema_migrations/20230718020825 new file mode 100644 index 00000000000..dda10a5fbfc --- /dev/null +++ b/db/schema_migrations/20230718020825 @@ -0,0 +1 @@ +07d465dbd1b81bd388516b78e75cfeb890e6166e6b8cab23254a9a79a4de60c5 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 53a027cb5b5..fadea9e1403 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -15933,7 +15933,7 @@ ALTER SEQUENCE error_tracking_errors_id_seq OWNED BY error_tracking_errors.id; CREATE TABLE events ( project_id integer, author_id integer NOT NULL, - target_id integer, + target_id_convert_to_bigint integer, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, action smallint NOT NULL, @@ -15941,7 +15941,7 @@ CREATE TABLE events ( group_id bigint, fingerprint bytea, id bigint NOT NULL, - target_id_convert_to_bigint bigint, + target_id bigint, CONSTRAINT check_97e06e05ad CHECK ((octet_length(fingerprint) <= 128)) ); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4ce4fb6fa1c..08b413f1d64 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -52559,6 +52559,9 @@ msgstr "" msgid "WorkItem|Work item not found" msgstr "" +msgid "WorkItem|Work items" +msgstr "" + msgid "WorkItem|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options." msgstr "" diff --git a/spec/migrations/swap_events_target_id_to_bigint_for_gitlab_dot_com_spec.rb b/spec/migrations/swap_events_target_id_to_bigint_for_gitlab_dot_com_spec.rb new file mode 100644 index 00000000000..a3dc73ecc38 --- /dev/null +++ b/spec/migrations/swap_events_target_id_to_bigint_for_gitlab_dot_com_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe SwapEventsTargetIdToBigintForGitlabDotCom, feature_category: :database do + describe '#up' do + before do + # A we call `schema_migrate_down!` before each example, and for this migration + # `#down` is same as `#up`, we need to ensure we start from the expected state. + connection = described_class.new.connection + connection.execute('ALTER TABLE events ALTER COLUMN target_id TYPE integer') + connection.execute('ALTER TABLE events ALTER COLUMN target_id_convert_to_bigint TYPE bigint') + end + + # rubocop: disable RSpec/AnyInstanceOf + it 'swaps the integer and bigint columns for GitLab.com, dev, or test' do + allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true) + + events = table(:events) + + disable_migrations_output do + reversible_migration do |migration| + migration.before -> { + events.reset_column_information + + expect(events.columns.find { |c| c.name == 'target_id' }.sql_type).to eq('integer') + expect(events.columns.find { |c| c.name == 'target_id_convert_to_bigint' }.sql_type).to eq('bigint') + } + + migration.after -> { + events.reset_column_information + + expect(events.columns.find { |c| c.name == 'target_id' }.sql_type).to eq('bigint') + expect(events.columns.find { |c| c.name == 'target_id_convert_to_bigint' }.sql_type) + .to eq('integer') + } + end + end + end + + it 'is a no-op for other instances' do + allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false) + + events = table(:events) + + disable_migrations_output do + reversible_migration do |migration| + migration.before -> { + events.reset_column_information + + expect(events.columns.find { |c| c.name == 'target_id' }.sql_type).to eq('integer') + expect(events.columns.find { |c| c.name == 'target_id_convert_to_bigint' }.sql_type).to eq('bigint') + } + + migration.after -> { + events.reset_column_information + + expect(events.columns.find { |c| c.name == 'target_id' }.sql_type).to eq('integer') + expect(events.columns.find { |c| c.name == 'target_id_convert_to_bigint' }.sql_type).to eq('bigint') + } + end + end + end + # rubocop: enable RSpec/AnyInstanceOf + end +end diff --git a/spec/requests/groups/work_items_controller_spec.rb b/spec/requests/groups/work_items_controller_spec.rb new file mode 100644 index 00000000000..c47b3f03ec1 --- /dev/null +++ b/spec/requests/groups/work_items_controller_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Group Level Work Items', feature_category: :team_planning do + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:developer) { create(:user).tap { |u| group.add_developer(u) } } + + describe 'GET /groups/:group/-/work_items' do + let(:work_items_path) { url_for(controller: 'groups/work_items', action: :index, group_id: group.full_path) } + + before do + sign_in(current_user) + end + + context 'when the user can read the group' do + let(:current_user) { developer } + + it 'renders index' do + get work_items_path + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'when the namespace_level_work_items feature flag is disabled' do + before do + stub_feature_flags(namespace_level_work_items: false) + end + + it 'returns not found' do + get work_items_path + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'when the user cannot read the group' do + let(:current_user) { create(:user) } + + it 'returns not found' do + get work_items_path + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end