Skip to content

Commit 0a99fc4

Browse files
alvarezmelissa87elasticmachinekibanamachine
authored
[ML][AI Connector] Add support for headers in the OpenAI integration (#238710)
## Summary This PR is the second part of an effort to add custom header support for OpenAI in the AI Connector. Because adding new properties to connector schema require a 2 step deployment (see #228371 (comment)), 2 PRs are needed for this. 1. step one - the schema changes here #239002 2. step two - things that can set the config property (so here, the UI) done in this PR Resolves #235687 This PR - adds support for 'map' type fields returned in the config fields for each provider - the UI now allows user specified headers in the form when creating the connector - 'enableCustomHeaders' form config prop has been added - this is only set to true for the connector - when updating a connector and it's corresponding inference endpoint, the 'update' inference method is now used instead of a 'delete'/'create' - this will ensure headers are persisted with the inference endpoint, which is what handles communication with the third party service and allows those headers to be passed along. Addresses #239219 https://github.com/user-attachments/assets/6c4cffa6-22be-41c7-9817-4ef805a32c93 ### NOTE: To test - add the schema changes from #239002 - [x] merge schema and InferenceConnector class definition update changes (#239002) - [x] remove schema changes from this PR - [ ] merge UI changes (#238710) ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [ ] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. --------- Co-authored-by: Elastic Machine <[email protected]> Co-authored-by: kibanamachine <[email protected]>
1 parent faf0c28 commit 0a99fc4

File tree

17 files changed

+741
-95
lines changed

17 files changed

+741
-95
lines changed

packages/kbn-optimizer/limits.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ pageLoadAssetSize:
164164
snapshotRestore: 22068
165165
spaces: 30782
166166
stackAlerts: 31499
167-
stackConnectors: 76260
167+
stackConnectors: 83751
168168
streams: 9000
169169
streamsApp: 17000
170170
synthetics: 31571

x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/configuration/authentication_form_items.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88
import React, { useMemo, useState } from 'react';
99
import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
1010
import { FormattedMessage } from '@kbn/i18n-react';
11-
import type { ConfigEntryView } from '../../types/types';
11+
import type { ConfigEntryView, Map } from '../../types/types';
1212
import { ItemFormRow } from './item_form_row';
1313

1414
interface AuthenticationFormItemsProps {
1515
isEdit?: boolean;
1616
isLoading: boolean;
1717
isPreconfigured?: boolean;
1818
items: ConfigEntryView[];
19-
setConfigEntry: (key: string, value: string | number | boolean | null) => void;
19+
setConfigEntry: (key: string, value: string | number | boolean | null | Map) => void;
2020
reenterSecretsOnEdit?: boolean;
2121
}
2222

x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/configuration/configuration_field.tsx

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,52 @@
77

88
import React, { useEffect, useState } from 'react';
99

10+
import type { EuiSwitchEvent } from '@elastic/eui';
1011
import {
1112
EuiAccordion,
13+
EuiButton,
14+
EuiButtonIcon,
1215
EuiFieldText,
1316
EuiFieldPassword,
17+
EuiFlexGroup,
18+
EuiFlexItem,
1419
EuiFormControlLayout,
20+
EuiFormRow,
1521
EuiSwitch,
1622
EuiTextArea,
1723
EuiFieldNumber,
1824
} from '@elastic/eui';
1925

2026
import { isEmpty } from 'lodash/fp';
21-
import type { ConfigEntryView } from '../../types/types';
22-
import { FieldType } from '../../types/types';
27+
import { FieldType, type Map, type ConfigEntryView } from '../../types/types';
28+
import {
29+
ADD_LABEL,
30+
DELETE_LABEL,
31+
HEADERS_KEY_LABEL,
32+
HEADERS_VALUE_LABEL,
33+
HEADERS_SWITCH_LABEL,
34+
} from '../../translations';
2335
import { ensureBooleanType, ensureCorrectTyping, ensureStringType } from './configuration_utils';
2436

2537
interface ConfigurationFieldProps {
2638
configEntry: ConfigEntryView;
2739
isLoading: boolean;
28-
setConfigValue: (value: number | string | boolean | null) => void;
40+
setConfigValue: (value: number | string | boolean | null | Map) => void;
2941
isEdit?: boolean;
3042
isPreconfigured?: boolean;
3143
}
3244

3345
interface ConfigInputFieldProps {
3446
configEntry: ConfigEntryView;
3547
isLoading: boolean;
36-
validateAndSetConfigValue: (value: string | boolean) => void;
48+
validateAndSetConfigValue: (value: string | boolean | Map) => void;
3749
isEdit?: boolean;
3850
isPreconfigured?: boolean;
3951
}
52+
53+
const KEY_INDEX = 0;
54+
const VALUE_INDEX = 1;
55+
4056
export const ConfigInputField: React.FC<ConfigInputFieldProps> = ({
4157
configEntry,
4258
isLoading,
@@ -200,14 +216,142 @@ export const ConfigInputPassword: React.FC<ConfigInputFieldProps> = ({
200216
);
201217
};
202218

219+
const emptyHeaders: Map = { '': '' };
220+
221+
export const ConfigInputMapField: React.FC<ConfigInputFieldProps> = ({
222+
configEntry,
223+
isLoading,
224+
validateAndSetConfigValue,
225+
isEdit = false,
226+
}) => {
227+
const { isValid, value, default_value: defaultValue, key, updatable } = configEntry;
228+
const [showHeaderInputs, setShowHeaderInputs] = useState<boolean>(false);
229+
const [headers, setHeaders] = useState<Map>((value as Map) ?? defaultValue ?? emptyHeaders);
230+
231+
const onChange = (e: EuiSwitchEvent) => {
232+
setShowHeaderInputs(e.target.checked);
233+
// clear headers if unchecking
234+
if (e.target.checked === false) {
235+
setHeaders(emptyHeaders);
236+
validateAndSetConfigValue('');
237+
}
238+
};
239+
240+
const iterableHeaders: [string, string][] = Object.entries(headers);
241+
242+
const handleHeaderChange = (
243+
e: React.ChangeEvent<HTMLInputElement>,
244+
headerIndex: number,
245+
elementIndex: number
246+
) => {
247+
setHeaders((prevHeaders) => {
248+
const newHeaders = [...Object.entries(prevHeaders)];
249+
newHeaders[headerIndex][elementIndex] = e.target.value;
250+
const headersObj = Object.fromEntries(newHeaders);
251+
validateAndSetConfigValue(headersObj);
252+
return headersObj;
253+
});
254+
};
255+
256+
return (
257+
<>
258+
<EuiFlexGroup direction="column" gutterSize="s" data-test-subj={'config-field-map-type'}>
259+
<EuiFlexItem grow={false}>
260+
<EuiSwitch
261+
data-test-subj={`${key}-switch-${showHeaderInputs ? 'checked' : 'unchecked'}`}
262+
label={HEADERS_SWITCH_LABEL}
263+
checked={showHeaderInputs}
264+
onChange={(e) => onChange(e)}
265+
/>
266+
</EuiFlexItem>
267+
{showHeaderInputs
268+
? iterableHeaders.map((header, index) => (
269+
<EuiFlexItem key={`${key}-header-${index}`}>
270+
<EuiFlexGroup gutterSize="s" alignItems="center">
271+
<EuiFlexItem>
272+
<EuiFormRow label={HEADERS_KEY_LABEL}>
273+
<EuiFieldText
274+
data-test-subj={`${key}-key-${index}`}
275+
isInvalid={!isValid}
276+
disabled={isLoading || (isEdit && !updatable)}
277+
value={header[0]}
278+
onChange={(e) => {
279+
handleHeaderChange(e, index, KEY_INDEX);
280+
}}
281+
aria-label={HEADERS_KEY_LABEL}
282+
/>
283+
</EuiFormRow>
284+
</EuiFlexItem>
285+
<EuiFlexItem>
286+
<EuiFormRow label={HEADERS_VALUE_LABEL}>
287+
<EuiFieldText
288+
data-test-subj={`${key}-value-${index}`}
289+
disabled={isLoading || (isEdit && !updatable)}
290+
value={header[1]}
291+
onChange={(e) => {
292+
handleHeaderChange(e, index, VALUE_INDEX);
293+
}}
294+
aria-label={HEADERS_VALUE_LABEL}
295+
/>
296+
</EuiFormRow>
297+
</EuiFlexItem>
298+
<EuiFlexItem grow={false}>
299+
<EuiButtonIcon
300+
disabled={isLoading || (isEdit && !updatable)}
301+
display="base"
302+
color="danger"
303+
css={{ marginTop: '22px' }}
304+
onClick={() => {
305+
const newHeaders = iterableHeaders.toSpliced(index, 1);
306+
const headersObj = Object.fromEntries(newHeaders);
307+
setHeaders(headersObj);
308+
validateAndSetConfigValue(headersObj);
309+
}}
310+
iconType="minusInCircle"
311+
aria-label={DELETE_LABEL}
312+
data-test-subj={`${key}-delete-button-${index}`}
313+
/>
314+
</EuiFlexItem>
315+
</EuiFlexGroup>
316+
</EuiFlexItem>
317+
))
318+
: null}
319+
<EuiFlexItem grow={false}>
320+
<span>
321+
<EuiButton
322+
size="s"
323+
disabled={
324+
isLoading ||
325+
(isEdit && !updatable) ||
326+
(!isEdit &&
327+
iterableHeaders.length === 1 &&
328+
(iterableHeaders[0][0] === '' || iterableHeaders[0][1] === ''))
329+
}
330+
iconType="plusInCircle"
331+
onClick={() => {
332+
const newHeaders = [...iterableHeaders, ['', '']];
333+
setHeaders(Object.fromEntries(newHeaders));
334+
}}
335+
data-test-subj={`${key}-add-button`}
336+
aria-label={ADD_LABEL}
337+
>
338+
{ADD_LABEL}
339+
</EuiButton>
340+
</span>
341+
</EuiFlexItem>
342+
</EuiFlexGroup>
343+
</>
344+
);
345+
};
346+
203347
export const ConfigurationField: React.FC<ConfigurationFieldProps> = ({
204348
configEntry,
205349
isLoading,
206350
setConfigValue,
207351
isEdit,
208352
isPreconfigured,
209353
}) => {
210-
const validateAndSetConfigValue = (value: number | string | boolean) => {
354+
const validateAndSetConfigValue = (value: number | string | boolean | Map) => {
211355
setConfigValue(ensureCorrectTyping(configEntry.type, value));
212356
};
213357

@@ -235,6 +379,16 @@ export const ConfigurationField: React.FC<ConfigurationFieldProps> = ({
235379
/>
236380
);
237381

382+
case FieldType.MAP:
383+
return (
384+
<ConfigInputMapField
385+
isEdit={isEdit}
386+
isLoading={isLoading}
387+
configEntry={configEntry}
388+
validateAndSetConfigValue={validateAndSetConfigValue}
389+
/>
390+
);
391+
238392
default:
239393
return sensitive ? (
240394
<ConfigInputPassword

x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/configuration/configuration_form_items.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import React from 'react';
99

1010
import { EuiFlexGroup } from '@elastic/eui';
11-
import type { ConfigEntryView } from '../../types/types';
11+
import type { ConfigEntryView, Map } from '../../types/types';
1212
import { ItemFormRow } from './item_form_row';
1313

1414
interface ConfigurationFormItemsProps {
@@ -20,7 +20,7 @@ interface ConfigurationFormItemsProps {
2020
isPreconfigured?: boolean;
2121
isInternalProvider?: boolean;
2222
items: ConfigEntryView[];
23-
setConfigEntry: (key: string, value: string | number | boolean | null) => void;
23+
setConfigEntry: (key: string, value: string | number | boolean | null | Map) => void;
2424
}
2525

2626
export const ConfigurationFormItems: React.FC<ConfigurationFormItemsProps> = ({

x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/configuration/configuration_utils.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
* 2.0.
66
*/
77

8-
import { FieldType } from '../../types/types';
8+
import { FieldType, type Map } from '../../types/types';
99

10-
export const validIntInput = (value: string | number | boolean | null): boolean => {
10+
export const validIntInput = (value: string | number | boolean | null | Map): boolean => {
1111
// reject non integers (including x.0 floats), but don't validate if empty
1212
return (value !== null || value !== '') &&
1313
(isNaN(Number(value)) ||
@@ -19,23 +19,25 @@ export const validIntInput = (value: string | number | boolean | null): boolean
1919

2020
export const ensureCorrectTyping = (
2121
type: FieldType,
22-
value: string | number | boolean | null
23-
): string | number | boolean | null => {
22+
value: string | number | boolean | null | Map
23+
): string | number | boolean | null | Map => {
2424
switch (type) {
2525
case FieldType.INTEGER:
2626
return validIntInput(value) ? ensureIntType(value) : value;
2727
case FieldType.BOOLEAN:
2828
return ensureBooleanType(value);
29+
case FieldType.MAP:
30+
return value;
2931
default:
3032
return ensureStringType(value);
3133
}
3234
};
3335

34-
export const ensureStringType = (value: string | number | boolean | null): string => {
36+
export const ensureStringType = (value: string | number | boolean | null | Map): string => {
3537
return value !== null ? String(value) : '';
3638
};
3739

38-
export const ensureIntType = (value: string | number | boolean | null): number | null => {
40+
export const ensureIntType = (value: string | number | boolean | null | Map): number | null => {
3941
// int is null-safe to prevent empty values from becoming zeroes
4042
if (value === null || value === '') {
4143
return null;
@@ -44,6 +46,6 @@ export const ensureIntType = (value: string | number | boolean | null): number |
4446
return parseInt(String(value), 10);
4547
};
4648

47-
export const ensureBooleanType = (value: string | number | boolean | null): boolean => {
49+
export const ensureBooleanType = (value: string | number | boolean | null | Map): boolean => {
4850
return Boolean(value);
4951
};

x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/src/components/configuration/item_form_row.tsx

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@elastic/eui';
1919

2020
import type { ConfigEntryView } from '../../types/types';
21+
import { FieldType, type Map } from '../../types/types';
2122
import { ConfigFieldTitularComponent } from './titular_component_registry';
2223
import { ConfigurationField } from './configuration_field';
2324
import * as LABELS from '../../translations';
@@ -30,8 +31,8 @@ interface ItemFormRowProps {
3031
isInternalProvider?: boolean;
3132
isEdit?: boolean;
3233
isLoading: boolean;
34+
setConfigEntry: (key: string, value: string | number | boolean | null | Map) => void;
3335
reenterSecretsOnEdit?: boolean;
34-
setConfigEntry: (key: string, value: string | number | boolean | null) => void;
3536
}
3637

3738
export const ItemFormRow: React.FC<ItemFormRowProps> = ({
@@ -87,28 +88,38 @@ export const ItemFormRow: React.FC<ItemFormRowProps> = ({
8788
</EuiText>
8889
) : undefined;
8990

91+
const wrapInFormRow = configEntry.type !== FieldType.MAP;
92+
93+
const configField = (
94+
<ConfigurationField
95+
configEntry={configEntry}
96+
isLoading={isLoading}
97+
setConfigValue={(value) => {
98+
setConfigEntry(key, value);
99+
}}
100+
isEdit={isEdit}
101+
isPreconfigured={isPreconfigured}
102+
/>
103+
);
104+
90105
return (
91106
<EuiFlexItem key={key}>
92107
<ConfigFieldTitularComponent configKey={key} />
93-
<EuiFormRow
94-
label={rowLabel}
95-
fullWidth
96-
helpText={helpText}
97-
error={validationErrors}
98-
isInvalid={!isValid}
99-
labelAppend={optionalLabel}
100-
data-test-subj={`configuration-formrow-${key}`}
101-
>
102-
<ConfigurationField
103-
configEntry={configEntry}
104-
isLoading={isLoading}
105-
setConfigValue={(value) => {
106-
setConfigEntry(key, value);
107-
}}
108-
isEdit={isEdit}
109-
isPreconfigured={isPreconfigured}
110-
/>
111-
</EuiFormRow>
108+
{wrapInFormRow ? (
109+
<EuiFormRow
110+
label={rowLabel}
111+
fullWidth
112+
helpText={helpText}
113+
error={validationErrors}
114+
isInvalid={!isValid}
115+
labelAppend={optionalLabel}
116+
data-test-subj={`configuration-formrow-${key}`}
117+
>
118+
{configField}
119+
</EuiFormRow>
120+
) : (
121+
configField
122+
)}
112123
{sensitive && reenterSecretsOnEdit ? (
113124
<>
114125
<EuiSpacer size="s" />

0 commit comments

Comments
 (0)