diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 05f04cd12307824734018604f4a5faf7469830ad..7f8d2dc4277bf01b9c2e9c83afad722682e240c1 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -14677,22 +14677,12 @@ An AI agent. | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="aiagent_links"></a>`_links` | [`AiAgentLinks!`](#aiagentlinks) | Map of links to perform actions on the agent. | | <a id="aiagentcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. | | <a id="aiagentid"></a>`id` | [`ID!`](#id) | ID of the agent. | | <a id="aiagentname"></a>`name` | [`String!`](#string) | Name of the agent. | +| <a id="aiagentrouteid"></a>`routeId` | [`Int!`](#int) | Route ID of the agent. | | <a id="aiagentversions"></a>`versions` | [`[AiAgentVersion!]`](#aiagentversion) | Versions of the agent. | -### `AiAgentLinks` - -Represents links to perform actions on the agent. - -#### Fields - -| Name | Type | Description | -| ---- | ---- | ----------- | -| <a id="aiagentlinksshowpath"></a>`showPath` | [`String`](#string) | Path to the details page of the agent. | - ### `AiAgentVersion` Version of an AI Agent. diff --git a/ee/app/assets/javascripts/ml/ai_agents/apps/index.js b/ee/app/assets/javascripts/ml/ai_agents/apps/index.js deleted file mode 100644 index fd49e5da3c5201f3cfee984dd7764050e226f3ad..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/ml/ai_agents/apps/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import ListAgents from './list_agents.vue'; -import CreateAgent from './create_agent.vue'; -import ShowAgent from './show_agent.vue'; - -export { ListAgents, CreateAgent, ShowAgent }; diff --git a/ee/app/assets/javascripts/ml/ai_agents/base_app.vue b/ee/app/assets/javascripts/ml/ai_agents/base_app.vue new file mode 100644 index 0000000000000000000000000000000000000000..982f701b95f040e47534553e0d4b8c824ba70a98 --- /dev/null +++ b/ee/app/assets/javascripts/ml/ai_agents/base_app.vue @@ -0,0 +1,3 @@ +<template> + <router-view ref="router-view" /> +</template> diff --git a/ee/app/assets/javascripts/ml/ai_agents/components/agent_list.vue b/ee/app/assets/javascripts/ml/ai_agents/components/agent_list.vue index 1d89ce9fcfa1852a870e127e24326b0f58017495..9edd37e1d0e293383ae89f5572dc2775f4449427 100644 --- a/ee/app/assets/javascripts/ml/ai_agents/components/agent_list.vue +++ b/ee/app/assets/javascripts/ml/ai_agents/components/agent_list.vue @@ -1,25 +1,19 @@ <script> -import { GlLink, GlLoadingIcon, GlTableLite, GlEmptyState } from '@gitlab/ui'; +import { GlLoadingIcon, GlTableLite, GlEmptyState } from '@gitlab/ui'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import { s__ } from '~/locale'; +import { ROUTE_SHOW_AGENT } from '../constants'; import getAiAgents from '../graphql/queries/get_ai_agents.query.graphql'; const GRAPHQL_PAGE_SIZE = 30; export default { components: { - GlLink, GlLoadingIcon, GlEmptyState, GlTableLite, }, inject: ['projectPath'], - props: { - createAgentPath: { - type: String, - required: true, - }, - }, data() { return { agents: {}, @@ -30,7 +24,6 @@ export default { emptyState: { title: s__('AiAgents|Create your own AI Agents'), description: s__('AiAgents|Create and manage your AI Agents'), - createNew: s__('AiAgents|Get started'), svgPath: '/assets/illustrations/tanuki_ai_logo.svg', }, }, @@ -72,6 +65,7 @@ export default { return this.agents?.nodes ?? []; }, }, + ROUTE_SHOW_AGENT, methods: { fetchPage(pageInfo) { const variables = { @@ -106,17 +100,19 @@ export default { data-testId="aiAgentsTable" > <template #cell(name)="{ item }"> - <gl-link :href="item._links.showPath" class="gl-text-body gl-line-height-24"> + <router-link + :to="{ name: $options.ROUTE_SHOW_AGENT, params: { agentId: item.routeId } }" + data-testid="agent-item" + class="gl-text-body gl-line-height-24" + > {{ item.name }} - </gl-link> + </router-link> </template> </gl-table-lite> <gl-empty-state v-else :title="$options.i18n.emptyState.title" - :primary-button-text="$options.i18n.emptyState.createNew" - :primary-button-link="createAgentPath" :svg-path="$options.i18n.emptyState.svgPath" :svg-height="null" :description="$options.i18n.emptyState.description" diff --git a/ee/app/assets/javascripts/ml/ai_agents/constants.js b/ee/app/assets/javascripts/ml/ai_agents/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..1b491ac05e20c5abe3e74e09605bf5457ac32d56 --- /dev/null +++ b/ee/app/assets/javascripts/ml/ai_agents/constants.js @@ -0,0 +1,3 @@ +export const ROUTE_LIST_AGENTS = 'list'; +export const ROUTE_NEW_AGENT = 'create'; +export const ROUTE_SHOW_AGENT = 'show'; diff --git a/ee/app/assets/javascripts/ml/ai_agents/graphql/mutations/create_ai_agent.mutation.graphql b/ee/app/assets/javascripts/ml/ai_agents/graphql/mutations/create_ai_agent.mutation.graphql index 76f9c9e129686418d15a46840a761188e754347b..e9ce1b7c6c17adfdeff03f38a71116179a7f20f7 100644 --- a/ee/app/assets/javascripts/ml/ai_agents/graphql/mutations/create_ai_agent.mutation.graphql +++ b/ee/app/assets/javascripts/ml/ai_agents/graphql/mutations/create_ai_agent.mutation.graphql @@ -2,9 +2,7 @@ mutation createAiAgent($projectPath: ID!, $name: String!, $prompt: String!) { aiAgentCreate(input: { projectPath: $projectPath, name: $name, prompt: $prompt }) { agent { id - _links { - showPath - } + routeId } errors } diff --git a/ee/app/assets/javascripts/ml/ai_agents/graphql/queries/get_ai_agents.query.graphql b/ee/app/assets/javascripts/ml/ai_agents/graphql/queries/get_ai_agents.query.graphql index b9e5f4f3f8cc1acffa51387de38b900c12e8b524..c811900ef2abecc373613cd3d0a6eb67cbaaf062 100644 --- a/ee/app/assets/javascripts/ml/ai_agents/graphql/queries/get_ai_agents.query.graphql +++ b/ee/app/assets/javascripts/ml/ai_agents/graphql/queries/get_ai_agents.query.graphql @@ -6,14 +6,12 @@ query getAiAgents($fullPath: ID!, $first: Int, $last: Int, $after: String, $befo aiAgents(after: $after, before: $before, first: $first, last: $last) { nodes { id + routeId name versions { id model } - _links { - showPath - } } pageInfo { ...PageInfo diff --git a/ee/app/assets/javascripts/ml/ai_agents/index.js b/ee/app/assets/javascripts/ml/ai_agents/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1d62f12aa75eecec27e34da4c3181342535a4565 --- /dev/null +++ b/ee/app/assets/javascripts/ml/ai_agents/index.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import createRouter from './router'; +import BaseApp from './base_app.vue'; + +Vue.use(VueApollo); + +export default () => { + const el = document.getElementById('js-mount-index-ml-agents'); + + if (!el) { + return false; + } + + const { basePath, projectPath } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + const router = createRouter(basePath); + + return new Vue({ + el, + name: 'AiAgents', + apolloProvider, + router, + provide: { + projectPath, + }, + render(h) { + return h(BaseApp); + }, + }); +}; diff --git a/ee/app/assets/javascripts/ml/ai_agents/router.js b/ee/app/assets/javascripts/ml/ai_agents/router.js new file mode 100644 index 0000000000000000000000000000000000000000..e384cb49e4e71f46ec9b7d9d0f3a1ff2c3f5938b --- /dev/null +++ b/ee/app/assets/javascripts/ml/ai_agents/router.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import ListAgents from 'ee/ml/ai_agents/views/list_agents.vue'; +import ShowAgent from 'ee/ml/ai_agents/views/show_agent.vue'; +import CreateAgent from 'ee/ml/ai_agents/views/create_agent.vue'; +import { ROUTE_LIST_AGENTS, ROUTE_NEW_AGENT, ROUTE_SHOW_AGENT } from './constants'; + +Vue.use(VueRouter); + +export default function createRouter(base) { + const router = new VueRouter({ + base, + mode: 'history', + routes: [ + { + name: ROUTE_LIST_AGENTS, + path: '/', + component: ListAgents, + }, + { + name: ROUTE_NEW_AGENT, + path: '/new', + component: CreateAgent, + }, + { + name: ROUTE_SHOW_AGENT, + path: '/:agentId', + component: ShowAgent, + }, + ], + }); + + return router; +} diff --git a/ee/app/assets/javascripts/ml/ai_agents/apps/create_agent.vue b/ee/app/assets/javascripts/ml/ai_agents/views/create_agent.vue similarity index 90% rename from ee/app/assets/javascripts/ml/ai_agents/apps/create_agent.vue rename to ee/app/assets/javascripts/ml/ai_agents/views/create_agent.vue index 0b94b33c15ce052451c8fc45f4dff12738eaf47c..3cc9658a2048f37e8e54cf4c2dcb1f2a83840f49 100644 --- a/ee/app/assets/javascripts/ml/ai_agents/apps/create_agent.vue +++ b/ee/app/assets/javascripts/ml/ai_agents/views/create_agent.vue @@ -11,7 +11,6 @@ import { import { s__ } from '~/locale'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import { helpPagePath } from '~/helpers/help_page_helper'; -import { visitUrl } from '~/lib/utils/url_utility'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import createAiAgent from '../graphql/mutations/create_ai_agent.mutation.graphql'; @@ -27,12 +26,7 @@ export default { GlButton, GlAlert, }, - props: { - projectPath: { - type: String, - required: true, - }, - }, + inject: ['projectPath'], data() { return { errorMessage: undefined, @@ -60,7 +54,10 @@ export default { if (error) { this.errorMessage = data.aiAgentCreate.errors.join(', '); } else { - visitUrl(data?.aiAgentCreate?.agent?._links?.showPath); + this.$router.push({ + name: 'show', + params: { agentId: data?.aiAgentCreate?.agent?.routeId }, + }); } } catch (error) { Sentry.captureException(error); @@ -89,7 +86,7 @@ export default { <gl-form @submit.prevent="createAgent"> <gl-form-group :label="s__('AIAgents|Agent name')"> - <gl-form-input v-model="agentName" /> + <gl-form-input v-model="agentName" data-testid="agent-name" /> </gl-form-group> <gl-form-group :label="__('Prompt')" optional> diff --git a/ee/app/assets/javascripts/ml/ai_agents/apps/list_agents.vue b/ee/app/assets/javascripts/ml/ai_agents/views/list_agents.vue similarity index 73% rename from ee/app/assets/javascripts/ml/ai_agents/apps/list_agents.vue rename to ee/app/assets/javascripts/ml/ai_agents/views/list_agents.vue index 6b536f720fc272cd45e8c59ad9ae8e75642e4380..e08e10eac924340ef538494501fbbc0edb5fdee1 100644 --- a/ee/app/assets/javascripts/ml/ai_agents/apps/list_agents.vue +++ b/ee/app/assets/javascripts/ml/ai_agents/views/list_agents.vue @@ -1,8 +1,9 @@ <script> -import { GlBadge, GlButton } from '@gitlab/ui'; +import { GlBadge } from '@gitlab/ui'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import { helpPagePath } from '~/helpers/help_page_helper'; import AgentList from '../components/agent_list.vue'; +import { ROUTE_NEW_AGENT } from '../constants'; export default { name: 'ListAiAgents', @@ -10,29 +11,20 @@ export default { TitleArea, AgentList, GlBadge, - GlButton, }, provide() { return { projectPath: this.projectPath, }; }, - props: { - projectPath: { - type: String, - required: true, - }, - createAgentPath: { - type: String, - required: true, - }, - }, + inject: ['projectPath'], data() { return { errorMessage: undefined, }; }, helpPagePath: helpPagePath('policy/experiment-beta-support', { anchor: 'experiment' }), + ROUTE_NEW_AGENT, }; </script> @@ -48,10 +40,15 @@ export default { </div> </template> <template #right-actions> - <gl-button :href="createAgentPath">{{ s__('AIAgents|Create agent') }}</gl-button> + <router-link + :to="{ name: $options.ROUTE_NEW_AGENT }" + class="btn btn-confirm btn-md gl-button" + > + {{ s__('AIAgents|Create agent') }} + </router-link> </template> </title-area> - <agent-list :create-agent-path="createAgentPath" /> + <agent-list /> </div> </template> diff --git a/ee/app/assets/javascripts/ml/ai_agents/apps/show_agent.vue b/ee/app/assets/javascripts/ml/ai_agents/views/show_agent.vue similarity index 81% rename from ee/app/assets/javascripts/ml/ai_agents/apps/show_agent.vue rename to ee/app/assets/javascripts/ml/ai_agents/views/show_agent.vue index a1a264ccd0e42bf9ecdae97045d929151f49132d..ea1712171e19f52e591d8ab830fd7b9f91f6257d 100644 --- a/ee/app/assets/javascripts/ml/ai_agents/apps/show_agent.vue +++ b/ee/app/assets/javascripts/ml/ai_agents/views/show_agent.vue @@ -14,19 +14,10 @@ export default { projectPath: this.projectPath, }; }, - props: { - projectPath: { - type: String, - required: true, - }, - agentId: { - type: String, - required: true, - }, - }, + inject: ['projectPath'], computed: { title() { - return sprintf(s__('AIAgent|AI Agent: %{agentId}'), { agentId: this.agentId }); + return sprintf(s__('AIAgent|AI Agent: %{agentId}'), { agentId: this.$route.params.agentId }); }, }, }; diff --git a/ee/app/assets/javascripts/pages/projects/ml/agents/index.js b/ee/app/assets/javascripts/pages/projects/ml/agents/index.js new file mode 100644 index 0000000000000000000000000000000000000000..39b998f32ae4291cb9e3d0b0ec00839a9c239755 --- /dev/null +++ b/ee/app/assets/javascripts/pages/projects/ml/agents/index.js @@ -0,0 +1,3 @@ +import initMlAiAgents from 'ee/ml/ai_agents'; + +initMlAiAgents(); diff --git a/ee/app/assets/javascripts/pages/projects/ml/agents/index/index.js b/ee/app/assets/javascripts/pages/projects/ml/agents/index/index.js deleted file mode 100644 index cea34c0e7ed04dc84ce21dc2878a892b9d286c33..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/pages/projects/ml/agents/index/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import { initSimpleApp } from '~/helpers/init_simple_app_helper'; -import { ListAgents } from 'ee/ml/ai_agents/apps'; - -initSimpleApp('#js-mount-index-ml-agents', ListAgents, { withApolloProvider: true }); diff --git a/ee/app/assets/javascripts/pages/projects/ml/agents/new/index.js b/ee/app/assets/javascripts/pages/projects/ml/agents/new/index.js deleted file mode 100644 index 72fdad4d653b7592f32f9e6cab94f2253e18f136..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/pages/projects/ml/agents/new/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import { initSimpleApp } from '~/helpers/init_simple_app_helper'; -import { CreateAgent } from 'ee/ml/ai_agents/apps'; - -initSimpleApp('#js-mount-new-ml-agent', CreateAgent, { withApolloProvider: true }); diff --git a/ee/app/assets/javascripts/pages/projects/ml/agents/show/index.js b/ee/app/assets/javascripts/pages/projects/ml/agents/show/index.js deleted file mode 100644 index 8a9370df8a5a537c387361b5c61729f8900fc79a..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/pages/projects/ml/agents/show/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import { initSimpleApp } from '~/helpers/init_simple_app_helper'; -import { ShowAgent } from 'ee/ml/ai_agents/apps'; - -initSimpleApp('#js-mount-show-ml-agent', ShowAgent, { withApolloProvider: true }); diff --git a/ee/app/controllers/projects/ml/agents_controller.rb b/ee/app/controllers/projects/ml/agents_controller.rb index 6de7b9c4d466eefa6d2c43be3dd95d3624bab6d7..132fa556bb9477f85a336d5c68732e2837635f0e 100644 --- a/ee/app/controllers/projects/ml/agents_controller.rb +++ b/ee/app/controllers/projects/ml/agents_controller.rb @@ -4,29 +4,16 @@ module Projects module Ml class AgentsController < Projects::ApplicationController before_action :authorize_read_ai_agents! - before_action :authorize_write_ai_agents!, only: [:new] feature_category :mlops - MAX_MODELS_PER_PAGE = 20 - def index; end - def new; end - - def show - @agent_id = params[:agent_id] - end - private def authorize_read_ai_agents! render_404 unless can?(current_user, :read_ai_agents, @project) end - - def authorize_write_ai_agents! - render_404 unless can?(current_user, :write_ai_agents, @project) - end end end end diff --git a/ee/app/graphql/types/ai/agents/agent_links_type.rb b/ee/app/graphql/types/ai/agents/agent_links_type.rb deleted file mode 100644 index 3fb94a24c03be065d34680f57f5515953c980feb..0000000000000000000000000000000000000000 --- a/ee/app/graphql/types/ai/agents/agent_links_type.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Types - module Ai - module Agents - # rubocop: disable Graphql/AuthorizeTypes -- authorization in resolver/mutation - class AgentLinksType < Types::BaseObject - graphql_name 'AiAgentLinks' - description 'Represents links to perform actions on the agent' - - present_using ::Ai::AgentPresenter - - field :show_path, GraphQL::Types::String, - null: true, - description: 'Path to the details page of the agent.', - method: :path - end - # rubocop: enable Graphql/AuthorizeTypes - end - end -end diff --git a/ee/app/graphql/types/ai/agents/agent_type.rb b/ee/app/graphql/types/ai/agents/agent_type.rb index 65fb939a7964105568005e13364506cb8c3077c4..b31338fae427ab7b241d09d6f4bc0bfdd57b41f1 100644 --- a/ee/app/graphql/types/ai/agents/agent_type.rb +++ b/ee/app/graphql/types/ai/agents/agent_type.rb @@ -10,11 +10,10 @@ class AgentType < ::Types::BaseObject present_using ::Ai::AgentPresenter - field :_links, ::Types::Ai::Agents::AgentLinksType, null: false, method: :itself, - description: 'Map of links to perform actions on the agent.' field :created_at, Types::TimeType, null: false, description: 'Date of creation.' field :id, GraphQL::Types::ID, null: false, description: 'ID of the agent.' field :name, GraphQL::Types::String, null: false, description: 'Name of the agent.' + field :route_id, GraphQL::Types::Int, null: false, description: 'Route ID of the agent.' field :versions, [Types::Ai::Agents::AgentVersionType], null: true, description: 'Versions of the agent.' end # rubocop: enable Graphql/AuthorizeTypes diff --git a/ee/app/presenters/ai/agent_presenter.rb b/ee/app/presenters/ai/agent_presenter.rb index cca6ad4702df1a56f61611653365e80351378460..eb82eb2854087696535f8a05ca76f93307c64b59 100644 --- a/ee/app/presenters/ai/agent_presenter.rb +++ b/ee/app/presenters/ai/agent_presenter.rb @@ -4,8 +4,8 @@ module Ai class AgentPresenter < Gitlab::View::Presenter::Delegated presents ::Ai::Agent, as: :agent - def path - project_ml_agent_path(agent.project, agent.id) + def route_id + agent.id end end end diff --git a/ee/app/views/projects/ml/agents/index.html.haml b/ee/app/views/projects/ml/agents/index.html.haml index 09c79773255761fe41efbb35aadff5c33cb21c62..615276826cd0b176cd1374bf7df42589a19bda1d 100644 --- a/ee/app/views/projects/ml/agents/index.html.haml +++ b/ee/app/views/projects/ml/agents/index.html.haml @@ -1,6 +1,4 @@ - breadcrumb_title s_('AIAgents|AI Agents') - page_title s_('AIAgents|AI Agents') -- create_agent_path = new_project_ml_agent_path(@project) -- view_model = Gitlab::Json.generate({ projectPath: @project.full_path, createAgentPath: create_agent_path }) -#js-mount-index-ml-agents{ data: { view_model: view_model } } +#js-mount-index-ml-agents{ data: { project_path: @project.full_path, base_path: namespace_project_ml_agents_path(@project.namespace, @project) } } diff --git a/ee/app/views/projects/ml/agents/new.html.haml b/ee/app/views/projects/ml/agents/new.html.haml deleted file mode 100644 index 7bb7166c61bf2eed39d9f56df70d867819e20c1b..0000000000000000000000000000000000000000 --- a/ee/app/views/projects/ml/agents/new.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- breadcrumb_title s_('AIAgents|AI Agents') -- page_title s_('AIAgents|New AI Agent') -- view_model = Gitlab::Json.generate({ projectPath: @project.full_path }) - -#js-mount-new-ml-agent{ data: { view_model: view_model } } diff --git a/ee/app/views/projects/ml/agents/show.html.haml b/ee/app/views/projects/ml/agents/show.html.haml deleted file mode 100644 index 1885c90bee4476ce36dab1d8c2bbd451c48a01a3..0000000000000000000000000000000000000000 --- a/ee/app/views/projects/ml/agents/show.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- breadcrumb_title s_('AiAgents|AI Agents') -- page_title s_('AiAgents|AI agent') -- view_model = Gitlab::Json.generate({ projectPath: @project.full_path, agentId: @agent_id }) - -#js-mount-show-ml-agent{ data: { view_model: view_model } } diff --git a/ee/config/routes/project.rb b/ee/config/routes/project.rb index 7620d24eb335858131180758c3e181712c90a7d4..986a51f8bc6ef141dad9f41b012c2d4af7a90e7a 100644 --- a/ee/config/routes/project.rb +++ b/ee/config/routes/project.rb @@ -156,7 +156,7 @@ resources :logs, only: [:index], controller: :logs namespace :ml do - resources :agents, only: [:index, :new, :show], controller: 'agents', param: :agent_id + resources :agents, path: 'agents(/*vueroute)', action: :index end end # End of the /-/ scope. diff --git a/ee/spec/controllers/projects/ml/agents_controller_spec.rb b/ee/spec/controllers/projects/ml/agents_controller_spec.rb index 6bf30203fee85f5f652c728862be6254f5f36608..1f5131988f7bf72755d07297378a9ee85f6d4fed 100644 --- a/ee/spec/controllers/projects/ml/agents_controller_spec.rb +++ b/ee/spec/controllers/projects/ml/agents_controller_spec.rb @@ -7,16 +7,12 @@ let_it_be(:user) { project.first_owner } let(:read_ai_agents) { true } - let(:write_ai_agents) { true } before do allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?) .with(user, :read_ai_agents, project) .and_return(read_ai_agents) - allow(Ability).to receive(:allowed?) - .with(user, :write_ai_agents, project) - .and_return(write_ai_agents) sign_in(user) end @@ -39,48 +35,4 @@ end end end - - describe 'GET new' do - subject(:new_request) do - get :new, params: { namespace_id: project.namespace, project_id: project } - response - end - - it 'renders the template' do - expect(new_request).to render_template(:new) - end - - context 'when user does not have access' do - let(:write_ai_agents) { false } - - it 'renders 404' do - expect(new_request).to have_gitlab_http_status(:not_found) - end - end - end - - describe 'GET show' do - subject(:show_request) do - get :show, params: { namespace_id: project.namespace, project_id: project, agent_id: 1 } - response - end - - it 'renders the template' do - expect(show_request).to render_template(:show) - end - - it 'assigns the correct param' do - show_request - - expect(assigns[:agent_id]).to eq('1') - end - - context 'when user does not have access' do - let(:read_ai_agents) { false } - - it 'renders 404' do - expect(show_request).to have_gitlab_http_status(:not_found) - end - end - end end diff --git a/ee/spec/features/ai_agents_spec.rb b/ee/spec/features/ai_agents_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..eeeee8d0dc087ebb713f1f65663875df606c7f12 --- /dev/null +++ b/ee/spec/features/ai_agents_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'AI Agents', :js, feature_category: :mlops do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user, namespace: project.namespace) } + let_it_be(:project_member) { create(:project_member, :reporter, project: project, user: user) } + + before do + stub_licensed_features(ai_agents: true) + stub_feature_flags(agent_registry: true) + sign_in(user) + end + + context 'with an authenticated user with reporter permissions' do + it 'shows the AI Agents empty state screen when no agents exist' do + visit project_ml_agents_path(project) + expect(page).to have_content('Create your own AI Agents') + end + + it 'shows a list of AI Agents when they exist for a project' do + create(:ai_agent, name: "my-agent", project: project) + + visit project_ml_agents_path(project) + + expect(page).to have_content('AI Agents') + expect(page).to have_content('my-agent') + end + + it 'shows the view screen when clicking on an agent name' do + agent1 = create(:ai_agent, name: "my-agent", project: project) + create(:ai_agent, name: "my-agent-2", project: project) + + visit project_ml_agents_path(project) + + expect(page).to have_content('AI Agents') + expect(page).to have_content('my-agent') + expect(page).to have_content('my-agent-2') + + click_on('my-agent') + + expect(page).to have_content("AI Agent: #{agent1.id}") + end + + it 'shows the create screen when the button is clicked' do + visit project_ml_agents_path(project) + expect(page).to have_content('Create your own AI Agents') + + click_on('Create agent') + + expect(page).to have_content('Agent name') + expect(page).to have_content('Prompt (optional)') + end + + it 'creates an AI agent when the data is supplied and the button clicked' do + visit new_project_ml_agent_path(project) + expect(page).to have_content('New agent') + expect(page).to have_content('Agent name') + expect(page).to have_content('Prompt (optional)') + + find('input[data-testid="agent-name"]').set('my-agent-name') + + click_on('Create agent') + + expect(page).to have_content("AI Agent:") + expect(page).not_to have_content('New agent') + expect(page).not_to have_content('Agent name') + expect(page).not_to have_content('Prompt (optional)') + end + end +end diff --git a/ee/spec/frontend/ml/ai_agents/components/agent_list_spec.js b/ee/spec/frontend/ml/ai_agents/components/agent_list_spec.js index 5139e0dda522ebb0dd0e91a1c20cf6885ed00ede..2fdbe792a8d870eab53b881d7a73cf3cb79e4675 100644 --- a/ee/spec/frontend/ml/ai_agents/components/agent_list_spec.js +++ b/ee/spec/frontend/ml/ai_agents/components/agent_list_spec.js @@ -1,4 +1,5 @@ import { GlTableLite, GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; +import { RouterLinkStub as RouterLink } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { mountExtended } from 'helpers/vue_test_utils_helper'; @@ -18,6 +19,9 @@ describe('AI Agents List View', () => { apolloProvider, provide: { projectPath: 'path/to/project' }, propsData: { createAgentPath: 'path/to/create' }, + stubs: { + RouterLink, + }, }); }; diff --git a/ee/spec/frontend/ml/ai_agents/graphql/mocks.js b/ee/spec/frontend/ml/ai_agents/graphql/mocks.js index dcfb18692cb5fdec7b47532b1e1c6b40f2f85129..c0b3b2ed04bfb17e517fbe2ba2d3ff8909439b8c 100644 --- a/ee/spec/frontend/ml/ai_agents/graphql/mocks.js +++ b/ee/spec/frontend/ml/ai_agents/graphql/mocks.js @@ -4,9 +4,7 @@ export const createAiAgentsResponses = { aiAgentCreate: { agent: { id: 'gid://gitlab/Ai::Agent/1', - _links: { - showPath: '/some/project/-/ml/agents/1', - }, + routeId: 2, }, errors: [], }, @@ -30,6 +28,7 @@ export const listAiAgentsResponses = { nodes: [ { id: 'gid://gitlab/Ai::Agent/1', + routeId: 2, name: 'agent-1', versions: [ { @@ -38,9 +37,6 @@ export const listAiAgentsResponses = { model: 'default', }, ], - _links: { - showPath: '/namespace/projects/-/ml/agents/1', - }, }, ], pageInfo: { diff --git a/ee/spec/frontend/ml/ai_agents/apps/create_agent_spec.js b/ee/spec/frontend/ml/ai_agents/views/create_agent_spec.js similarity index 86% rename from ee/spec/frontend/ml/ai_agents/apps/create_agent_spec.js rename to ee/spec/frontend/ml/ai_agents/views/create_agent_spec.js index 40830a1172a38eb7c53abadcc2b9d8bfe33e644f..d136903ef2281ff4614b5de6ffc53e1274f5baa3 100644 --- a/ee/spec/frontend/ml/ai_agents/apps/create_agent_spec.js +++ b/ee/spec/frontend/ml/ai_agents/views/create_agent_spec.js @@ -8,25 +8,24 @@ import { } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import { CreateAgent } from 'ee/ml/ai_agents/apps'; +import CreateAgent from 'ee/ml/ai_agents/views/create_agent.vue'; import createAiAgentMutation from 'ee/ml/ai_agents/graphql/mutations/create_ai_agent.mutation.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { visitUrl } from '~/lib/utils/url_utility'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import { createAiAgentsResponses } from '../graphql/mocks'; -jest.mock('~/lib/utils/url_utility', () => ({ - ...jest.requireActual('~/lib/utils/url_utility'), - visitUrl: jest.fn(), -})); - -describe('ee/ml/ai_agents/apps/create_agent', () => { +describe('ee/ml/ai_agents/views/create_agent', () => { let wrapper; let apolloProvider; + const push = jest.fn(); + const $router = { + push, + }; + Vue.use(VueApollo); beforeEach(() => { @@ -41,7 +40,10 @@ describe('ee/ml/ai_agents/apps/create_agent', () => { wrapper = shallowMountExtended(CreateAgent, { apolloProvider, - propsData: { projectPath: 'project/path' }, + provide: { projectPath: 'project/path' }, + mocks: { + $router, + }, }); }; @@ -99,7 +101,10 @@ describe('ee/ml/ai_agents/apps/create_agent', () => { await submitForm(); - expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/agents/1'); + expect($router.push).toHaveBeenCalledWith({ + name: 'show', + params: { agentId: 2 }, + }); }); it('shows errors when result is a top level error', async () => { @@ -109,7 +114,7 @@ describe('ee/ml/ai_agents/apps/create_agent', () => { await submitForm(); expect(findErrorAlert().text()).toBe('An error has occurred when saving the agent.'); - expect(visitUrl).not.toHaveBeenCalled(); + expect(push).not.toHaveBeenCalled(); }); it('shows errors when result is a validation error', async () => { @@ -118,6 +123,6 @@ describe('ee/ml/ai_agents/apps/create_agent', () => { await submitForm(); expect(findErrorAlert().text()).toBe("Name is invalid, Name can't be blank"); - expect(visitUrl).not.toHaveBeenCalled(); + expect(push).not.toHaveBeenCalled(); }); }); diff --git a/ee/spec/frontend/ml/ai_agents/apps/list_agents_spec.js b/ee/spec/frontend/ml/ai_agents/views/list_agents_spec.js similarity index 66% rename from ee/spec/frontend/ml/ai_agents/apps/list_agents_spec.js rename to ee/spec/frontend/ml/ai_agents/views/list_agents_spec.js index dc174aaecbc4b96086e36ec7027bc2e6f39baef1..4bd2f8afa48d8fe42642b29120906f0994caecd1 100644 --- a/ee/spec/frontend/ml/ai_agents/apps/list_agents_spec.js +++ b/ee/spec/frontend/ml/ai_agents/views/list_agents_spec.js @@ -1,6 +1,7 @@ -import { GlBadge, GlButton } from '@gitlab/ui'; +import { GlBadge } from '@gitlab/ui'; +import { RouterLinkStub as RouterLink } from '@vue/test-utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { ListAgents } from 'ee/ml/ai_agents/apps'; +import ListAgents from 'ee/ml/ai_agents/views/list_agents.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import AgentList from 'ee/ml/ai_agents/components/agent_list.vue'; @@ -8,16 +9,19 @@ let wrapper; const createWrapper = () => { wrapper = shallowMountExtended(ListAgents, { - propsData: { projectPath: 'path/to/project', createAgentPath: 'path/to/create' }, + provide: { projectPath: 'path/to/project' }, + stubs: { + RouterLink, + }, }); }; const findTitleArea = () => wrapper.findComponent(TitleArea); -const findCreateButton = () => findTitleArea().findComponent(GlButton); +const findCreateButton = () => findTitleArea().findComponent(RouterLink); const findBadge = () => wrapper.findComponent(GlBadge); const findAgentList = () => wrapper.findComponent(AgentList); -describe('ee/ml/ai_agents/apps/list_agents', () => { +describe('ee/ml/ai_agents/views/list_agents', () => { beforeEach(() => createWrapper()); it('shows the title', () => { @@ -29,7 +33,9 @@ describe('ee/ml/ai_agents/apps/list_agents', () => { }); it('shows create agent button', () => { - expect(findCreateButton().attributes().href).toBe('path/to/create'); + expect(findCreateButton().props('to')).toMatchObject({ + name: 'create', + }); }); it('shows the agent list', () => { diff --git a/ee/spec/frontend/ml/ai_agents/apps/show_agent_spec.js b/ee/spec/frontend/ml/ai_agents/views/show_agent_spec.js similarity index 59% rename from ee/spec/frontend/ml/ai_agents/apps/show_agent_spec.js rename to ee/spec/frontend/ml/ai_agents/views/show_agent_spec.js index ebda18af7f0fa1cfc7768790bb5389c31cb5eed2..fb73d3043363fc08dea10f2e422d56caa44aef6a 100644 --- a/ee/spec/frontend/ml/ai_agents/apps/show_agent_spec.js +++ b/ee/spec/frontend/ml/ai_agents/views/show_agent_spec.js @@ -1,20 +1,27 @@ import { GlExperimentBadge } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { ShowAgent } from 'ee/ml/ai_agents/apps'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ShowAgent from 'ee/ml/ai_agents/views/show_agent.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; let wrapper; const createWrapper = () => { - wrapper = shallowMount(ShowAgent, { - propsData: { projectPath: 'path/to/project', agentId: '2' }, + wrapper = shallowMountExtended(ShowAgent, { + provide: { projectPath: 'path/to/project' }, + mocks: { + $route: { + params: { + agentId: 2, + }, + }, + }, }); }; const findTitleArea = () => wrapper.findComponent(TitleArea); const findBadge = () => wrapper.findComponent(GlExperimentBadge); -describe('ee/ml/ai_agents/apps/create_agent', () => { +describe('ee/ml/ai_agents/views/create_agent', () => { beforeEach(() => createWrapper()); it('shows the title', () => { diff --git a/ee/spec/graphql/types/ai/agents/agent_links_type_spec.rb b/ee/spec/graphql/types/ai/agents/agent_links_type_spec.rb deleted file mode 100644 index 8eeb1febc0e31771a6568aaaa390d39d1ab2fd1e..0000000000000000000000000000000000000000 --- a/ee/spec/graphql/types/ai/agents/agent_links_type_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe GitlabSchema.types['AiAgentLinks'], feature_category: :mlops do - it 'has the expected fields' do - expected_fields = %w[showPath] - - expect(described_class).to include_graphql_fields(*expected_fields) - end -end diff --git a/ee/spec/graphql/types/ai/agents/agent_type_spec.rb b/ee/spec/graphql/types/ai/agents/agent_type_spec.rb index 8e5489e6a4a06f2eedc7c145cac2701863e52689..f0c7116cb8078d91a76e0af2dcb7a82cda5cd483 100644 --- a/ee/spec/graphql/types/ai/agents/agent_type_spec.rb +++ b/ee/spec/graphql/types/ai/agents/agent_type_spec.rb @@ -4,7 +4,7 @@ RSpec.describe GitlabSchema.types['AiAgent'], feature_category: :mlops do it 'has specific fields' do - expected_fields = %w[id name created_at versions _links] + expected_fields = %w[id name created_at versions] expect(described_class).to include_graphql_fields(*expected_fields) end diff --git a/ee/spec/presenters/ai/agent_presenter_spec.rb b/ee/spec/presenters/ai/agent_presenter_spec.rb index a566373d625ae41fc30384458839a28aae7b2513..e91f8df6f374d773509b90c607edc8a4bd4accda 100644 --- a/ee/spec/presenters/ai/agent_presenter_spec.rb +++ b/ee/spec/presenters/ai/agent_presenter_spec.rb @@ -6,9 +6,9 @@ let(:project) { build_stubbed(:project) } let(:agent) { build_stubbed(:ai_agent, project: project) } - describe '#path' do - subject { agent.present.path } + describe '#route_id' do + subject { agent.present.route_id } - it { is_expected.to eq("/#{project.full_path}/-/ml/agents/#{agent.id}") } + it { is_expected.to eq(agent.id) } end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c087347f81527be9164a535bfe0f379e753a31e6..a54584ff1b2b022d324a3816fda6379fcfdd8f8e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1931,9 +1931,6 @@ msgstr "" msgid "AIAgents|Create agent" msgstr "" -msgid "AIAgents|New AI Agent" -msgstr "" - msgid "AIAgents|New agent" msgstr "" @@ -4427,12 +4424,6 @@ msgstr "" msgid "Agent not found for provided id." msgstr "" -msgid "AiAgents|AI Agents" -msgstr "" - -msgid "AiAgents|AI agent" -msgstr "" - msgid "AiAgents|Agent Name" msgstr "" @@ -4442,9 +4433,6 @@ msgstr "" msgid "AiAgents|Create your own AI Agents" msgstr "" -msgid "AiAgents|Get started" -msgstr "" - msgid "Akismet" msgstr ""