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

Fix apiToken form field

- return api_token with self-hosted model payload data
- Use InputCopyToggleVisibility instead of GlFormFields for rendering api token as it allows for visibility toggling
上级 c5667874
No related branches found
No related tags found
无相关合并请求
显示
294 个添加43 个删除
<script>
import { s__ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import updateSelfHostedModelMutation from '../graphql/mutations/update_self_hosted_model.mutation.graphql';
import { SELF_HOSTED_MODEL_MUTATIONS } from '../constants';
import SelfHostedModelForm from './self_hosted_model_form.vue';
export default {
name: 'EditSelfHostedModel',
components: {
SelfHostedModelForm,
},
props: {
basePath: {
type: String,
required: true,
},
model: {
type: Object,
required: true,
},
modelOptions: {
type: Array,
required: true,
},
},
computed: {
modelData() {
return convertObjectPropsToCamelCase(this.model);
},
},
i18n: {
title: s__('AdminSelfHostedModels|Edit self-hosted model'),
description: s__(
'AdminSelfHostedModels|Edit the AI model that can be used for GitLab Duo features.',
),
},
mutationData: {
name: SELF_HOSTED_MODEL_MUTATIONS.UPDATE,
mutation: updateSelfHostedModelMutation,
},
};
</script>
<template>
<div>
<h1>{{ $options.i18n.title }}</h1>
<p class="gl-pb-2 gl-pt-3">
{{ $options.i18n.description }}
</p>
<self-hosted-model-form
:initial-form-values="modelData"
:base-path="basePath"
:model-options="modelOptions"
:mutation-data="$options.mutationData"
:submit-button-text="s__('AdminSelfHostedModels|Edit self-hosted model')"
/>
</div>
</template>
<script>
import { s__ } from '~/locale';
import selfHostedModelCreateMutation from '../graphql/mutations/create_self_hosted_model.mutation.graphql';
import { SELF_HOSTED_MODEL_MUTATIONS } from '../constants';
import SelfHostedModelForm from './self_hosted_model_form.vue';
const MUTATION_NAME = 'aiSelfHostedModelCreate';
export default {
name: 'NewSelfHostedModel',
components: {
......@@ -27,7 +26,7 @@ export default {
),
},
mutationData: {
name: MUTATION_NAME,
name: SELF_HOSTED_MODEL_MUTATIONS.CREATE,
mutation: selfHostedModelCreateMutation,
},
};
......
<script>
import { GlForm, GlButton, GlCollapsibleListbox, GlFormFields } from '@gitlab/ui';
import { formValidators } from '@gitlab/ui/dist/utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
export default {
name: 'SelfHostedModelForm',
......@@ -12,6 +14,7 @@ export default {
GlButton,
GlCollapsibleListbox,
GlFormFields,
InputCopyToggleVisibility,
},
props: {
basePath: {
......@@ -31,10 +34,15 @@ export default {
type: Object,
required: true,
},
initialFormValues: {
type: Object,
required: false,
default: () => ({}),
},
},
i18n: {
defaultError: s__(
'AdminSelfHostedModels|There was an error creating the self-hosted model. Please try again.',
'AdminSelfHostedModels|There was an error saving the self-hosted model. Please try again.',
),
missingDeploymentNameError: s__('AdminSelfHostedModels|Please enter a deployment name.'),
missingEndpointError: s__('AdminSelfHostedModels|Please enter an endpoint.'),
......@@ -46,6 +54,9 @@ export default {
},
formId: 'self-hosted-model-form',
data() {
const { name = '', model = '', endpoint = '', apiToken = '' } = this.initialFormValues;
const modelToUpperCase = model.toUpperCase();
return {
fields: {
name: {
......@@ -60,17 +71,19 @@ export default {
label: s__('AdminSelfHostedModels|Endpoint'),
validators: [formValidators.required(this.$options.i18n.missingEndpointError)],
},
apiToken: {
label: s__('AdminSelfHostedModels|API Key (optional)'),
},
},
formValues: {
name: '',
model: '',
endpoint: '',
apiToken: '',
baseFormValues: {
name,
endpoint,
model: modelToUpperCase,
},
apiToken,
selectedModel: {
modelValue: model || '',
modelName:
this.modelOptions.find(({ modelValue }) => modelValue === model.toUpperCase())
?.modelName || '',
},
selectedModel: { modelValue: '', modelName: '' },
serverValidations: {},
isSaving: false,
};
......@@ -87,11 +100,14 @@ export default {
},
hasValidInput() {
return (
this.formValues.name !== '' &&
this.formValues.model !== '' &&
this.formValues.endpoint !== ''
this.baseFormValues.name !== '' &&
this.baseFormValues.model !== '' &&
this.baseFormValues.endpoint !== ''
);
},
isEditing() {
return Boolean(this.initialFormValues.id);
},
},
methods: {
async onSubmit() {
......@@ -99,12 +115,24 @@ export default {
const { mutation } = this.mutationData;
const formValues = {
apiToken: this.apiToken,
...this.baseFormValues,
...(this.isEditing
? {
id: convertToGraphQLId('Ai::SelfHostedModel', this.initialFormValues.id),
}
: {}),
};
this.isSaving = true;
try {
const { data } = await this.$apollo.mutate({
mutation,
variables: {
input: { ...this.formValues },
input: {
...formValues,
},
},
});
if (data) {
......@@ -131,7 +159,7 @@ export default {
onSelect(model) {
this.onInputField({ name: 'model' });
this.selectedModel = this.modelOptions.find((item) => item.modelValue === model);
this.formValues.model = this.selectedModel.modelValue;
this.baseFormValues.model = this.selectedModel.modelValue;
},
// clears the validation error
onInputField({ name }) {
......@@ -177,12 +205,12 @@ export default {
<template>
<gl-form :id="$options.formId" class="gl-max-w-48" @submit.prevent="onSubmit">
<gl-form-fields
v-model="formValues"
v-model="baseFormValues"
:fields="fields"
:form-id="$options.formId"
:server-validations="serverValidations"
@input-field="onInputField"
@submit="$emit('submit', formValues)"
@submit="$emit('submit', baseFormValues)"
>
<template #input(model)>
<gl-collapsible-listbox
......@@ -195,6 +223,14 @@ export default {
/>
</template>
</gl-form-fields>
<input-copy-toggle-visibility
v-model="apiToken"
:value="apiToken"
:label="s__('AdminSelfHostedModels|API Key (optional)')"
:initial-visibility="false"
:disabled="isSaving"
:show-copy-button="false"
/>
<div class="gl-pt-5">
<gl-button
type="submit"
......
export const SELF_HOSTED_MODEL_MUTATIONS = {
CREATE: 'aiSelfHostedModelCreate',
UPDATE: 'aiSelfHostedModelUpdate',
};
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import EditSelfHostedModel from '../components/edit_self_hosted_model.vue';
initSimpleApp('#js-edit-self-hosted-model', EditSelfHostedModel, { withApolloProvider: true });
mutation updateSelfHostedModel($input: AiSelfHostedModelUpdateInput!) {
aiSelfHostedModelUpdate(input: $input) {
errors
}
}
- add_to_breadcrumbs s_('AdminSelfHostedModels|Self-hosted models'), admin_labels_path
- breadcrumb_title s_('AdminSelfHostedModels|Edit self-hosted model')
- page_title _('Edit'), @self_hosted_model.model, s_('AdminSelfHostedModels|Self-hosted models')
%h1.page-title.gl-text-size-h-display
= s_('AdminSelfHostedModels|Edit self-hosted model')
- if Feature.enabled?(:custom_models_vue_app, current_user)
- model_options = Ai::SelfHostedModel.models.map { |name, _| { modelValue: name.upcase, modelName: name.capitalize } }
= render 'form', url: admin_ai_self_hosted_model_path(@self_hosted_model), back_path: admin_ai_self_hosted_models_path
#js-edit-self-hosted-model{ data: { view_model: { model: @self_hosted_model, modelOptions: model_options, basePath: admin_ai_self_hosted_models_path }.to_json(methods: [:api_token]) } }
- else
- page_title _('Edit'), @self_hosted_model.model, s_('AdminSelfHostedModels|Self-hosted models')
%h1.page-title.gl-text-size-h-display
= s_('AdminSelfHostedModels|Edit self-hosted model')
= render 'form', url: admin_ai_self_hosted_model_path(@self_hosted_model), back_path: admin_ai_self_hosted_models_path
import { shallowMount } from '@vue/test-utils';
import SelfHostedModelForm from 'ee/pages/admin/ai/self_hosted_models/components/self_hosted_model_form.vue';
import updateSelfHostedModelMutation from 'ee/pages/admin/ai/self_hosted_models/graphql/mutations/update_self_hosted_model.mutation.graphql';
import EditSelfHostedModel from 'ee/pages/admin/ai/self_hosted_models/components/edit_self_hosted_model.vue';
import { SELF_HOSTED_MODEL_MUTATIONS } from 'ee/pages/admin/ai/self_hosted_models/constants';
import { SELF_HOSTED_MODEL_OPTIONS, mockSelfHostedModel } from './mock_data';
describe('EditSelfHostedModel', () => {
let wrapper;
const basePath = '/admin/ai/self_hosted_models';
const createComponent = ({ props = {} }) => {
wrapper = shallowMount(EditSelfHostedModel, {
propsData: {
basePath,
...props,
},
});
};
beforeEach(() => {
createComponent({
props: { modelOptions: SELF_HOSTED_MODEL_OPTIONS, model: mockSelfHostedModel },
});
});
const findSelfHostedModelForm = () => wrapper.findComponent(SelfHostedModelForm);
it('has a title', () => {
expect(wrapper.text()).toMatch('Edit self-hosted model');
});
it('has a description', () => {
expect(wrapper.text()).toMatch('Edit the AI model that can be used for GitLab Duo features.');
});
it('renders the self-hosted model form and passes the correct props', () => {
const selfHostedModelForm = findSelfHostedModelForm();
expect(selfHostedModelForm.exists()).toBe(true);
expect(selfHostedModelForm.props('basePath')).toBe(basePath);
expect(selfHostedModelForm.props('initialFormValues')).toEqual(mockSelfHostedModel);
expect(selfHostedModelForm.props('modelOptions')).toBe(SELF_HOSTED_MODEL_OPTIONS);
expect(selfHostedModelForm.props('mutationData')).toEqual({
name: SELF_HOSTED_MODEL_MUTATIONS.UPDATE,
mutation: updateSelfHostedModelMutation,
});
expect(selfHostedModelForm.props('submitButtonText')).toBe('Edit self-hosted model');
});
});
......@@ -3,7 +3,7 @@ export const mockSelfHostedModel = {
name: 'mock-self-hosted-model',
model: 'mixtral',
endpoint: 'https://mock-endpoint.com',
hasApiToken: false,
apiToken: '',
};
export const mockSelfHostedModelsList = [
......
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { shallowMount } from '@vue/test-utils';
import SelfHostedModelForm from 'ee/pages/admin/ai/self_hosted_models/components/self_hosted_model_form.vue';
import createSelfHostedModelMutation from 'ee/pages/admin/ai/self_hosted_models/graphql/mutations/create_self_hosted_model.mutation.graphql';
import NewSelfHostedModel from 'ee/pages/admin/ai/self_hosted_models/components/new_self_hosted_model.vue';
import { SELF_HOSTED_MODEL_MUTATIONS } from 'ee/pages/admin/ai/self_hosted_models/constants';
import { SELF_HOSTED_MODEL_OPTIONS } from './mock_data';
describe('NewSelfHostedModel', () => {
......@@ -10,7 +11,7 @@ describe('NewSelfHostedModel', () => {
const basePath = '/admin/ai/self_hosted_models';
const createComponent = () => {
wrapper = mountExtended(NewSelfHostedModel, {
wrapper = shallowMount(NewSelfHostedModel, {
propsData: {
basePath,
modelOptions: SELF_HOSTED_MODEL_OPTIONS,
......@@ -39,7 +40,7 @@ describe('NewSelfHostedModel', () => {
expect(selfHostedModelForm.props('basePath')).toBe(basePath);
expect(selfHostedModelForm.props('modelOptions')).toBe(SELF_HOSTED_MODEL_OPTIONS);
expect(selfHostedModelForm.props('mutationData')).toEqual({
name: 'aiSelfHostedModelCreate',
name: SELF_HOSTED_MODEL_MUTATIONS.CREATE,
mutation: createSelfHostedModelMutation,
});
});
......
......@@ -5,9 +5,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SelfHostedModelForm from 'ee/pages/admin/ai/self_hosted_models/components/self_hosted_model_form.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
import createSelfHostedModelMutation from 'ee/pages/admin/ai/self_hosted_models/graphql/mutations/create_self_hosted_model.mutation.graphql';
import updateSelfHostedModelMutation from 'ee/pages/admin/ai/self_hosted_models/graphql/mutations/update_self_hosted_model.mutation.graphql';
import { createAlert } from '~/alert';
import { SELF_HOSTED_MODEL_OPTIONS } from './mock_data';
import { SELF_HOSTED_MODEL_MUTATIONS } from 'ee/pages/admin/ai/self_hosted_models/constants';
import { SELF_HOSTED_MODEL_OPTIONS, mockSelfHostedModel as mockModelData } from './mock_data';
Vue.use(VueApollo);
......@@ -25,6 +28,12 @@ describe('SelfHostedModelForm', () => {
});
const createComponent = async ({
props = {
mutationData: {
name: SELF_HOSTED_MODEL_MUTATIONS.CREATE,
mutation: createSelfHostedModelMutation,
},
},
apolloHandlers = [[createSelfHostedModelMutation, createMutationSuccessHandler]],
} = {}) => {
const mockApollo = createMockApollo([...apolloHandlers]);
......@@ -35,10 +44,7 @@ describe('SelfHostedModelForm', () => {
propsData: {
basePath,
modelOptions: SELF_HOSTED_MODEL_OPTIONS,
mutationData: {
name: 'aiSelfHostedModelCreate',
mutation: createSelfHostedModelMutation,
},
...props,
},
});
......@@ -46,13 +52,20 @@ describe('SelfHostedModelForm', () => {
};
beforeEach(async () => {
await createComponent();
await createComponent({
props: {
mutationData: {
name: SELF_HOSTED_MODEL_MUTATIONS.CREATE,
mutation: createSelfHostedModelMutation,
},
},
});
});
const findGlForm = () => wrapper.findComponent(GlForm);
const findNameInputField = () => wrapper.findByLabelText('Deployment name');
const findEndpointInputField = () => wrapper.findByLabelText('Endpoint');
const findApiKeyInputField = () => wrapper.findByLabelText('API Key (optional)');
const findApiKeyInputField = () => wrapper.findComponent(InputCopyToggleVisibility);
const findCollapsibleListBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findCreateButton = () => wrapper.find('button[type="submit"]');
const findCancelButton = () => wrapper.findByText('Cancel');
......@@ -98,12 +111,6 @@ describe('SelfHostedModelForm', () => {
});
});
it('renders a create button', () => {
const button = findCreateButton();
expect(button.text()).toBe('Create self-hosted model');
});
it('renders a cancel button', () => {
expect(findCancelButton().exists()).toBe(true);
});
......@@ -198,7 +205,7 @@ describe('SelfHostedModelForm', () => {
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: 'There was an error creating the self-hosted model. Please try again.',
message: 'There was an error saving the self-hosted model. Please try again.',
error,
captureError: true,
}),
......@@ -206,4 +213,82 @@ describe('SelfHostedModelForm', () => {
});
});
});
describe('When creating a self-hosted model', () => {
it('renders the submit button with the correct text', () => {
const button = findCreateButton();
expect(button.text()).toBe('Create self-hosted model');
});
it('invokes the create mutation with correct input variables', async () => {
await findNameInputField().setValue('test deployment');
await findEndpointInputField().setValue('http://test.com');
await findCollapsibleListBox().vm.$emit('select', 'MIXTRAL');
wrapper.find('form').trigger('submit.prevent');
await waitForPromises();
expect(createMutationSuccessHandler).toHaveBeenCalledWith({
input: {
name: 'test deployment',
endpoint: 'http://test.com',
model: 'MIXTRAL',
apiToken: '',
},
});
});
});
describe('When editing a self-hosted model', () => {
const updateMutationSuccessHandler = jest.fn().mockResolvedValue({
data: {
aiSelfHostedModelCreate: {
errors: [],
},
},
});
beforeEach(async () => {
await createComponent({
props: {
initialFormValues: mockModelData,
mutationData: {
name: SELF_HOSTED_MODEL_MUTATIONS.UPDATE,
mutation: updateSelfHostedModelMutation,
},
submitButtonText: 'Edit self-hosted model',
},
apolloHandlers: [[updateSelfHostedModelMutation, updateMutationSuccessHandler]],
});
});
it('renders the submit button with the correct text', () => {
const button = findCreateButton();
expect(button.text()).toBe('Edit self-hosted model');
});
it('invokes the update mutation with correct input variables', async () => {
await findNameInputField().setValue('test deployment');
await findEndpointInputField().setValue('http://test.com');
await findCollapsibleListBox().vm.$emit('select', 'MIXTRAL');
await findApiKeyInputField().vm.$emit('input', 'abc123');
wrapper.find('form').trigger('submit.prevent');
await waitForPromises();
expect(updateMutationSuccessHandler).toHaveBeenCalledWith({
input: {
id: mockModelData.id,
name: 'test deployment',
endpoint: 'http://test.com',
model: 'MIXTRAL',
apiToken: 'abc123',
},
});
});
});
});
......@@ -3909,6 +3909,9 @@ msgstr ""
msgid "AdminSelfHostedModels|Edit self-hosted model"
msgstr ""
 
msgid "AdminSelfHostedModels|Edit the AI model that can be used for GitLab Duo features."
msgstr ""
msgid "AdminSelfHostedModels|Enable self-hosted models"
msgstr ""
 
......@@ -3960,7 +3963,7 @@ msgstr ""
msgid "AdminSelfHostedModels|Self-hosted models are an experimental feature."
msgstr ""
 
msgid "AdminSelfHostedModels|There was an error creating the self-hosted model. Please try again."
msgid "AdminSelfHostedModels|There was an error saving the self-hosted model. Please try again."
msgstr ""
 
msgid "AdminSettings|%{generate_manually_link_start}Generate%{generate_manually_link_end} Service Ping to preview and download service usage data payload."
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册