{{ activeItem.name }}
@@ -89,7 +91,15 @@ export default {
-
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_list_item.vue b/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_list_item.vue
index 59f7db228ae2c8a7e7917cf333e53e8285e614d0..4becac2a8c0ea263abb1e3e1bef461207444de76 100644
--- a/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_list_item.vue
+++ b/ee/app/assets/javascripts/ai/catalog/components/ai_catalog_list_item.vue
@@ -19,6 +19,8 @@ import {
TIMESTAMP_TYPE_CREATED_AT,
} from '~/vue_shared/components/resource_lists/constants';
import { AI_CATALOG_SHOW_QUERY_PARAM } from '../router/constants';
+import { VERIFICATION_LEVELS } from '../constants';
+import VerifiedItem from './verified_item.vue';
export default {
name: 'AiCatalogListItem',
@@ -28,6 +30,7 @@ export default {
GlDisclosureDropdownItem,
GlIcon,
ListItem,
+ VerifiedItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -98,6 +101,9 @@ export default {
visibilityTooltip() {
return this.itemTypeConfig.visibilityTooltip?.[this.visibilityLevel];
},
+ isVerified() {
+ return this.item.verificationLevel === VERIFICATION_LEVELS.GITLAB_VERIFIED;
+ },
},
methods: {
onClickAvatar(event) {
@@ -132,6 +138,7 @@ export default {
:title="visibilityTooltip"
variant="subtle"
/>
+
+import { uniqueId } from 'lodash';
+import { s__ } from '~/locale';
+import VerificationLevel from '~/vue_shared/components/verification_level.vue';
+
+export default {
+ components: {
+ VerificationLevel,
+ },
+ props: {
+ itemId: {
+ type: String,
+ required: true,
+ },
+ showText: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ id() {
+ return uniqueId(`verified-item-${this.itemId}`);
+ },
+ text() {
+ return this.showText ? s__('AICatalog|GitLab-maintained') : '';
+ },
+ },
+};
+
+
+
+
+
diff --git a/ee/app/assets/javascripts/ai/catalog/constants.js b/ee/app/assets/javascripts/ai/catalog/constants.js
index 0dfd430a8c0b8cce65c3ecfbbc0c2ff699f9daa5..8da5a3a742c8ac4cbf85866ed147075d441b96c2 100644
--- a/ee/app/assets/javascripts/ai/catalog/constants.js
+++ b/ee/app/assets/javascripts/ai/catalog/constants.js
@@ -11,6 +11,10 @@ export const AI_CATALOG_ITEM_LABELS = {
[AI_CATALOG_TYPE_AGENT]: s__('AICatalog|agent'),
[AI_CATALOG_TYPE_FLOW]: s__('AICatalog|flow'),
};
+export const VERIFICATION_LEVELS = {
+ GITLAB_MAINTAINED: 'GITLAB_MAINTAINED',
+ UNVERIFIED: 'unverified',
+};
export const PAGE_SIZE = 20;
diff --git a/ee/app/assets/javascripts/ai/catalog/graphql/fragments/ai_catalog_item.fragment.graphql b/ee/app/assets/javascripts/ai/catalog/graphql/fragments/ai_catalog_item.fragment.graphql
index b3b293a87b2a52e00a3a8a308e32cfcafc19eea2..9eb5bf021faa695c7835478f8faf68e2a90a5860 100644
--- a/ee/app/assets/javascripts/ai/catalog/graphql/fragments/ai_catalog_item.fragment.graphql
+++ b/ee/app/assets/javascripts/ai/catalog/graphql/fragments/ai_catalog_item.fragment.graphql
@@ -6,6 +6,7 @@ fragment BaseAiCatalogItem on AiCatalogItem {
name
public
updatedAt
+ verificationLevel
latestVersion {
id
updatedAt
diff --git a/ee/spec/frontend/ai/catalog/components/verified_item_spec.js b/ee/spec/frontend/ai/catalog/components/verified_item_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..2fc660f556e868b28143c279bef1db16c3831ec2
--- /dev/null
+++ b/ee/spec/frontend/ai/catalog/components/verified_item_spec.js
@@ -0,0 +1,48 @@
+import { shallowMount } from '@vue/test-utils';
+import VerifiedItem from 'ee/ai/catalog/components/verified_item.vue';
+import VerificationLevel from '~/vue_shared/components/verification_level.vue';
+
+describe('VerifiedItem', () => {
+ let wrapper;
+
+ const defaultProps = {
+ itemId: 'test-item-123',
+ showText: true,
+ };
+
+ const findVerificationLevel = () => wrapper.findComponent(VerificationLevel);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(VerifiedItem, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('if showText is true', () => {
+ it('passes the text if showText is true', () => {
+ expect(findVerificationLevel().props('text')).toBe('GitLab-maintained');
+ });
+ });
+
+ describe('if showText is false', () => {
+ beforeEach(() => {
+ createComponent({ showText: false });
+ });
+
+ it('passes the text if showText is true', () => {
+ expect(findVerificationLevel().props('text')).toBe('');
+ });
+ });
+
+ it('generates a unique id based on itemId', () => {
+ const id = findVerificationLevel().props('id');
+ expect(id).toContain('verified-item-test-item-123');
+ });
+});
diff --git a/ee/spec/frontend/ai/catalog/mock_data.js b/ee/spec/frontend/ai/catalog/mock_data.js
index 4cbb18aa8fdc987f85cd654efc27803130fb246a..d3bca3c39c7db7497eaa470ee297fd7c3a867916 100644
--- a/ee/spec/frontend/ai/catalog/mock_data.js
+++ b/ee/spec/frontend/ai/catalog/mock_data.js
@@ -119,6 +119,7 @@ const mockAgentFactory = (overrides = {}) => ({
public: true,
updatedAt: '2024-08-21T14:30:00Z',
latestVersion: mockBaseLatestVersion,
+ verificationLevel: 'unverified',
userPermissions: {
adminAiCatalogItem: true,
},
@@ -323,6 +324,7 @@ const mockFlowFactory = (overrides = {}) => ({
public: true,
updatedAt: '2024-08-21T14:30:00Z',
latestVersion: mockBaseLatestVersion,
+ verificationLevel: 'unverified',
userPermissions: {
adminAiCatalogItem: true,
},
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 10f7c9e4ad874d8cdf6da92e8dfe51ef15a5259d..33254a27524391ce371718a9a0e4d40aa03d149a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2573,6 +2573,9 @@ msgstr ""
msgid "AICatalog|Create flow"
msgstr ""
+msgid "AICatalog|Created and maintained by %{boldStart}GitLab%{boldEnd}"
+msgstr ""
+
msgid "AICatalog|Define the agent's personality, expertise, and behavioral guidelines. This shapes how the agent responds and approaches tasks."
msgstr ""
@@ -2675,6 +2678,9 @@ msgstr ""
msgid "AICatalog|Get started with the AI Catalog"
msgstr ""
+msgid "AICatalog|GitLab-maintained"
+msgstr ""
+
msgid "AICatalog|Input cannot exceed %{value} characters. Please shorten your input."
msgstr ""
diff --git a/spec/frontend/ci/catalog/components/shared/ci_verification_badge_spec.js b/spec/frontend/ci/catalog/components/shared/ci_verification_badge_spec.js
index 0b60530662d49a135ef307c6df81d705a0e26972..f6baa694e0d154432743b54b0eb8054ffa78ee3f 100644
--- a/spec/frontend/ci/catalog/components/shared/ci_verification_badge_spec.js
+++ b/spec/frontend/ci/catalog/components/shared/ci_verification_badge_spec.js
@@ -1,7 +1,7 @@
-import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CiVerificationBadge from '~/ci/catalog/components/shared/ci_verification_badge.vue';
+import VerificationLevel from '~/vue_shared/components/verification_level.vue';
import { VERIFICATION_LEVELS } from '~/ci/catalog/constants';
describe('Catalog Verification Badge', () => {
@@ -13,9 +13,7 @@ describe('Catalog Verification Badge', () => {
verificationLevel: 'GITLAB_MAINTAINED',
};
- const findVerificationIcon = () => wrapper.findComponent(GlIcon);
- const findLink = () => wrapper.findComponent(GlLink);
- const findVerificationText = () => wrapper.findByTestId('verification-badge-text');
+ const findVerificationLevel = () => wrapper.findComponent(VerificationLevel);
const createComponent = (props = defaultProps) => {
wrapper = extendedWrapper(
@@ -32,12 +30,12 @@ describe('Catalog Verification Badge', () => {
createComponent();
});
- it('renders an icon', () => {
- expect(findVerificationIcon().exists()).toBe(true);
+ it('renders the VerificationLevel component', () => {
+ expect(findVerificationLevel().exists()).toBe(true);
});
- it('renders a link', () => {
- expect(findLink().exists()).toBe(true);
+ it('passes the correct resourceId prop', () => {
+ expect(findVerificationLevel().props('id')).toBe(defaultProps.resourceId);
});
});
@@ -47,8 +45,8 @@ describe('Catalog Verification Badge', () => {
createComponent();
});
- it('renders badge text', () => {
- expect(findVerificationText().exists()).toBe(true);
+ it('passes the text to VerificationLevel', () => {
+ expect(findVerificationLevel().props('text')).toBeDefined();
});
});
@@ -57,8 +55,8 @@ describe('Catalog Verification Badge', () => {
createComponent({ ...defaultProps, showText: false });
});
- it('does not render badge text', () => {
- expect(findVerificationText().exists()).toBe(false);
+ it('does not pass the text to VerificationLevel', () => {
+ expect(findVerificationLevel().props('text')).toBe('');
});
});
});
@@ -74,16 +72,22 @@ describe('Catalog Verification Badge', () => {
createComponent({ ...defaultProps, verificationLevel });
});
- it('renders the correct icon', () => {
- expect(findVerificationIcon().props('name')).toBe(
- VERIFICATION_LEVELS[verificationLevel].icon,
+ it('passes the correct text prop', () => {
+ expect(findVerificationLevel().props('text')).toBe(
+ VERIFICATION_LEVELS[verificationLevel].badgeText,
);
});
- it('displays the correct badge text', () => {
- expect(findVerificationText().text()).toContain(
- VERIFICATION_LEVELS[verificationLevel].badgeText,
+ it('passes the correct message prop', () => {
+ expect(findVerificationLevel().props('message')).toBe(
+ VERIFICATION_LEVELS[verificationLevel].popoverText,
);
});
+
+ it('passes the helpPath object with correct structure', () => {
+ const helpPath = findVerificationLevel().props('helpPath');
+ expect(helpPath).toHaveProperty('path');
+ expect(helpPath).toHaveProperty('text');
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/verification_level_spec.js b/spec/frontend/vue_shared/components/verification_level_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..221a3080a143678f3ec5baca319955f6a8068227
--- /dev/null
+++ b/spec/frontend/vue_shared/components/verification_level_spec.js
@@ -0,0 +1,132 @@
+import { GlIcon, GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
+import { GlBreakpointInstance } from '@gitlab/ui/src/utils';
+import { shallowMount } from '@vue/test-utils';
+import VerificationLevel from '~/vue_shared/components/verification_level.vue';
+
+describe('VerificationLevel', () => {
+ let wrapper;
+
+ const defaultProps = {
+ id: 'test-resource-id',
+ text: 'GitLab',
+ message: 'This is maintained by %{bold}GitLab%{/bold}',
+ iconName: 'tanuki-verified',
+ helpPath: {
+ path: '/help/ci/components/index#verified-component-creators',
+ text: 'Learn more about verified creators',
+ },
+ };
+
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findSprintf = () => wrapper.findComponent(GlSprintf);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(VerificationLevel, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ describe('rendering', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the verification icon', () => {
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe(defaultProps.iconName);
+ });
+
+ it('renders the popover', () => {
+ expect(findPopover().exists()).toBe(true);
+ });
+
+ it('sets the correct popover target', () => {
+ expect(findPopover().props('target')).toBe(`${defaultProps.id}-verification-icon`);
+ });
+
+ it('renders GlSprintf with the message', () => {
+ expect(findSprintf().exists()).toBe(true);
+ expect(findSprintf().attributes('message')).toBe(defaultProps.message);
+ });
+ });
+
+ describe('help link', () => {
+ describe('when helpPath is provided', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets the correct href', () => {
+ expect(findLink().attributes('href')).toBe(defaultProps.helpPath.path);
+ });
+
+ it('opens link in new tab', () => {
+ expect(findLink().attributes('target')).toBe('_blank');
+ });
+
+ it('displays the correct link text', () => {
+ expect(findLink().text()).toBe(`${defaultProps.helpPath.text}.`);
+ });
+ });
+
+ describe('when helpPath is null', () => {
+ beforeEach(() => {
+ createComponent({ helpPath: null });
+ });
+
+ it('does not render the help link', () => {
+ expect(findLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when helpPath is not provided', () => {
+ beforeEach(() => {
+ createComponent({ helpPath: undefined });
+ });
+
+ it('does not render the help link', () => {
+ expect(findLink().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('popover placement', () => {
+ describe('on desktop', () => {
+ beforeEach(() => {
+ jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue('md');
+ createComponent();
+ });
+
+ it('sets placement to right', () => {
+ expect(findPopover().props('placement')).toBe('right');
+ });
+ });
+
+ describe('on mobile (xs)', () => {
+ beforeEach(() => {
+ jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue('xs');
+ createComponent();
+ });
+
+ it('sets placement to bottom', () => {
+ expect(findPopover().props('placement')).toBe('bottom');
+ });
+ });
+
+ describe('on mobile (sm)', () => {
+ beforeEach(() => {
+ jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue('sm');
+ createComponent();
+ });
+
+ it('sets placement to bottom', () => {
+ expect(findPopover().props('placement')).toBe('bottom');
+ });
+ });
+ });
+});