diff --git a/app/assets/javascripts/vue_shared/components/errors_alert.stories.js b/app/assets/javascripts/vue_shared/components/errors_alert.stories.js
deleted file mode 100644
index e23ce665f1714437c253ebdf809642f91f0508d7..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_shared/components/errors_alert.stories.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import ErrorsAlert from './errors_alert.vue';
-
-export default {
- component: ErrorsAlert,
- title: 'vue_shared/errors_alert',
-};
-
-const defaultArgs = {
- errors: ['The item could not be created.'],
-};
-
-const Template = (args, { argTypes }) => ({
- components: { ErrorsAlert },
- props: Object.keys(argTypes),
- template: ``,
-});
-
-export const Default = Template.bind({});
-Default.args = {
- ...defaultArgs,
-};
-
-export const ErrorsList = Template.bind({});
-ErrorsList.args = {
- ...defaultArgs,
- errors: ['The item could not be created.', 'The item could not be updated.'],
-};
-
-export const WithTitle = Template.bind({});
-WithTitle.args = {
- ...defaultArgs,
- title: 'Following errors occured:',
-};
diff --git a/app/assets/javascripts/vue_shared/components/errors_alert.vue b/app/assets/javascripts/vue_shared/components/errors_alert.vue
deleted file mode 100644
index b5dd8a219cfff3349a5e78f8e2ace7c13851d498..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_shared/components/errors_alert.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
-
- {{ errors[0] }}
-
-
-
-
diff --git a/app/assets/javascripts/vue_shared/components/status_alert.stories.js b/app/assets/javascripts/vue_shared/components/status_alert.stories.js
new file mode 100644
index 0000000000000000000000000000000000000000..1c0765d7b4a4b15f7902726d5c5b08796a6bd51a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/status_alert.stories.js
@@ -0,0 +1,56 @@
+import StatusAlert from './status_alert.vue';
+
+export default {
+ component: StatusAlert,
+ title: 'vue_shared/status_alert',
+};
+
+const defaultArgs = {
+ messages: ['The item could not be created.'],
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { StatusAlert },
+ props: Object.keys(argTypes),
+ template: ``,
+});
+
+const SlotTemplate = (args, { argTypes }) => ({
+ components: { StatusAlert },
+ props: Object.keys(argTypes),
+ template: `
+
+ ${args.slotContent || ''}
+
+ `,
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ ...defaultArgs,
+};
+
+export const ErrorsList = Template.bind({});
+ErrorsList.args = {
+ ...defaultArgs,
+ messages: ['The item could not be created.', 'The item could not be updated.'],
+};
+
+export const WithTitle = Template.bind({});
+WithTitle.args = {
+ ...defaultArgs,
+ variant: 'success',
+ title: 'Item created successfully',
+ messages: ['All is well.', "A little more and we're there."],
+};
+
+export const WithCustomSlotContent = SlotTemplate.bind({});
+WithCustomSlotContent.args = {
+ title: 'Custom Content Alert using the default Slot',
+ variant: 'info',
+ slotContent: `
+
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/status_alert.vue b/app/assets/javascripts/vue_shared/components/status_alert.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d4d2b157b22c62cee13af3a3503dfa2e17a293d7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/status_alert.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+ {{ messages[0] }}
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_agent_form.vue b/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_agent_form.vue
index 50c2d73cad00f7e6556b174f2b984e9f6e46d943..9bdf7a47b840515e5c069e67b18b0c6461697b67 100644
--- a/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_agent_form.vue
+++ b/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_agent_form.vue
@@ -14,7 +14,7 @@ import {
AGENT_VISIBILITY_LEVEL_DESCRIPTIONS,
} from 'ee/ai/catalog/constants';
import { __, s__ } from '~/locale';
-import ErrorsAlert from '~/vue_shared/components/errors_alert.vue';
+import StatusAlert from '~/vue_shared/components/status_alert.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { AI_CATALOG_AGENTS_ROUTE } from '../router/constants';
import { createFieldValidators } from '../utils';
@@ -25,7 +25,6 @@ import VisibilityLevelRadioGroup from './visibility_level_radio_group.vue';
export default {
components: {
- ErrorsAlert,
AiCatalogFormButtons,
FormProjectDropdown,
GlButton,
@@ -33,6 +32,7 @@ export default {
GlFormFields,
GlFormTextarea,
GlTokenSelector,
+ StatusAlert,
VisibilityLevelRadioGroup,
},
mixins: [glFeatureFlagsMixin()],
@@ -254,7 +254,7 @@ export default {
-
+
-
+
-
+
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import PageHeading from '~/vue_shared/components/page_heading.vue';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import { createAlert } from '~/alert';
-import ErrorsAlert from '~/vue_shared/components/errors_alert.vue';
+import StatusAlert from '~/vue_shared/components/status_alert.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import AiCatalogAgentRunForm from '../components/ai_catalog_agent_run_form.vue';
import executeAiCatalogAgent from '../graphql/mutations/execute_ai_catalog_agent.mutation.graphql';
@@ -12,8 +12,10 @@ export default {
name: 'AiCatalogAgentsRun',
components: {
AiCatalogAgentRunForm,
- ErrorsAlert,
+ GlLink,
+ GlSprintf,
PageHeading,
+ StatusAlert,
},
props: {
aiCatalogAgent: {
@@ -24,6 +26,7 @@ export default {
data() {
return {
errors: [],
+ workflow: null,
isSubmitting: false,
};
},
@@ -31,32 +34,26 @@ export default {
pageTitle() {
return `${s__('AICatalog|Run agent')}: ${this.aiCatalogAgent.name}`;
},
+ sessionId() {
+ return this.workflow ? getIdFromGraphQLId(this.workflow.id) : null;
+ },
+ link() {
+ return this.workflow
+ ? `/${this.workflow.project.fullPath}/-/automate/agent-sessions/${this.sessionId}`
+ : '';
+ },
},
methods: {
dismissErrors() {
this.errors = [];
},
- successAlert({ id, project }) {
- const formattedId = getIdFromGraphQLId(id);
-
- // Hardcoded for an easier integration with any page at GitLab.
- // https://gitlab.com/gitlab-org/gitlab/-/blob/b50c9e5ad666bab0e45365b2c6994d99407a68d1/ee/app/assets/javascripts/ai/duo_agents_platform/router/index.js#L61
- const link = `/${project.fullPath}/-/automate/agent-sessions/${formattedId}`;
-
- return {
- message: sprintf(
- s__(
- `AICatalog|Test run executed successfully, see %{linkStart}Session %{formattedId}%{linkEnd}.`,
- ),
- { formattedId },
- ),
- variant: 'success',
- messageLinks: { link },
- };
+ resetWorkflow() {
+ this.workflow = null;
},
async onSubmit({ userPrompt }) {
try {
this.dismissErrors();
+ this.resetWorkflow();
this.isSubmitting = true;
const { data } = await this.$apollo.mutate({
@@ -71,7 +68,7 @@ export default {
this.errors = errors;
return;
}
- createAlert(this.successAlert(workflow));
+ this.workflow = workflow;
}
} catch (error) {
this.errors = [
@@ -98,7 +95,32 @@ export default {
-
+
+
+
+
+ {{ sessionId }}
+
+
+
+ {{ sessionId }}
+
+
+
+
+
-
+
-
+
{
let wrapper;
let mockApollo;
- const findErrorAlert = () => wrapper.findComponent(ErrorsAlert);
+ const findStatusAlert = () => wrapper.findComponent(StatusAlert);
const findFormFields = () => wrapper.findComponent(GlFormFields);
const findProjectDropdown = () => wrapper.findComponent(FormProjectDropdown);
const findVisibilityLevelRadioGroup = () => wrapper.findComponent(VisibilityLevelRadioGroup);
@@ -189,7 +189,7 @@ describe('AiCatalogAgentForm', () => {
});
it('passes error alert', () => {
- expect(findErrorAlert().props('errors')).toEqual([mockErrorMessage]);
+ expect(findStatusAlert().props('messages')).toEqual([mockErrorMessage]);
});
it('renders errors with form errors', async () => {
@@ -197,11 +197,11 @@ describe('AiCatalogAgentForm', () => {
await findProjectDropdown().vm.$emit('error', formError);
- expect(findErrorAlert().props('errors')).toEqual([mockErrorMessage, formError]);
+ expect(findStatusAlert().props('messages')).toEqual([mockErrorMessage, formError]);
});
it('emits dismiss-errors event', () => {
- findErrorAlert().vm.$emit('dismiss');
+ findStatusAlert().vm.$emit('dismiss');
expect(wrapper.emitted('dismiss-errors')).toHaveLength(1);
});
diff --git a/ee/spec/frontend/ai/catalog/components/ai_catalog_flow_form_spec.js b/ee/spec/frontend/ai/catalog/components/ai_catalog_flow_form_spec.js
index 5b3394172d4c9e407560cf94556845ffaef1d6d5..a91934d28b7321ad947519346bf19a2fbe2cdd69 100644
--- a/ee/spec/frontend/ai/catalog/components/ai_catalog_flow_form_spec.js
+++ b/ee/spec/frontend/ai/catalog/components/ai_catalog_flow_form_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { GlFormFields } from '@gitlab/ui';
-import ErrorsAlert from '~/vue_shared/components/errors_alert.vue';
+import StatusAlert from '~/vue_shared/components/status_alert.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AiCatalogFlowForm from 'ee/ai/catalog/components/ai_catalog_flow_form.vue';
import AiCatalogStepsEditor from 'ee/ai/catalog/components/ai_catalog_steps_editor.vue';
@@ -12,7 +12,7 @@ import { VISIBILITY_LEVEL_PRIVATE, VISIBILITY_LEVEL_PUBLIC } from 'ee/ai/catalog
describe('AiCatalogFlowForm', () => {
let wrapper;
- const findErrorAlert = () => wrapper.findComponent(ErrorsAlert);
+ const findStatusAlert = () => wrapper.findComponent(StatusAlert);
const findFormFields = () => wrapper.findComponent(GlFormFields);
const findProjectDropdown = () => wrapper.findComponent(FormProjectDropdown);
const findVisibilityLevelRadioGroup = () => wrapper.findComponent(VisibilityLevelRadioGroup);
@@ -161,7 +161,7 @@ describe('AiCatalogFlowForm', () => {
});
it('passes error alert', () => {
- expect(findErrorAlert().props('errors')).toEqual([mockError]);
+ expect(findStatusAlert().props('messages')).toEqual([mockError]);
});
it('renders errors with form errors', async () => {
@@ -169,11 +169,11 @@ describe('AiCatalogFlowForm', () => {
await findProjectDropdown().vm.$emit('error', formError);
- expect(findErrorAlert().props('errors')).toEqual([mockError, formError]);
+ expect(findStatusAlert().props('messages')).toEqual([mockError, formError]);
});
it('emits dismiss-errors event', () => {
- findErrorAlert().vm.$emit('dismiss');
+ findStatusAlert().vm.$emit('dismiss');
expect(wrapper.emitted('dismiss-errors')).toHaveLength(1);
});
diff --git a/ee/spec/frontend/ai/catalog/pages/ai_catalog_agents_run_spec.js b/ee/spec/frontend/ai/catalog/pages/ai_catalog_agents_run_spec.js
index 6969e89d0e8f56c0e8edd514b0480ba30ab231eb..318f697974754ac76eb8059725f8d4b9ce7e68c1 100644
--- a/ee/spec/frontend/ai/catalog/pages/ai_catalog_agents_run_spec.js
+++ b/ee/spec/frontend/ai/catalog/pages/ai_catalog_agents_run_spec.js
@@ -1,9 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ErrorsAlert from '~/vue_shared/components/errors_alert.vue';
import PageHeading from '~/vue_shared/components/page_heading.vue';
-import { createAlert } from '~/alert';
import AiCatalogAgentsRun from 'ee/ai/catalog/pages/ai_catalog_agents_run.vue';
import AiCatalogAgentRunForm from 'ee/ai/catalog/components/ai_catalog_agent_run_form.vue';
import executeAiCatalogAgent from 'ee/ai/catalog/graphql/mutations/execute_ai_catalog_agent.mutation.graphql';
@@ -46,12 +45,16 @@ describe('AiCatalogAgentsRun', () => {
propsData: {
...defaultProps,
},
+ stubs: {
+ GlSprintf,
+ },
});
};
const findPageHeading = () => wrapper.findComponent(PageHeading);
const findRunForm = () => wrapper.findComponent(AiCatalogAgentRunForm);
- const findErrorsAlert = () => wrapper.findComponent(ErrorsAlert);
+ const findErrorAlert = () => wrapper.findByTestId('error-alert');
+ const findSuccessAlert = () => wrapper.findByTestId('success-alert');
const userPrompt = 'prompt';
@@ -85,14 +88,18 @@ describe('AiCatalogAgentsRun', () => {
});
});
- it('shows success alert', async () => {
- await submitForm();
- await waitForPromises();
+ describe('when request succeeds', () => {
+ beforeEach(async () => {
+ await submitForm();
+ await waitForPromises();
+ });
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Test run executed successfully, see %{linkStart}Session 1%{linkEnd}.',
- messageLinks: { link: '/gitlab-duo/test/-/automate/agent-sessions/1' },
- variant: 'success',
+ it('shows success alert with appropriate text', () => {
+ expect(findSuccessAlert().text()).toBe('Test run executed successfully, see Session 1.');
+ });
+
+ it('does not pass messages to the error alert', () => {
+ expect(findErrorAlert().props('messages')).toStrictEqual([]);
});
});
@@ -105,8 +112,12 @@ describe('AiCatalogAgentsRun', () => {
await waitForPromises();
});
- it('shows an alert', () => {
- expect(findErrorsAlert().props('errors')).toEqual(['Could not find agent ID']);
+ it('shows the error alert with appropriate message', () => {
+ expect(findErrorAlert().props('messages')).toStrictEqual(['Could not find agent ID']);
+ });
+
+ it('does not show the success alert', () => {
+ expect(findSuccessAlert().exists()).toBe(false);
});
});
@@ -118,8 +129,12 @@ describe('AiCatalogAgentsRun', () => {
await submitForm();
});
- it('shows the error alert', () => {
- expect(findErrorsAlert().props('errors')).toEqual(['The test run failed. Error']);
+ it('shows the error alert with appropriate message', () => {
+ expect(findErrorAlert().props('messages')).toStrictEqual(['The test run failed. Error']);
+ });
+
+ it('does not show the success alert', () => {
+ expect(findSuccessAlert().exists()).toBe(false);
});
});
});
diff --git a/ee/spec/frontend/ai/catalog/pages/ai_catalog_agents_spec.js b/ee/spec/frontend/ai/catalog/pages/ai_catalog_agents_spec.js
index 16fb62f5f390668ae5ec06df4edbf65ff8acd94c..06a9f5170454d0fd7be84aeb9e9a6ad2d26648f6 100644
--- a/ee/spec/frontend/ai/catalog/pages/ai_catalog_agents_spec.js
+++ b/ee/spec/frontend/ai/catalog/pages/ai_catalog_agents_spec.js
@@ -1,6 +1,6 @@
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
-import ErrorsAlert from '~/vue_shared/components/errors_alert.vue';
+import StatusAlert from '~/vue_shared/components/status_alert.vue';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -90,7 +90,7 @@ describe('AiCatalogAgents', () => {
});
};
- const findErrorsAlert = () => wrapper.findComponent(ErrorsAlert);
+ const findStatusAlert = () => wrapper.findComponent(StatusAlert);
const findAiCatalogList = () => wrapper.findComponent(AiCatalogList);
const findAiCatalogItemDrawer = () => wrapper.findComponent(AiCatalogItemDrawer);
const findAiCatalogItemConsumerModal = () => wrapper.findComponent(AiCatalogItemConsumerModal);
@@ -277,7 +277,7 @@ describe('AiCatalogAgents', () => {
});
it('displays permission error message', () => {
- expect(findErrorsAlert().props('errors')).toStrictEqual([agentNotFoundErrorMessage]);
+ expect(findStatusAlert().props('messages')).toStrictEqual([agentNotFoundErrorMessage]);
});
it('does not log to Sentry for permission issues', () => {
@@ -304,7 +304,7 @@ describe('AiCatalogAgents', () => {
});
it('displays error message', () => {
- expect(findErrorsAlert().props('errors')).toStrictEqual(['Network error']);
+ expect(findStatusAlert().props('messages')).toStrictEqual(['Network error']);
});
it('logs error to Sentry', () => {
@@ -375,7 +375,7 @@ describe('AiCatalogAgents', () => {
createConsumer();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toEqual([
+ expect(findStatusAlert().props('messages')).toEqual([
`Agent could not be added: ${mockAgent.name}`,
'Item already configured.',
]);
@@ -389,7 +389,7 @@ describe('AiCatalogAgents', () => {
createConsumer();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toEqual([
+ expect(findStatusAlert().props('messages')).toEqual([
'The agent could not be added to the project. Try again. Error: Request failed',
]);
expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
@@ -434,7 +434,7 @@ describe('AiCatalogAgents', () => {
deleteAgent();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toStrictEqual([
+ expect(findStatusAlert().props('messages')).toStrictEqual([
'Failed to delete agent. You do not have permission to delete this AI agent.',
]);
});
@@ -447,7 +447,7 @@ describe('AiCatalogAgents', () => {
deleteAgent();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toStrictEqual([
+ expect(findStatusAlert().props('messages')).toStrictEqual([
'Failed to delete agent. Error: Request failed',
]);
expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
@@ -501,7 +501,7 @@ describe('AiCatalogAgents', () => {
it('shows error message and logs to Sentry', async () => {
await duplicateAgent(3);
- expect(findErrorsAlert().props('errors')).toStrictEqual([agentNotFoundErrorMessage]);
+ expect(findStatusAlert().props('messages')).toStrictEqual([agentNotFoundErrorMessage]);
expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
});
});
diff --git a/ee/spec/frontend/ai/catalog/pages/ai_catalog_flows_spec.js b/ee/spec/frontend/ai/catalog/pages/ai_catalog_flows_spec.js
index f83eb9ba4f7f9ed0062016ad225520fc4bbfd794..03578f936a8473a71ba1c7ac7254de9f828a58e8 100644
--- a/ee/spec/frontend/ai/catalog/pages/ai_catalog_flows_spec.js
+++ b/ee/spec/frontend/ai/catalog/pages/ai_catalog_flows_spec.js
@@ -2,7 +2,7 @@ import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import ErrorsAlert from '~/vue_shared/components/errors_alert.vue';
+import StatusAlert from '~/vue_shared/components/status_alert.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -80,7 +80,7 @@ describe('AiCatalogFlows', () => {
});
};
- const findErrorsAlert = () => wrapper.findComponent(ErrorsAlert);
+ const findStatusAlert = () => wrapper.findComponent(StatusAlert);
const findAiCatalogList = () => wrapper.findComponent(AiCatalogList);
const findAiCatalogItemDrawer = () => wrapper.findComponent(AiCatalogItemDrawer);
const findAiCatalogItemConsumerModal = () => wrapper.findComponent(AiCatalogItemConsumerModal);
@@ -238,7 +238,7 @@ describe('AiCatalogFlows', () => {
});
it('displays permission error message', () => {
- expect(findErrorsAlert().props('errors')).toEqual(['Flow not found.']);
+ expect(findStatusAlert().props('messages')).toEqual(['Flow not found.']);
});
it('does not log to Sentry for permission issues', () => {
@@ -265,7 +265,7 @@ describe('AiCatalogFlows', () => {
});
it('displays error message', () => {
- expect(findErrorsAlert().props('errors')).toEqual(['Network error']);
+ expect(findStatusAlert().props('messages')).toEqual(['Network error']);
});
it('logs error to Sentry', () => {
@@ -309,7 +309,7 @@ describe('AiCatalogFlows', () => {
deleteFlow();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toEqual([
+ expect(findStatusAlert().props('messages')).toEqual([
'Failed to delete flow. You do not have permission to delete this AI flow.',
]);
});
@@ -322,7 +322,7 @@ describe('AiCatalogFlows', () => {
deleteFlow();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toEqual([
+ expect(findStatusAlert().props('messages')).toEqual([
'Failed to delete flow. Error: Request failed',
]);
expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
@@ -423,7 +423,7 @@ describe('AiCatalogFlows', () => {
createConsumer();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toEqual([
+ expect(findStatusAlert().props('messages')).toEqual([
`Flow could not be added: ${mockFlow.name}`,
'Item already configured.',
]);
@@ -437,7 +437,7 @@ describe('AiCatalogFlows', () => {
createConsumer();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toEqual([
+ expect(findStatusAlert().props('messages')).toEqual([
'The flow could not be added to the project. Try again. Error: Request failed',
]);
expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
diff --git a/ee/spec/frontend/ai/duo_agents_platform/pages/flows/ai_flows_spec.js b/ee/spec/frontend/ai/duo_agents_platform/pages/flows/ai_flows_spec.js
index 2432b8e1f81a994655b61400d3c9bd7ffe156e17..cd0ce41f4991644e2df4c3a5202bcdff1999cf60 100644
--- a/ee/spec/frontend/ai/duo_agents_platform/pages/flows/ai_flows_spec.js
+++ b/ee/spec/frontend/ai/duo_agents_platform/pages/flows/ai_flows_spec.js
@@ -7,7 +7,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AiFlows from 'ee/ai/duo_agents_platform/pages/flows/ai_flows.vue';
import AiCatalogList from 'ee/ai/catalog/components/ai_catalog_list.vue';
-import ErrorsAlert from '~/vue_shared/components/errors_alert.vue';
+import StatusAlert from '~/vue_shared/components/status_alert.vue';
import PageHeading from '~/vue_shared/components/page_heading.vue';
import ResourceListsEmptyState from '~/vue_shared/components/resource_lists/empty_state.vue';
import aiCatalogConfiguredItemsQuery from 'ee/ai/catalog/graphql/queries/ai_catalog_configured_items.query.graphql';
@@ -66,7 +66,7 @@ describe('AiFlows', () => {
});
};
- const findErrorsAlert = () => wrapper.findComponent(ErrorsAlert);
+ const findStatusAlert = () => wrapper.findComponent(StatusAlert);
const findAiCatalogList = () => wrapper.findComponent(AiCatalogList);
const findEmptyState = () => wrapper.findComponent(ResourceListsEmptyState);
@@ -158,7 +158,7 @@ describe('AiFlows', () => {
deleteFlow();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toStrictEqual([
+ expect(findStatusAlert().props('messages')).toStrictEqual([
'Failed to remove flow. You do not have permission to delete this AI flow.',
]);
});
@@ -171,7 +171,7 @@ describe('AiFlows', () => {
deleteFlow();
await waitForPromises();
- expect(findErrorsAlert().props('errors')).toStrictEqual([
+ expect(findStatusAlert().props('messages')).toStrictEqual([
'Failed to remove flow. Error: Request failed',
]);
expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index acebe4296602a4b7af80c9ac020b6e226bd8e013..a9c7ae99af561c1a7002908227dcb034e27f55d9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2707,7 +2707,7 @@ msgstr ""
msgid "AICatalog|Test run agents to see how they respond."
msgstr ""
-msgid "AICatalog|Test run executed successfully, see %{linkStart}Session %{formattedId}%{linkEnd}."
+msgid "AICatalog|Test run executed successfully, see %{linkStart}Session %{sessionId}%{linkEnd}."
msgstr ""
msgid "AICatalog|The agent could not be added to the project. Try again. %{error}"
diff --git a/spec/frontend/vue_shared/components/errors_alert_spec.js b/spec/frontend/vue_shared/components/errors_alert_spec.js
deleted file mode 100644
index 79cd8e782dd95049c3dbf729622fc824f9566e69..0000000000000000000000000000000000000000
--- a/spec/frontend/vue_shared/components/errors_alert_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { nextTick } from 'vue';
-import { shallowMount } from '@vue/test-utils';
-import { GlAlert } from '@gitlab/ui';
-import ErrorsAlert from '~/vue_shared/components/errors_alert.vue';
-
-describe('ErrorsAlert', () => {
- let wrapper;
- const mockError1 = 'The item could not be created';
- const mockError2 = 'The item could not be updated';
-
- const findErrorAlert = () => wrapper.findComponent(GlAlert);
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMount(ErrorsAlert, {
- propsData: {
- ...props,
- },
- });
- };
-
- describe('Initial Rendering', () => {
- it('does not render error alert', () => {
- createWrapper();
-
- expect(findErrorAlert().exists()).toBe(false);
- });
-
- it('renders error alert when there is one error', () => {
- createWrapper({ errors: [mockError1] });
-
- expect(findErrorAlert().text()).toBe(mockError1);
- });
-
- it('renders error alert with list for multiple errors', () => {
- createWrapper({ errors: [mockError1, mockError2] });
-
- expect(findErrorAlert().findAll('li')).toHaveLength(2);
- });
-
- it('renders the default CSS class', () => {
- createWrapper({ errors: [mockError1] });
-
- expect(findErrorAlert().attributes('class')).toBe('gl-mb-5');
- });
-
- it('does not render the default CSS class when overridden', () => {
- createWrapper({ errors: [mockError1], alertClass: 'gl-mb-4' });
-
- expect(findErrorAlert().attributes('class')).toBe('gl-mb-4');
- });
- });
-
- describe('when the component receives an error after initial rendering', () => {
- const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
- const scrollIntoViewMock = jest.fn();
-
- beforeEach(() => {
- HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
- });
-
- afterEach(() => {
- HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
- });
-
- it('scrolls to error alert when errors are set', async () => {
- createWrapper();
- await wrapper.setProps({ errors: ['Error occurred'] });
- await nextTick();
-
- expect(scrollIntoViewMock).toHaveBeenCalledWith({
- behavior: 'smooth',
- block: 'center',
- });
- });
-
- describe('but the property to scrollOnError is false', () => {
- it('does not to error alert when errors are set', async () => {
- createWrapper({ scrollOnError: false });
- await wrapper.setProps({ errors: ['Error occurred'] });
- await nextTick();
-
- expect(scrollIntoViewMock).not.toHaveBeenCalledWith({
- behavior: 'smooth',
- block: 'center',
- });
- });
- });
- });
-
- describe('Interactions', () => {
- describe('when dismissing the alert', () => {
- beforeEach(() => {
- createWrapper({ errors: [mockError1] });
- });
-
- it('emits dismiss event when clicked on dismiss icon', () => {
- findErrorAlert().vm.$emit('dismiss');
-
- expect(wrapper.emitted('dismiss')).toHaveLength(1);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/status_alert_spec.js b/spec/frontend/vue_shared/components/status_alert_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c77d19f08b294e3c7e5c095e82af435b357ee366
--- /dev/null
+++ b/spec/frontend/vue_shared/components/status_alert_spec.js
@@ -0,0 +1,158 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import StatusAlert from '~/vue_shared/components/status_alert.vue';
+
+describe('StatusAlert', () => {
+ let wrapper;
+ const mockError1 = 'The item could not be created';
+ const mockError2 = 'The item could not be updated';
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMount(StatusAlert, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('Initial Rendering', () => {
+ it('does not render error alert', () => {
+ createWrapper();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('renders error alert when there is one error', () => {
+ createWrapper({ messages: [mockError1] });
+
+ expect(findAlert().text()).toBe(mockError1);
+ });
+
+ it('renders error alert with list for multiple errors', () => {
+ createWrapper({ messages: [mockError1, mockError2] });
+
+ expect(findAlert().findAll('li')).toHaveLength(2);
+ });
+
+ it('renders the default CSS class', () => {
+ createWrapper({ messages: [mockError1] });
+
+ expect(findAlert().attributes('class')).toBe('gl-mb-5');
+ });
+
+ it('does not render the default CSS class when overridden', () => {
+ createWrapper({ messages: [mockError1], alertClass: 'gl-mb-4' });
+
+ expect(findAlert().attributes('class')).toBe('gl-mb-4');
+ });
+
+ describe('Slot functionality', () => {
+ const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
+ const scrollIntoViewMock = jest.fn();
+
+ beforeEach(() => {
+ HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+ });
+
+ afterEach(() => {
+ HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
+ });
+
+ it('renders alert when slot content is provided', () => {
+ wrapper = shallowMount(StatusAlert, {
+ slots: {
+ default: 'Custom slot content
',
+ },
+ });
+
+ expect(findAlert().exists()).toBe(true);
+ expect(wrapper.find('.custom-content').exists()).toBe(true);
+ expect(wrapper.find('.custom-content').text()).toBe('Custom slot content');
+ });
+
+ it('scrolls to alert when slot content triggers rendering', async () => {
+ wrapper = shallowMount(StatusAlert, {
+ slots: {
+ default: 'Slot content that should trigger scroll
',
+ },
+ });
+
+ await nextTick();
+
+ expect(scrollIntoViewMock).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ });
+
+ it('does not scroll when slot content is present but scrollOnMessage is false', async () => {
+ wrapper = shallowMount(StatusAlert, {
+ propsData: {
+ scrollOnMessage: false,
+ },
+ slots: {
+ default: 'Slot content without scroll
',
+ },
+ });
+
+ await nextTick();
+
+ expect(scrollIntoViewMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when the component receives an error after initial rendering', () => {
+ const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
+ const scrollIntoViewMock = jest.fn();
+
+ beforeEach(() => {
+ HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+ });
+
+ afterEach(() => {
+ HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
+ });
+
+ it('scrolls to error alert when errors are set', async () => {
+ createWrapper();
+ await wrapper.setProps({ messages: ['Error occurred'] });
+ await nextTick();
+
+ expect(scrollIntoViewMock).toHaveBeenCalledWith({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ });
+
+ describe('but the property to scrollOnMessage is false', () => {
+ it('does not to error alert when errors are set', async () => {
+ createWrapper({ scrollOnMessage: false });
+ await wrapper.setProps({ messages: ['Error occurred'] });
+ await nextTick();
+
+ expect(scrollIntoViewMock).not.toHaveBeenCalledWith({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ });
+ });
+ });
+
+ describe('Interactions', () => {
+ describe('when dismissing the alert', () => {
+ beforeEach(() => {
+ createWrapper({ messages: [mockError1] });
+ });
+
+ it('emits dismiss event when clicked on dismiss icon', () => {
+ findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted('dismiss')).toHaveLength(1);
+ });
+ });
+ });
+});