From b09bc9230663f7095c6fe235d92b7998100ec48d Mon Sep 17 00:00:00 2001 From: GitLab Duo Date: Thu, 11 Dec 2025 19:54:34 +0000 Subject: [PATCH 1/3] feat: Add links to Automate > Agents view in toast notifications Add actionable links to toast notifications when enabling agents or flows at the group level in the AI Catalog. Users can now click 'View in Automate' to navigate directly to the Automate > Agents view. - Update GraphQL mutation to include fullPath for groups and projects - Add toast action links in agents and flows show pages - Implement URL construction based on target type (group/project) - Set toast auto-hide delay to 10 seconds for better UX --- ...te_ai_catalog_item_consumer.mutation.graphql | 2 ++ .../ai/catalog/pages/ai_catalog_agents_show.vue | 17 ++++++++++++++++- .../ai/catalog/pages/ai_catalog_flows_show.vue | 17 ++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ee/app/assets/javascripts/ai/catalog/graphql/mutations/create_ai_catalog_item_consumer.mutation.graphql b/ee/app/assets/javascripts/ai/catalog/graphql/mutations/create_ai_catalog_item_consumer.mutation.graphql index 9a236375954634..616a8fdfa4c503 100644 --- a/ee/app/assets/javascripts/ai/catalog/graphql/mutations/create_ai_catalog_item_consumer.mutation.graphql +++ b/ee/app/assets/javascripts/ai/catalog/graphql/mutations/create_ai_catalog_item_consumer.mutation.graphql @@ -6,10 +6,12 @@ mutation createAiCatalogItemConsumer($input: AiCatalogItemConsumerCreateInput!) project { id name + fullPath } group { id name + fullPath } } } diff --git a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue index 5090d67e9b15ff..92747b40f16682 100644 --- a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue +++ b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue @@ -3,6 +3,7 @@ import { GlAlert, GlButton, GlExperimentBadge } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import { InternalEvents } from '~/tracking'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import { visitUrl } from '~/lib/utils/url_utility'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import PageHeading from '~/vue_shared/components/page_heading.vue'; import { @@ -168,8 +169,22 @@ export default { } const name = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]?.name || ''; + const fullPath = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]?.fullPath || ''; + const href = targetType === AI_CATALOG_CONSUMER_TYPE_GROUP + ? `/groups/${fullPath}/-/automate/agents` + : `/${fullPath}/-/automate/agents`; - this.$toast.show(sprintf(s__('AICatalog|Agent enabled in %{name}.'), { name })); + this.$toast.show(sprintf(s__('AICatalog|Agent enabled in %{name}.'), { name }), { + autoHideDelay: 10000, + action: { + text: s__('AICatalog|View in Automate'), + href, + onClick: (e) => { + e?.preventDefault(); + visitUrl(href); + }, + }, + }); } } catch (error) { this.errors = [ diff --git a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue index 923d91c5ed2295..a68172911e7409 100644 --- a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue +++ b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue @@ -3,6 +3,7 @@ import { GlAlert, GlButton, GlExperimentBadge } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import { InternalEvents } from '~/tracking'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; +import { visitUrl } from '~/lib/utils/url_utility'; import PageHeading from '~/vue_shared/components/page_heading.vue'; import { AI_CATALOG_CONSUMER_TYPE_GROUP, @@ -161,8 +162,22 @@ export default { } const name = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]?.name || ''; + const fullPath = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]?.fullPath || ''; + const href = targetType === AI_CATALOG_CONSUMER_TYPE_GROUP + ? `/groups/${fullPath}/-/automate/agents` + : `/${fullPath}/-/automate/agents`; - this.$toast.show(sprintf(s__('AICatalog|Flow enabled in %{name}.'), { name })); + this.$toast.show(sprintf(s__('AICatalog|Flow enabled in %{name}.'), { name }), { + autoHideDelay: 10000, + action: { + text: s__('AICatalog|View in Automate'), + href, + onClick: (e) => { + e?.preventDefault(); + visitUrl(href); + }, + }, + }); } } catch (error) { this.errors = [ -- GitLab From 94d9e35c003587613b102817c6320210f364c123 Mon Sep 17 00:00:00 2001 From: Angus Ryer Date: Fri, 12 Dec 2025 09:53:11 -0500 Subject: [PATCH 2/3] Fix link to point to specific item --- .../ai/catalog/pages/ai_catalog_agents_show.vue | 16 +++++++++------- .../ai/catalog/pages/ai_catalog_flows_show.vue | 16 +++++++++------- locale/gitlab.pot | 3 +++ 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue index 92747b40f16682..5557e0eea4a82f 100644 --- a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue +++ b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue @@ -4,6 +4,7 @@ import { s__, sprintf } from '~/locale'; import { InternalEvents } from '~/tracking'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { visitUrl } from '~/lib/utils/url_utility'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import PageHeading from '~/vue_shared/components/page_heading.vue'; import { @@ -168,16 +169,17 @@ export default { return; } - const name = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]?.name || ''; - const fullPath = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]?.fullPath || ''; - const href = targetType === AI_CATALOG_CONSUMER_TYPE_GROUP - ? `/groups/${fullPath}/-/automate/agents` - : `/${fullPath}/-/automate/agents`; + const newConsumer = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]; + const name = newConsumer?.name || ''; + const fullPath = newConsumer?.fullPath || ''; + + const id = getIdFromGraphQLId(this.aiCatalogAgent.id); + const path = `/${fullPath}/-/automate/agents/${id}`; + const href = targetType === AI_CATALOG_CONSUMER_TYPE_GROUP ? `/groups/${path}` : path; this.$toast.show(sprintf(s__('AICatalog|Agent enabled in %{name}.'), { name }), { - autoHideDelay: 10000, action: { - text: s__('AICatalog|View in Automate'), + text: sprintf(s__('AICatalog|View in %{targetTypeLabel}'), { targetTypeLabel }), href, onClick: (e) => { e?.preventDefault(); diff --git a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue index a68172911e7409..721d276e2bd49e 100644 --- a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue +++ b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue @@ -4,6 +4,7 @@ import { s__, sprintf } from '~/locale'; import { InternalEvents } from '~/tracking'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { visitUrl } from '~/lib/utils/url_utility'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import PageHeading from '~/vue_shared/components/page_heading.vue'; import { AI_CATALOG_CONSUMER_TYPE_GROUP, @@ -161,16 +162,17 @@ export default { return; } - const name = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]?.name || ''; - const fullPath = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]?.fullPath || ''; - const href = targetType === AI_CATALOG_CONSUMER_TYPE_GROUP - ? `/groups/${fullPath}/-/automate/agents` - : `/${fullPath}/-/automate/agents`; + const newConsumer = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]; + const name = newConsumer?.name || ''; + const fullPath = newConsumer?.fullPath || ''; + + const id = getIdFromGraphQLId(this.aiCatalogFlow.id); + const path = `/${fullPath}/-/automate/flows/${id}`; + const href = targetType === AI_CATALOG_CONSUMER_TYPE_GROUP ? `/groups/${path}` : path; this.$toast.show(sprintf(s__('AICatalog|Flow enabled in %{name}.'), { name }), { - autoHideDelay: 10000, action: { - text: s__('AICatalog|View in Automate'), + text: sprintf(s__('AICatalog|View in %{targetTypeLabel}'), { targetTypeLabel }), href, onClick: (e) => { e?.preventDefault(); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index de441c898f70f4..ea62aabb83454a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3142,6 +3142,9 @@ msgstr "" msgid "AICatalog|View enabled version" msgstr "" +msgid "AICatalog|View in %{targetTypeLabel}" +msgstr "" + msgid "AICatalog|View latest version" msgstr "" -- GitLab From 86300317e4d871ea68708ecc0530f7a266dd2e4f Mon Sep 17 00:00:00 2001 From: Angus Ryer Date: Fri, 12 Dec 2025 15:48:07 -0500 Subject: [PATCH 3/3] Extract href creation utility for CreateItemConsumer - Add fields to create mutation for easier path detection --- ..._ai_catalog_item_consumer.mutation.graphql | 8 ++++++-- .../catalog/pages/ai_catalog_agents_show.vue | 12 ++++-------- .../catalog/pages/ai_catalog_flows_show.vue | 12 ++++-------- ee/app/assets/javascripts/ai/catalog/utils.js | 19 +++++++++++++++++++ .../pages/agents/ai_agents_index.vue | 19 ++++++++++++++++--- .../pages/flows/ai_flows_index.vue | 19 ++++++++++++++++--- ee/spec/frontend/ai/catalog/mock_data.js | 6 ++++++ .../pages/ai_catalog_agents_show_spec.js | 11 ++++++++++- .../pages/ai_catalog_flows_show_spec.js | 11 ++++++++++- .../pages/agents/ai_agents_index_spec.js | 11 ++++++++++- .../pages/flows/ai_flows_index_spec.js | 11 ++++++++++- 11 files changed, 111 insertions(+), 28 deletions(-) diff --git a/ee/app/assets/javascripts/ai/catalog/graphql/mutations/create_ai_catalog_item_consumer.mutation.graphql b/ee/app/assets/javascripts/ai/catalog/graphql/mutations/create_ai_catalog_item_consumer.mutation.graphql index 616a8fdfa4c503..3e901a1647ceb9 100644 --- a/ee/app/assets/javascripts/ai/catalog/graphql/mutations/create_ai_catalog_item_consumer.mutation.graphql +++ b/ee/app/assets/javascripts/ai/catalog/graphql/mutations/create_ai_catalog_item_consumer.mutation.graphql @@ -3,15 +3,19 @@ mutation createAiCatalogItemConsumer($input: AiCatalogItemConsumerCreateInput!) errors itemConsumer { id + item { + id + itemType + } project { id name - fullPath + webPath } group { id name - fullPath + webPath } } } diff --git a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue index 5557e0eea4a82f..389590e70aa82a 100644 --- a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue +++ b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_agents_show.vue @@ -4,7 +4,6 @@ import { s__, sprintf } from '~/locale'; import { InternalEvents } from '~/tracking'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { visitUrl } from '~/lib/utils/url_utility'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import PageHeading from '~/vue_shared/components/page_heading.vue'; import { @@ -21,7 +20,7 @@ import { VERSION_PINNED_GROUP, } from 'ee/ai/catalog/constants'; import ErrorsAlert from '~/vue_shared/components/errors_alert.vue'; -import { prerequisitesError } from '../utils'; +import { prerequisitesError, createLinkToItemConsumer } from '../utils'; import AiCatalogItemActions from '../components/ai_catalog_item_actions.vue'; import AiCatalogItemView from '../components/ai_catalog_item_view.vue'; import aiCatalogAgentQuery from '../graphql/queries/ai_catalog_agent.query.graphql'; @@ -169,13 +168,10 @@ export default { return; } - const newConsumer = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]; - const name = newConsumer?.name || ''; - const fullPath = newConsumer?.fullPath || ''; + const createdItemConsumer = data.aiCatalogItemConsumerCreate.itemConsumer; + const href = createLinkToItemConsumer(createdItemConsumer, targetTypeLabel); - const id = getIdFromGraphQLId(this.aiCatalogAgent.id); - const path = `/${fullPath}/-/automate/agents/${id}`; - const href = targetType === AI_CATALOG_CONSUMER_TYPE_GROUP ? `/groups/${path}` : path; + const { name } = createdItemConsumer[targetTypeLabel]; this.$toast.show(sprintf(s__('AICatalog|Agent enabled in %{name}.'), { name }), { action: { diff --git a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue index 721d276e2bd49e..d4800a029249db 100644 --- a/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue +++ b/ee/app/assets/javascripts/ai/catalog/pages/ai_catalog_flows_show.vue @@ -4,7 +4,6 @@ import { s__, sprintf } from '~/locale'; import { InternalEvents } from '~/tracking'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { visitUrl } from '~/lib/utils/url_utility'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import PageHeading from '~/vue_shared/components/page_heading.vue'; import { AI_CATALOG_CONSUMER_TYPE_GROUP, @@ -21,7 +20,7 @@ import { } from 'ee/ai/catalog/constants'; import ErrorsAlert from '~/vue_shared/components/errors_alert.vue'; import FoundationalIcon from 'ee/ai/components/foundational_icon.vue'; -import { prerequisitesError } from '../utils'; +import { prerequisitesError, createLinkToItemConsumer } from '../utils'; import AiCatalogItemActions from '../components/ai_catalog_item_actions.vue'; import AiCatalogItemView from '../components/ai_catalog_item_view.vue'; import aiCatalogFlowQuery from '../graphql/queries/ai_catalog_flow.query.graphql'; @@ -162,13 +161,10 @@ export default { return; } - const newConsumer = data.aiCatalogItemConsumerCreate.itemConsumer[targetType]; - const name = newConsumer?.name || ''; - const fullPath = newConsumer?.fullPath || ''; + const createdItemConsumer = data.aiCatalogItemConsumerCreate.itemConsumer; + const href = createLinkToItemConsumer(createdItemConsumer, targetTypeLabel); - const id = getIdFromGraphQLId(this.aiCatalogFlow.id); - const path = `/${fullPath}/-/automate/flows/${id}`; - const href = targetType === AI_CATALOG_CONSUMER_TYPE_GROUP ? `/groups/${path}` : path; + const { name } = createdItemConsumer[targetTypeLabel]; this.$toast.show(sprintf(s__('AICatalog|Flow enabled in %{name}.'), { name }), { action: { diff --git a/ee/app/assets/javascripts/ai/catalog/utils.js b/ee/app/assets/javascripts/ai/catalog/utils.js index 0cf62f96bc59f4..18923762452754 100644 --- a/ee/app/assets/javascripts/ai/catalog/utils.js +++ b/ee/app/assets/javascripts/ai/catalog/utils.js @@ -1,5 +1,7 @@ import { sprintf } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { AI_CATALOG_ITEM_PLURAL_LABELS } from './constants'; export const prerequisitesPath = helpPagePath('user/duo_agent_platform/ai_catalog', { anchor: 'view-the-ai-catalog', @@ -17,6 +19,23 @@ export const prerequisitesError = (message, params = {}) => { ); }; +/** + * Returns a link to a specific agent or flow. + * + * @param {Object} itemConsumer - Must include: + * - `project` or `group` object with a `webPath` property + * - `item` object with `id` and `itemType` properties + * @param {String} targetNamespace either `project` or `group` + */ +export const createLinkToItemConsumer = (itemConsumer, targetNamespace) => { + const namespaceConsumer = itemConsumer[targetNamespace]; + const webPath = namespaceConsumer?.webPath || ''; + const id = getIdFromGraphQLId(itemConsumer.item.id); + const itemTypePath = AI_CATALOG_ITEM_PLURAL_LABELS[itemConsumer.item.itemType]; + + return `${webPath}/-/automate/${itemTypePath}/${id}`; +}; + /** * Note that this utility method *does not* use the `pinnedItemVersion`. */ diff --git a/ee/app/assets/javascripts/ai/duo_agents_platform/pages/agents/ai_agents_index.vue b/ee/app/assets/javascripts/ai/duo_agents_platform/pages/agents/ai_agents_index.vue index f0b61b92ed30b7..b3d7b8e38b3557 100644 --- a/ee/app/assets/javascripts/ai/duo_agents_platform/pages/agents/ai_agents_index.vue +++ b/ee/app/assets/javascripts/ai/duo_agents_platform/pages/agents/ai_agents_index.vue @@ -1,6 +1,7 @@