Skip to content
代码片段 群组 项目
未验证 提交 d1451e56 编辑于 作者: Julie Huang's avatar Julie Huang 提交者: GitLab
浏览文件

Invoke aiFeatureSettings query

- Invoke query
- Update tests
- Remove data stub
上级 7d45055d
No related branches found
No related tags found
无相关合并请求
显示
212 个添加135 个删除
<script>
import stubbedAiFeatureSettings from '../data_stub';
import { GlSkeletonLoader } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import getAiFeatureSettingsQuery from '../graphql/queries/get_ai_feature_settings.query.graphql';
import AiFeatureSettingsTable from './feature_settings_table.vue';
export default {
name: 'FeatureSettingsApp',
components: {
GlSkeletonLoader,
AiFeatureSettingsTable,
},
props: {
models: {
type: Array,
required: true,
},
newSelfHostedModelPath: {
type: String,
required: true,
},
// Return stubbed data for now
featureSettings: {
type: Array,
required: false,
default: () => {
return stubbedAiFeatureSettings;
},
i18n: {
title: s__('AdminAIPoweredFeatures|AI-powered features'),
description: s__(
'AdminAIPoweredFeatures|Features that can be enabled, disabled, or linked to a cloud-based or self-hosted model.',
),
errorMessage: s__(
'AdminAIPoweredFeatures|An error occurred while loading the AI feature settings. Please try again.',
),
},
data() {
return {
aiFeatureSettings: [],
};
},
apollo: {
aiFeatureSettings: {
query: getAiFeatureSettingsQuery,
update(data) {
return data?.aiFeatureSettings?.nodes || [];
},
error(error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
},
},
},
computed: {
isLoading() {
return this.$apollo?.queries?.aiFeatureSettings?.loading;
},
},
};
</script>
<template>
<div>
<section>
<h1 class="page-title gl-text-size-h-display">
{{ s__('AdminAIPoweredFeatures|AI-powered features') }}
{{ $options.i18n.title }}
</h1>
<div class="gl-items-top gl-flex gl-justify-between">
<p>
{{
s__(
'AdminAIPoweredFeatures|Features that can be enabled, disabled, or linked to a cloud-based or self-hosted model.',
)
}}
</p>
<p>{{ $options.i18n.description }}</p>
</div>
</section>
<gl-skeleton-loader v-if="isLoading" />
<ai-feature-settings-table
:feature-settings="featureSettings"
v-else
:ai-feature-settings="aiFeatureSettings"
:new-self-hosted-model-path="newSelfHostedModelPath"
:models="models"
/>
</div>
</template>
......@@ -10,7 +10,7 @@ export default {
ModelSelectDropdown,
},
props: {
featureSettings: {
aiFeatureSettings: {
type: Array,
required: true,
},
......@@ -18,10 +18,6 @@ export default {
type: String,
required: true,
},
models: {
type: Array,
required: true,
},
},
fields: [
{
......@@ -48,7 +44,7 @@ export default {
<template>
<gl-table-lite
:fields="$options.fields"
:items="featureSettings"
:items="aiFeatureSettings"
stacked="md"
:hover="true"
:selectable="false"
......@@ -61,8 +57,7 @@ export default {
</template>
<template #cell(model_name)="{ item }">
<model-select-dropdown
:feature-setting="item"
:models="models"
:ai-feature-setting="item"
:new-self-hosted-model-path="newSelfHostedModelPath"
/>
</template>
......
......@@ -2,13 +2,12 @@
import { GlCollapsibleListbox, GlButton } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { createAlert } from '~/alert';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import updateAiFeatureSetting from '../graphql/mutations/update_ai_feature_setting.graphql';
import updateAiFeatureSetting from '../graphql/mutations/update_ai_feature_setting.mutation.graphql';
const PROVIDERS = {
DISABLED: 'DISABLED',
VENDORED: 'VENDORED',
SELF_HOSTED: 'SELF_HOSTED',
DISABLED: 'disabled',
VENDORED: 'vendored',
SELF_HOSTED: 'self_hosted',
};
const FEATURE_DISABLED = 'DISABLED';
......@@ -20,14 +19,10 @@ export default {
GlButton,
},
props: {
featureSetting: {
aiFeatureSetting: {
type: Object,
required: true,
},
models: {
type: Array,
required: true,
},
newSelfHostedModelPath: {
type: String,
required: true,
......@@ -40,17 +35,21 @@ export default {
successMessage: s__('AdminSelfHostedModels|Successfully updated %{mainFeature} / %{title}'),
},
data() {
const { provider, selfHostedModel, validModels } = this.aiFeatureSetting;
const selectedOption = provider === PROVIDERS.DISABLED ? FEATURE_DISABLED : selfHostedModel?.id;
return {
feature: this.featureSetting.feature,
provider: this.featureSetting.provider,
selfHostedModelId: this.featureSetting.selfHostedModelId,
selectedOption: null,
provider,
selfHostedModelId: selfHostedModel?.id,
compatibleModels: validModels?.nodes,
selectedOption,
isSaving: false,
};
},
computed: {
dropdownItems() {
const modelOptions = this.models.map((model) => ({
const modelOptions = this.compatibleModels.map((model) => ({
value: model.id,
text: `${model.name} (${model.model})`,
}));
......@@ -64,13 +63,13 @@ export default {
return [...modelOptions, disableOption];
},
selectedModel() {
return this.models.find((m) => m.id === this.selfHostedModelId);
return this.compatibleModels.find((m) => m.id === this.selfHostedModelId);
},
dropdownToggleText() {
if (!this.selectedOption) {
if (this.provider === PROVIDERS.VENDORED) {
return s__('AdminAIPoweredFeatures|Select a self-hosted model');
}
if (this.selectedOption === FEATURE_DISABLED) {
if (this.provider === PROVIDERS.DISABLED) {
return s__('AdminAIPoweredFeatures|Disabled');
}
......@@ -82,18 +81,19 @@ export default {
this.isSaving = true;
try {
const provider = option === FEATURE_DISABLED ? PROVIDERS.DISABLED : PROVIDERS.SELF_HOSTED;
const selectedOption = {
option,
provider: option === FEATURE_DISABLED ? PROVIDERS.DISABLED : PROVIDERS.SELF_HOSTED,
selfHostedModelId: option === FEATURE_DISABLED ? null : option,
};
const { data } = await this.$apollo.mutate({
mutation: updateAiFeatureSetting,
variables: {
input: {
feature: this.feature.toUpperCase(),
provider,
selfHostedModelId:
option === FEATURE_DISABLED
? null
: convertToGraphQLId('Ai::SelfHostedModel', option),
feature: this.aiFeatureSetting.feature.toUpperCase(),
provider: selectedOption.provider.toUpperCase(),
selfHostedModelId: selectedOption.selfHostedModelId,
},
},
});
......@@ -105,11 +105,9 @@ export default {
throw new Error(errors[0]);
}
this.selectedOption = option;
this.provider = provider;
this.selfHostedModelId = option === FEATURE_DISABLED ? null : option;
this.updateSelection(selectedOption);
this.isSaving = false;
this.$toast.show(this.successMessage(this.featureSetting));
this.$toast.show(this.successMessage(this.aiFeatureSetting));
}
} catch (error) {
createAlert({
......@@ -120,13 +118,18 @@ export default {
this.isSaving = false;
}
},
updateSelection(selectedOption) {
this.selectedOption = selectedOption.option;
this.provider = selectedOption.provider;
this.selfHostedModelId = selectedOption.selfHostedModelId;
},
errorMessage(error) {
return error.message || this.$options.i18n.defaultErrorMessage;
},
successMessage(featureSetting) {
successMessage(aiFeatureSetting) {
return sprintf(this.$options.i18n.successMessage, {
mainFeature: featureSetting.mainFeature,
title: featureSetting.title,
mainFeature: aiFeatureSetting.mainFeature,
title: aiFeatureSetting.title,
});
},
},
......@@ -138,7 +141,7 @@ export default {
:selected="selectedOption"
:items="dropdownItems"
:toggle-text="dropdownToggleText"
:header-text="s__('AdminAIPoweredFeatures|Self-hosted models')"
:header-text="s__('AdminAIPoweredFeatures|Compatible models')"
:loading="isSaving"
category="primary"
block
......
/**
* Temporary data stub for feature settings table
*
* TODO: Remove when feature settings data is ready.
* See https://gitlab.com/gitlab-org/gitlab/-/issues/473005#note_2076997154
*/
/* eslint-disable @gitlab/require-i18n-strings */
const stubbedAiFeatureSettings = [
{
feature: 'code_generations',
title: 'Code Generation',
mainFeature: 'Code Suggestions',
provider: 'self_hosted',
selfHostedModel: null,
},
{
feature: 'code_completions',
title: 'Code Completion',
mainFeature: 'Code Suggestions',
provider: 'vendored',
selfHostedModel: null,
},
{
feature: 'duo_chat',
title: 'Duo Chat',
mainFeature: 'Duo Chat',
provider: 'self_hosted',
selfHostedModel: null,
},
];
/* eslint-enable @gitlab/require-i18n-strings */
export default stubbedAiFeatureSettings;
query getAiFeatureSetting {
aiFeatureSettings {
nodes {
feature
title
mainFeature
provider
selfHostedModel {
id
name
model
}
validModels {
nodes {
id
name
model
}
}
}
}
}
......@@ -12,10 +12,6 @@ class FeatureSettingsController < Admin::ApplicationController
def index
@feature_settings = ::Ai::FeatureSettings::FeatureSettingFinder.new.execute
return unless Feature.enabled?(:custom_models_feature_settings_vue_app, current_user)
@self_hosted_models = ::Ai::SelfHostedModel.all
end
# rubocop:disable CodeReuse/ActiveRecord -- Using find_or_initialize_by is reasonable
......
......@@ -2,7 +2,7 @@
- add_page_specific_style 'page_bundles/labels'
- if Feature.enabled?(:custom_models_feature_settings_vue_app, current_user)
#js-ai-powered-features{ data: { view_model: { models: @self_hosted_models, newSelfHostedModelPath: new_admin_ai_self_hosted_model_path }.to_json } }
#js-ai-powered-features{ data: { view_model: { newSelfHostedModelPath: new_admin_ai_self_hosted_model_path }.to_json } }
- else
= render ::Layouts::CrudComponent.new(s_('AdminAIPoweredFeatures|AI-powered features'),
options: { class: 'gl-mt-5' }) do |c|
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import FeatureSettingsApp from 'ee/pages/admin/ai/feature_settings/components/app.vue';
import getAiFeatureSettingsQuery from 'ee/pages/admin/ai/feature_settings/graphql/queries/get_ai_feature_settings.query.graphql';
import AiFeatureSettingsTable from 'ee/pages/admin/ai/feature_settings/components/feature_settings_table.vue';
import { createAlert } from '~/alert';
import { mockAiFeatureSettings } from './mock_data';
Vue.use(VueApollo);
jest.mock('~/alert');
describe('FeatureSettingsApp', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(FeatureSettingsApp);
const getAiFeatureSettingsSuccessHandler = jest.fn().mockResolvedValue({
data: {
aiFeatureSettings: {
nodes: mockAiFeatureSettings,
errors: [],
},
},
});
const createComponent = ({
apolloHandlers = [[getAiFeatureSettingsQuery, getAiFeatureSettingsSuccessHandler]],
} = {}) => {
const mockApollo = createMockApollo([...apolloHandlers]);
const newSelfHostedModelPath = '/admin/ai/self_hosted_models/new';
wrapper = shallowMount(FeatureSettingsApp, {
apolloProvider: mockApollo,
propsData: {
newSelfHostedModelPath,
},
});
};
beforeEach(() => {
createComponent();
});
const findAiFeatureSettingsTable = () => wrapper.findComponent(AiFeatureSettingsTable);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
it('has a title', () => {
const title = wrapper.find('h1');
......@@ -23,4 +58,53 @@ describe('FeatureSettingsApp', () => {
'Features that can be enabled, disabled, or linked to a cloud-based or self-hosted model.',
);
});
describe('when AI feature settings are loading', () => {
it('renders skeleton loader', () => {
expect(findLoader().exists()).toBe(true);
});
});
describe('when the API request is successful', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it('renders the feature settings table and passes the correct props', () => {
expect(findAiFeatureSettingsTable().props('aiFeatureSettings')).toEqual(
mockAiFeatureSettings,
);
expect(findAiFeatureSettingsTable().props('newSelfHostedModelPath')).toEqual(
'/admin/ai/self_hosted_models/new',
);
});
});
describe('when the API request is unsuccessful', () => {
const getAiFeatureSettingsErrorHandler = jest.fn().mockResolvedValue({
data: {
aiFeatureSettings: {
errors: ['An error occured'],
},
},
});
beforeEach(async () => {
createComponent({
apolloHandlers: [[getAiFeatureSettingsQuery, getAiFeatureSettingsErrorHandler]],
});
await waitForPromises();
});
it('displays an error message', () => {
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: 'An error occurred while loading the AI feature settings. Please try again.',
}),
);
});
});
});
import { GlTableLite } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import FeatureSettingsTable from 'ee/pages/admin/ai/feature_settings/components/feature_settings_table.vue';
import { mockAiFeatureSettings, mockSelfHostedModels } from './mock_data';
import { mockAiFeatureSettings } from './mock_data';
describe('FeatureSettingsTable', () => {
let wrapper;
......@@ -19,8 +19,7 @@ describe('FeatureSettingsTable', () => {
beforeEach(() => {
createComponent({
props: {
featureSettings: mockAiFeatureSettings,
models: mockSelfHostedModels,
aiFeatureSettings: mockAiFeatureSettings,
newSelfHostedModelPath,
},
});
......
export const mockSelfHostedModels = [
{ id: 1, name: 'Model 1', model: 'mistral' },
{ id: 2, name: 'Model 2', model: 'codellama' },
{ id: 3, name: 'Model 3', model: 'codegemma' },
];
export const mockAiFeatureSettings = [
{
feature: 'code_generations',
title: 'Code Generation',
mainFeature: 'Code Suggestions',
provider: 'self_hosted',
provider: 'vendored',
selfHostedModel: null,
validModels: { nodes: mockSelfHostedModels },
},
{
feature: 'code_completions',
title: 'Code Completion',
mainFeature: 'Code Suggestions',
provider: 'vendored',
provider: 'disabled',
selfHostedModel: null,
validModels: { nodes: mockSelfHostedModels },
},
{
feature: 'duo_chat',
title: 'Duo Chat',
mainFeature: 'Duo Chat',
provider: 'self_hosted',
selfHostedModel: null,
selfHostedModel: {
id: 2,
name: 'Model 2',
model: 'codellama',
},
validModels: { nodes: mockSelfHostedModels },
},
];
export const mockSelfHostedModels = [
{ id: 1, name: 'Model 1', model: 'mistral' },
{ id: 2, name: 'Model 2', model: 'codellama' },
{ id: 3, name: 'Model 3', model: 'codegemma' },
];
......@@ -5,7 +5,7 @@ import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import ModelSelectDropdown from 'ee/pages/admin/ai/feature_settings/components/model_select_dropdown.vue';
import updateAiFeatureSetting from 'ee/pages/admin/ai/feature_settings/graphql/mutations/update_ai_feature_setting.graphql';
import updateAiFeatureSetting from 'ee/pages/admin/ai/feature_settings/graphql/mutations/update_ai_feature_setting.mutation.graphql';
import { createAlert } from '~/alert';
import { mockSelfHostedModels, mockAiFeatureSettings } from './mock_data';
......@@ -38,8 +38,7 @@ describe('ModelSelectDropdown', () => {
apolloProvider: mockApollo,
propsData: {
newSelfHostedModelPath,
featureSetting: mockAiFeatureSetting,
models: mockSelfHostedModels,
aiFeatureSetting: mockAiFeatureSetting,
...props,
},
mocks: {
......
......@@ -50,20 +50,6 @@
end
describe 'GET #index' do
context 'when custom_models_feature_settings_vue_app feature flag is enabled' do
let_it_be(:model) { create(:ai_self_hosted_model) }
before do
stub_feature_flags(custom_models_feature_settings_vue_app: true)
end
it 'returns self-hosted models' do
get admin_ai_feature_settings_path
expect(assigns(:self_hosted_models)).to match_array([model])
end
end
it 'returns `flagged_features` if ai_duo_chat_sub_features_settings is enabled' do
# it expect to go through the ::Ai::FeatureSetting.flagged_features method
# So it only shows the stable features
......
......@@ -3587,6 +3587,12 @@ msgstr ""
msgid "AdminAIPoweredFeatures|Add self-hosted model"
msgstr ""
 
msgid "AdminAIPoweredFeatures|An error occurred while loading the AI feature settings. Please try again."
msgstr ""
msgid "AdminAIPoweredFeatures|Compatible models"
msgstr ""
msgid "AdminAIPoweredFeatures|Disabled"
msgstr ""
 
......@@ -3602,9 +3608,6 @@ msgstr ""
msgid "AdminAIPoweredFeatures|Select a self-hosted model"
msgstr ""
 
msgid "AdminAIPoweredFeatures|Self-hosted models"
msgstr ""
msgid "AdminAIPoweredFeatures|Sub feature"
msgstr ""
 
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册