From cbe1104d28b168450aca3e1fbb2e131bdfc150cb Mon Sep 17 00:00:00 2001 From: Justin Ho Tuan Duong Date: Wed, 22 Oct 2025 21:02:52 +0700 Subject: [PATCH 1/4] Add menu item, route, permissions For Group > Automate > Flows. --- .rubocop_todo/gitlab/bounded_contexts.yml | 2 + ee/app/policies/ee/group_policy.rb | 8 +++ ee/config/routes/group.rb | 5 ++ .../ee/sidebars/groups/super_sidebar_panel.rb | 21 ++++++++ .../super_sidebar_menus/duo_agents_menu.rb | 51 +++++++++++++++++++ lib/sidebars/groups/super_sidebar_panel.rb | 2 + 6 files changed, 89 insertions(+) create mode 100644 ee/lib/ee/sidebars/groups/super_sidebar_panel.rb create mode 100644 ee/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu.rb diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml index 3a4f85cae55e96..90b002692177a2 100644 --- a/.rubocop_todo/gitlab/bounded_contexts.yml +++ b/.rubocop_todo/gitlab/bounded_contexts.yml @@ -3495,6 +3495,7 @@ Gitlab/BoundedContexts: - 'ee/lib/ee/sidebars/groups/menus/settings_menu.rb' - 'ee/lib/ee/sidebars/groups/menus/work_items_menu.rb' - 'ee/lib/ee/sidebars/groups/panel.rb' + - 'ee/lib/ee/sidebars/groups/super_sidebar_panel.rb' - 'ee/lib/ee/sidebars/projects/menus/analytics_menu.rb' - 'ee/lib/ee/sidebars/projects/menus/ci_cd_menu.rb' - 'ee/lib/ee/sidebars/projects/menus/issues_menu.rb' @@ -3590,6 +3591,7 @@ Gitlab/BoundedContexts: - 'ee/lib/sidebars/groups/menus/security_compliance_menu.rb' - 'ee/lib/sidebars/groups/menus/wiki_menu.rb' - 'ee/lib/sidebars/groups/menus/work_item_epics_menu.rb' + - 'ee/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu.rb' - 'ee/lib/sidebars/projects/menus/learn_gitlab_menu.rb' - 'ee/lib/sidebars/projects/super_sidebar_menus/duo_agents_menu.rb' - 'ee/lib/sidebars/user_settings/menus/profile_billing_menu.rb' diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 8f33178ab0902b..49fbf9f23830a2 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -1040,6 +1040,10 @@ module GroupPolicy prevent :create_group_link end + condition(:duo_workflow_enabled) do + ::Feature.enabled?(:duo_workflow, @user) + end + with_scope :subject condition(:duo_workflow_available) do @subject.duo_features_enabled && @@ -1047,6 +1051,10 @@ module GroupPolicy @user&.allowed_to_use?(:duo_agent_platform) end + rule { duo_workflow_enabled & duo_workflow_available & can?(:developer_access) }.policy do + enable :duo_workflow + end + rule { duo_workflow_available & can?(:admin_group) }.policy do enable :admin_duo_workflow end diff --git a/ee/config/routes/group.rb b/ee/config/routes/group.rb index 7d691833026f58..f0ca549e32ee13 100644 --- a/ee/config/routes/group.rb +++ b/ee/config/routes/group.rb @@ -282,6 +282,11 @@ end end + scope :automate do + get '/(*vueroute)' => 'duo_agents_platform#show', as: :automate, format: false + get 'flows', to: 'duo_agents_platform#show', as: :automate_flows, format: false + end + draw :virtual_registries end end diff --git a/ee/lib/ee/sidebars/groups/super_sidebar_panel.rb b/ee/lib/ee/sidebars/groups/super_sidebar_panel.rb new file mode 100644 index 00000000000000..66af3888933776 --- /dev/null +++ b/ee/lib/ee/sidebars/groups/super_sidebar_panel.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module EE + module Sidebars + module Groups + module SuperSidebarPanel + extend ::Gitlab::Utils::Override + + override :configure_menus + def configure_menus + super + + insert_menu_after( + ::Sidebars::Groups::SuperSidebarMenus::PlanMenu, + ::Sidebars::Groups::SuperSidebarMenus::DuoAgentsMenu.new(context) + ) + end + end + end + end +end diff --git a/ee/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu.rb b/ee/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu.rb new file mode 100644 index 00000000000000..8ea0a4c12e5d8a --- /dev/null +++ b/ee/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Sidebars + module Groups + module SuperSidebarMenus + class DuoAgentsMenu < ::Sidebars::Menu + override :configure_menu_items + def configure_menu_items + return false unless current_user&.can?(:duo_workflow, context.group) && + context.group.duo_features_enabled && + show_flows_menu_item? + + add_item(ai_catalog_flows_menu_item) if show_flows_menu_item? + + true + end + + override :title + def title + s_('DuoAgentsPlatform|Automate') + end + + override :sprite_icon + def sprite_icon + 'tanuki-ai' + end + + override :active_routes + def active_routes + { controller: :duo_agents_platform } + end + + private + + def show_flows_menu_item? + Feature.enabled?(:global_ai_catalog, context.current_user) && + Feature.enabled?(:ai_catalog_flows, context.current_user) + end + + def ai_catalog_flows_menu_item + ::Sidebars::MenuItem.new( + title: s_('AICatalog|Flows'), + link: group_automate_flows_path(context.group), + active_routes: nil, + item_id: :ai_flows + ) + end + end + end + end +end diff --git a/lib/sidebars/groups/super_sidebar_panel.rb b/lib/sidebars/groups/super_sidebar_panel.rb index bf4cc5e846e152..54bb30c5563e28 100644 --- a/lib/sidebars/groups/super_sidebar_panel.rb +++ b/lib/sidebars/groups/super_sidebar_panel.rb @@ -43,3 +43,5 @@ def super_sidebar_context_header end end end + +Sidebars::Groups::SuperSidebarPanel.prepend_mod_with('Sidebars::Groups::SuperSidebarPanel') -- GitLab From 8ff6c0070f453605750cce63061088fd0cae9833 Mon Sep 17 00:00:00 2001 From: Justin Ho Tuan Duong Date: Wed, 22 Oct 2025 22:04:04 +0700 Subject: [PATCH 2/4] Add controller with basic logic For Group > Automate > Flows. --- .../groups/duo_agents_platform_controller.rb | 39 +++++++++++++++++++ .../ee/projects/duo_agents_platform_helper.rb | 9 +++++ .../groups/duo_agents_platform/show.html.haml | 4 ++ 3 files changed, 52 insertions(+) create mode 100644 ee/app/controllers/groups/duo_agents_platform_controller.rb create mode 100644 ee/app/views/groups/duo_agents_platform/show.html.haml diff --git a/ee/app/controllers/groups/duo_agents_platform_controller.rb b/ee/app/controllers/groups/duo_agents_platform_controller.rb new file mode 100644 index 00000000000000..071238d57f0747 --- /dev/null +++ b/ee/app/controllers/groups/duo_agents_platform_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Groups + class DuoAgentsPlatformController < Groups::ApplicationController + feature_category :agent_foundations + before_action :check_access + before_action do + push_frontend_feature_flag(:ai_catalog_flows, current_user) + end + + def show; end + + private + + def check_access + return render_404 unless group&.duo_features_enabled && current_user.can?(:duo_workflow, group) + + return unless specific_vueroute? + + render_404 unless authorized_for_route? + end + + def specific_vueroute? + %w[flows].include?(duo_agents_platform_params[:vueroute]) + end + + def authorized_for_route? + case duo_agents_platform_params[:vueroute] + when 'flows' + Feature.enabled?(:global_ai_catalog, current_user) && + Feature.enabled?(:ai_catalog_flows, current_user) + end + end + + def duo_agents_platform_params + params.permit(:vueroute) + end + end +end diff --git a/ee/app/helpers/ee/projects/duo_agents_platform_helper.rb b/ee/app/helpers/ee/projects/duo_agents_platform_helper.rb index d1efca2cd3165c..83a2e95684dee7 100644 --- a/ee/app/helpers/ee/projects/duo_agents_platform_helper.rb +++ b/ee/app/helpers/ee/projects/duo_agents_platform_helper.rb @@ -12,6 +12,15 @@ def duo_agents_platform_data(project) flow_triggers_event_type_options: ai_flow_triggers_event_type_options } end + + def duo_agents_group_data(group) + { + agents_platform_base_route: group_automate_path(group), + group_id: group.id, + group_path: group.full_path, + explore_ai_catalog_path: explore_ai_catalog_path + } + end end end end diff --git a/ee/app/views/groups/duo_agents_platform/show.html.haml b/ee/app/views/groups/duo_agents_platform/show.html.haml new file mode 100644 index 00000000000000..dc07bb1cde6b94 --- /dev/null +++ b/ee/app/views/groups/duo_agents_platform/show.html.haml @@ -0,0 +1,4 @@ +- page_title _('Automate') +- @skip_current_level_breadcrumb = true + +#js-duo-agents-platform-page{ data: duo_agents_group_data(@group)} -- GitLab From e7fbbd57a0734c14466138dcecfff257b8776e04 Mon Sep 17 00:00:00 2001 From: Justin Ho Tuan Duong Date: Wed, 22 Oct 2025 22:04:46 +0700 Subject: [PATCH 3/4] Add frontend boilerplate code --- .../javascripts/ai/duo_agents_platform/constants.js | 1 + .../assets/javascripts/ai/duo_agents_platform/index.js | 2 +- .../ai/duo_agents_platform/namespace/group/index.js | 9 +++++++++ .../pages/groups/duo_agents_platform/index.js | 3 +++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/index.js create mode 100644 ee/app/assets/javascripts/pages/groups/duo_agents_platform/index.js diff --git a/ee/app/assets/javascripts/ai/duo_agents_platform/constants.js b/ee/app/assets/javascripts/ai/duo_agents_platform/constants.js index d84e0157d0d960..fd942797798c44 100644 --- a/ee/app/assets/javascripts/ai/duo_agents_platform/constants.js +++ b/ee/app/assets/javascripts/ai/duo_agents_platform/constants.js @@ -7,6 +7,7 @@ export const TOOL_MESSAGE_TYPE = 'tool'; export const AGENT_PLATFORM_INDEX_COMPONENT_NAME = 'DuoAgentPlatformIndex'; export const AGENT_PLATFORM_PROJECT_PAGE = 'project'; +export const AGENT_PLATFORM_GROUP_PAGE = 'group'; export const AGENT_PLATFORM_USER_PAGE = 'user'; export const AGENT_PLATFORM_STATUS_ICON = { diff --git a/ee/app/assets/javascripts/ai/duo_agents_platform/index.js b/ee/app/assets/javascripts/ai/duo_agents_platform/index.js index dbcbae5875fa7c..c0b37c1bd5acf1 100644 --- a/ee/app/assets/javascripts/ai/duo_agents_platform/index.js +++ b/ee/app/assets/javascripts/ai/duo_agents_platform/index.js @@ -34,7 +34,7 @@ export const initDuoAgentsPlatformPage = ({ namespaceDatasetProperties = [], nam el, provide: { exploreAiCatalogPath, - flowTriggersEventTypeOptions: JSON.parse(flowTriggersEventTypeOptions), + flowTriggersEventTypeOptions: JSON.parse(flowTriggersEventTypeOptions || '[]'), ...namespaceProvideData, }, }); diff --git a/ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/index.js b/ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/index.js new file mode 100644 index 00000000000000..c97c003362eab3 --- /dev/null +++ b/ee/app/assets/javascripts/ai/duo_agents_platform/namespace/group/index.js @@ -0,0 +1,9 @@ +import { initDuoAgentsPlatformPage } from '../../index'; +import { AGENT_PLATFORM_GROUP_PAGE } from '../../constants'; + +export const initDuoAgentsPlatformGroupPage = () => { + initDuoAgentsPlatformPage({ + namespace: AGENT_PLATFORM_GROUP_PAGE, + namespaceDatasetProperties: ['groupPath', 'groupId'], + }); +}; diff --git a/ee/app/assets/javascripts/pages/groups/duo_agents_platform/index.js b/ee/app/assets/javascripts/pages/groups/duo_agents_platform/index.js new file mode 100644 index 00000000000000..aee7957805deec --- /dev/null +++ b/ee/app/assets/javascripts/pages/groups/duo_agents_platform/index.js @@ -0,0 +1,3 @@ +import { initDuoAgentsPlatformGroupPage } from 'ee/ai/duo_agents_platform/namespace/group'; + +initDuoAgentsPlatformGroupPage(); -- GitLab From e549671f4fa14159b529b4019a4949080ce78b47 Mon Sep 17 00:00:00 2001 From: Justin Ho Tuan Duong Date: Wed, 22 Oct 2025 22:58:42 +0700 Subject: [PATCH 4/4] Add some backend specs --- .../groups/duo_agents_platform/show.html.haml | 2 +- .../duo_agents_menu_spec.rb | 94 +++++++++++++++++++ .../duo_agents_platform_controller_spec.rb | 89 ++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 ee/spec/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu_spec.rb create mode 100644 ee/spec/requests/groups/duo_agents_platform_controller_spec.rb diff --git a/ee/app/views/groups/duo_agents_platform/show.html.haml b/ee/app/views/groups/duo_agents_platform/show.html.haml index dc07bb1cde6b94..b5382b3a85de18 100644 --- a/ee/app/views/groups/duo_agents_platform/show.html.haml +++ b/ee/app/views/groups/duo_agents_platform/show.html.haml @@ -1,4 +1,4 @@ - page_title _('Automate') - @skip_current_level_breadcrumb = true -#js-duo-agents-platform-page{ data: duo_agents_group_data(@group)} +#js-duo-agents-platform-page{ data: duo_agents_group_data(@group) } diff --git a/ee/spec/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu_spec.rb b/ee/spec/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu_spec.rb new file mode 100644 index 00000000000000..3f50e31f9469e5 --- /dev/null +++ b/ee/spec/lib/sidebars/groups/super_sidebar_menus/duo_agents_menu_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Groups::SuperSidebarMenus::DuoAgentsMenu, feature_category: :duo_agent_platform do + let_it_be(:group) { build_stubbed(:group) } + let_it_be(:user) { build_stubbed(:user) } + let(:context) { Sidebars::Groups::Context.new(current_user: user, container: group) } + + subject(:menu) { described_class.new(context) } + + describe '#configure_menu_items' do + using RSpec::Parameterized::TableSyntax + + where(:duo_features_enabled, :ai_catalog, :ai_catalog_flows_ff, :configure_result, :expected_items) do + true | true | true | true | [:ai_flows] + true | true | false | false | [] + true | false | true | false | [] + false | true | true | false | [] + end + + with_them do + before do + allow(group).to receive(:duo_features_enabled).and_return(duo_features_enabled) + stub_feature_flags(global_ai_catalog: ai_catalog) + stub_feature_flags(ai_catalog_flows: ai_catalog_flows_ff) + allow(user).to receive(:can?).with(:duo_workflow, group).and_return(true) + end + + it "returns correct configure result" do + expect(menu.configure_menu_items).to eq(configure_result) + end + + it "renders expected menu items" do + expect(menu.renderable_items.size).to eq(expected_items.size) + + if expected_items.any? + expect(menu.renderable_items.map(&:item_id)).to match_array(expected_items) + else + expect(menu.renderable_items).to be_empty + end + end + end + end + + describe "when user does not have `duo_workflow` ability" do + before do + allow(user).to receive(:can?).with(:duo_workflow, group).and_return(false) + end + + it('does not render any menu items') do + expect(menu.configure_menu_items).to be false + end + end + + describe '#title' do + it 'returns correct title' do + expect(menu.title).to eq('Automate') + end + end + + describe '#sprite_icon' do + it 'returns correct icon' do + expect(menu.sprite_icon).to eq('tanuki-ai') + end + end + + describe 'flows menu item' do + before do + allow(group).to receive(:duo_features_enabled).and_return(true) + allow(user).to receive(:can?).with(:duo_workflow, group).and_return(true) + + menu.configure_menu_items + end + + let(:flows_menu_item) { menu.renderable_items.find { |item| item.item_id == :ai_flows } } + + it 'has correct title' do + expect(flows_menu_item.title).to eq('Flows') + end + + it 'has correct link' do + expect(flows_menu_item.link).to eq("/groups/#{group.full_path}/-/automate/flows") + end + + it 'has correct active routes' do + expect(flows_menu_item.active_routes).to be_nil + end + + it 'has correct item id' do + expect(flows_menu_item.item_id).to eq(:ai_flows) + end + end +end diff --git a/ee/spec/requests/groups/duo_agents_platform_controller_spec.rb b/ee/spec/requests/groups/duo_agents_platform_controller_spec.rb new file mode 100644 index 00000000000000..0847faeea71aea --- /dev/null +++ b/ee/spec/requests/groups/duo_agents_platform_controller_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Groups::DuoAgentsPlatform', feature_category: :duo_agent_platform do + let(:group) { create(:group) } + let(:user) { create(:user) } + + before do + group.add_developer(user) + group.namespace_settings.update!(duo_features_enabled: true) + + sign_in(user) + allow(user).to receive(:can?).and_return(true) + allow(user).to receive(:can?).with(:duo_workflow, group).and_return(true) + end + + describe 'GET /:group/-/automate' do + context 'when user has access to duo_workflow' do + it 'renders successfully' do + get group_automate_flows_path(group) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when user does not have access to duo_workflow' do + before do + allow(user).to receive(:can?).with(:duo_workflow, group).and_return(false) + end + + it 'does not render' do + get group_automate_flows_path(group) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when duo_features_enabled setting is disabled for the group' do + before do + group.namespace_settings.update!(duo_features_enabled: false) + end + + it 'returns 404' do + get group_automate_flows_path(group) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when vueroute is flows' do + context 'when ai_catalog_flows feature is enabled' do + before do + stub_feature_flags(global_ai_catalog: true, ai_catalog_flows: true) + end + + it 'returns successfully' do + get group_automate_flows_path(group) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when ai_catalog_flows is disabled' do + before do + stub_feature_flags(global_ai_catalog: true, ai_catalog_flows: false) + end + + it 'returns 404' do + get group_automate_flows_path(group) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when global_ai_catalog is disabled' do + before do + stub_feature_flags(global_ai_catalog: false, ai_catalog_flows: true) + end + + it 'returns 404' do + get group_automate_flows_path(group) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end +end -- GitLab