mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Adds ability to deploy trained models for data frame analytics jobs (#162537)
## Summary Related issue: https://github.com/elastic/kibana/issues/161026 This PR adds a 'Deploy model' action in Machine Learning > Model Management > Trained models. Action in list: <img width="1392" alt="image" src="65978519
-a2c2-41ff-8709-9a868587c5af"> Details step: <img width="1406" alt="image" src="eba9ebf3
-ab69-4e8e-9c02-120982d94373"> Configure processor: <img width="1403" alt="image" src="39ae977f
-163f-4a86-9034-817abf123ac2"> Configure processor edit: <img width="1394" alt="image" src="8dcee306
-baef-4871-8ca7-668c77fa95cb"> Configure processor additional: <img width="1396" alt="image" src="268c7cf8
-19eb-4f29-afbb-8d5e8a0c8598"> Handle failures: <img width="1310" alt="image" src="a692c892
-6730-4a93-8c27-2acc417505e1"> <img width="1310" alt="image" src="37087120
-b9e6-4399-a2ce-012a17d57a52"> <img width="1308" alt="image" src="57d840a8
-41bf-40ff-9702-39ca7facf610"> Test: <img width="1397" alt="image" src="2a07b358
-1945-47c7-8ba3-82942f3cf359"> Create: <img width="1404" alt="image" src="8bd163b3
-64df-4611-924e-06f0e8df1330"> Create success: <img width="1404" alt="image" src="be9c7732
-9635-465f-9e03-28f11e157711"> ### Checklist Delete any items that are not applicable to this PR. - [ ] 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/packages/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 - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] 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 renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c7053d4d4b
commit
327448b726
27 changed files with 2651 additions and 4 deletions
|
@ -319,6 +319,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
overview: `${KIBANA_DOCS}upgrade-assistant.html`,
|
||||
batchReindex: `${KIBANA_DOCS}batch-start-resume-reindex.html`,
|
||||
remoteReindex: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-from-remote`,
|
||||
reindexWithPipeline: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-with-an-ingest-pipeline`,
|
||||
},
|
||||
rollupJobs: `${KIBANA_DOCS}data-rollups.html`,
|
||||
elasticsearch: {
|
||||
|
|
|
@ -298,6 +298,7 @@ export interface DocLinks {
|
|||
readonly overview: string;
|
||||
readonly batchReindex: string;
|
||||
readonly remoteReindex: string;
|
||||
readonly reindexWithPipeline: string;
|
||||
};
|
||||
readonly rollupJobs: string;
|
||||
readonly elasticsearch: Record<string, string>;
|
||||
|
|
|
@ -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 React, { FC, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutFooter,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { extractErrorProperties } from '@kbn/ml-error-utils';
|
||||
|
||||
import { ModelItem } from '../../model_management/models_list';
|
||||
import type { AddInferencePipelineSteps } from './types';
|
||||
import { ADD_INFERENCE_PIPELINE_STEPS } from './constants';
|
||||
import { AddInferencePipelineFooter } from './components/add_inference_pipeline_footer';
|
||||
import { AddInferencePipelineHorizontalSteps } from './components/add_inference_pipeline_horizontal_steps';
|
||||
import { getInitialState, getModelType } from './state';
|
||||
import { PipelineDetails } from './components/pipeline_details';
|
||||
import { ProcessorConfiguration } from './components/processor_configuration';
|
||||
import { OnFailureConfiguration } from './components/on_failure_configuration';
|
||||
import { TestPipeline } from './components/test_pipeline';
|
||||
import { ReviewAndCreatePipeline } from './components/review_and_create_pipeline';
|
||||
import { useMlApiContext } from '../../contexts/kibana';
|
||||
import { getPipelineConfig } from './get_pipeline_config';
|
||||
import { validateInferencePipelineConfigurationStep } from './validation';
|
||||
import type { MlInferenceState, InferenceModelTypes } from './types';
|
||||
import { useFetchPipelines } from './hooks/use_fetch_pipelines';
|
||||
|
||||
export interface AddInferencePipelineFlyoutProps {
|
||||
onClose: () => void;
|
||||
model: ModelItem;
|
||||
}
|
||||
|
||||
export const AddInferencePipelineFlyout: FC<AddInferencePipelineFlyoutProps> = ({
|
||||
onClose,
|
||||
model,
|
||||
}) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const initialState = useMemo(() => getInitialState(model), [model.model_id]);
|
||||
const [formState, setFormState] = useState<MlInferenceState>(initialState);
|
||||
const [step, setStep] = useState<AddInferencePipelineSteps>(ADD_INFERENCE_PIPELINE_STEPS.DETAILS);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
trainedModels: { createInferencePipeline },
|
||||
} = useMlApiContext();
|
||||
|
||||
const modelType = getModelType(model);
|
||||
|
||||
const createPipeline = async () => {
|
||||
setFormState({ ...formState, creatingPipeline: true });
|
||||
try {
|
||||
await createInferencePipeline(formState.pipelineName, getPipelineConfig(formState));
|
||||
setFormState({
|
||||
...formState,
|
||||
pipelineCreated: true,
|
||||
creatingPipeline: false,
|
||||
pipelineError: undefined,
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
const errorProperties = extractErrorProperties(e);
|
||||
setFormState({
|
||||
...formState,
|
||||
creatingPipeline: false,
|
||||
pipelineError: errorProperties.message ?? e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const pipelineNames = useFetchPipelines();
|
||||
|
||||
const handleConfigUpdate = (configUpdate: Partial<MlInferenceState>) => {
|
||||
setFormState({ ...formState, ...configUpdate });
|
||||
};
|
||||
|
||||
const { pipelineName: pipelineNameError, targetField: targetFieldError } = useMemo(() => {
|
||||
const errors = validateInferencePipelineConfigurationStep(
|
||||
formState.pipelineName,
|
||||
pipelineNames
|
||||
);
|
||||
return errors;
|
||||
}, [pipelineNames, formState.pipelineName]);
|
||||
|
||||
const sourceIndex = useMemo(
|
||||
() =>
|
||||
Array.isArray(model.metadata?.analytics_config.source.index)
|
||||
? model.metadata?.analytics_config.source.index.join()
|
||||
: model.metadata?.analytics_config.source.index,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[model?.model_id]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} className="mlTrainedModelsInferencePipelineFlyout" size="l">
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.title',
|
||||
{
|
||||
defaultMessage: 'Deploy analytics model',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<AddInferencePipelineHorizontalSteps
|
||||
step={step}
|
||||
setStep={setStep}
|
||||
isDetailsStepValid={pipelineNameError === undefined && targetFieldError === undefined}
|
||||
isConfigureProcessorStepValid={hasUnsavedChanges === false}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{step === ADD_INFERENCE_PIPELINE_STEPS.DETAILS && (
|
||||
<PipelineDetails
|
||||
handlePipelineConfigUpdate={handleConfigUpdate}
|
||||
pipelineName={formState.pipelineName}
|
||||
pipelineNameError={pipelineNameError}
|
||||
pipelineDescription={formState.pipelineDescription}
|
||||
modelId={model.model_id}
|
||||
targetField={formState.targetField}
|
||||
targetFieldError={targetFieldError}
|
||||
/>
|
||||
)}
|
||||
{step === ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR && model && (
|
||||
<ProcessorConfiguration
|
||||
condition={formState.condition}
|
||||
fieldMap={formState.fieldMap}
|
||||
handleAdvancedConfigUpdate={handleConfigUpdate}
|
||||
inferenceConfig={formState.inferenceConfig}
|
||||
modelInferenceConfig={model.inference_config}
|
||||
modelInputFields={model.input ?? []}
|
||||
modelType={modelType as InferenceModelTypes}
|
||||
setHasUnsavedChanges={setHasUnsavedChanges}
|
||||
tag={formState.tag}
|
||||
/>
|
||||
)}
|
||||
{step === ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE && (
|
||||
<OnFailureConfiguration
|
||||
ignoreFailure={formState.ignoreFailure}
|
||||
takeActionOnFailure={formState.takeActionOnFailure}
|
||||
handleAdvancedConfigUpdate={handleConfigUpdate}
|
||||
onFailure={formState.onFailure}
|
||||
/>
|
||||
)}
|
||||
{step === ADD_INFERENCE_PIPELINE_STEPS.TEST && (
|
||||
<TestPipeline sourceIndex={sourceIndex} state={formState} />
|
||||
)}
|
||||
{step === ADD_INFERENCE_PIPELINE_STEPS.CREATE && (
|
||||
<ReviewAndCreatePipeline
|
||||
inferencePipeline={getPipelineConfig(formState)}
|
||||
modelType={modelType}
|
||||
pipelineName={formState.pipelineName}
|
||||
pipelineCreated={formState.pipelineCreated}
|
||||
pipelineError={formState.pipelineError}
|
||||
/>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter className="mlTrainedModelsInferencePipelineFlyoutFooter">
|
||||
<AddInferencePipelineFooter
|
||||
onClose={onClose}
|
||||
onCreate={createPipeline}
|
||||
step={step}
|
||||
setStep={setStep}
|
||||
isDetailsStepValid={pipelineNameError === undefined && targetFieldError === undefined}
|
||||
isConfigureProcessorStepValid={hasUnsavedChanges === false}
|
||||
pipelineCreated={formState.pipelineCreated}
|
||||
creatingPipeline={formState.creatingPipeline}
|
||||
/>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 React, { FC, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { AddInferencePipelineSteps } from '../types';
|
||||
import {
|
||||
BACK_BUTTON_LABEL,
|
||||
CANCEL_BUTTON_LABEL,
|
||||
CLOSE_BUTTON_LABEL,
|
||||
CONTINUE_BUTTON_LABEL,
|
||||
} from '../constants';
|
||||
import { getSteps } from '../get_steps';
|
||||
|
||||
interface Props {
|
||||
isDetailsStepValid: boolean;
|
||||
isConfigureProcessorStepValid: boolean;
|
||||
pipelineCreated: boolean;
|
||||
creatingPipeline: boolean;
|
||||
step: AddInferencePipelineSteps;
|
||||
onClose: () => void;
|
||||
onCreate: () => void;
|
||||
setStep: React.Dispatch<React.SetStateAction<AddInferencePipelineSteps>>;
|
||||
}
|
||||
|
||||
export const AddInferencePipelineFooter: FC<Props> = ({
|
||||
isDetailsStepValid,
|
||||
isConfigureProcessorStepValid,
|
||||
creatingPipeline,
|
||||
pipelineCreated,
|
||||
onClose,
|
||||
onCreate,
|
||||
step,
|
||||
setStep,
|
||||
}) => {
|
||||
const { nextStep, previousStep, isContinueButtonEnabled } = useMemo(
|
||||
() => getSteps(step, isDetailsStepValid, isConfigureProcessorStepValid),
|
||||
[isDetailsStepValid, isConfigureProcessorStepValid, step]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
{pipelineCreated ? CLOSE_BUTTON_LABEL : CANCEL_BUTTON_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
<EuiFlexItem grow={false}>
|
||||
{previousStep !== undefined && pipelineCreated === false ? (
|
||||
<EuiButtonEmpty
|
||||
flush="both"
|
||||
iconType="arrowLeft"
|
||||
onClick={() => setStep(previousStep as AddInferencePipelineSteps)}
|
||||
>
|
||||
{BACK_BUTTON_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{nextStep !== undefined ? (
|
||||
<EuiButton
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
onClick={() => setStep(nextStep as AddInferencePipelineSteps)}
|
||||
disabled={!isContinueButtonEnabled}
|
||||
fill
|
||||
>
|
||||
{CONTINUE_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton
|
||||
color="success"
|
||||
disabled={!isContinueButtonEnabled || creatingPipeline || pipelineCreated}
|
||||
fill
|
||||
onClick={onCreate}
|
||||
isLoading={creatingPipeline}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.addInferencePipelineModal.footer.create',
|
||||
{
|
||||
defaultMessage: 'Create pipeline',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 React, { FC, memo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiStepsHorizontal, EuiStepsHorizontalProps } from '@elastic/eui';
|
||||
import type { AddInferencePipelineSteps } from '../types';
|
||||
import { ADD_INFERENCE_PIPELINE_STEPS } from '../constants';
|
||||
|
||||
const steps = Object.values(ADD_INFERENCE_PIPELINE_STEPS);
|
||||
|
||||
interface Props {
|
||||
step: AddInferencePipelineSteps;
|
||||
setStep: React.Dispatch<React.SetStateAction<AddInferencePipelineSteps>>;
|
||||
isDetailsStepValid: boolean;
|
||||
isConfigureProcessorStepValid: boolean;
|
||||
}
|
||||
|
||||
export const AddInferencePipelineHorizontalSteps: FC<Props> = memo(
|
||||
({ step, setStep, isDetailsStepValid, isConfigureProcessorStepValid }) => {
|
||||
const currentStepIndex = steps.findIndex((s) => s === step);
|
||||
const navSteps: EuiStepsHorizontalProps['steps'] = [
|
||||
{
|
||||
// Details
|
||||
onClick: () => setStep(ADD_INFERENCE_PIPELINE_STEPS.DETAILS),
|
||||
status: isDetailsStepValid ? 'complete' : 'incomplete',
|
||||
title: i18n.translate(
|
||||
'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.details.title',
|
||||
{
|
||||
defaultMessage: 'Details',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
// Processor configuration
|
||||
onClick: () => {
|
||||
if (!isDetailsStepValid) return;
|
||||
setStep(ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR);
|
||||
},
|
||||
status:
|
||||
isDetailsStepValid && isConfigureProcessorStepValid && currentStepIndex > 1
|
||||
? 'complete'
|
||||
: 'incomplete',
|
||||
title: i18n.translate(
|
||||
'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.configureProcessor.title',
|
||||
{
|
||||
defaultMessage: 'Configure processor',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
// handle failures
|
||||
onClick: () => {
|
||||
if (!isDetailsStepValid) return;
|
||||
setStep(ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE);
|
||||
},
|
||||
status: currentStepIndex > 2 ? 'complete' : 'incomplete',
|
||||
title: i18n.translate(
|
||||
'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.handleFailures.title',
|
||||
{
|
||||
defaultMessage: 'Handle failures',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
// Test
|
||||
onClick: () => {
|
||||
if (!isConfigureProcessorStepValid || !isDetailsStepValid) return;
|
||||
setStep(ADD_INFERENCE_PIPELINE_STEPS.TEST);
|
||||
},
|
||||
status: currentStepIndex > 3 ? 'complete' : 'incomplete',
|
||||
title: i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.transforms.addInferencePipelineModal.steps.test.title',
|
||||
{
|
||||
defaultMessage: 'Test (Optional)',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
// Review and Create
|
||||
onClick: () => {
|
||||
if (!isConfigureProcessorStepValid) return;
|
||||
setStep(ADD_INFERENCE_PIPELINE_STEPS.CREATE);
|
||||
},
|
||||
status: isDetailsStepValid && isConfigureProcessorStepValid ? 'incomplete' : 'disabled',
|
||||
title: i18n.translate(
|
||||
'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.create.title',
|
||||
{
|
||||
defaultMessage: 'Create',
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
switch (step) {
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.DETAILS:
|
||||
navSteps[0].status = 'current';
|
||||
break;
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR:
|
||||
navSteps[1].status = 'current';
|
||||
break;
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE:
|
||||
navSteps[2].status = 'current';
|
||||
break;
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.TEST:
|
||||
navSteps[3].status = 'current';
|
||||
break;
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.CREATE:
|
||||
navSteps[4].status = 'current';
|
||||
break;
|
||||
}
|
||||
return <EuiStepsHorizontal steps={navSteps} />;
|
||||
}
|
||||
);
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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 React, { FC, useState, memo, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiFlexGroup,
|
||||
EuiFieldText,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiTextArea,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { AdditionalSettings, MlInferenceState } from '../types';
|
||||
import { SaveChangesButton } from './save_changes_button';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
|
||||
interface Props {
|
||||
condition?: string;
|
||||
tag?: string;
|
||||
handleAdvancedConfigUpdate: (configUpdate: Partial<MlInferenceState>) => void;
|
||||
}
|
||||
|
||||
export const AdditionalAdvancedSettings: FC<Props> = memo(
|
||||
({ handleAdvancedConfigUpdate, condition, tag }) => {
|
||||
const [additionalSettings, setAdditionalSettings] = useState<
|
||||
Partial<AdditionalSettings> | undefined
|
||||
>(condition || tag ? { condition, tag } : undefined);
|
||||
|
||||
const {
|
||||
services: {
|
||||
docLinks: { links },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const handleAdditionalSettingsChange = (settingsChange: Partial<AdditionalSettings>) => {
|
||||
setAdditionalSettings({ ...additionalSettings, ...settingsChange });
|
||||
};
|
||||
|
||||
const accordionId = useMemo(() => htmlIdGenerator()(), []);
|
||||
const additionalSettingsUpdated = useMemo(
|
||||
() => additionalSettings?.tag !== tag || additionalSettings?.condition !== condition,
|
||||
[additionalSettings, tag, condition]
|
||||
);
|
||||
|
||||
const updateAdditionalSettings = () => {
|
||||
handleAdvancedConfigUpdate({ ...additionalSettings });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiAccordion
|
||||
id={accordionId}
|
||||
buttonContent={
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.additionalSettingsLabel"
|
||||
defaultMessage="Additional settings"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{additionalSettingsUpdated ? (
|
||||
<SaveChangesButton
|
||||
onClick={updateAdditionalSettings}
|
||||
disabled={additionalSettings === undefined}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<EuiPanel color="subdued">
|
||||
<EuiFlexGroup>
|
||||
{/* CONDITION */}
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.conditionLabel"
|
||||
defaultMessage="Condition (optional)"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.conditionHelpText"
|
||||
defaultMessage="This condition must be written as a {painlessDocs} script. If provided, this inference processor only runs when condition is true."
|
||||
values={{
|
||||
painlessDocs: (
|
||||
<EuiLink
|
||||
external
|
||||
target="_blank"
|
||||
href={links.scriptedFields.painlessWalkthrough}
|
||||
>
|
||||
Painless
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.conditionAriaLabel',
|
||||
{ defaultMessage: 'Optional condition for running the processor' }
|
||||
)}
|
||||
value={additionalSettings?.condition ?? ''}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
handleAdditionalSettingsChange({ condition: e.target.value })
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
{/* TAG */}
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.TagLabel"
|
||||
defaultMessage="Tag (optional)"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.tagHelpText"
|
||||
defaultMessage="Identifier for the processor. Useful for debugging and metrics."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={additionalSettings?.tag ?? ''}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
handleAdditionalSettingsChange({ tag: e.target.value })
|
||||
}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.tagAriaLabel',
|
||||
{ defaultMessage: 'Optional tag identifier for the processor' }
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiAccordion>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
* 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 React, { FC, useState, memo } from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCode,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { SaveChangesButton } from './save_changes_button';
|
||||
import type { MlInferenceState } from '../types';
|
||||
import { getDefaultOnFailureConfiguration } from '../state';
|
||||
import { CANCEL_EDIT_MESSAGE, EDIT_MESSAGE } from '../constants';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
import { isValidJson } from '../../../../../common/util/validation_utils';
|
||||
|
||||
interface Props {
|
||||
handleAdvancedConfigUpdate: (configUpdate: Partial<MlInferenceState>) => void;
|
||||
ignoreFailure: boolean;
|
||||
onFailure: MlInferenceState['onFailure'];
|
||||
takeActionOnFailure: MlInferenceState['takeActionOnFailure'];
|
||||
}
|
||||
|
||||
export const OnFailureConfiguration: FC<Props> = memo(
|
||||
({ handleAdvancedConfigUpdate, ignoreFailure, onFailure, takeActionOnFailure }) => {
|
||||
const {
|
||||
services: {
|
||||
docLinks: { links },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const [editOnFailure, setEditOnFailure] = useState<boolean>(false);
|
||||
const [isOnFailureValid, setIsOnFailureValid] = useState<boolean>(false);
|
||||
const [onFailureString, setOnFailureString] = useState<string>(
|
||||
JSON.stringify(onFailure, null, 2)
|
||||
);
|
||||
|
||||
const updateIgnoreFailure = (e: EuiSwitchEvent) => {
|
||||
const checked = e.target.checked;
|
||||
handleAdvancedConfigUpdate({
|
||||
ignoreFailure: checked,
|
||||
...(checked === true ? { takeActionOnFailure: false, onFailure: undefined } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
const updateOnFailure = () => {
|
||||
handleAdvancedConfigUpdate({ onFailure: JSON.parse(onFailureString) });
|
||||
setEditOnFailure(false);
|
||||
};
|
||||
|
||||
const handleOnFailureChange = (json: string) => {
|
||||
setOnFailureString(json);
|
||||
const valid = isValidJson(json);
|
||||
setIsOnFailureValid(valid);
|
||||
};
|
||||
|
||||
const handleTakeActionOnFailureChange = (checked: boolean) => {
|
||||
handleAdvancedConfigUpdate({
|
||||
takeActionOnFailure: checked,
|
||||
onFailure: checked === false ? undefined : getDefaultOnFailureConfiguration(),
|
||||
});
|
||||
if (checked === false) {
|
||||
setEditOnFailure(false);
|
||||
setIsOnFailureValid(true);
|
||||
}
|
||||
};
|
||||
|
||||
const resetOnFailure = () => {
|
||||
setOnFailureString(JSON.stringify(getDefaultOnFailureConfiguration(), null, 2));
|
||||
setIsOnFailureValid(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.onFailureTitle',
|
||||
{ defaultMessage: 'Ingesting problematic documents' }
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.handleFailuresExplanation"
|
||||
defaultMessage="If the model fails to produce a prediction, the document will be ingested without the prediction."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.handleFailuresDescription"
|
||||
defaultMessage="By default, pipeline processing stops on failure. To run the pipeline's remaining processors despite the failure, {ignoreFailure} is set to true. {inferenceDocsLink}."
|
||||
values={{
|
||||
ignoreFailure: <EuiCode>{'ignore_failure'}</EuiCode>,
|
||||
inferenceDocsLink: (
|
||||
<EuiLink external target="_blank" href={links.ingest.pipelineFailure}>
|
||||
Learn more.
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.onFailureDescription"
|
||||
defaultMessage="The {onFailure} configuration shown will be used as a default. It is used to specify a list of processors to run immediately after the inference processor failure and provides information on why the failure occurred. {onFailureDocsLink}"
|
||||
values={{
|
||||
onFailure: <EuiCode>{'on_failure'}</EuiCode>,
|
||||
onFailureDocsLink: (
|
||||
<EuiLink external target="_blank" href={links.ingest.pipelineFailure}>
|
||||
Learn more.
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.ignoreFailureLabel"
|
||||
defaultMessage="Ignore failure and run the pipeline's remaining processors"
|
||||
/>
|
||||
}
|
||||
checked={ignoreFailure}
|
||||
onChange={updateIgnoreFailure}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{ignoreFailure === false ? (
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.noActionOnFailureLabel"
|
||||
defaultMessage="Take action on failure"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
}
|
||||
checked={takeActionOnFailure}
|
||||
onChange={(e: EuiSwitchEvent) =>
|
||||
handleTakeActionOnFailureChange(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{takeActionOnFailure === true && ignoreFailure === false ? (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.onFailureHeadingLabel',
|
||||
{ defaultMessage: 'Actions to take on failure' }
|
||||
)}
|
||||
</strong>
|
||||
</EuiText>
|
||||
}
|
||||
labelAppend={
|
||||
<EuiFlexGroup gutterSize="xs" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="pencil"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setEditOnFailure(!editOnFailure);
|
||||
}}
|
||||
>
|
||||
{editOnFailure ? CANCEL_EDIT_MESSAGE : EDIT_MESSAGE}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{editOnFailure ? (
|
||||
<SaveChangesButton
|
||||
onClick={updateOnFailure}
|
||||
disabled={isOnFailureValid === false}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{editOnFailure ? (
|
||||
<EuiButtonEmpty size="xs" onClick={resetOnFailure}>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.resetOnFailureButton',
|
||||
{ defaultMessage: 'Reset' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.onFailureHelpText"
|
||||
defaultMessage="In case of failure, this configuration stores the document, provides the timestamp at which ingest failed, and the context for the failure."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{!editOnFailure ? (
|
||||
<EuiCodeBlock
|
||||
isCopyable={true}
|
||||
overflowHeight={350}
|
||||
css={{ height: '350px' }}
|
||||
>
|
||||
{JSON.stringify(onFailure, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
) : null}
|
||||
{editOnFailure ? (
|
||||
<CodeEditor
|
||||
height={300}
|
||||
languageId="json"
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'off',
|
||||
tabSize: 2,
|
||||
}}
|
||||
value={onFailureString}
|
||||
onChange={handleOnFailureChange}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* 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 React, { FC, memo } from 'react';
|
||||
|
||||
import {
|
||||
EuiCode,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiTextArea,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
import type { MlInferenceState } from '../types';
|
||||
|
||||
interface Props {
|
||||
handlePipelineConfigUpdate: (configUpdate: Partial<MlInferenceState>) => void;
|
||||
modelId: string;
|
||||
pipelineNameError: string | undefined;
|
||||
pipelineName: string;
|
||||
pipelineDescription: string;
|
||||
targetField: string;
|
||||
targetFieldError: string | undefined;
|
||||
}
|
||||
|
||||
export const PipelineDetails: FC<Props> = memo(
|
||||
({
|
||||
handlePipelineConfigUpdate,
|
||||
modelId,
|
||||
pipelineName,
|
||||
pipelineNameError,
|
||||
pipelineDescription,
|
||||
targetField,
|
||||
targetFieldError,
|
||||
}) => {
|
||||
const {
|
||||
services: {
|
||||
docLinks: { links },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const handleConfigChange = (value: string, type: string) => {
|
||||
handlePipelineConfigUpdate({ [type]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.title',
|
||||
{ defaultMessage: 'Create a pipeline' }
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.description"
|
||||
defaultMessage="Build a {pipeline} to use the trained data frame analytics model - {modelId} - for inference."
|
||||
values={{
|
||||
modelId: <EuiCode>{modelId}</EuiCode>,
|
||||
pipeline: (
|
||||
<EuiLink external target="_blank" href={links.ingest.pipelines}>
|
||||
pipeline
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.descriptionUsePipelines"
|
||||
defaultMessage="Use {pipelineSimulateLink} or {reindexLink} to pass data into this pipeline. Predictions are stored in the Target field."
|
||||
values={{
|
||||
reindexLink: (
|
||||
<EuiLink
|
||||
external
|
||||
target="_blank"
|
||||
href={links.upgradeAssistant.reindexWithPipeline}
|
||||
>
|
||||
_reindex API
|
||||
</EuiLink>
|
||||
),
|
||||
pipelineSimulateLink: (
|
||||
<EuiLink external target="_blank" href={links.apis.simulatePipeline}>
|
||||
pipeline/_simulate
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
<EuiPanel hasBorder={false} hasShadow={false}>
|
||||
{/* NAME */}
|
||||
<EuiForm component="form">
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.nameLabel',
|
||||
{
|
||||
defaultMessage: 'Name',
|
||||
}
|
||||
)}
|
||||
helpText={
|
||||
!pipelineNameError && (
|
||||
<EuiText size="xs">
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'Pipeline names are unique within a deployment and can only contain letters, numbers, underscores, and hyphens.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
)
|
||||
}
|
||||
error={pipelineNameError}
|
||||
isInvalid={pipelineNameError !== undefined}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
placeholder={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.namePlaceholder',
|
||||
{
|
||||
defaultMessage: 'Enter a unique name for this pipeline',
|
||||
}
|
||||
)}
|
||||
value={pipelineName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
handleConfigChange(e.target.value, 'pipelineName')
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{/* DESCRIPTION */}
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.descriptionLabel',
|
||||
{
|
||||
defaultMessage: 'Description',
|
||||
}
|
||||
)}
|
||||
helpText={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.description.helpText',
|
||||
{
|
||||
defaultMessage: 'A description of what this pipeline does.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiTextArea
|
||||
compressed
|
||||
fullWidth
|
||||
placeholder={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.descriptionPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Add a description of what this pipeline does.',
|
||||
}
|
||||
)}
|
||||
value={pipelineDescription}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
handleConfigChange(e.target.value, 'pipelineDescription')
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{/* TARGET FIELD */}
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.targetFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Target field (optional)',
|
||||
}
|
||||
)}
|
||||
helpText={
|
||||
!targetFieldError && (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.targetFieldHelpText"
|
||||
defaultMessage="Field used to contain inference processor results. Defaults to {targetField}."
|
||||
values={{ targetField: <EuiCode>{'ml.inference.<processor_tag>'}</EuiCode> }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
error={targetFieldError}
|
||||
isInvalid={targetFieldError !== undefined}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
value={targetField}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
handleConfigChange(e.target.value, 'targetField')
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,391 @@
|
|||
/*
|
||||
* 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 React, { FC, useState, memo } from 'react';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCode,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
|
||||
import { ModelItem } from '../../../model_management/models_list';
|
||||
import {
|
||||
EDIT_MESSAGE,
|
||||
CANCEL_EDIT_MESSAGE,
|
||||
CREATE_FIELD_MAPPING_MESSAGE,
|
||||
CLEAR_BUTTON_LABEL,
|
||||
} from '../constants';
|
||||
import { validateInferenceConfig } from '../validation';
|
||||
import { isValidJson } from '../../../../../common/util/validation_utils';
|
||||
import { SaveChangesButton } from './save_changes_button';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
import type { MlInferenceState, InferenceModelTypes } from '../types';
|
||||
import { AdditionalAdvancedSettings } from './additional_advanced_settings';
|
||||
import { validateFieldMap } from '../validation';
|
||||
|
||||
function getDefaultFieldMapString() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
field_map: {
|
||||
incoming_field: 'field_the_model_expects',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
condition?: string;
|
||||
fieldMap: MlInferenceState['fieldMap'];
|
||||
handleAdvancedConfigUpdate: (configUpdate: Partial<MlInferenceState>) => void;
|
||||
inferenceConfig: ModelItem['inference_config'];
|
||||
modelInferenceConfig: ModelItem['inference_config'];
|
||||
modelInputFields: ModelItem['input'];
|
||||
modelType?: InferenceModelTypes;
|
||||
setHasUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export const ProcessorConfiguration: FC<Props> = memo(
|
||||
({
|
||||
condition,
|
||||
fieldMap,
|
||||
handleAdvancedConfigUpdate,
|
||||
inferenceConfig,
|
||||
modelInputFields,
|
||||
modelInferenceConfig,
|
||||
modelType,
|
||||
setHasUnsavedChanges,
|
||||
tag,
|
||||
}) => {
|
||||
const {
|
||||
services: {
|
||||
docLinks: { links },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
const [editInferenceConfig, setEditInferenceConfig] = useState<boolean>(false);
|
||||
const [editFieldMapping, setEditFieldMapping] = useState<boolean>(false);
|
||||
const [inferenceConfigString, setInferenceConfigString] = useState<string>(
|
||||
JSON.stringify(inferenceConfig, null, 2)
|
||||
);
|
||||
const [inferenceConfigError, setInferenceConfigError] = useState<string | undefined>();
|
||||
const [fieldMapError, setFieldMapError] = useState<string | undefined>();
|
||||
const [fieldMappingString, setFieldMappingString] = useState<string>(
|
||||
fieldMap ? JSON.stringify(fieldMap, null, 2) : getDefaultFieldMapString()
|
||||
);
|
||||
const [isInferenceConfigValid, setIsInferenceConfigValid] = useState<boolean>(true);
|
||||
const [isFieldMapValid, setIsFieldMapValid] = useState<boolean>(true);
|
||||
|
||||
const handleInferenceConfigChange = (json: string) => {
|
||||
setInferenceConfigString(json);
|
||||
const valid = isValidJson(json);
|
||||
setIsInferenceConfigValid(valid);
|
||||
};
|
||||
|
||||
const updateInferenceConfig = () => {
|
||||
const invalidInferenceConfigMessage = validateInferenceConfig(
|
||||
JSON.parse(inferenceConfigString),
|
||||
modelType
|
||||
);
|
||||
|
||||
if (invalidInferenceConfigMessage === undefined) {
|
||||
handleAdvancedConfigUpdate({ inferenceConfig: JSON.parse(inferenceConfigString) });
|
||||
setHasUnsavedChanges(false);
|
||||
setEditInferenceConfig(false);
|
||||
setInferenceConfigError(undefined);
|
||||
} else {
|
||||
setHasUnsavedChanges(true);
|
||||
setIsInferenceConfigValid(false);
|
||||
setInferenceConfigError(invalidInferenceConfigMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const resetInferenceConfig = () => {
|
||||
setInferenceConfigString(JSON.stringify(modelInferenceConfig, null, 2));
|
||||
setIsInferenceConfigValid(true);
|
||||
setInferenceConfigError(undefined);
|
||||
};
|
||||
|
||||
const clearFieldMap = () => {
|
||||
setFieldMappingString('{}');
|
||||
setIsFieldMapValid(true);
|
||||
setFieldMapError(undefined);
|
||||
};
|
||||
|
||||
const handleFieldMapChange = (json: string) => {
|
||||
setFieldMappingString(json);
|
||||
const valid = isValidJson(json);
|
||||
setIsFieldMapValid(valid);
|
||||
};
|
||||
|
||||
const updateFieldMap = () => {
|
||||
const invalidFieldMapMessage = validateFieldMap(
|
||||
modelInputFields.field_names ?? [],
|
||||
JSON.parse(fieldMappingString)
|
||||
);
|
||||
|
||||
if (invalidFieldMapMessage === undefined) {
|
||||
handleAdvancedConfigUpdate({ fieldMap: JSON.parse(fieldMappingString) });
|
||||
setHasUnsavedChanges(false);
|
||||
setEditFieldMapping(false);
|
||||
setFieldMapError(undefined);
|
||||
} else {
|
||||
setHasUnsavedChanges(true);
|
||||
setIsFieldMapValid(false);
|
||||
setFieldMapError(invalidFieldMapMessage);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
{/* INFERENCE CONFIG */}
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.inferenceConfigurationTitle',
|
||||
{ defaultMessage: 'Inference configuration' }
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.description"
|
||||
defaultMessage="The inference type and its options. Unless otherwise specified, the default configuration options are used. {inferenceDocsLink}."
|
||||
values={{
|
||||
inferenceDocsLink: (
|
||||
<EuiLink external target="_blank" href={links.ingest.inference}>
|
||||
Learn more.
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
labelAppend={
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="pencil"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
if (!editInferenceConfig === false) {
|
||||
setInferenceConfigError(undefined);
|
||||
setIsInferenceConfigValid(true);
|
||||
}
|
||||
setEditInferenceConfig(!editInferenceConfig);
|
||||
}}
|
||||
>
|
||||
{editInferenceConfig ? CANCEL_EDIT_MESSAGE : EDIT_MESSAGE}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{editInferenceConfig ? (
|
||||
<SaveChangesButton
|
||||
onClick={updateInferenceConfig}
|
||||
disabled={isInferenceConfigValid === false}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{editInferenceConfig ? (
|
||||
<EuiButtonEmpty size="xs" onClick={resetInferenceConfig}>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.resetInferenceConfigButton',
|
||||
{ defaultMessage: 'Reset' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
error={inferenceConfigError ?? inferenceConfigError}
|
||||
isInvalid={inferenceConfigError !== undefined || inferenceConfigError !== undefined}
|
||||
>
|
||||
{editInferenceConfig ? (
|
||||
<CodeEditor
|
||||
height={300}
|
||||
languageId="json"
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'off',
|
||||
tabSize: 2,
|
||||
}}
|
||||
value={inferenceConfigString}
|
||||
onChange={handleInferenceConfigChange}
|
||||
/>
|
||||
) : (
|
||||
<EuiCodeBlock isCopyable={true}>
|
||||
{JSON.stringify(inferenceConfig, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{/* FIELD MAP */}
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.fieldMapTitle',
|
||||
{ defaultMessage: 'Fields' }
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.fieldMapDescriptionTwo"
|
||||
defaultMessage="The model expects certain input fields. {fieldsList}"
|
||||
values={{
|
||||
fieldsList: (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiLink onClick={() => setIsPopoverOpen(!isPopoverOpen)}>
|
||||
You can review them here.
|
||||
</EuiLink>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiCodeBlock isCopyable={true}>
|
||||
{JSON.stringify(modelInputFields, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</EuiPopover>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.fieldMapExtendedDescription"
|
||||
defaultMessage="If the fields for the incoming data differ, a {fieldMap} must be created to map the input document field name to the name of the field that the model expects. It must be in JSON format. {inferenceDocsLink}"
|
||||
values={{
|
||||
fieldMap: <EuiCode>{'field_map'}</EuiCode>,
|
||||
inferenceDocsLink: (
|
||||
<EuiLink external target="_blank" href={links.ingest.inference}>
|
||||
Learn more.
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
labelAppend={
|
||||
<EuiFlexGroup gutterSize="xs" justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="pencil"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
const editingState = !editFieldMapping;
|
||||
if (editingState === false) {
|
||||
setFieldMapError(undefined);
|
||||
setIsFieldMapValid(true);
|
||||
setHasUnsavedChanges(false);
|
||||
}
|
||||
setEditFieldMapping(editingState);
|
||||
}}
|
||||
>
|
||||
{editFieldMapping
|
||||
? CANCEL_EDIT_MESSAGE
|
||||
: fieldMap !== undefined
|
||||
? EDIT_MESSAGE
|
||||
: CREATE_FIELD_MAPPING_MESSAGE}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
{editFieldMapping ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<SaveChangesButton
|
||||
onClick={updateFieldMap}
|
||||
disabled={isFieldMapValid === false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{editFieldMapping ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty size="xs" onClick={clearFieldMap}>
|
||||
{CLEAR_BUTTON_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
error={fieldMapError}
|
||||
isInvalid={fieldMapError !== undefined}
|
||||
>
|
||||
<>
|
||||
{!editFieldMapping ? (
|
||||
<EuiCodeBlock isCopyable={true} overflowHeight={350}>
|
||||
{JSON.stringify(fieldMap ?? {}, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
) : null}
|
||||
{editFieldMapping ? (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<CodeEditor
|
||||
height={300}
|
||||
languageId="json"
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'off',
|
||||
tabSize: 2,
|
||||
}}
|
||||
value={fieldMappingString}
|
||||
onChange={handleFieldMapChange}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{/* ADDITIONAL ADVANCED SETTINGS */}
|
||||
<EuiFlexItem>
|
||||
<AdditionalAdvancedSettings
|
||||
handleAdvancedConfigUpdate={handleAdvancedConfigUpdate}
|
||||
condition={condition}
|
||||
tag={tag}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* 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 React, { FC, useMemo } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiCallOut,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
|
||||
const MANAGEMENT_APP_ID = 'management';
|
||||
|
||||
interface Props {
|
||||
inferencePipeline: IngestPipeline;
|
||||
modelType?: string;
|
||||
pipelineName: string;
|
||||
pipelineCreated: boolean;
|
||||
pipelineError?: string;
|
||||
}
|
||||
|
||||
export const ReviewAndCreatePipeline: FC<Props> = ({
|
||||
inferencePipeline,
|
||||
modelType,
|
||||
pipelineName,
|
||||
pipelineCreated,
|
||||
pipelineError,
|
||||
}) => {
|
||||
const {
|
||||
services: {
|
||||
application,
|
||||
docLinks: { links },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const inferenceProcessorLink =
|
||||
modelType === 'regression'
|
||||
? links.ingest.inferenceRegression
|
||||
: links.ingest.inferenceClassification;
|
||||
|
||||
const accordionId = useMemo(() => htmlIdGenerator()(), []);
|
||||
|
||||
const configCodeBlock = useMemo(
|
||||
() => (
|
||||
<EuiCodeBlock language="json" isCopyable overflowHeight="400px">
|
||||
{JSON.stringify(inferencePipeline ?? {}, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
),
|
||||
[inferencePipeline]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={3}>
|
||||
{pipelineCreated === false ? (
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.title',
|
||||
{
|
||||
defaultMessage: "Review the pipeline configuration for '{pipelineName}'",
|
||||
values: { pipelineName },
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
) : null}
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
{pipelineCreated === true && pipelineError === undefined ? (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.successMessage',
|
||||
{
|
||||
defaultMessage: "'{pipelineName}' has been created successfully.",
|
||||
values: { pipelineName },
|
||||
}
|
||||
)}
|
||||
color="success"
|
||||
iconType="check"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.reIndexingMessage"
|
||||
defaultMessage="You can use this pipeline to infer against new data or infer against existing data by {reindexLink} with the pipeline."
|
||||
values={{
|
||||
reindexLink: (
|
||||
<EuiLink
|
||||
href={links.upgradeAssistant.reindexWithPipeline}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
{'reindexing'}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{application.capabilities.management?.ingest?.ingest_pipelines ? (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.ingestPipelinesManagementMessage"
|
||||
defaultMessage=" Navigate to {pipelineManagementLink} to view and manage pipelines."
|
||||
values={{
|
||||
pipelineManagementLink: (
|
||||
<EuiLink
|
||||
onClick={async () => {
|
||||
await application.navigateToApp(MANAGEMENT_APP_ID, {
|
||||
path: `/ingest/ingest_pipelines/?pipeline=${pipelineName}`,
|
||||
openInNewTab: true,
|
||||
});
|
||||
}}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
{'Ingest Pipelines'}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
) : null}
|
||||
{pipelineError !== undefined ? (
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.failureMessage',
|
||||
{
|
||||
defaultMessage: "Unable to create '{pipelineName}'.",
|
||||
values: { pipelineName },
|
||||
}
|
||||
)}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
>
|
||||
<p>{pipelineError}</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.docLinkInErrorMessage"
|
||||
defaultMessage="Learn more about {ingestPipelineConfigLink} and {inferencePipelineConfigLink} configuration."
|
||||
values={{
|
||||
ingestPipelineConfigLink: (
|
||||
<EuiLink href={links.ingest.pipelines} external target={'_blank'}>
|
||||
{'ingest pipeline'}
|
||||
</EuiLink>
|
||||
),
|
||||
inferencePipelineConfigLink: (
|
||||
<EuiLink
|
||||
href={modelType ? inferenceProcessorLink : links.ingest.inference}
|
||||
external
|
||||
target={'_blank'}
|
||||
>
|
||||
{'inference processor'}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
{!pipelineCreated ? (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.description"
|
||||
defaultMessage="This pipeline will be created with the configuration below."
|
||||
/>
|
||||
) : null}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
{pipelineCreated ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiAccordion
|
||||
id={accordionId}
|
||||
buttonContent={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.viewConfig"
|
||||
defaultMessage="View configuration"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{configCodeBlock}
|
||||
</EuiAccordion>
|
||||
</>
|
||||
) : (
|
||||
[configCodeBlock]
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 React, { FC } from 'react';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface SaveChangesButtonProps {
|
||||
onClick: () => void;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const SaveChangesButton: FC<SaveChangesButtonProps> = ({ onClick, disabled }) => (
|
||||
<EuiButtonEmpty size="xs" onClick={onClick} disabled={disabled}>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.saveChangesButton',
|
||||
{ defaultMessage: 'Save changes' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
);
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* 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 React, { FC, memo, useEffect, useCallback, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCode,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiResizableContainer,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
useIsWithinMaxBreakpoint,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { IngestSimulateDocument } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { extractErrorProperties } from '@kbn/ml-error-utils';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
|
||||
import { useMlApiContext, useMlKibana } from '../../../contexts/kibana';
|
||||
import { getPipelineConfig } from '../get_pipeline_config';
|
||||
import { isValidJson } from '../../../../../common/util/validation_utils';
|
||||
import type { MlInferenceState } from '../types';
|
||||
|
||||
interface Props {
|
||||
sourceIndex?: string;
|
||||
state: MlInferenceState;
|
||||
}
|
||||
|
||||
export const TestPipeline: FC<Props> = memo(({ state, sourceIndex }) => {
|
||||
const [simulatePipelineResult, setSimulatePipelineResult] = useState<
|
||||
undefined | estypes.IngestSimulateResponse
|
||||
>();
|
||||
const [simulatePipelineError, setSimulatePipelineError] = useState<undefined | string>();
|
||||
const [sampleDocsString, setSampleDocsString] = useState<string>('');
|
||||
const [isValid, setIsValid] = useState<boolean>(true);
|
||||
const {
|
||||
esSearch,
|
||||
trainedModels: { trainedModelPipelineSimulate },
|
||||
} = useMlApiContext();
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useMlKibana();
|
||||
|
||||
const isSmallerViewport = useIsWithinMaxBreakpoint('s');
|
||||
|
||||
const simulatePipeline = async () => {
|
||||
try {
|
||||
const pipelineConfig = getPipelineConfig(state);
|
||||
const result = await trainedModelPipelineSimulate(
|
||||
pipelineConfig,
|
||||
JSON.parse(sampleDocsString) as IngestSimulateDocument[]
|
||||
);
|
||||
setSimulatePipelineResult(result);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
const errorProperties = extractErrorProperties(error);
|
||||
setSimulatePipelineError(error);
|
||||
toasts.danger({
|
||||
title: i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.errorSimulatingPipeline',
|
||||
{
|
||||
defaultMessage: 'Unable to simulate pipeline.',
|
||||
}
|
||||
),
|
||||
body: errorProperties.message,
|
||||
toastLifeTimeMs: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clearResults = () => {
|
||||
setSimulatePipelineResult(undefined);
|
||||
setSimulatePipelineError(undefined);
|
||||
};
|
||||
|
||||
const onChange = (json: string) => {
|
||||
setSampleDocsString(json);
|
||||
const valid = isValidJson(json);
|
||||
setIsValid(valid);
|
||||
};
|
||||
|
||||
const getSampleDocs = useCallback(async () => {
|
||||
let records: IngestSimulateDocument[] = [];
|
||||
let resp;
|
||||
|
||||
try {
|
||||
resp = await esSearch({
|
||||
index: sourceIndex,
|
||||
body: {
|
||||
size: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (resp && resp.hits.total.value > 0) {
|
||||
records = resp.hits.hits;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
setSampleDocsString(JSON.stringify(records, null, 2));
|
||||
setIsValid(true);
|
||||
}, [sourceIndex, esSearch]);
|
||||
|
||||
useEffect(
|
||||
function fetchSampleDocsFromSource() {
|
||||
if (sourceIndex) {
|
||||
getSampleDocs();
|
||||
}
|
||||
},
|
||||
[sourceIndex, getSampleDocs]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h4>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.title',
|
||||
{ defaultMessage: 'Test the pipeline results' }
|
||||
)}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="s">
|
||||
<p>
|
||||
<strong>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.optionalCallout',
|
||||
{ defaultMessage: 'This is an optional step.' }
|
||||
)}
|
||||
</strong>
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.description"
|
||||
defaultMessage="Run a simulation of the pipeline to confirm it produces the anticipated results."
|
||||
/>{' '}
|
||||
{state.targetField && (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.targetFieldHint"
|
||||
defaultMessage="Check for the target field {targetField} for the prediction in the Result tab."
|
||||
values={{ targetField: <EuiCode>{state.targetField}</EuiCode> }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={false} hasShadow={false}>
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
<EuiButton
|
||||
onClick={simulatePipeline}
|
||||
disabled={sampleDocsString === '' || !isValid}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.runButton',
|
||||
{ defaultMessage: 'Simulate pipeline' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={clearResults}
|
||||
disabled={simulatePipelineResult === undefined}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.clearResultsButton',
|
||||
{ defaultMessage: 'Clear results' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={getSampleDocs}
|
||||
disabled={sampleDocsString === ''}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.resetSampleDocsButton',
|
||||
{ defaultMessage: 'Reset sample docs' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h5>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.subtitle.documents',
|
||||
{ defaultMessage: 'Raw document' }
|
||||
)}
|
||||
</h5>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h5>
|
||||
{i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.subtitle.result',
|
||||
{ defaultMessage: 'Result' }
|
||||
)}
|
||||
</h5>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiResizableContainer
|
||||
direction={isSmallerViewport ? 'vertical' : 'horizontal'}
|
||||
css={css`
|
||||
min-height: calc(${euiThemeVars.euiSizeXL} * 10);
|
||||
`}
|
||||
>
|
||||
{(EuiResizablePanel, EuiResizableButton) => (
|
||||
<>
|
||||
<EuiResizablePanel grow hasBorder initialSize={50} paddingSize="xs">
|
||||
<CodeEditor
|
||||
languageId="json"
|
||||
options={{
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'off',
|
||||
tabSize: 2,
|
||||
}}
|
||||
value={sampleDocsString}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiResizablePanel>
|
||||
|
||||
<EuiResizableButton />
|
||||
|
||||
<EuiResizablePanel grow={false} hasBorder initialSize={50} paddingSize="xs">
|
||||
<EuiCodeBlock language="json" isCopyable className="reviewCodeBlock">
|
||||
{simulatePipelineError
|
||||
? JSON.stringify(simulatePipelineError, null, 2)
|
||||
: simulatePipelineResult
|
||||
? JSON.stringify(simulatePipelineResult, null, 2)
|
||||
: '{}'}
|
||||
</EuiCodeBlock>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</EuiResizableContainer>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer />
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export const ADD_INFERENCE_PIPELINE_STEPS = {
|
||||
DETAILS: 'Details',
|
||||
CONFIGURE_PROCESSOR: 'Configure processor',
|
||||
ON_FAILURE: 'Failure handling',
|
||||
TEST: 'Test',
|
||||
CREATE: 'create',
|
||||
} as const;
|
||||
|
||||
export const CANCEL_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.ml.trainedModels.actions.cancelButtonLabel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
);
|
||||
|
||||
export const CLEAR_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.ml.trainedModels.actions.clearButtonLabel',
|
||||
{ defaultMessage: 'Clear' }
|
||||
);
|
||||
|
||||
export const CLOSE_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.ml.trainedModels.actions.closeButtonLabel',
|
||||
{ defaultMessage: 'Close' }
|
||||
);
|
||||
|
||||
export const BACK_BUTTON_LABEL = i18n.translate('xpack.ml.trainedModels.actions.backButtonLabel', {
|
||||
defaultMessage: 'Back',
|
||||
});
|
||||
|
||||
export const CONTINUE_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.ml.trainedModels.actions.continueButtonLabel',
|
||||
{ defaultMessage: 'Continue' }
|
||||
);
|
||||
|
||||
export const EDIT_MESSAGE = i18n.translate(
|
||||
'xpack.ml.trainedModels.actions.create.advancedDetails.editButtonText',
|
||||
{
|
||||
defaultMessage: 'Edit',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FIELD_MAPPING_MESSAGE = i18n.translate(
|
||||
'xpack.ml.trainedModels.actions.create.advancedDetails.createFieldMapText',
|
||||
{
|
||||
defaultMessage: 'Create field map',
|
||||
}
|
||||
);
|
||||
|
||||
export const CANCEL_EDIT_MESSAGE = i18n.translate(
|
||||
'xpack.ml.trainedModels.actions.create.advancedDetails.cancelEditButtonText',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 type { MlInferenceState } from './types';
|
||||
|
||||
export function getPipelineConfig(state: MlInferenceState) {
|
||||
const {
|
||||
condition,
|
||||
fieldMap,
|
||||
ignoreFailure,
|
||||
inferenceConfig,
|
||||
modelId,
|
||||
onFailure,
|
||||
pipelineDescription,
|
||||
tag,
|
||||
targetField,
|
||||
} = state;
|
||||
return {
|
||||
description: pipelineDescription,
|
||||
processors: [
|
||||
{
|
||||
inference: {
|
||||
model_id: modelId,
|
||||
ignore_failure: ignoreFailure,
|
||||
...(targetField && targetField !== '' ? { target_field: targetField } : {}),
|
||||
...(fieldMap && Object.keys(fieldMap).length > 0 ? { field_map: fieldMap } : {}),
|
||||
...(inferenceConfig && Object.keys(inferenceConfig).length > 0
|
||||
? { inference_config: inferenceConfig }
|
||||
: {}),
|
||||
...(condition && condition !== '' ? { if: condition } : {}),
|
||||
...(tag && tag !== '' ? { tag } : {}),
|
||||
...(onFailure && Object.keys(onFailure).length > 0 ? { on_failure: onFailure } : {}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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 { AddInferencePipelineSteps } from './types';
|
||||
import { ADD_INFERENCE_PIPELINE_STEPS } from './constants';
|
||||
|
||||
export function getSteps(
|
||||
step: AddInferencePipelineSteps,
|
||||
isConfigureStepValid: boolean,
|
||||
isPipelineDataValid: boolean
|
||||
) {
|
||||
let nextStep: AddInferencePipelineSteps | undefined;
|
||||
let previousStep: AddInferencePipelineSteps | undefined;
|
||||
let isContinueButtonEnabled = false;
|
||||
|
||||
switch (step) {
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.DETAILS:
|
||||
nextStep = ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR;
|
||||
isContinueButtonEnabled = isConfigureStepValid;
|
||||
break;
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR:
|
||||
nextStep = ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE;
|
||||
previousStep = ADD_INFERENCE_PIPELINE_STEPS.DETAILS;
|
||||
isContinueButtonEnabled = isPipelineDataValid;
|
||||
break;
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE:
|
||||
nextStep = ADD_INFERENCE_PIPELINE_STEPS.TEST;
|
||||
previousStep = ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR;
|
||||
isContinueButtonEnabled = isPipelineDataValid;
|
||||
break;
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.TEST:
|
||||
nextStep = ADD_INFERENCE_PIPELINE_STEPS.CREATE;
|
||||
previousStep = ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE;
|
||||
isContinueButtonEnabled = true;
|
||||
break;
|
||||
case ADD_INFERENCE_PIPELINE_STEPS.CREATE:
|
||||
previousStep = ADD_INFERENCE_PIPELINE_STEPS.TEST;
|
||||
isContinueButtonEnabled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return { nextStep, previousStep, isContinueButtonEnabled };
|
||||
}
|
|
@ -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 { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useMlApiContext, useMlKibana } from '../../../contexts/kibana';
|
||||
|
||||
export const useFetchPipelines = () => {
|
||||
const [pipelineNames, setPipelineNames] = useState<string[]>([]);
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useMlKibana();
|
||||
|
||||
const {
|
||||
trainedModels: { getAllIngestPipelines },
|
||||
} = useMlApiContext();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPipelines() {
|
||||
let names: string[] = [];
|
||||
try {
|
||||
const results = await getAllIngestPipelines();
|
||||
names = Object.keys(results);
|
||||
setPipelineNames(names);
|
||||
} catch (e) {
|
||||
toasts.danger({
|
||||
title: i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.fetchIngestPipelinesError',
|
||||
{
|
||||
defaultMessage: 'Unable to fetch ingest pipelines.',
|
||||
}
|
||||
),
|
||||
body: e.message,
|
||||
toastLifeTimeMs: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchPipelines();
|
||||
}, [getAllIngestPipelines, toasts]);
|
||||
|
||||
return pipelineNames;
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { AddInferencePipelineFlyout } from './add_inference_pipeline_flyout';
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { getAnalysisType } from '@kbn/ml-data-frame-analytics-utils';
|
||||
import type { MlInferenceState } from './types';
|
||||
import { ModelItem } from '../../model_management/models_list';
|
||||
|
||||
export const getModelType = (model: ModelItem): string | undefined => {
|
||||
const analysisConfig = model.metadata?.analytics_config?.analysis;
|
||||
return analysisConfig !== undefined ? getAnalysisType(analysisConfig) : undefined;
|
||||
};
|
||||
|
||||
export const getDefaultOnFailureConfiguration = (): MlInferenceState['onFailure'] => [
|
||||
{
|
||||
set: {
|
||||
description: "Index document to 'failed-<index>'",
|
||||
field: '_index',
|
||||
value: 'failed-{{{ _index }}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
set: {
|
||||
field: 'event.timestamp',
|
||||
value: '{{{ _ingest.timestamp }}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
set: {
|
||||
field: 'event.failure.message',
|
||||
value: '{{{ _ingest.on_failure_message }}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
set: {
|
||||
field: 'event.failure.processor_type',
|
||||
value: '{{{ _ingest.on_failure_processor_type }}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
set: {
|
||||
field: 'event.failure.processor_tag',
|
||||
value: '{{{ _ingest.on_failure_processor_tag }}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
set: {
|
||||
field: 'event.failure.pipeline',
|
||||
value: '{{{ _ingest.on_failure_pipeline }}}',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const getInitialState = (model: ModelItem): MlInferenceState => {
|
||||
const modelType = getModelType(model);
|
||||
let targetField;
|
||||
|
||||
if (modelType !== undefined) {
|
||||
targetField = model.inference_config
|
||||
? `ml.inference.${model.inference_config[modelType].results_field}`
|
||||
: undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
condition: undefined,
|
||||
creatingPipeline: false,
|
||||
error: false,
|
||||
fieldMap: undefined,
|
||||
ignoreFailure: false,
|
||||
inferenceConfig: model.inference_config,
|
||||
modelId: model.model_id,
|
||||
onFailure: getDefaultOnFailureConfiguration(),
|
||||
pipelineDescription: `Uses the pre-trained data frame analytics model ${model.model_id} to infer against the data that is being ingested in the pipeline`,
|
||||
pipelineName: `ml-inference-${model.model_id}`,
|
||||
pipelineCreated: false,
|
||||
tag: undefined,
|
||||
takeActionOnFailure: true,
|
||||
targetField: targetField ?? '',
|
||||
};
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { IngestInferenceProcessor } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { ADD_INFERENCE_PIPELINE_STEPS } from './constants';
|
||||
|
||||
export type AddInferencePipelineSteps =
|
||||
typeof ADD_INFERENCE_PIPELINE_STEPS[keyof typeof ADD_INFERENCE_PIPELINE_STEPS];
|
||||
|
||||
export interface MlInferenceState {
|
||||
condition?: string;
|
||||
creatingPipeline: boolean;
|
||||
error: boolean;
|
||||
fieldMap?: IngestInferenceProcessor['field_map'];
|
||||
fieldMapError?: string;
|
||||
ignoreFailure: boolean;
|
||||
inferenceConfig: IngestInferenceProcessor['inference_config'];
|
||||
inferenceConfigError?: string;
|
||||
modelId: string;
|
||||
onFailure?: IngestInferenceProcessor['on_failure'];
|
||||
pipelineName: string;
|
||||
pipelineNameError?: string;
|
||||
pipelineDescription: string;
|
||||
pipelineCreated: boolean;
|
||||
pipelineError?: string;
|
||||
tag?: string;
|
||||
targetField: string;
|
||||
targetFieldError?: string;
|
||||
takeActionOnFailure: boolean;
|
||||
}
|
||||
|
||||
export interface AddInferencePipelineFormErrors {
|
||||
targetField?: string;
|
||||
fieldMap?: string;
|
||||
inferenceConfig?: string;
|
||||
pipelineName?: string;
|
||||
}
|
||||
|
||||
export type InferenceModelTypes = 'regression' | 'classification';
|
||||
|
||||
export interface AdditionalSettings {
|
||||
condition?: string;
|
||||
tag?: string;
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 { IngestInferenceProcessor } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { InferenceModelTypes } from './types';
|
||||
import type { AddInferencePipelineFormErrors } from './types';
|
||||
|
||||
const INVALID_PIPELINE_NAME_ERROR = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.invalidPipelineName',
|
||||
{
|
||||
defaultMessage: 'Name must only contain letters, numbers, underscores, and hyphens.',
|
||||
}
|
||||
);
|
||||
const FIELD_REQUIRED_ERROR = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.emptyValueError',
|
||||
{
|
||||
defaultMessage: 'Field is required.',
|
||||
}
|
||||
);
|
||||
const NO_EMPTY_INFERENCE_CONFIG_OBJECT = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.noEmptyInferenceConfigObjectError',
|
||||
{
|
||||
defaultMessage: 'Inference configuration cannot be an empty object.',
|
||||
}
|
||||
);
|
||||
const PIPELINE_NAME_EXISTS_ERROR = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.pipelineNameExistsError',
|
||||
{
|
||||
defaultMessage: 'Name already used by another pipeline.',
|
||||
}
|
||||
);
|
||||
const FIELD_MAP_REQUIRED_FIELDS_ERROR = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.emptyValueError',
|
||||
{
|
||||
defaultMessage: 'Field map must include fields expected by the model.',
|
||||
}
|
||||
);
|
||||
const INFERENCE_CONFIG_MODEL_TYPE_ERROR = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.incorrectModelTypeError',
|
||||
{
|
||||
defaultMessage: 'Inference configuration inference type must match model type.',
|
||||
}
|
||||
);
|
||||
|
||||
const VALID_PIPELINE_NAME_REGEX = /^[\w\-]+$/;
|
||||
export const isValidPipelineName = (input: string): boolean => {
|
||||
return input.length > 0 && VALID_PIPELINE_NAME_REGEX.test(input);
|
||||
};
|
||||
|
||||
export const validateInferencePipelineConfigurationStep = (
|
||||
pipelineName: string,
|
||||
pipelineNames: string[]
|
||||
) => {
|
||||
const errors: AddInferencePipelineFormErrors = {};
|
||||
|
||||
if (pipelineName.trim().length === 0 || pipelineName === '') {
|
||||
errors.pipelineName = FIELD_REQUIRED_ERROR;
|
||||
} else if (!isValidPipelineName(pipelineName)) {
|
||||
errors.pipelineName = INVALID_PIPELINE_NAME_ERROR;
|
||||
}
|
||||
|
||||
const pipelineNameExists = pipelineNames.find((name) => name === pipelineName) !== undefined;
|
||||
|
||||
if (pipelineNameExists) {
|
||||
errors.pipelineName = PIPELINE_NAME_EXISTS_ERROR;
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const validateInferenceConfig = (
|
||||
inferenceConfig: IngestInferenceProcessor['inference_config'],
|
||||
modelType?: InferenceModelTypes
|
||||
) => {
|
||||
const inferenceConfigKeys = Object.keys(inferenceConfig ?? {});
|
||||
let error;
|
||||
|
||||
// If inference config has been changed, it cannot be an empty object
|
||||
if (inferenceConfig && Object.keys(inferenceConfig).length === 0) {
|
||||
error = NO_EMPTY_INFERENCE_CONFIG_OBJECT;
|
||||
return error;
|
||||
}
|
||||
|
||||
// If populated, inference config must have the correct model type
|
||||
if (inferenceConfig && inferenceConfigKeys.length > 0) {
|
||||
if (modelType === inferenceConfigKeys[0]) {
|
||||
return error;
|
||||
} else {
|
||||
error = INFERENCE_CONFIG_MODEL_TYPE_ERROR;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
export const validateFieldMap = (
|
||||
modelInputFields: string[],
|
||||
fieldMap: IngestInferenceProcessor['field_map']
|
||||
) => {
|
||||
let error;
|
||||
const fieldMapValues: string[] = Object.values(fieldMap?.field_map ?? {});
|
||||
|
||||
// If populated, field map must include at least some model input fields as values.
|
||||
if (fieldMap && fieldMapValues.length > 0) {
|
||||
if (fieldMapValues.some((v) => modelInputFields.includes(v))) {
|
||||
return error;
|
||||
} else {
|
||||
error = FIELD_MAP_REQUIRED_FIELDS_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
|
@ -35,6 +35,7 @@ import { ModelItem } from './models_list';
|
|||
export function useModelActions({
|
||||
onTestAction,
|
||||
onModelsDeleteRequest,
|
||||
onModelDeployRequest,
|
||||
onLoading,
|
||||
isLoading,
|
||||
fetchModels,
|
||||
|
@ -43,6 +44,7 @@ export function useModelActions({
|
|||
isLoading: boolean;
|
||||
onTestAction: (model: ModelItem) => void;
|
||||
onModelsDeleteRequest: (models: ModelItem[]) => void;
|
||||
onModelDeployRequest: (model: ModelItem) => void;
|
||||
onLoading: (isLoading: boolean) => void;
|
||||
fetchModels: () => Promise<void>;
|
||||
modelAndDeploymentIds: string[];
|
||||
|
@ -412,6 +414,54 @@ export function useModelActions({
|
|||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: (model) => {
|
||||
const hasDeployments = model.state === MODEL_STATE.STARTED;
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="left"
|
||||
content={
|
||||
hasDeployments
|
||||
? i18n.translate(
|
||||
'xpack.ml.trainedModels.modelsList.deleteDisabledWithDeploymentsTooltip',
|
||||
{
|
||||
defaultMessage: 'Model has started deployments',
|
||||
}
|
||||
)
|
||||
: null
|
||||
}
|
||||
>
|
||||
<>
|
||||
{i18n.translate('xpack.ml.trainedModels.modelsList.deployModelActionLabel', {
|
||||
defaultMessage: 'Deploy model',
|
||||
})}
|
||||
</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
description: i18n.translate('xpack.ml.trainedModels.modelsList.deployModelActionLabel', {
|
||||
defaultMessage: 'Deploy model',
|
||||
}),
|
||||
'data-test-subj': 'mlModelsTableRowDeployAction',
|
||||
icon: 'continuityAbove',
|
||||
type: 'icon',
|
||||
isPrimary: false,
|
||||
onClick: (model) => {
|
||||
onModelDeployRequest(model);
|
||||
},
|
||||
available: (item) => {
|
||||
const isDfaTrainedModel = item.metadata?.analytics_config !== undefined;
|
||||
return (
|
||||
isDfaTrainedModel &&
|
||||
!isBuiltInModel(item) &&
|
||||
!item.putModelConfig &&
|
||||
canManageIngestPipelines
|
||||
);
|
||||
},
|
||||
enabled: (item) => {
|
||||
return item.state !== MODEL_STATE.STARTED;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: (model) => {
|
||||
const hasDeployments = model.state === MODEL_STATE.STARTED;
|
||||
|
@ -492,6 +542,7 @@ export function useModelActions({
|
|||
displayErrorToast,
|
||||
getUserConfirmation,
|
||||
onModelsDeleteRequest,
|
||||
onModelDeployRequest,
|
||||
canDeleteTrainedModels,
|
||||
isBuiltInModel,
|
||||
onTestAction,
|
||||
|
|
|
@ -62,6 +62,7 @@ import { useFieldFormatter } from '../contexts/kibana/use_field_formatter';
|
|||
import { useRefresh } from '../routing/use_refresh';
|
||||
import { SavedObjectsWarning } from '../components/saved_objects_warning';
|
||||
import { TestTrainedModelFlyout } from './test_models';
|
||||
import { AddInferencePipelineFlyout } from '../components/ml_inference';
|
||||
|
||||
type Stats = Omit<TrainedModelStat, 'model_id' | 'deployment_stats'>;
|
||||
|
||||
|
@ -134,6 +135,7 @@ export const ModelsList: FC<Props> = ({
|
|||
const [items, setItems] = useState<ModelItem[]>([]);
|
||||
const [selectedModels, setSelectedModels] = useState<ModelItem[]>([]);
|
||||
const [modelsToDelete, setModelsToDelete] = useState<ModelItem[]>([]);
|
||||
const [modelToDeploy, setModelToDeploy] = useState<ModelItem | undefined>();
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, JSX.Element>>(
|
||||
{}
|
||||
);
|
||||
|
@ -349,6 +351,7 @@ export const ModelsList: FC<Props> = ({
|
|||
fetchModels: fetchModelsData,
|
||||
onTestAction: setModelToTest,
|
||||
onModelsDeleteRequest: setModelsToDelete,
|
||||
onModelDeployRequest: setModelToDeploy,
|
||||
onLoading: setIsLoading,
|
||||
modelAndDeploymentIds,
|
||||
});
|
||||
|
@ -642,6 +645,12 @@ export const ModelsList: FC<Props> = ({
|
|||
{modelToTest === null ? null : (
|
||||
<TestTrainedModelFlyout model={modelToTest} onClose={setModelToTest.bind(null, null)} />
|
||||
)}
|
||||
{modelToDeploy !== undefined ? (
|
||||
<AddInferencePipelineFlyout
|
||||
onClose={setModelToDeploy.bind(null, undefined)}
|
||||
model={modelToDeploy}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import type { HttpFetchQuery } from '@kbn/core/public';
|
||||
|
@ -58,7 +59,6 @@ export function trainedModelsApiProvider(httpService: HttpService) {
|
|||
return {
|
||||
/**
|
||||
* Fetches configuration information for a trained inference model.
|
||||
*
|
||||
* @param modelId - Model ID, collection of Model IDs or Model ID pattern.
|
||||
* Fetches all In case nothing is provided.
|
||||
* @param params - Optional query params
|
||||
|
@ -76,7 +76,6 @@ export function trainedModelsApiProvider(httpService: HttpService) {
|
|||
|
||||
/**
|
||||
* Fetches usage information for trained inference models.
|
||||
*
|
||||
* @param modelId - Model ID, collection of Model IDs or Model ID pattern.
|
||||
* Fetches all In case nothing is provided.
|
||||
* @param params - Optional query params
|
||||
|
@ -93,7 +92,6 @@ export function trainedModelsApiProvider(httpService: HttpService) {
|
|||
|
||||
/**
|
||||
* Fetches pipelines associated with provided models
|
||||
*
|
||||
* @param modelId - Model ID, collection of Model IDs.
|
||||
*/
|
||||
getTrainedModelPipelines(modelId: string | string[]) {
|
||||
|
@ -109,9 +107,31 @@ export function trainedModelsApiProvider(httpService: HttpService) {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches all ingest pipelines
|
||||
*/
|
||||
getAllIngestPipelines() {
|
||||
return httpService.http<NodesOverviewResponse>({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/trained_models/ingest_pipelines`,
|
||||
method: 'GET',
|
||||
version: '1',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates inference pipeline
|
||||
*/
|
||||
createInferencePipeline(pipelineName: string, pipeline: IngestPipeline) {
|
||||
return httpService.http<estypes.IngestSimulateResponse>({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/trained_models/create_inference_pipeline`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ pipeline, pipelineName }),
|
||||
version: '1',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes an existing trained inference model.
|
||||
*
|
||||
* @param modelId - Model ID
|
||||
*/
|
||||
deleteTrainedModel(
|
||||
|
|
|
@ -177,6 +177,8 @@
|
|||
"DeleteTrainedModel",
|
||||
"SimulateIngestPipeline",
|
||||
"InferTrainedModelDeployment",
|
||||
"CreateInferencePipeline",
|
||||
"GetIngestPipelines",
|
||||
|
||||
"Alerting",
|
||||
"PreviewAlert",
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
*/
|
||||
|
||||
import type { IScopedClusterClient } from '@kbn/core/server';
|
||||
import {
|
||||
IngestPipeline,
|
||||
IngestSimulateDocument,
|
||||
IngestSimulateRequest,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { PipelineDefinition } from '../../../common/types/trained_models';
|
||||
|
||||
export type ModelService = ReturnType<typeof modelsProvider>;
|
||||
|
@ -64,5 +69,64 @@ export function modelsProvider(client: IScopedClusterClient) {
|
|||
pipelinesIds.map((id) => client.asCurrentUser.ingest.deletePipeline({ id }))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Simulates the effect of the pipeline on given document.
|
||||
*
|
||||
*/
|
||||
async simulatePipeline(docs: IngestSimulateDocument[], pipelineConfig: IngestPipeline) {
|
||||
const simulateRequest: IngestSimulateRequest = {
|
||||
docs,
|
||||
pipeline: pipelineConfig,
|
||||
};
|
||||
let result = {};
|
||||
try {
|
||||
result = await client.asCurrentUser.ingest.simulate(simulateRequest);
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404) {
|
||||
// ES returns 404 when there are no pipelines
|
||||
// Instead, we should return an empty response and a 200
|
||||
return result;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the pipeline
|
||||
*
|
||||
*/
|
||||
async createInferencePipeline(pipelineConfig: IngestPipeline, pipelineName: string) {
|
||||
let result = {};
|
||||
|
||||
result = await client.asCurrentUser.ingest.putPipeline({
|
||||
id: pipelineName,
|
||||
...pipelineConfig,
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves existing pipelines.
|
||||
*
|
||||
*/
|
||||
async getPipelines() {
|
||||
let result = {};
|
||||
try {
|
||||
result = await client.asCurrentUser.ingest.getPipeline();
|
||||
} catch (error) {
|
||||
if (error.statusCode === 404) {
|
||||
// ES returns 404 when there are no pipelines
|
||||
// Instead, we should return an empty response and a 200
|
||||
return result;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -77,3 +77,13 @@ export const deleteTrainedModelQuerySchema = schema.object({
|
|||
with_pipelines: schema.maybe(schema.boolean({ defaultValue: false })),
|
||||
force: schema.maybe(schema.boolean({ defaultValue: false })),
|
||||
});
|
||||
|
||||
export const createIngestPipelineSchema = schema.object({
|
||||
pipelineName: schema.string(),
|
||||
pipeline: schema.maybe(
|
||||
schema.object({
|
||||
processors: schema.arrayOf(schema.any()),
|
||||
description: schema.maybe(schema.string()),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
putTrainedModelQuerySchema,
|
||||
threadingParamsSchema,
|
||||
updateDeploymentParamsSchema,
|
||||
createIngestPipelineSchema,
|
||||
} from './schemas/inference_schema';
|
||||
import { TrainedModelConfigResponse } from '../../common/types/trained_models';
|
||||
import { mlLog } from '../lib/log';
|
||||
|
@ -237,6 +238,78 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization)
|
|||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @apiGroup TrainedModels
|
||||
*
|
||||
* @api {get} /internal/ml/trained_models/ingest_pipelines Get ingest pipelines
|
||||
* @apiName GetIngestPipelines
|
||||
* @apiDescription Retrieves pipelines
|
||||
*/
|
||||
router.versioned
|
||||
.get({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/trained_models/ingest_pipelines`,
|
||||
access: 'internal',
|
||||
options: {
|
||||
tags: ['access:ml:canGetTrainedModels'], // TODO: update permissions
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: false,
|
||||
},
|
||||
routeGuard.fullLicenseAPIGuard(async ({ client, request, mlClient, response }) => {
|
||||
try {
|
||||
const body = await modelsProvider(client).getPipelines();
|
||||
return response.ok({
|
||||
body,
|
||||
});
|
||||
} catch (e) {
|
||||
return response.customError(wrapError(e));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @apiGroup TrainedModels
|
||||
*
|
||||
* @api {post} /internal/ml/trained_models/create_inference_pipeline creates the pipeline with inference processor
|
||||
* @apiName CreateInferencePipeline
|
||||
* @apiDescription Creates the inference pipeline
|
||||
*/
|
||||
router.versioned
|
||||
.post({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/trained_models/create_inference_pipeline`,
|
||||
access: 'internal',
|
||||
options: {
|
||||
tags: ['access:ml:canCreateTrainedModels'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: {
|
||||
request: {
|
||||
body: createIngestPipelineSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
routeGuard.fullLicenseAPIGuard(async ({ client, request, mlClient, response }) => {
|
||||
try {
|
||||
const { pipeline, pipelineName } = request.body;
|
||||
const body = await modelsProvider(client).createInferencePipeline(
|
||||
pipeline!,
|
||||
pipelineName
|
||||
);
|
||||
return response.ok({
|
||||
body,
|
||||
});
|
||||
} catch (e) {
|
||||
return response.customError(wrapError(e));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @apiGroup TrainedModels
|
||||
*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue