mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Semantic Text UI] Add toast, modal and banner for deployment status (#180246)
In this PR, we added the following items. - Add a toast for letting the user know that the underlying model is being deployed - Start the deployment of trained model if the model deployment has not been started - Add a modal to display the current status of trained model deployment - Show a link to the model management page. - Create a inference endpoint for default inference_ids if they are missing - Display a badge for inference_endpoint - Show warning if mappingsDefinition is null Please be aware that currently, we won't be able to save the mapping using the 'Save mappings' button because the 'semantic_text' functionality doesn't support 'inference_id'. However, there is ongoing parallel work in a GitHub [branch](https://github.com/elastic/elasticsearch/tree/feature/semantic-text) to enable 'inference_id' in 'semantic_text' for Elasticsearch. ### How to test the changes locally - Download the elasticsearch changes from GitHub [branch](https://github.com/elastic/elasticsearch/tree/feature/semantic-text) - Run the elasticsearch: `./gradlew :run -Drun.license_type=trial` - Download the changes of this PR in local kibana and do the following steps + Set isSemanticTextEnabled = true in this [location](https://github.com/elastic/kibana/pull/180246/files#diff-92f4739f8a4a6917951a1b6e1af21a96d54313eaa2b5ce4c0e0553dd2ee11fcaL80) + Run `yarn start` - Create an index named 'books' using the following command: <details> <summary>Click to expand</summary> ``` PUT books { "mappings": { "dynamic_templates": [], "properties": { "date_published": { "type": "date" }, "price": { "type": "float" }, "title": { "type": "text" }, "attributes": { "type": "nested", "properties": { "authors": { "type": "nested", "properties": { "author_name": { "type": "text" }, "author_birthdate": { "type": "date" } // Add more author attributes as needed } }, "genres": { "type": "nested", "properties": { "genre_name": { "type": "keyword" }, "genre_description": { "type": "text" } // Add more genre attributes as needed } } } } } } } ``` </details> - Follow the steps mentioned in this video:f1ef71e3
-8adf-4bcd-837c-754929fe6f1c ### Screenshots       --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Sander Philipse <sander.philipse@elastic.co>
This commit is contained in:
parent
b92890a051
commit
19b0543fd9
36 changed files with 1365 additions and 319 deletions
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
|
@ -187,6 +187,14 @@ export type InferenceServiceSettings =
|
|||
model_id: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
service: 'elasticsearch';
|
||||
service_settings: {
|
||||
num_allocations: number;
|
||||
num_threads: number;
|
||||
model_id: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
service: 'openai';
|
||||
service_settings: {
|
||||
|
|
|
@ -51,6 +51,7 @@ export interface IndexDetailsPageTestBed extends TestBed {
|
|||
getTreeViewContent: (fieldName: string) => string;
|
||||
clickToggleViewButton: () => Promise<void>;
|
||||
isSearchBarDisabled: () => boolean;
|
||||
isSemanticTextBannerVisible: () => boolean;
|
||||
};
|
||||
settings: {
|
||||
getCodeBlockContent: () => string;
|
||||
|
@ -218,6 +219,9 @@ export const setup = async ({
|
|||
isSearchBarDisabled: () => {
|
||||
return find('indexDetailsMappingsFieldSearch').prop('disabled');
|
||||
},
|
||||
isSemanticTextBannerVisible: () => {
|
||||
return exists('indexDetailsMappingsSemanticTextBanner');
|
||||
},
|
||||
clickAddFieldButton: async () => {
|
||||
expect(exists('indexDetailsMappingsAddField')).toBe(true);
|
||||
await act(async () => {
|
||||
|
|
|
@ -497,6 +497,10 @@ describe('<IndexDetailsPage />', () => {
|
|||
expect(testBed.actions.mappings.isSearchBarDisabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('semantic text banner is not visible', async () => {
|
||||
expect(testBed.actions.mappings.isSemanticTextBannerVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('sets the docs link href from the documentation service', async () => {
|
||||
const docsLinkHref = testBed.actions.mappings.getDocsLinkHref();
|
||||
// the url from the mocked docs mock
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { registerTestBed } from '@kbn/test-jest-helpers';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { SemanticTextBanner } from '../../../public/application/sections/home/index_list/details_page/semantic_text_banner';
|
||||
|
||||
describe('When semantic_text is enabled', () => {
|
||||
const setup = registerTestBed(SemanticTextBanner, {
|
||||
defaultProps: { isSemanticTextEnabled: true },
|
||||
memoryRouter: { wrapComponent: false },
|
||||
});
|
||||
const { exists, find } = setup();
|
||||
|
||||
it('should display the banner', () => {
|
||||
expect(exists('indexDetailsMappingsSemanticTextBanner')).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain content related to semantic_text', () => {
|
||||
expect(find('indexDetailsMappingsSemanticTextBanner').text()).toContain(
|
||||
'semantic_text field type now available!'
|
||||
);
|
||||
});
|
||||
|
||||
it('should hide the banner if dismiss is clicked', async () => {
|
||||
await act(async () => {
|
||||
find('SemanticTextBannerDismissButton').simulate('click');
|
||||
});
|
||||
expect(exists('indexDetailsMappingsSemanticTextBanner')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When semantic_text is disabled', () => {
|
||||
const setup = registerTestBed(SemanticTextBanner, {
|
||||
defaultProps: { isSemanticTextEnabled: false },
|
||||
memoryRouter: { wrapComponent: false },
|
||||
});
|
||||
const { exists } = setup();
|
||||
|
||||
it('should not display the banner', () => {
|
||||
expect(exists('indexDetailsMappingsSemanticTextBanner')).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { registerTestBed } from '@kbn/test-jest-helpers';
|
||||
import { TrainedModelsDeploymentModal } from '../../../public/application/sections/home/index_list/details_page/trained_models_deployment_modal';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
const refreshModal = jest.fn();
|
||||
const setIsModalVisible = jest.fn();
|
||||
const tryAgainForErrorModal = jest.fn();
|
||||
const setIsVisibleForErrorModal = jest.fn();
|
||||
|
||||
describe('When semantic_text is enabled', () => {
|
||||
describe('When there is no error in the model deployment', () => {
|
||||
const setup = registerTestBed(TrainedModelsDeploymentModal, {
|
||||
defaultProps: {
|
||||
isSemanticTextEnabled: true,
|
||||
pendingDeployments: ['.elser-test-3'],
|
||||
setIsModalVisible,
|
||||
refreshModal,
|
||||
},
|
||||
memoryRouter: { wrapComponent: false },
|
||||
});
|
||||
const { exists, find } = setup();
|
||||
|
||||
it('should display the modal', () => {
|
||||
expect(exists('trainedModelsDeploymentModal')).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain content related to semantic_text', () => {
|
||||
expect(find('trainedModelsDeploymentModalText').text()).toContain(
|
||||
'Some fields are referencing models'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call refresh method if refresh button is pressed', async () => {
|
||||
await act(async () => {
|
||||
find('confirmModalConfirmButton').simulate('click');
|
||||
});
|
||||
expect(refreshModal.mock.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call setIsModalVisible method if cancel button is pressed', async () => {
|
||||
await act(async () => {
|
||||
find('confirmModalCancelButton').simulate('click');
|
||||
});
|
||||
expect(setIsModalVisible.mock.calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When there is error in the model deployment', () => {
|
||||
const setup = registerTestBed(TrainedModelsDeploymentModal, {
|
||||
defaultProps: {
|
||||
isSemanticTextEnabled: true,
|
||||
pendingDeployments: ['.elser-test-3'],
|
||||
setIsModalVisible: setIsVisibleForErrorModal,
|
||||
refreshModal: tryAgainForErrorModal,
|
||||
errorsInTrainedModelDeployment: ['.elser-test-3'],
|
||||
},
|
||||
memoryRouter: { wrapComponent: false },
|
||||
});
|
||||
const { exists, find } = setup();
|
||||
|
||||
it('should display the modal', () => {
|
||||
expect(exists('trainedModelsErroredDeploymentModal')).toBe(true);
|
||||
});
|
||||
|
||||
it('should contain content related to semantic_text', () => {
|
||||
expect(find('trainedModelsErrorDeploymentModalText').text()).toContain(
|
||||
'There was an error when trying to deploy'
|
||||
);
|
||||
});
|
||||
|
||||
it("should call refresh method if 'Try again' button is pressed", async () => {
|
||||
await act(async () => {
|
||||
find('confirmModalConfirmButton').simulate('click');
|
||||
});
|
||||
expect(tryAgainForErrorModal.mock.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call setIsModalVisible method if cancel button is pressed', async () => {
|
||||
await act(async () => {
|
||||
find('confirmModalCancelButton').simulate('click');
|
||||
});
|
||||
expect(setIsVisibleForErrorModal.mock.calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When semantic_text is disabled', () => {
|
||||
const setup = registerTestBed(TrainedModelsDeploymentModal, {
|
||||
defaultProps: { isSemanticTextEnabled: false },
|
||||
memoryRouter: { wrapComponent: false },
|
||||
});
|
||||
const { exists } = setup();
|
||||
it('it should not display the modal', () => {
|
||||
expect(exists('trainedModelsDeploymentModal')).toBe(false);
|
||||
});
|
||||
});
|
|
@ -15,19 +15,19 @@ import {
|
|||
FatalErrorsStart,
|
||||
ScopedHistory,
|
||||
DocLinksStart,
|
||||
IUiSettingsClient,
|
||||
ExecutionContextStart,
|
||||
HttpSetup,
|
||||
IUiSettingsClient,
|
||||
} from '@kbn/core/public';
|
||||
import type { MlPluginStart } from '@kbn/ml-plugin/public';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
|
||||
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import { EuiBreadcrumb } from '@elastic/eui';
|
||||
import type { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
import type { ConsolePluginStart } from '@kbn/console-plugin/public';
|
||||
import { EuiBreadcrumb } from '@elastic/eui';
|
||||
import type { MlPluginStart } from '@kbn/ml-plugin/public';
|
||||
import type { SettingsStart } from '@kbn/core-ui-settings-browser';
|
||||
import { ExtensionsService } from '../services';
|
||||
import { UiMetricService, NotificationService, HttpService } from './services';
|
||||
import { HttpService, NotificationService, UiMetricService } from './services';
|
||||
import { IndexManagementBreadcrumb } from './services/breadcrumbs';
|
||||
|
||||
export const AppContext = createContext<AppDependencies | undefined>(undefined);
|
||||
|
|
|
@ -26,6 +26,7 @@ const { GlobalFlyoutProvider } = GlobalFlyout;
|
|||
// We provide the minimum deps required to make the tests pass
|
||||
const appDependencies = {
|
||||
docLinks: {} as any,
|
||||
plugins: { ml: {} as any },
|
||||
} as any;
|
||||
|
||||
export const componentTemplatesDependencies = (httpSetup: HttpSetup, coreStart?: CoreStart) => ({
|
||||
|
|
|
@ -11,6 +11,15 @@ import { componentHelpers, MappingsEditorTestBed } from '../helpers';
|
|||
|
||||
const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor;
|
||||
|
||||
jest.mock('../../../../component_templates/component_templates_context', () => ({
|
||||
useComponentTemplatesContext: jest.fn().mockReturnValue({
|
||||
toasts: {
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Mappings editor: other datatype', () => {
|
||||
/**
|
||||
* Variable to store the mappings data forwarded to the consumer component
|
||||
|
|
|
@ -11,6 +11,15 @@ import { componentHelpers, MappingsEditorTestBed } from './helpers';
|
|||
|
||||
const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor;
|
||||
|
||||
jest.mock('../../../component_templates/component_templates_context', () => ({
|
||||
useComponentTemplatesContext: jest.fn().mockReturnValue({
|
||||
toasts: {
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Mappings editor: core', () => {
|
||||
/**
|
||||
* Variable to store the mappings data forwarded to the consumer component
|
||||
|
|
|
@ -11,6 +11,15 @@ import { componentHelpers, MappingsEditorTestBed } from './helpers';
|
|||
|
||||
const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor;
|
||||
|
||||
jest.mock('../../../component_templates/component_templates_context', () => ({
|
||||
useComponentTemplatesContext: jest.fn().mockReturnValue({
|
||||
toasts: {
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Mappings editor: runtime fields', () => {
|
||||
/**
|
||||
* Variable to store the mappings data forwarded to the consumer component
|
||||
|
|
|
@ -5,23 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { GlobalFlyout } from '../../shared_imports';
|
||||
import { useMappingsState, useDispatch } from '../../mappings_state_context';
|
||||
import { deNormalize } from '../../lib';
|
||||
import { EditFieldContainer, EditFieldContainerProps, defaultFlyoutProps } from './fields';
|
||||
import { useDispatch, useMappingsState } from '../../mappings_state_context';
|
||||
import { GlobalFlyout } from '../../shared_imports';
|
||||
import {
|
||||
defaultFlyoutProps,
|
||||
EditFieldContainer,
|
||||
EditFieldContainerProps,
|
||||
SemanticTextInfo,
|
||||
} from './fields';
|
||||
import { DocumentFieldsJsonEditor } from './fields_json_editor';
|
||||
import { DocumentFieldsTreeEditor } from './fields_tree_editor';
|
||||
|
||||
const { useGlobalFlyout } = GlobalFlyout;
|
||||
|
||||
interface Props {
|
||||
searchComponent?: React.ReactElement;
|
||||
searchResultComponent?: React.ReactElement;
|
||||
onCancelAddingNewFields?: () => void;
|
||||
isAddingFields?: boolean;
|
||||
isSemanticTextEnabled?: boolean;
|
||||
indexName?: string;
|
||||
semanticTextInfo?: SemanticTextInfo;
|
||||
}
|
||||
export const DocumentFields = React.memo(
|
||||
({
|
||||
|
@ -29,8 +34,7 @@ export const DocumentFields = React.memo(
|
|||
searchResultComponent,
|
||||
onCancelAddingNewFields,
|
||||
isAddingFields,
|
||||
isSemanticTextEnabled,
|
||||
indexName,
|
||||
semanticTextInfo,
|
||||
}: Props) => {
|
||||
const { fields, documentFields } = useMappingsState();
|
||||
const dispatch = useDispatch();
|
||||
|
@ -53,8 +57,7 @@ export const DocumentFields = React.memo(
|
|||
<DocumentFieldsTreeEditor
|
||||
onCancelAddingNewFields={onCancelAddingNewFields}
|
||||
isAddingFields={isAddingFields}
|
||||
isSemanticTextEnabled={isSemanticTextEnabled}
|
||||
indexName={indexName}
|
||||
semanticTextInfo={semanticTextInfo}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -42,8 +42,13 @@ import { useLoadInferenceModels } from '../../../../../services/api';
|
|||
interface Props {
|
||||
onChange(value: string): void;
|
||||
'data-test-subj'?: string;
|
||||
setValue: (value: string) => void;
|
||||
}
|
||||
export const SelectInferenceId = ({ onChange, 'data-test-subj': dataTestSubj }: Props) => {
|
||||
export const SelectInferenceId = ({
|
||||
onChange,
|
||||
'data-test-subj': dataTestSubj,
|
||||
setValue,
|
||||
}: Props) => {
|
||||
const {
|
||||
core: { application },
|
||||
docLinks,
|
||||
|
@ -142,6 +147,9 @@ export const SelectInferenceId = ({ onChange, 'data-test-subj': dataTestSubj }:
|
|||
return subscription.unsubscribe;
|
||||
}, [subscribe, onChange]);
|
||||
const selectedOptionLabel = options.find((option) => option.checked)?.label;
|
||||
useEffect(() => {
|
||||
setValue(selectedOptionLabel ?? 'elser_model_2');
|
||||
}, [selectedOptionLabel, setValue]);
|
||||
const [isInferencePopoverVisible, setIsInferencePopoverVisible] = useState<boolean>(false);
|
||||
const [inferenceEndpointError, setInferenceEndpointError] = useState<string | undefined>(
|
||||
undefined
|
||||
|
|
|
@ -5,10 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
|
@ -17,28 +13,38 @@ import {
|
|||
EuiOutsideClickDetector,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { FieldWithSemanticTextInfo } from '../../../../types';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MlPluginStart } from '@kbn/ml-plugin/public';
|
||||
import classNames from 'classnames';
|
||||
import React, { useEffect } from 'react';
|
||||
import { EUI_SIZE, TYPE_DEFINITION } from '../../../../constants';
|
||||
import { fieldSerializer } from '../../../../lib';
|
||||
import { useDispatch } from '../../../../mappings_state_context';
|
||||
import {
|
||||
Form,
|
||||
FormDataProvider,
|
||||
FormHook,
|
||||
UseField,
|
||||
useForm,
|
||||
useFormData,
|
||||
} from '../../../../shared_imports';
|
||||
import { MainType, NormalizedFields } from '../../../../types';
|
||||
import { Form, FormDataProvider, UseField, useForm, useFormData } from '../../../../shared_imports';
|
||||
import { Field, MainType, NormalizedFields } from '../../../../types';
|
||||
import { NameParameter, SubTypeParameter, TypeParameter } from '../../field_parameters';
|
||||
import { SelectInferenceId } from '../../field_parameters/select_inference_id';
|
||||
import { ReferenceFieldSelects } from '../../field_parameters/reference_field_selects';
|
||||
import { SelectInferenceId } from '../../field_parameters/select_inference_id';
|
||||
import { FieldBetaBadge } from '../field_beta_badge';
|
||||
import { getRequiredParametersFormForType } from './required_parameters_forms';
|
||||
import { useSemanticText } from './semantic_text/use_semantic_text';
|
||||
|
||||
const formWrapper = (props: any) => <form {...props} />;
|
||||
export interface InferenceToModelIdMap {
|
||||
[key: string]: {
|
||||
trainedModelId?: string;
|
||||
isDeployed: boolean;
|
||||
isDeployable: boolean;
|
||||
defaultInferenceEndpoint: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SemanticTextInfo {
|
||||
isSemanticTextEnabled?: boolean;
|
||||
indexName?: string;
|
||||
ml?: MlPluginStart;
|
||||
setErrorsInTrainedModelDeployment: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
interface Props {
|
||||
allFields: NormalizedFields['byId'];
|
||||
isRootLevelField: boolean;
|
||||
|
@ -48,23 +54,9 @@ interface Props {
|
|||
maxNestedDepth?: number;
|
||||
onCancelAddingNewFields?: () => void;
|
||||
isAddingFields?: boolean;
|
||||
isSemanticTextEnabled?: boolean;
|
||||
indexName?: string;
|
||||
semanticTextInfo?: SemanticTextInfo;
|
||||
}
|
||||
|
||||
const useFieldEffect = (
|
||||
form: FormHook,
|
||||
fieldName: string,
|
||||
setState: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
) => {
|
||||
const fieldValue = form.getFields()?.[fieldName]?.value as string;
|
||||
useEffect(() => {
|
||||
if (fieldValue !== undefined) {
|
||||
setState(fieldValue);
|
||||
}
|
||||
}, [form, fieldValue, setState]);
|
||||
};
|
||||
|
||||
export const CreateField = React.memo(function CreateFieldComponent({
|
||||
allFields,
|
||||
isRootLevelField,
|
||||
|
@ -74,12 +66,14 @@ export const CreateField = React.memo(function CreateFieldComponent({
|
|||
maxNestedDepth,
|
||||
onCancelAddingNewFields,
|
||||
isAddingFields,
|
||||
isSemanticTextEnabled,
|
||||
indexName,
|
||||
semanticTextInfo,
|
||||
}: Props) {
|
||||
const { isSemanticTextEnabled, indexName, ml, setErrorsInTrainedModelDeployment } =
|
||||
semanticTextInfo ?? {};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { form } = useForm<FieldWithSemanticTextInfo>({
|
||||
const { form } = useForm<Field>({
|
||||
serializer: fieldSerializer,
|
||||
options: { stripEmptyFields: false },
|
||||
});
|
||||
|
@ -104,26 +98,24 @@ export const CreateField = React.memo(function CreateFieldComponent({
|
|||
}
|
||||
};
|
||||
|
||||
const [referenceFieldComboValue, setReferenceFieldComboValue] = useState<string>();
|
||||
const [nameValue, setNameValue] = useState<string>();
|
||||
const [inferenceIdComboValue, setInferenceIdComboValue] = useState<string>();
|
||||
const [semanticFieldType, setSemanticTextFieldType] = useState<string>();
|
||||
const {
|
||||
referenceFieldComboValue,
|
||||
nameValue,
|
||||
inferenceIdComboValue,
|
||||
setInferenceValue,
|
||||
semanticFieldType,
|
||||
handleSemanticText,
|
||||
} = useSemanticText({
|
||||
form,
|
||||
setErrorsInTrainedModelDeployment,
|
||||
ml,
|
||||
});
|
||||
|
||||
useFieldEffect(form, 'referenceField', setReferenceFieldComboValue);
|
||||
useFieldEffect(form, 'inferenceId', setInferenceIdComboValue);
|
||||
useFieldEffect(form, 'name', setNameValue);
|
||||
|
||||
const fieldTypeValue = form.getFields()?.type?.value as Array<{ value: string }>;
|
||||
useEffect(() => {
|
||||
if (fieldTypeValue === undefined || fieldTypeValue.length === 0) {
|
||||
return;
|
||||
}
|
||||
setSemanticTextFieldType(
|
||||
fieldTypeValue[0]?.value === 'semantic_text' ? fieldTypeValue[0].value : undefined
|
||||
);
|
||||
}, [form, fieldTypeValue]);
|
||||
|
||||
const submitForm = async (e?: React.FormEvent, exitAfter: boolean = false) => {
|
||||
const submitForm = async (
|
||||
e?: React.FormEvent,
|
||||
exitAfter: boolean = false,
|
||||
clickOutside: boolean = false
|
||||
) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
@ -132,8 +124,9 @@ export const CreateField = React.memo(function CreateFieldComponent({
|
|||
|
||||
if (isValid) {
|
||||
form.reset();
|
||||
if (data.type === 'semantic_text') {
|
||||
dispatch({ type: 'field.addSemanticText', value: data });
|
||||
|
||||
if (data.type === 'semantic_text' && !clickOutside) {
|
||||
handleSemanticText(data);
|
||||
} else {
|
||||
dispatch({ type: 'field.add', value: data });
|
||||
}
|
||||
|
@ -152,7 +145,7 @@ export const CreateField = React.memo(function CreateFieldComponent({
|
|||
cancel();
|
||||
}
|
||||
} else {
|
||||
submitForm(undefined, true);
|
||||
submitForm(undefined, true, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -238,70 +231,66 @@ export const CreateField = React.memo(function CreateFieldComponent({
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiOutsideClickDetector onOutsideClick={onClickOutside}>
|
||||
<Form
|
||||
form={form}
|
||||
FormWrapper={formWrapper}
|
||||
onSubmit={submitForm}
|
||||
data-test-subj="createFieldForm"
|
||||
>
|
||||
<div
|
||||
className={classNames('mappingsEditor__createFieldWrapper', {
|
||||
'mappingsEditor__createFieldWrapper--toggle':
|
||||
Boolean(maxNestedDepth) && maxNestedDepth! > 0,
|
||||
'mappingsEditor__createFieldWrapper--multiField': isMultiField,
|
||||
})}
|
||||
style={{
|
||||
paddingLeft: `${
|
||||
isMultiField
|
||||
? paddingLeft! - EUI_SIZE * 1.5 // As there are no "L" bullet list we need to substract some indent
|
||||
: paddingLeft
|
||||
}px`,
|
||||
}}
|
||||
<>
|
||||
<EuiOutsideClickDetector onOutsideClick={onClickOutside}>
|
||||
<Form
|
||||
form={form}
|
||||
FormWrapper={formWrapper}
|
||||
onSubmit={submitForm}
|
||||
data-test-subj="createFieldForm"
|
||||
>
|
||||
<div className="mappingsEditor__createFieldContent">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>{renderFormFields()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div
|
||||
className={classNames('mappingsEditor__createFieldWrapper', {
|
||||
'mappingsEditor__createFieldWrapper--toggle':
|
||||
Boolean(maxNestedDepth) && maxNestedDepth! > 0,
|
||||
'mappingsEditor__createFieldWrapper--multiField': isMultiField,
|
||||
})}
|
||||
style={{
|
||||
paddingLeft: `${
|
||||
isMultiField
|
||||
? paddingLeft! - EUI_SIZE * 1.5 // As there are no "L" bullet list we need to substract some indent
|
||||
: paddingLeft
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
<div className="mappingsEditor__createFieldContent">
|
||||
{renderFormFields()}
|
||||
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
const RequiredParametersForm = getRequiredParametersFormForType(
|
||||
type?.[0]?.value,
|
||||
subType?.[0]?.value
|
||||
);
|
||||
<FormDataProvider pathsToWatch={['type', 'subType']}>
|
||||
{({ type, subType }) => {
|
||||
const RequiredParametersForm = getRequiredParametersFormForType(
|
||||
type?.[0]?.value,
|
||||
subType?.[0]?.value
|
||||
);
|
||||
|
||||
if (!RequiredParametersForm) {
|
||||
return null;
|
||||
}
|
||||
if (!RequiredParametersForm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeDefinition = TYPE_DEFINITION[type?.[0].value as MainType];
|
||||
const typeDefinition = TYPE_DEFINITION[type?.[0].value as MainType];
|
||||
|
||||
return (
|
||||
<div className="mappingsEditor__createFieldRequiredProps">
|
||||
{typeDefinition.isBeta ? (
|
||||
<>
|
||||
<FieldBetaBadge />
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
return (
|
||||
<div className="mappingsEditor__createFieldRequiredProps">
|
||||
{typeDefinition.isBeta ? (
|
||||
<>
|
||||
<FieldBetaBadge />
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<RequiredParametersForm key={subType ?? type} allFields={allFields} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</FormDataProvider>
|
||||
{/* Field inference_id for semantic_text field type */}
|
||||
<InferenceIdCombo />
|
||||
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={true} />
|
||||
<EuiFlexItem grow={false}>{renderFormActions()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<RequiredParametersForm key={subType ?? type} allFields={allFields} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</FormDataProvider>
|
||||
{/* Field inference_id for semantic_text field type */}
|
||||
<InferenceIdCombo setValue={setInferenceValue} />
|
||||
{renderFormActions()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</EuiOutsideClickDetector>
|
||||
</Form>
|
||||
</EuiOutsideClickDetector>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -321,7 +310,11 @@ function ReferenceFieldCombo({ indexName }: { indexName?: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
function InferenceIdCombo() {
|
||||
interface InferenceProps {
|
||||
setValue: (value: string) => void;
|
||||
}
|
||||
|
||||
function InferenceIdCombo({ setValue }: InferenceProps) {
|
||||
const [{ type }] = useFormData({ watch: 'type' });
|
||||
|
||||
if (type === undefined || type[0]?.value !== 'semantic_text') {
|
||||
|
@ -332,7 +325,7 @@ function InferenceIdCombo() {
|
|||
<>
|
||||
<EuiSpacer />
|
||||
<UseField path="inferenceId">
|
||||
{(field) => <SelectInferenceId onChange={field.setValue} />}
|
||||
{(field) => <SelectInferenceId onChange={field.setValue} setValue={setValue} />}
|
||||
</UseField>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { Field } from '../../../../../types';
|
||||
import { useSemanticText } from './use_semantic_text';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
const mlMock: any = {
|
||||
mlApi: {
|
||||
inferenceModels: {
|
||||
createInferenceEndpoint: jest.fn(),
|
||||
},
|
||||
trainedModels: {
|
||||
startModelAllocation: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockFieldData = {
|
||||
name: 'name',
|
||||
type: 'semantic_text',
|
||||
inferenceId: 'elser_model_2',
|
||||
} as Field;
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
|
||||
jest.mock('../../../../../mappings_state_context', () => ({
|
||||
useMappingsState: jest.fn().mockReturnValue({
|
||||
inferenceToModelIdMap: {
|
||||
e5: {
|
||||
defaultInferenceEndpoint: false,
|
||||
isDeployed: false,
|
||||
isDeployable: true,
|
||||
trainedModelId: '.multilingual-e5-small',
|
||||
},
|
||||
elser_model_2: {
|
||||
defaultInferenceEndpoint: true,
|
||||
isDeployed: false,
|
||||
isDeployable: true,
|
||||
trainedModelId: '.elser_model_2',
|
||||
},
|
||||
},
|
||||
}),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../../component_templates/component_templates_context', () => ({
|
||||
useComponentTemplatesContext: jest.fn().mockReturnValue({
|
||||
toasts: {
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useSemanticText', () => {
|
||||
let form: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
form = {
|
||||
getFields: jest.fn().mockReturnValue({
|
||||
referenceField: { value: 'title' },
|
||||
name: { value: 'sem' },
|
||||
type: { value: [{ value: 'semantic_text' }] },
|
||||
inferenceId: { value: 'e5' },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
it('should populate the values from the form', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSemanticText({ form, setErrorsInTrainedModelDeployment: jest.fn(), ml: mlMock })
|
||||
);
|
||||
|
||||
expect(result.current.referenceFieldComboValue).toBe('title');
|
||||
expect(result.current.nameValue).toBe('sem');
|
||||
expect(result.current.inferenceIdComboValue).toBe('e5');
|
||||
expect(result.current.semanticFieldType).toBe('semantic_text');
|
||||
});
|
||||
|
||||
it('should handle semantic text correctly', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSemanticText({ form, setErrorsInTrainedModelDeployment: jest.fn(), ml: mlMock })
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleSemanticText(mockFieldData);
|
||||
});
|
||||
|
||||
expect(mlMock.mlApi.inferenceModels.createInferenceEndpoint).toHaveBeenCalledWith(
|
||||
'elser_model_2',
|
||||
'text_embedding',
|
||||
{
|
||||
service: 'elasticsearch',
|
||||
service_settings: {
|
||||
num_allocations: 1,
|
||||
num_threads: 1,
|
||||
model_id: '.elser_model_2',
|
||||
},
|
||||
}
|
||||
);
|
||||
expect(mlMock.mlApi.trainedModels.startModelAllocation).toHaveBeenCalledWith('.elser_model_2');
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
type: 'field.addSemanticText',
|
||||
value: mockFieldData,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles errors correctly', async () => {
|
||||
const mockError = new Error('Test error');
|
||||
mlMock.mlApi.inferenceModels.createInferenceEndpoint.mockImplementationOnce(() => {
|
||||
throw mockError;
|
||||
});
|
||||
|
||||
const setErrorsInTrainedModelDeployment = jest.fn();
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSemanticText({ form, setErrorsInTrainedModelDeployment, ml: mlMock })
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
result.current.handleSemanticText(mockFieldData);
|
||||
});
|
||||
|
||||
expect(setErrorsInTrainedModelDeployment).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MlPluginStart } from '@kbn/ml-plugin/public';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useComponentTemplatesContext } from '../../../../../../component_templates/component_templates_context';
|
||||
import { useDispatch, useMappingsState } from '../../../../../mappings_state_context';
|
||||
import { FormHook } from '../../../../../shared_imports';
|
||||
import { Field } from '../../../../../types';
|
||||
|
||||
interface UseSemanticTextProps {
|
||||
form: FormHook<Field, Field>;
|
||||
ml?: MlPluginStart;
|
||||
setErrorsInTrainedModelDeployment: React.Dispatch<React.SetStateAction<string[]>> | undefined;
|
||||
}
|
||||
|
||||
export function useSemanticText(props: UseSemanticTextProps) {
|
||||
const { form, setErrorsInTrainedModelDeployment, ml } = props;
|
||||
const { inferenceToModelIdMap } = useMappingsState();
|
||||
const { toasts } = useComponentTemplatesContext();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [referenceFieldComboValue, setReferenceFieldComboValue] = useState<string>();
|
||||
const [nameValue, setNameValue] = useState<string>();
|
||||
const [inferenceIdComboValue, setInferenceIdComboValue] = useState<string>();
|
||||
const [semanticFieldType, setSemanticTextFieldType] = useState<string>();
|
||||
const [inferenceValue, setInferenceValue] = useState<string>('elser_model_2');
|
||||
|
||||
const useFieldEffect = (
|
||||
semanticTextform: FormHook,
|
||||
fieldName: string,
|
||||
setState: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
) => {
|
||||
const fieldValue = semanticTextform.getFields()?.[fieldName]?.value;
|
||||
useEffect(() => {
|
||||
if (typeof fieldValue === 'string') {
|
||||
setState(fieldValue);
|
||||
}
|
||||
}, [semanticTextform, fieldValue, setState]);
|
||||
};
|
||||
|
||||
useFieldEffect(form, 'referenceField', setReferenceFieldComboValue);
|
||||
useFieldEffect(form, 'name', setNameValue);
|
||||
|
||||
const fieldTypeValue = form.getFields()?.type?.value;
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(fieldTypeValue) || fieldTypeValue.length === 0) {
|
||||
return;
|
||||
}
|
||||
setSemanticTextFieldType(
|
||||
fieldTypeValue[0]?.value === 'semantic_text' ? fieldTypeValue[0].value : undefined
|
||||
);
|
||||
}, [form, fieldTypeValue]);
|
||||
|
||||
const inferenceId = form.getFields()?.inferenceId?.value;
|
||||
useEffect(() => {
|
||||
if (typeof inferenceId === 'string') {
|
||||
setInferenceIdComboValue(inferenceId);
|
||||
}
|
||||
}, [form, inferenceId, inferenceToModelIdMap]);
|
||||
|
||||
const handleSemanticText = (data: Field) => {
|
||||
data.inferenceId = inferenceValue;
|
||||
if (data.inferenceId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inferenceData = inferenceToModelIdMap?.[data.inferenceId];
|
||||
|
||||
if (!inferenceData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { trainedModelId, defaultInferenceEndpoint, isDeployed, isDeployable } = inferenceData;
|
||||
|
||||
if (trainedModelId && defaultInferenceEndpoint) {
|
||||
const modelConfig = {
|
||||
service: 'elasticsearch',
|
||||
service_settings: {
|
||||
num_allocations: 1,
|
||||
num_threads: 1,
|
||||
model_id: trainedModelId,
|
||||
},
|
||||
};
|
||||
try {
|
||||
ml?.mlApi?.inferenceModels?.createInferenceEndpoint(
|
||||
data.inferenceId,
|
||||
'text_embedding',
|
||||
modelConfig
|
||||
);
|
||||
} catch (error) {
|
||||
setErrorsInTrainedModelDeployment?.((prevItems) => [...prevItems, trainedModelId]);
|
||||
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
|
||||
title: i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.createField.inferenceEndpointCreationErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Inference endpoint creation failed',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isDeployable && trainedModelId && !isDeployed) {
|
||||
try {
|
||||
ml?.mlApi?.trainedModels.startModelAllocation(trainedModelId);
|
||||
toasts?.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.createField.modelDeploymentStartedNotification',
|
||||
{
|
||||
defaultMessage: 'Model deployment started',
|
||||
}
|
||||
),
|
||||
text: i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.createField.modelDeploymentNotification',
|
||||
{
|
||||
defaultMessage: '1 model is being deployed on your ml_node.',
|
||||
}
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
setErrorsInTrainedModelDeployment?.((prevItems) => [...prevItems, trainedModelId]);
|
||||
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
|
||||
title: i18n.translate(
|
||||
'xpack.idxMgmt.mappingsEditor.createField.modelDeploymentErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Model deployment failed',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({ type: 'field.addSemanticText', value: data });
|
||||
};
|
||||
|
||||
return {
|
||||
referenceFieldComboValue,
|
||||
nameValue,
|
||||
inferenceIdComboValue,
|
||||
semanticFieldType,
|
||||
handleSemanticText,
|
||||
setInferenceValue,
|
||||
};
|
||||
}
|
|
@ -98,11 +98,14 @@ function FieldListItemComponent(
|
|||
isExpanded,
|
||||
path,
|
||||
} = field;
|
||||
|
||||
// When there aren't any "child" fields (the maxNestedDepth === 0), there is no toggle icon on the left of any field.
|
||||
// For that reason, we need to compensate and substract some indent to left align on the page.
|
||||
const substractIndentAmount = maxNestedDepth === 0 ? CHILD_FIELD_INDENT_SIZE * 0.5 : 0;
|
||||
const indent = treeDepth * CHILD_FIELD_INDENT_SIZE - substractIndentAmount;
|
||||
|
||||
const isSemanticText = source.type === 'semantic_text';
|
||||
|
||||
const indentCreateField =
|
||||
(treeDepth + 1) * CHILD_FIELD_INDENT_SIZE +
|
||||
LEFT_PADDING_SIZE_FIELD_ITEM_WRAPPER -
|
||||
|
@ -288,6 +291,12 @@ function FieldListItemComponent(
|
|||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
|
||||
{isSemanticText && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">{source.inference_id}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{isShadowed && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={i18nTexts.fieldIsShadowedLabel}>
|
||||
|
|
|
@ -5,25 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useMappingsState, useDispatch } from '../../mappings_state_context';
|
||||
import { FieldsList, CreateField } from './fields';
|
||||
import { useDispatch, useMappingsState } from '../../mappings_state_context';
|
||||
import { CreateField, FieldsList, SemanticTextInfo } from './fields';
|
||||
|
||||
interface Props {
|
||||
onCancelAddingNewFields?: () => void;
|
||||
isAddingFields?: boolean;
|
||||
isSemanticTextEnabled?: boolean;
|
||||
indexName?: string;
|
||||
semanticTextInfo?: SemanticTextInfo;
|
||||
}
|
||||
|
||||
export const DocumentFieldsTreeEditor = ({
|
||||
onCancelAddingNewFields,
|
||||
isAddingFields,
|
||||
isSemanticTextEnabled = false,
|
||||
indexName,
|
||||
semanticTextInfo,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
|
@ -53,8 +51,7 @@ export const DocumentFieldsTreeEditor = ({
|
|||
isRootLevelField
|
||||
onCancelAddingNewFields={onCancelAddingNewFields}
|
||||
isAddingFields={isAddingFields}
|
||||
isSemanticTextEnabled={isSemanticTextEnabled}
|
||||
indexName={indexName}
|
||||
semanticTextInfo={semanticTextInfo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,34 +8,34 @@
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
DataType,
|
||||
Fields,
|
||||
Field,
|
||||
NormalizedFields,
|
||||
NormalizedField,
|
||||
FieldMeta,
|
||||
MainType,
|
||||
SubType,
|
||||
ChildFieldName,
|
||||
ParameterName,
|
||||
ComboBoxOption,
|
||||
DataType,
|
||||
Field,
|
||||
FieldMeta,
|
||||
Fields,
|
||||
GenericObject,
|
||||
RuntimeFields,
|
||||
MainType,
|
||||
NormalizedField,
|
||||
NormalizedFields,
|
||||
NormalizedRuntimeFields,
|
||||
ParameterName,
|
||||
RuntimeFields,
|
||||
SubType,
|
||||
} from '../types';
|
||||
|
||||
import {
|
||||
SUB_TYPE_MAP_TO_MAIN,
|
||||
MAIN_DATA_TYPE_DEFINITION,
|
||||
MAX_DEPTH_DEFAULT_EDITOR,
|
||||
PARAMETERS_DEFINITION,
|
||||
SUB_TYPE_MAP_TO_MAIN,
|
||||
TYPE_DEFINITION,
|
||||
TYPE_NOT_ALLOWED_MULTIFIELD,
|
||||
TYPE_ONLY_ALLOWED_AT_ROOT_LEVEL,
|
||||
TYPE_DEFINITION,
|
||||
MAIN_DATA_TYPE_DEFINITION,
|
||||
} from '../constants';
|
||||
|
||||
import { FieldConfig } from '../shared_imports';
|
||||
import { TreeItem } from '../components/tree';
|
||||
import { FieldConfig } from '../shared_imports';
|
||||
|
||||
export const getUniqueId = () => uuidv4();
|
||||
|
||||
|
|
|
@ -25,13 +25,13 @@ import {
|
|||
MappingsTemplates,
|
||||
RuntimeFields,
|
||||
} from './types';
|
||||
import { extractMappingsDefinition } from './lib';
|
||||
import { useDispatch, useMappingsState } from './mappings_state_context';
|
||||
import { useMappingsStateListener } from './use_state_listener';
|
||||
import { useConfig } from './config_context';
|
||||
import { DocLinksStart } from './shared_imports';
|
||||
import { DocumentFieldsHeader } from './components/document_fields/document_fields_header';
|
||||
import { SearchResult } from './components/document_fields/search_fields';
|
||||
import { parseMappings } from '../../shared/parse_mappings';
|
||||
|
||||
type TabName = 'fields' | 'runtimeFields' | 'advanced' | 'templates';
|
||||
|
||||
|
@ -56,50 +56,10 @@ export interface Props {
|
|||
|
||||
export const MappingsEditor = React.memo(
|
||||
({ onChange, value, docLinks, indexSettings, esNodesPlugins }: Props) => {
|
||||
const { parsedDefaultValue, multipleMappingsDeclared } =
|
||||
useMemo<MappingsEditorParsedMetadata>(() => {
|
||||
const mappingsDefinition = extractMappingsDefinition(value);
|
||||
|
||||
if (mappingsDefinition === null) {
|
||||
return { multipleMappingsDeclared: true };
|
||||
}
|
||||
|
||||
const {
|
||||
_source,
|
||||
_meta,
|
||||
_routing,
|
||||
_size,
|
||||
dynamic,
|
||||
properties,
|
||||
runtime,
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
numeric_detection,
|
||||
date_detection,
|
||||
dynamic_date_formats,
|
||||
dynamic_templates,
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
} = mappingsDefinition;
|
||||
|
||||
const parsed = {
|
||||
configuration: {
|
||||
_source,
|
||||
_meta,
|
||||
_routing,
|
||||
_size,
|
||||
dynamic,
|
||||
numeric_detection,
|
||||
date_detection,
|
||||
dynamic_date_formats,
|
||||
},
|
||||
fields: properties,
|
||||
templates: {
|
||||
dynamic_templates,
|
||||
},
|
||||
runtime,
|
||||
};
|
||||
return { parsedDefaultValue: parsed, multipleMappingsDeclared: false };
|
||||
}, [value]);
|
||||
|
||||
const { parsedDefaultValue, multipleMappingsDeclared } = useMemo<MappingsEditorParsedMetadata>(
|
||||
() => parseMappings(value),
|
||||
[value]
|
||||
);
|
||||
/**
|
||||
* Hook that will listen to:
|
||||
* 1. "value" prop changes in order to reset the mappings editor
|
||||
|
|
|
@ -54,6 +54,7 @@ export const StateProvider: React.FC = ({ children }) => {
|
|||
term: '',
|
||||
result: [],
|
||||
},
|
||||
inferenceToModelIdMap: {},
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
|
|
@ -16,14 +16,7 @@ import {
|
|||
shouldDeleteChildFieldsAfterTypeChange,
|
||||
updateFieldsPathAfterFieldNameChange,
|
||||
} from './lib';
|
||||
import {
|
||||
Action,
|
||||
Field,
|
||||
FieldWithSemanticTextInfo,
|
||||
NormalizedField,
|
||||
NormalizedFields,
|
||||
State,
|
||||
} from './types';
|
||||
import { Action, Field, NormalizedField, NormalizedFields, State } from './types';
|
||||
|
||||
export const addFieldToState = (field: Field, state: State): State => {
|
||||
const updatedFields = { ...state.fields };
|
||||
|
@ -324,7 +317,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||
return addFieldToState(action.value, state);
|
||||
}
|
||||
case 'field.addSemanticText': {
|
||||
const addTexFieldWithCopyToActionValue: FieldWithSemanticTextInfo = {
|
||||
const addTexFieldWithCopyToActionValue: Field = {
|
||||
name: action.value.referenceField as string,
|
||||
type: 'text',
|
||||
copy_to: [action.value.name],
|
||||
|
@ -333,7 +326,7 @@ export const reducer = (state: State, action: Action): State => {
|
|||
// Add text field to state with copy_to of semantic_text field
|
||||
let updatedState = addFieldToState(addTexFieldWithCopyToActionValue, state);
|
||||
|
||||
const addSemanticTextFieldActionValue: FieldWithSemanticTextInfo = {
|
||||
const addSemanticTextFieldActionValue: Field = {
|
||||
name: action.value.name,
|
||||
inference_id: action.value.inferenceId,
|
||||
type: 'semantic_text',
|
||||
|
@ -621,5 +614,11 @@ export const reducer = (state: State, action: Action): State => {
|
|||
isValid: action.value,
|
||||
};
|
||||
}
|
||||
case 'inferenceToModelIdMap.update': {
|
||||
return {
|
||||
...state,
|
||||
inferenceToModelIdMap: action.value.inferenceToModelIdMap,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,8 +9,8 @@ import { ReactNode } from 'react';
|
|||
|
||||
import { GenericObject } from './mappings_editor';
|
||||
|
||||
import { FieldConfig, RuntimeField } from '../shared_imports';
|
||||
import { PARAMETERS_DEFINITION } from '../constants';
|
||||
import { FieldConfig, RuntimeField } from '../shared_imports';
|
||||
|
||||
export interface DataTypeDefinition {
|
||||
label: string;
|
||||
|
@ -183,6 +183,9 @@ interface FieldBasic {
|
|||
subType?: SubType;
|
||||
properties?: { [key: string]: Omit<Field, 'name'> };
|
||||
fields?: { [key: string]: Omit<Field, 'name'> };
|
||||
referenceField?: string;
|
||||
inferenceId?: string;
|
||||
inference_id?: string;
|
||||
|
||||
// other* exist together as a holder of types that the mappings editor does not yet know about but
|
||||
// enables the user to create mappings with them.
|
||||
|
@ -195,11 +198,6 @@ type FieldParams = {
|
|||
|
||||
export type Field = FieldBasic & Partial<FieldParams>;
|
||||
|
||||
export interface FieldWithSemanticTextInfo extends Field {
|
||||
referenceField?: string;
|
||||
inferenceId?: string;
|
||||
}
|
||||
|
||||
export interface FieldMeta {
|
||||
childFieldsName: ChildFieldName | undefined;
|
||||
canHaveChildFields: boolean;
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { InferenceToModelIdMap } from '../components/document_fields/fields';
|
||||
import { FormHook, OnFormUpdateArg, RuntimeField } from '../shared_imports';
|
||||
import {
|
||||
Field,
|
||||
FieldWithSemanticTextInfo,
|
||||
NormalizedFields,
|
||||
NormalizedRuntimeField,
|
||||
NormalizedRuntimeFields,
|
||||
|
@ -98,20 +98,22 @@ export interface State {
|
|||
result: SearchResult[];
|
||||
};
|
||||
templates: TemplatesFormState;
|
||||
inferenceToModelIdMap?: InferenceToModelIdMap;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| { type: 'editor.replaceMappings'; value: { [key: string]: any } }
|
||||
| {
|
||||
type: 'inferenceToModelIdMap.update';
|
||||
value: { inferenceToModelIdMap?: InferenceToModelIdMap };
|
||||
}
|
||||
| { type: 'configuration.update'; value: Partial<ConfigurationFormState> }
|
||||
| { type: 'configuration.save'; value: MappingsConfiguration }
|
||||
| { type: 'templates.update'; value: Partial<State['templates']> }
|
||||
| { type: 'templates.save'; value: MappingsTemplates }
|
||||
| { type: 'fieldForm.update'; value: OnFormUpdateArg<any> }
|
||||
| { type: 'field.add'; value: Field }
|
||||
| {
|
||||
type: 'field.addSemanticText';
|
||||
value: FieldWithSemanticTextInfo;
|
||||
}
|
||||
| { type: 'field.addSemanticText'; value: Field }
|
||||
| { type: 'field.remove'; value: string }
|
||||
| { type: 'field.edit'; value: Field }
|
||||
| { type: 'field.toggleExpand'; value: { fieldId: string; isExpanded?: boolean } }
|
||||
|
|
|
@ -10,7 +10,6 @@ import SemVer from 'semver/classes/semver';
|
|||
import { CoreSetup, CoreStart, ScopedHistory } from '@kbn/core/public';
|
||||
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
|
||||
import { CloudSetup } from '@kbn/cloud-plugin/public';
|
||||
import { UIM_APP_NAME } from '../../common/constants';
|
||||
import { PLUGIN } from '../../common/constants/plugin';
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiCodeBlock,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
|
@ -19,41 +21,38 @@ import {
|
|||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiEmptyPrompt,
|
||||
useGeneratedHtmlId,
|
||||
EuiFilterGroup,
|
||||
EuiFilterButton,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Index } from '../../../../../../common';
|
||||
import { useDetailsPageMappingsModelManagement } from '../../../../../hooks/use_details_page_mappings_model_management';
|
||||
import { useAppContext } from '../../../../app_context';
|
||||
import { DocumentFields } from '../../../../components/mappings_editor/components';
|
||||
import { DocumentFieldsSearch } from '../../../../components/mappings_editor/components/document_fields/document_fields_search';
|
||||
import { FieldsList } from '../../../../components/mappings_editor/components/document_fields/fields';
|
||||
import { SearchResult } from '../../../../components/mappings_editor/components/document_fields/search_fields';
|
||||
import {
|
||||
extractMappingsDefinition,
|
||||
searchFields,
|
||||
} from '../../../../components/mappings_editor/lib';
|
||||
import { MultipleMappingsWarning } from '../../../../components/mappings_editor/components/multiple_mappings_warning';
|
||||
import { deNormalize, searchFields } from '../../../../components/mappings_editor/lib';
|
||||
import { MappingsEditorParsedMetadata } from '../../../../components/mappings_editor/mappings_editor';
|
||||
import {
|
||||
useDispatch,
|
||||
useMappingsState,
|
||||
} from '../../../../components/mappings_editor/mappings_state_context';
|
||||
import { useMappingsStateListener } from '../../../../components/mappings_editor/use_state_listener';
|
||||
import { documentationService } from '../../../../services';
|
||||
import { DocumentFields } from '../../../../components/mappings_editor/components';
|
||||
import { deNormalize } from '../../../../components/mappings_editor/lib';
|
||||
import { updateIndexMappings } from '../../../../services/api';
|
||||
import { notificationService } from '../../../../services/notification';
|
||||
import {
|
||||
NormalizedField,
|
||||
NormalizedFields,
|
||||
State,
|
||||
} from '../../../../components/mappings_editor/types';
|
||||
import { useMappingsStateListener } from '../../../../components/mappings_editor/use_state_listener';
|
||||
import { documentationService } from '../../../../services';
|
||||
import { updateIndexMappings } from '../../../../services/api';
|
||||
import { notificationService } from '../../../../services/notification';
|
||||
import { SemanticTextBanner } from './semantic_text_banner';
|
||||
import { TrainedModelsDeploymentModal } from './trained_models_deployment_modal';
|
||||
import { parseMappings } from '../../../../shared/parse_mappings';
|
||||
|
||||
const getFieldsFromState = (state: State) => {
|
||||
const getField = (fieldId: string) => {
|
||||
|
@ -64,6 +63,7 @@ const getFieldsFromState = (state: State) => {
|
|||
};
|
||||
return fields();
|
||||
};
|
||||
|
||||
export const DetailsPageMappingsContent: FunctionComponent<{
|
||||
index: Index;
|
||||
data: string;
|
||||
|
@ -82,7 +82,20 @@ export const DetailsPageMappingsContent: FunctionComponent<{
|
|||
const {
|
||||
services: { extensionsService },
|
||||
core: { getUrlForApp },
|
||||
plugins: { ml },
|
||||
url,
|
||||
} = useAppContext();
|
||||
|
||||
const [errorsInTrainedModelDeployment, setErrorsInTrainedModelDeployment] = useState<string[]>(
|
||||
[]
|
||||
);
|
||||
const semanticTextInfo = {
|
||||
isSemanticTextEnabled,
|
||||
indexName: index.name,
|
||||
ml,
|
||||
setErrorsInTrainedModelDeployment,
|
||||
};
|
||||
|
||||
const state = useMappingsState();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -107,50 +120,16 @@ export const DetailsPageMappingsContent: FunctionComponent<{
|
|||
setIsJSONVisible(!isJSONVisible);
|
||||
};
|
||||
|
||||
const mappingsDefinition = extractMappingsDefinition(jsonData);
|
||||
const { parsedDefaultValue } = useMemo<MappingsEditorParsedMetadata>(() => {
|
||||
if (mappingsDefinition === null) {
|
||||
return { multipleMappingsDeclared: true };
|
||||
}
|
||||
|
||||
const {
|
||||
_source,
|
||||
_meta,
|
||||
_routing,
|
||||
_size,
|
||||
dynamic,
|
||||
properties,
|
||||
runtime,
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
numeric_detection,
|
||||
date_detection,
|
||||
dynamic_date_formats,
|
||||
dynamic_templates,
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
} = mappingsDefinition;
|
||||
|
||||
const parsed = {
|
||||
configuration: {
|
||||
_source,
|
||||
_meta,
|
||||
_routing,
|
||||
_size,
|
||||
dynamic,
|
||||
numeric_detection,
|
||||
date_detection,
|
||||
dynamic_date_formats,
|
||||
},
|
||||
fields: properties,
|
||||
templates: {
|
||||
dynamic_templates,
|
||||
},
|
||||
runtime,
|
||||
};
|
||||
|
||||
return { parsedDefaultValue: parsed, multipleMappingsDeclared: false };
|
||||
}, [mappingsDefinition]);
|
||||
const { parsedDefaultValue, multipleMappingsDeclared } = useMemo<MappingsEditorParsedMetadata>(
|
||||
() => parseMappings(jsonData),
|
||||
[jsonData]
|
||||
);
|
||||
|
||||
useMappingsStateListener({ value: parsedDefaultValue, status: 'disabled' });
|
||||
const { fetchInferenceToModelIdMap, pendingDeployments } = useDetailsPageMappingsModelManagement(
|
||||
state.fields,
|
||||
state.inferenceToModelIdMap
|
||||
);
|
||||
|
||||
const onCancelAddingNewFields = useCallback(() => {
|
||||
setAddingFields(!isAddingFields);
|
||||
|
@ -189,9 +168,49 @@ export const DetailsPageMappingsContent: FunctionComponent<{
|
|||
});
|
||||
}, [dispatch, isAddingFields, state]);
|
||||
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSemanticTextEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
await fetchInferenceToModelIdMap();
|
||||
};
|
||||
|
||||
fetchData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const refreshModal = useCallback(async () => {
|
||||
try {
|
||||
if (!isSemanticTextEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchInferenceToModelIdMap();
|
||||
|
||||
setIsModalVisible(pendingDeployments.length > 0);
|
||||
} catch (exception) {
|
||||
setSaveMappingError(exception.message);
|
||||
}
|
||||
}, [fetchInferenceToModelIdMap, isSemanticTextEnabled, pendingDeployments]);
|
||||
|
||||
const updateMappings = useCallback(async () => {
|
||||
try {
|
||||
const { error } = await updateIndexMappings(indexName, deNormalize(state.fields));
|
||||
if (isSemanticTextEnabled) {
|
||||
await fetchInferenceToModelIdMap();
|
||||
|
||||
if (pendingDeployments.length > 0) {
|
||||
setIsModalVisible(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const denormalizedFields = deNormalize(state.fields);
|
||||
|
||||
const { error } = await updateIndexMappings(indexName, denormalizedFields);
|
||||
|
||||
if (!error) {
|
||||
notificationService.showSuccessToast(
|
||||
|
@ -206,7 +225,8 @@ export const DetailsPageMappingsContent: FunctionComponent<{
|
|||
} catch (exception) {
|
||||
setSaveMappingError(exception.message);
|
||||
}
|
||||
}, [state.fields, indexName, refetchMapping]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state.fields, pendingDeployments]);
|
||||
|
||||
const onSearchChange = useCallback(
|
||||
(value: string) => {
|
||||
|
@ -273,27 +293,8 @@ export const DetailsPageMappingsContent: FunctionComponent<{
|
|||
);
|
||||
const treeViewBlock = (
|
||||
<>
|
||||
{mappingsDefinition === null ? (
|
||||
<EuiEmptyPrompt
|
||||
color="danger"
|
||||
iconType="error"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.invalidMappingKeysErrorMessageTitle"
|
||||
defaultMessage="Unable to load the mapping"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.invalidMappingKeysErrorMessageBody"
|
||||
defaultMessage="The mapping contains invalid keys. Please provide a mapping with valid keys."
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
{multipleMappingsDeclared ? (
|
||||
<MultipleMappingsWarning />
|
||||
) : searchTerm !== '' ? (
|
||||
searchResultComponent
|
||||
) : (
|
||||
|
@ -449,6 +450,9 @@ export const DetailsPageMappingsContent: FunctionComponent<{
|
|||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem grow={true}>
|
||||
<SemanticTextBanner isSemanticTextEnabled={isSemanticTextEnabled} />
|
||||
</EuiFlexItem>
|
||||
{errorSavingMappings}
|
||||
{isAddingFields && (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -488,14 +492,12 @@ export const DetailsPageMappingsContent: FunctionComponent<{
|
|||
<DocumentFields
|
||||
onCancelAddingNewFields={onCancelAddingNewFields}
|
||||
isAddingFields={isAddingFields}
|
||||
isSemanticTextEnabled={isSemanticTextEnabled}
|
||||
indexName={indexName}
|
||||
semanticTextInfo={semanticTextInfo}
|
||||
/>
|
||||
) : (
|
||||
<DocumentFields
|
||||
isAddingFields={isAddingFields}
|
||||
isSemanticTextEnabled={isSemanticTextEnabled}
|
||||
indexName={indexName}
|
||||
semanticTextInfo={semanticTextInfo}
|
||||
/>
|
||||
)}
|
||||
</EuiPanel>
|
||||
|
@ -517,6 +519,16 @@ export const DetailsPageMappingsContent: FunctionComponent<{
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
{isModalVisible && (
|
||||
<TrainedModelsDeploymentModal
|
||||
pendingDeployments={pendingDeployments}
|
||||
errorsInTrainedModelDeployment={errorsInTrainedModelDeployment}
|
||||
isSemanticTextEnabled={isSemanticTextEnabled}
|
||||
setIsModalVisible={setIsModalVisible}
|
||||
refreshModal={refreshModal}
|
||||
url={url}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface SemanticTextBannerProps {
|
||||
isSemanticTextEnabled: boolean;
|
||||
}
|
||||
|
||||
export function SemanticTextBanner({ isSemanticTextEnabled }: SemanticTextBannerProps) {
|
||||
const [isSemanticTextBannerDisplayable, setIsSemanticTextBannerDisplayable] =
|
||||
useState<boolean>(true);
|
||||
return isSemanticTextBannerDisplayable && isSemanticTextEnabled ? (
|
||||
<>
|
||||
<EuiPanel color="success" data-test-subj="indexDetailsMappingsSemanticTextBanner">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="m" color="success">
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.semanticTextBanner.description"
|
||||
defaultMessage="{label} Add a field to your mapping and choose 'semantic_text' to get started.'"
|
||||
values={{
|
||||
label: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.semanticTextBanner.semanticTextFieldAvailable"
|
||||
defaultMessage="semantic_text field type now available!"
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
onClick={() => setIsSemanticTextBannerDisplayable(false)}
|
||||
data-test-subj="SemanticTextBannerDismissButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.idxMgmt.indexDetails.mappings.semanticTextBanner.dismiss"
|
||||
defaultMessage="Dismiss"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</>
|
||||
) : null;
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiConfirmModal, useGeneratedHtmlId, EuiHealth } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface SemanticTextProps {
|
||||
isSemanticTextEnabled: boolean;
|
||||
setIsModalVisible: (isVisible: boolean) => void;
|
||||
refreshModal: () => void;
|
||||
pendingDeployments: Array<string | undefined>;
|
||||
errorsInTrainedModelDeployment: string[];
|
||||
url?: SharePluginStart['url'];
|
||||
}
|
||||
|
||||
const ML_APP_LOCATOR = 'ML_APP_LOCATOR';
|
||||
const TRAINED_MODELS_MANAGE = 'trained_models';
|
||||
|
||||
export function TrainedModelsDeploymentModal({
|
||||
isSemanticTextEnabled,
|
||||
setIsModalVisible,
|
||||
refreshModal,
|
||||
pendingDeployments = [],
|
||||
errorsInTrainedModelDeployment = [],
|
||||
url,
|
||||
}: SemanticTextProps) {
|
||||
const modalTitleId = useGeneratedHtmlId();
|
||||
const closeModal = () => setIsModalVisible(false);
|
||||
const [mlManagementPageUrl, setMlManagementPageUrl] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
const mlLocator = url?.locators.get(ML_APP_LOCATOR);
|
||||
const generateUrl = async () => {
|
||||
if (mlLocator) {
|
||||
const mlURL = await mlLocator.getUrl({
|
||||
page: TRAINED_MODELS_MANAGE,
|
||||
});
|
||||
if (!isCancelled) {
|
||||
setMlManagementPageUrl(mlURL);
|
||||
}
|
||||
}
|
||||
};
|
||||
generateUrl();
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
const ErroredDeployments = pendingDeployments.filter(
|
||||
(deployment) => deployment !== undefined && errorsInTrainedModelDeployment.includes(deployment)
|
||||
);
|
||||
|
||||
const PendingModelsDeploymentModal = () => {
|
||||
const pendingDeploymentsList = pendingDeployments.map((deployment, index) => (
|
||||
<li key={index}>
|
||||
<EuiHealth textSize="xs" color="warning">
|
||||
{deployment}
|
||||
</EuiHealth>
|
||||
</li>
|
||||
));
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
aria-labelledby={modalTitleId}
|
||||
style={{ width: 600 }}
|
||||
title={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.titleLabel',
|
||||
{
|
||||
defaultMessage: 'Models still deploying',
|
||||
}
|
||||
)}
|
||||
titleProps={{ id: modalTitleId }}
|
||||
onCancel={closeModal}
|
||||
onConfirm={refreshModal}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.cancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.refreshButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Refresh',
|
||||
}
|
||||
)}
|
||||
defaultFocusedButton="confirm"
|
||||
data-test-subj="trainedModelsDeploymentModal"
|
||||
>
|
||||
<p data-test-subj="trainedModelsDeploymentModalText">
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.textAboutDeploymentsNotCompleted',
|
||||
{
|
||||
defaultMessage:
|
||||
'Some fields are referencing models that have not yet completed deployment. Deployment may take a few minutes to complete.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<ul style={{ listStyleType: 'none' }}>{pendingDeploymentsList}</ul>
|
||||
<EuiLink href={mlManagementPageUrl} target="_blank">
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.textTrainedModelManagementLink',
|
||||
{
|
||||
defaultMessage: 'Go to Trained Model Management',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
const ErroredModelsDeploymentModal = () => {
|
||||
const pendingDeploymentsList = pendingDeployments.map((deployment, index) => (
|
||||
<li key={index}>
|
||||
<EuiHealth textSize="xs" color="danger">
|
||||
{deployment}
|
||||
</EuiHealth>
|
||||
</li>
|
||||
));
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
aria-labelledby={modalTitleId}
|
||||
style={{ width: 600 }}
|
||||
title={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.deploymentErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Models could not be deployed',
|
||||
}
|
||||
)}
|
||||
titleProps={{ id: modalTitleId }}
|
||||
onCancel={closeModal}
|
||||
onConfirm={refreshModal}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.deploymentErrorCancelButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.deploymentErrorTryAgainButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Try again',
|
||||
}
|
||||
)}
|
||||
defaultFocusedButton="confirm"
|
||||
data-test-subj="trainedModelsErroredDeploymentModal"
|
||||
>
|
||||
<p data-test-subj="trainedModelsErrorDeploymentModalText">
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.deploymentErrorText',
|
||||
{
|
||||
defaultMessage: 'There was an error when trying to deploy the following models.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<ul style={{ listStyleType: 'none' }}>{pendingDeploymentsList}</ul>
|
||||
<EuiLink href={mlManagementPageUrl} target="_blank">
|
||||
{i18n.translate(
|
||||
'xpack.idxMgmt.indexDetails.trainedModelsDeploymentModal.deploymentErrorTrainedModelManagementLink',
|
||||
{
|
||||
defaultMessage: 'Go to Trained Model Management',
|
||||
}
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
return isSemanticTextEnabled ? (
|
||||
ErroredDeployments.length > 0 ? (
|
||||
<ErroredModelsDeploymentModal />
|
||||
) : (
|
||||
<PendingModelsDeploymentModal />
|
||||
)
|
||||
) : null;
|
||||
}
|
|
@ -442,6 +442,13 @@ export function updateIndexMappings(indexName: string, newFields: Fields) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getInferenceModels() {
|
||||
return sendRequest({
|
||||
path: `${API_BASE_PATH}/inference/all`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function useLoadInferenceModels() {
|
||||
return useRequest<InferenceAPIConfigResponse[]>({
|
||||
path: `${API_BASE_PATH}/inference/all`,
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { extractMappingsDefinition } from '../components/mappings_editor/lib';
|
||||
import { MappingsEditorParsedMetadata } from '../components/mappings_editor/mappings_editor';
|
||||
|
||||
interface MappingsDefinition {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const parseMappings = (
|
||||
value: MappingsDefinition | undefined
|
||||
): MappingsEditorParsedMetadata => {
|
||||
const mappingsDefinition = extractMappingsDefinition(value);
|
||||
|
||||
if (mappingsDefinition === null) {
|
||||
return { multipleMappingsDeclared: true };
|
||||
}
|
||||
|
||||
const {
|
||||
_source,
|
||||
_meta,
|
||||
_routing,
|
||||
_size,
|
||||
dynamic,
|
||||
properties,
|
||||
runtime,
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
numeric_detection,
|
||||
date_detection,
|
||||
dynamic_date_formats,
|
||||
dynamic_templates,
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
} = mappingsDefinition;
|
||||
|
||||
const parsed = {
|
||||
configuration: {
|
||||
_source,
|
||||
_meta,
|
||||
_routing,
|
||||
_size,
|
||||
dynamic,
|
||||
numeric_detection,
|
||||
date_detection,
|
||||
dynamic_date_formats,
|
||||
},
|
||||
fields: properties,
|
||||
templates: {
|
||||
dynamic_templates,
|
||||
},
|
||||
runtime,
|
||||
};
|
||||
|
||||
return { parsedDefaultValue: parsed, multipleMappingsDeclared: false };
|
||||
};
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { InferenceToModelIdMap } from '../application/components/mappings_editor/components/document_fields/fields';
|
||||
import { NormalizedFields } from '../application/components/mappings_editor/types';
|
||||
import { useDetailsPageMappingsModelManagement } from './use_details_page_mappings_model_management';
|
||||
|
||||
jest.mock('../application/app_context', () => ({
|
||||
useAppContext: () => ({
|
||||
plugins: {
|
||||
ml: {
|
||||
mlApi: {
|
||||
trainedModels: {
|
||||
getTrainedModelStats: jest.fn().mockResolvedValue({
|
||||
trained_model_stats: [
|
||||
{
|
||||
model_id: '.elser_model_2',
|
||||
deployment_stats: {
|
||||
deployment_id: 'elser_model_2',
|
||||
model_id: '.elser_model_2',
|
||||
state: 'started',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../application/services/api', () => ({
|
||||
getInferenceModels: jest.fn().mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
model_id: 'e5',
|
||||
task_type: 'text_embedding',
|
||||
service: 'elasticsearch',
|
||||
service_settings: {
|
||||
num_allocations: 1,
|
||||
num_threads: 1,
|
||||
model_id: '.multilingual-e5-small',
|
||||
},
|
||||
task_settings: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../application/components/mappings_editor/mappings_state_context', () => ({
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
const mockDispatch = jest.fn();
|
||||
const fields = {
|
||||
byId: {
|
||||
'88ebcfdb-19b7-4458-9ea2-9488df54453d': {
|
||||
id: '88ebcfdb-19b7-4458-9ea2-9488df54453d',
|
||||
isMultiField: false,
|
||||
source: {
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
copy_to: ['semantic'],
|
||||
},
|
||||
path: ['title'],
|
||||
nestedDepth: 0,
|
||||
childFieldsName: 'fields',
|
||||
canHaveChildFields: false,
|
||||
hasChildFields: false,
|
||||
canHaveMultiFields: true,
|
||||
hasMultiFields: false,
|
||||
isExpanded: false,
|
||||
},
|
||||
'c5d86c82-ea07-4457-b469-3ffd4b96db81': {
|
||||
id: 'c5d86c82-ea07-4457-b469-3ffd4b96db81',
|
||||
isMultiField: false,
|
||||
source: {
|
||||
name: 'semantic',
|
||||
inference_id: 'elser_model_2',
|
||||
type: 'semantic_text',
|
||||
},
|
||||
path: ['semantic'],
|
||||
nestedDepth: 0,
|
||||
childFieldsName: 'fields',
|
||||
canHaveChildFields: false,
|
||||
hasChildFields: false,
|
||||
canHaveMultiFields: true,
|
||||
hasMultiFields: false,
|
||||
isExpanded: false,
|
||||
},
|
||||
},
|
||||
aliases: {},
|
||||
rootLevelFields: ['88ebcfdb-19b7-4458-9ea2-9488df54453d', 'c5d86c82-ea07-4457-b469-3ffd4b96db81'],
|
||||
maxNestedDepth: 2,
|
||||
} as NormalizedFields;
|
||||
|
||||
const inferenceToModelIdMap = {
|
||||
elser_model_2: {
|
||||
trainedModelId: '.elser_model_2',
|
||||
isDeployed: true,
|
||||
isDeployable: true,
|
||||
defaultInferenceEndpoint: false,
|
||||
},
|
||||
e5: {
|
||||
trainedModelId: '.multilingual-e5-small',
|
||||
isDeployed: true,
|
||||
isDeployable: true,
|
||||
defaultInferenceEndpoint: false,
|
||||
},
|
||||
} as InferenceToModelIdMap;
|
||||
|
||||
describe('useDetailsPageMappingsModelManagement', () => {
|
||||
it('should call the dispatch with correct parameters', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useDetailsPageMappingsModelManagement(fields, inferenceToModelIdMap)
|
||||
);
|
||||
|
||||
await result.current.fetchInferenceToModelIdMap();
|
||||
|
||||
const expectedMap = {
|
||||
type: 'inferenceToModelIdMap.update',
|
||||
value: {
|
||||
inferenceToModelIdMap: {
|
||||
e5: {
|
||||
defaultInferenceEndpoint: false,
|
||||
isDeployed: false,
|
||||
isDeployable: true,
|
||||
trainedModelId: '.multilingual-e5-small',
|
||||
},
|
||||
elser_model_2: {
|
||||
defaultInferenceEndpoint: true,
|
||||
isDeployed: true,
|
||||
isDeployable: true,
|
||||
trainedModelId: '.elser_model_2',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(mockDispatch).toHaveBeenCalledWith(expectedMap);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { InferenceStatsResponse } from '@kbn/ml-plugin/public/application/services/ml_api_service/trained_models';
|
||||
import type { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useAppContext } from '../application/app_context';
|
||||
import { InferenceToModelIdMap } from '../application/components/mappings_editor/components/document_fields/fields';
|
||||
import { deNormalize } from '../application/components/mappings_editor/lib';
|
||||
import { useDispatch } from '../application/components/mappings_editor/mappings_state_context';
|
||||
import { NormalizedFields } from '../application/components/mappings_editor/types';
|
||||
import { getInferenceModels } from '../application/services/api';
|
||||
|
||||
interface InferenceModel {
|
||||
data: InferenceAPIConfigResponse[];
|
||||
}
|
||||
|
||||
type DeploymentStatusType = Record<string, 'deployed' | 'not_deployed'>;
|
||||
|
||||
const getCustomInferenceIdMap = (
|
||||
deploymentStatsByModelId: DeploymentStatusType,
|
||||
models?: InferenceModel
|
||||
) => {
|
||||
return models?.data.reduce<InferenceToModelIdMap>((inferenceMap, model) => {
|
||||
const inferenceId = model.model_id;
|
||||
const trainedModelId =
|
||||
'model_id' in model.service_settings ? model.service_settings.model_id : '';
|
||||
|
||||
inferenceMap[inferenceId] = {
|
||||
trainedModelId,
|
||||
isDeployable: model.service === 'elser' || model.service === 'elasticsearch',
|
||||
isDeployed: deploymentStatsByModelId[trainedModelId] === 'deployed',
|
||||
defaultInferenceEndpoint: false,
|
||||
};
|
||||
return inferenceMap;
|
||||
}, {});
|
||||
};
|
||||
|
||||
const getTrainedModelStats = (modelStats?: InferenceStatsResponse): DeploymentStatusType => {
|
||||
return (
|
||||
modelStats?.trained_model_stats.reduce<DeploymentStatusType>((acc, modelStat) => {
|
||||
if (modelStat.model_id) {
|
||||
acc[modelStat.model_id] =
|
||||
modelStat?.deployment_stats?.state === 'started' ? 'deployed' : 'not_deployed';
|
||||
}
|
||||
return acc;
|
||||
}, {}) || {}
|
||||
);
|
||||
};
|
||||
|
||||
const getDefaultInferenceIds = (deploymentStatsByModelId: DeploymentStatusType) => {
|
||||
return {
|
||||
elser_model_2: {
|
||||
trainedModelId: '.elser_model_2',
|
||||
isDeployable: true,
|
||||
isDeployed: deploymentStatsByModelId['.elser_model_2'] === 'deployed',
|
||||
defaultInferenceEndpoint: true,
|
||||
},
|
||||
e5: {
|
||||
trainedModelId: '.multilingual-e5-small',
|
||||
isDeployable: true,
|
||||
isDeployed: deploymentStatsByModelId['.multilingual-e5-small'] === 'deployed',
|
||||
defaultInferenceEndpoint: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useDetailsPageMappingsModelManagement = (
|
||||
fields: NormalizedFields,
|
||||
inferenceToModelIdMap?: InferenceToModelIdMap
|
||||
) => {
|
||||
const {
|
||||
plugins: { ml },
|
||||
} = useAppContext();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const fetchInferenceModelsAndTrainedModelStats = useCallback(async () => {
|
||||
const inferenceModels = await getInferenceModels();
|
||||
|
||||
const trainedModelStats = await ml?.mlApi?.trainedModels.getTrainedModelStats();
|
||||
|
||||
return { inferenceModels, trainedModelStats };
|
||||
}, [ml]);
|
||||
|
||||
const fetchInferenceToModelIdMap = useCallback(async () => {
|
||||
const { inferenceModels, trainedModelStats } = await fetchInferenceModelsAndTrainedModelStats();
|
||||
const deploymentStatsByModelId = getTrainedModelStats(trainedModelStats);
|
||||
const defaultInferenceIds = getDefaultInferenceIds(deploymentStatsByModelId);
|
||||
const modelIdMap = getCustomInferenceIdMap(deploymentStatsByModelId, inferenceModels);
|
||||
|
||||
dispatch({
|
||||
type: 'inferenceToModelIdMap.update',
|
||||
value: { inferenceToModelIdMap: { ...defaultInferenceIds, ...modelIdMap } },
|
||||
});
|
||||
}, [dispatch, fetchInferenceModelsAndTrainedModelStats]);
|
||||
|
||||
const inferenceIdsInPendingList = useMemo(() => {
|
||||
return Object.values(deNormalize(fields))
|
||||
.filter((field) => field.type === 'semantic_text' && field.inference_id)
|
||||
.map((field) => field.inference_id);
|
||||
}, [fields]);
|
||||
|
||||
const pendingDeployments = useMemo(() => {
|
||||
return inferenceIdsInPendingList
|
||||
.map((inferenceId) => {
|
||||
if (inferenceId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const trainedModelId = inferenceToModelIdMap?.[inferenceId]?.trainedModelId ?? '';
|
||||
return trainedModelId && !inferenceToModelIdMap?.[inferenceId]?.isDeployed
|
||||
? trainedModelId
|
||||
: undefined;
|
||||
})
|
||||
.filter((trainedModelId) => !!trainedModelId);
|
||||
}, [inferenceIdsInPendingList, inferenceToModelIdMap]);
|
||||
|
||||
return {
|
||||
pendingDeployments,
|
||||
fetchInferenceToModelIdMap,
|
||||
fetchInferenceModelsAndTrainedModelStats,
|
||||
};
|
||||
};
|
|
@ -11,6 +11,7 @@ import { ManagementSetup } from '@kbn/management-plugin/public';
|
|||
import { MlPluginStart } from '@kbn/ml-plugin/public';
|
||||
import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
|
||||
export interface SetupDependencies {
|
||||
fleet?: unknown;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
|
|
|
@ -5,10 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
import { registerGetAllRoute } from './register_get_route';
|
||||
|
||||
export function registerInferenceModelRoutes(dependencies: RouteDependencies) {
|
||||
registerGetAllRoute(dependencies);
|
||||
}
|
||||
export { registerInferenceModelRoutes } from './register_inference_route';
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { RouteDependencies } from '../../../types';
|
||||
|
||||
import { registerGetAllRoute } from './register_get_route';
|
||||
|
||||
export function registerInferenceModelRoutes(dependencies: RouteDependencies) {
|
||||
registerGetAllRoute(dependencies);
|
||||
}
|
|
@ -50,9 +50,7 @@
|
|||
"@kbn/inference_integration_flyout",
|
||||
"@kbn/ml-plugin",
|
||||
"@kbn/ml-error-utils",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/react-kibana-context-render"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
]
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue