mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ML] Data Frame Analytics Trained models: add ability to reindex after pipeline creation success (#166312)
## Summary Related issue: https://github.com/elastic/kibana/issues/164997 In the Trained Models list 'Deploy Model' flyout for DFA models: - adds the ability to reindex in the last step of the flyout after pipeline creation success <img width="1343" alt="image" src="ae76fb04
-889c-45da-a9c0-394b5cd039c2"> <img width="1206" alt="image" src="39bee9b1
-338b-4b5b-a295-789363f6d387"> <img width="1107" alt="image" src="aa98ff65
-1b37-46a4-b411-4adb3fe9df17"> <img width="1216" alt="image" src="2e7c4930
-1036-415e-8d27-19f7e6012701"> <img width="1343" alt="image" src="3654cc6f
-4113-44cc-86ab-93dbdf49a9a9"> ### 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
2234237cd4
commit
e5cd6fb493
12 changed files with 707 additions and 151 deletions
|
@ -650,6 +650,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
searchPreference: `${ELASTICSEARCH_DOCS}search-search.html#search-preference`,
|
||||
securityApis: `${ELASTICSEARCH_DOCS}security-api.html`,
|
||||
simulatePipeline: `${ELASTICSEARCH_DOCS}simulate-pipeline-api.html`,
|
||||
tasks: `${ELASTICSEARCH_DOCS}tasks.html`,
|
||||
timeUnits: `${ELASTICSEARCH_DOCS}api-conventions.html#time-units`,
|
||||
unfreezeIndex: `${ELASTICSEARCH_DOCS}unfreeze-index-api.html`,
|
||||
updateTransform: `${ELASTICSEARCH_DOCS}update-transform.html`,
|
||||
|
|
|
@ -389,6 +389,7 @@ export interface DocLinks {
|
|||
searchPreference: string;
|
||||
securityApis: string;
|
||||
simulatePipeline: string;
|
||||
tasks: string;
|
||||
timeUnits: string;
|
||||
unfreezeIndex: string;
|
||||
updateTransform: string;
|
||||
|
|
|
@ -159,13 +159,14 @@ export const AddInferencePipelineFlyout: FC<AddInferencePipelineFlyoutProps> = (
|
|||
{step === ADD_INFERENCE_PIPELINE_STEPS.TEST && (
|
||||
<TestPipeline sourceIndex={sourceIndex} state={formState} />
|
||||
)}
|
||||
{step === ADD_INFERENCE_PIPELINE_STEPS.CREATE && (
|
||||
{step === ADD_INFERENCE_PIPELINE_STEPS.CREATE && sourceIndex && (
|
||||
<ReviewAndCreatePipeline
|
||||
inferencePipeline={getPipelineConfig(formState)}
|
||||
modelType={modelType}
|
||||
pipelineName={formState.pipelineName}
|
||||
pipelineCreated={formState.pipelineCreated}
|
||||
pipelineError={formState.pipelineError}
|
||||
sourceIndex={sourceIndex}
|
||||
/>
|
||||
)}
|
||||
</EuiFlyoutBody>
|
||||
|
|
|
@ -0,0 +1,435 @@
|
|||
/*
|
||||
* 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, { type ChangeEvent, FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiCheckbox,
|
||||
EuiComboBox,
|
||||
type EuiComboBoxOptionOption,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
htmlIdGenerator,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { extractErrorMessage } from '@kbn/ml-error-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { debounce } from 'lodash';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
import { isValidIndexName } from '../../../../../common/util/es_utils';
|
||||
import { createKibanaDataView, checkIndexExists } from '../retry_create_data_view';
|
||||
import { useToastNotificationService } from '../../../services/toast_notification_service';
|
||||
|
||||
const destIndexEmpty = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexEmpty',
|
||||
{
|
||||
defaultMessage: 'Enter a valid destination index',
|
||||
}
|
||||
);
|
||||
|
||||
const destIndexExists = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexExists',
|
||||
{
|
||||
defaultMessage: 'An index with this name already exists.',
|
||||
}
|
||||
);
|
||||
|
||||
const destIndexInvalid = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexInvalid',
|
||||
{
|
||||
defaultMessage:
|
||||
'Index name cannot be empty. It must be lowercase. It cannot start with -, _, +. It cannot include \\\\, /, *, ?, ", <, >, |, space character, comma, #, :',
|
||||
}
|
||||
);
|
||||
|
||||
interface Props {
|
||||
pipelineName: string;
|
||||
sourceIndex: string;
|
||||
}
|
||||
|
||||
export const ReindexWithPipeline: FC<Props> = ({ pipelineName, sourceIndex }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState<EuiComboBoxOptionOption[]>([
|
||||
{ label: sourceIndex },
|
||||
]);
|
||||
const [options, setOptions] = useState<EuiComboBoxOptionOption[]>([]);
|
||||
const [destinationIndex, setDestinationIndex] = useState<string>('');
|
||||
const [destinationIndexExists, setDestinationIndexExists] = useState<boolean>(false);
|
||||
const [destinationIndexInvalidMessage, setDestinationIndexInvalidMessage] = useState<
|
||||
string | undefined
|
||||
>(destIndexEmpty);
|
||||
const [reindexingTaskId, setReindexingTaskId] = useState<estypes.TaskId | undefined>();
|
||||
const [discoverLink, setDiscoverLink] = useState<string | undefined>();
|
||||
const [shouldCreateDataView, setShouldCreateDataView] = useState<boolean>(false);
|
||||
const [canReindex, setCanReindex] = useState<boolean>(false);
|
||||
const [canReindexError, setCanReindexError] = useState<string | undefined>();
|
||||
|
||||
const {
|
||||
services: {
|
||||
application: { capabilities },
|
||||
share,
|
||||
data,
|
||||
mlServices: {
|
||||
mlApiServices: { getIndices, reindexWithPipeline, hasPrivileges },
|
||||
},
|
||||
docLinks: { links },
|
||||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const { displayErrorToast } = useToastNotificationService();
|
||||
|
||||
const canCreateDataView = useMemo(
|
||||
() =>
|
||||
capabilities.savedObjectsManagement.edit === true || capabilities.indexPatterns.save === true,
|
||||
[capabilities]
|
||||
);
|
||||
const id = useMemo(() => htmlIdGenerator()(), []);
|
||||
|
||||
const discoverLocator = useMemo(
|
||||
() => share.url.locators.get('DISCOVER_APP_LOCATOR'),
|
||||
[share.url.locators]
|
||||
);
|
||||
|
||||
const showDiscoverLink = useMemo(
|
||||
() => capabilities.discover?.show !== undefined && discoverLocator !== undefined,
|
||||
[capabilities.discover?.show, discoverLocator]
|
||||
);
|
||||
|
||||
const generateDiscoverUrl = useCallback(
|
||||
async (dataViewId: string) => {
|
||||
if (discoverLocator !== undefined) {
|
||||
const url = await discoverLocator.getRedirectUrl({
|
||||
indexPatternId: dataViewId,
|
||||
timeRange: data.query.timefilter.timefilter.getTime(),
|
||||
filters: data.query.filterManager.getFilters(),
|
||||
});
|
||||
|
||||
return url;
|
||||
}
|
||||
},
|
||||
[discoverLocator, data]
|
||||
);
|
||||
|
||||
const debouncedIndexCheck = debounce(async () => {
|
||||
const checkResp = await checkIndexExists(destinationIndex);
|
||||
if (checkResp.errorMessage !== undefined) {
|
||||
displayErrorToast(
|
||||
checkResp.errorMessage,
|
||||
i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.errorCheckingIndexExists',
|
||||
{
|
||||
defaultMessage: 'An error occurred getting the existing index names',
|
||||
}
|
||||
)
|
||||
);
|
||||
} else if (checkResp.resp) {
|
||||
setDestinationIndexExists(checkResp.resp[destinationIndex].exists);
|
||||
}
|
||||
}, 400);
|
||||
|
||||
const onReindex = async () => {
|
||||
try {
|
||||
const srcIndex = selectedIndex[0].label;
|
||||
const result = await reindexWithPipeline(pipelineName, srcIndex, destinationIndex);
|
||||
setReindexingTaskId(result.task);
|
||||
} catch (error) {
|
||||
displayErrorToast(
|
||||
error,
|
||||
i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.reindexErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred reindexing',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (selected: EuiComboBoxOptionOption[]) => {
|
||||
setSelectedIndex(selected);
|
||||
};
|
||||
|
||||
const onDestIndexNameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setDestinationIndex(e.target.value);
|
||||
if (e.target.value === '') {
|
||||
setDestinationIndexInvalidMessage(destIndexEmpty);
|
||||
} else if (!isValidIndexName(e.target.value)) {
|
||||
setDestinationIndexInvalidMessage(destIndexInvalid);
|
||||
} else {
|
||||
setDestinationIndexInvalidMessage(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(
|
||||
function canReindexCheck() {
|
||||
async function checkPrivileges() {
|
||||
try {
|
||||
const privilege = await hasPrivileges({
|
||||
index: [
|
||||
{
|
||||
names: [selectedIndex[0].label], // uses wildcard
|
||||
privileges: ['read'],
|
||||
},
|
||||
{
|
||||
names: [destinationIndex], // uses wildcard
|
||||
privileges: ['write'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setCanReindex(
|
||||
privilege.hasPrivileges === undefined ||
|
||||
privilege.hasPrivileges.has_all_requested === true
|
||||
);
|
||||
} catch (e) {
|
||||
const error = extractErrorMessage(e);
|
||||
const errorMessage = i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.indexPrivilegeErrorMessage',
|
||||
{
|
||||
defaultMessage: 'User does not have required permissions to reindex {index}. {error}',
|
||||
values: { error, index: selectedIndex[0].label },
|
||||
}
|
||||
);
|
||||
setCanReindexError(errorMessage);
|
||||
}
|
||||
}
|
||||
if (hasPrivileges !== undefined) {
|
||||
checkPrivileges();
|
||||
}
|
||||
},
|
||||
[hasPrivileges, sourceIndex, destinationIndex, selectedIndex]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function checkDestIndexExists() {
|
||||
if (destinationIndex !== undefined && destinationIndex !== '') {
|
||||
debouncedIndexCheck();
|
||||
}
|
||||
},
|
||||
[destinationIndex, debouncedIndexCheck]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function getIndexOptions() {
|
||||
async function getAllIndices() {
|
||||
const indices = await getIndices();
|
||||
const indexOptions = indices.map((index) => ({ label: index.name }));
|
||||
setOptions(indexOptions);
|
||||
}
|
||||
getAllIndices();
|
||||
},
|
||||
[getIndices]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function createDiscoverLink() {
|
||||
async function createDataView() {
|
||||
const dataViewCreationResult = await createKibanaDataView(destinationIndex, data.dataViews);
|
||||
if (
|
||||
dataViewCreationResult?.success === true &&
|
||||
dataViewCreationResult?.dataViewId &&
|
||||
showDiscoverLink
|
||||
) {
|
||||
const url = await generateDiscoverUrl(dataViewCreationResult.dataViewId);
|
||||
setDiscoverLink(url);
|
||||
}
|
||||
}
|
||||
if (reindexingTaskId !== undefined && shouldCreateDataView === true) {
|
||||
createDataView();
|
||||
}
|
||||
},
|
||||
[
|
||||
reindexingTaskId,
|
||||
destinationIndex,
|
||||
data?.dataViews,
|
||||
generateDiscoverUrl,
|
||||
showDiscoverLink,
|
||||
shouldCreateDataView,
|
||||
]
|
||||
);
|
||||
|
||||
const reindexButton = (
|
||||
<EuiButton
|
||||
onClick={onReindex}
|
||||
disabled={
|
||||
(destinationIndexInvalidMessage !== undefined && selectedIndex.length > 0) ||
|
||||
!canReindex ||
|
||||
destinationIndexExists
|
||||
}
|
||||
size="s"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.reindexLabel"
|
||||
defaultMessage="Reindex"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder={false}>
|
||||
{reindexingTaskId === undefined ? (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.reindexingTitle"
|
||||
defaultMessage="Reindex with pipeline"
|
||||
/>
|
||||
</h4>
|
||||
</EuiText>
|
||||
<EuiText size="xs" color="GrayText">
|
||||
<EuiLink
|
||||
href={links.upgradeAssistant.reindexWithPipeline}
|
||||
target="_blank"
|
||||
external
|
||||
>
|
||||
{'Learn more.'}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiIconTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.reindexingTooltip"
|
||||
defaultMessage="Reindex data from the source index to a destination index using the new pipeline, which adds inference results to each document."
|
||||
/>
|
||||
}
|
||||
position="right"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.selectIndexLabel"
|
||||
defaultMessage="Select index to reindex"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiComboBox
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={options}
|
||||
selectedOptions={selectedIndex}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destinationIndexLabel"
|
||||
defaultMessage="Destination index name"
|
||||
/>
|
||||
}
|
||||
isInvalid={destinationIndexInvalidMessage !== undefined || destinationIndexExists}
|
||||
error={
|
||||
destinationIndexInvalidMessage || destinationIndexExists
|
||||
? destinationIndexInvalidMessage ?? destIndexExists
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={destinationIndex}
|
||||
onChange={onDestIndexNameChange}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexFieldAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Enter the name of the destination index',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
{canCreateDataView ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCheckbox
|
||||
id={id}
|
||||
label={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.createDataViewLabel',
|
||||
{
|
||||
defaultMessage: 'Create data view',
|
||||
}
|
||||
)}
|
||||
checked={shouldCreateDataView}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setShouldCreateDataView(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
{canReindexError ? (
|
||||
<EuiToolTip position="top" content={canReindexError}>
|
||||
{reindexButton}
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
reindexButton
|
||||
)}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiCallOut
|
||||
data-test-subj="mlTrainedModelsInferenceReviewAndCreateStepSuccessCallout"
|
||||
title={i18n.translate(
|
||||
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.reindexStartedMessage',
|
||||
{
|
||||
defaultMessage: 'Reindexing of {sourceIndex} to {destinationIndex} has started.',
|
||||
values: { sourceIndex, destinationIndex },
|
||||
}
|
||||
)}
|
||||
color="success"
|
||||
iconType="check"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.reindexingTaskIdMessage"
|
||||
defaultMessage="Reindexing task id {taskId} can be used to monitor the progress via the {tasksApi}."
|
||||
values={{
|
||||
taskId: reindexingTaskId,
|
||||
tasksApi: (
|
||||
<EuiLink href={`${links.apis.tasks}`} target="_blank" external>
|
||||
{'task management API'}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{discoverLink !== undefined ? (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.reindexedlinkToDiscover"
|
||||
defaultMessage=" View {destIndexInDiscover} in Discover."
|
||||
values={{
|
||||
destIndexInDiscover: (
|
||||
<EuiLink href={`${discoverLink}`} target="_blank" external>
|
||||
{destinationIndex}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import {
|
||||
|
@ -24,6 +24,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
import { ReindexWithPipeline } from './reindex_with_pipeline';
|
||||
|
||||
const MANAGEMENT_APP_ID = 'management';
|
||||
|
||||
|
@ -33,6 +34,7 @@ interface Props {
|
|||
pipelineName: string;
|
||||
pipelineCreated: boolean;
|
||||
pipelineError?: string;
|
||||
sourceIndex: string;
|
||||
}
|
||||
|
||||
export const ReviewAndCreatePipeline: FC<Props> = ({
|
||||
|
@ -41,6 +43,7 @@ export const ReviewAndCreatePipeline: FC<Props> = ({
|
|||
pipelineName,
|
||||
pipelineCreated,
|
||||
pipelineError,
|
||||
sourceIndex,
|
||||
}) => {
|
||||
const {
|
||||
services: {
|
||||
|
@ -49,6 +52,10 @@ export const ReviewAndCreatePipeline: FC<Props> = ({
|
|||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const [isNextStepsAccordionOpen, setIsNextStepsAccordionOpen] = useState<'open' | 'closed'>(
|
||||
'closed'
|
||||
);
|
||||
|
||||
const inferenceProcessorLink =
|
||||
modelType === 'regression'
|
||||
? links.ingest.inferenceRegression
|
||||
|
@ -112,11 +119,7 @@ export const ReviewAndCreatePipeline: FC<Props> = ({
|
|||
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
|
||||
>
|
||||
<EuiLink onClick={() => setIsNextStepsAccordionOpen('open')}>
|
||||
{'reindexing'}
|
||||
</EuiLink>
|
||||
),
|
||||
|
@ -198,6 +201,28 @@ export const ReviewAndCreatePipeline: FC<Props> = ({
|
|||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
{pipelineCreated ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiAccordion
|
||||
id={accordionId}
|
||||
buttonContent={
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.nextStepsLabel"
|
||||
defaultMessage="Next steps"
|
||||
/>
|
||||
}
|
||||
forceState={isNextStepsAccordionOpen}
|
||||
onToggle={(isOpen) => {
|
||||
setIsNextStepsAccordionOpen(isOpen ? 'open' : 'closed');
|
||||
}}
|
||||
>
|
||||
<ReindexWithPipeline pipelineName={pipelineName} sourceIndex={sourceIndex} />
|
||||
</EuiAccordion>
|
||||
</>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
{pipelineCreated ? (
|
||||
<>
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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 { extractErrorMessage } from '@kbn/ml-error-utils';
|
||||
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
|
||||
import { DuplicateDataViewError } from '@kbn/data-plugin/public';
|
||||
import { ml } from '../../services/ml_api_service';
|
||||
import type { FormMessage } from '../../data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state';
|
||||
|
||||
interface CreateKibanaDataViewResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
message: string;
|
||||
dataViewId?: string;
|
||||
}
|
||||
|
||||
function delay(ms = 1000) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export async function checkIndexExists(destIndex: string) {
|
||||
let resp;
|
||||
let errorMessage;
|
||||
try {
|
||||
resp = await ml.checkIndicesExists({ indices: [destIndex] });
|
||||
} catch (e) {
|
||||
errorMessage = extractErrorMessage(e);
|
||||
}
|
||||
return { resp, errorMessage };
|
||||
}
|
||||
|
||||
export async function retryIndexExistsCheck(destIndex: string): Promise<{
|
||||
success: boolean;
|
||||
indexExists: boolean;
|
||||
errorMessage?: string;
|
||||
}> {
|
||||
let retryCount = 15;
|
||||
|
||||
let resp = await checkIndexExists(destIndex);
|
||||
let indexExists = resp.resp && resp.resp[destIndex] && resp.resp[destIndex].exists;
|
||||
|
||||
while (retryCount > 1 && !indexExists) {
|
||||
retryCount--;
|
||||
await delay(1000);
|
||||
resp = await checkIndexExists(destIndex);
|
||||
indexExists = resp.resp && resp.resp[destIndex] && resp.resp[destIndex].exists;
|
||||
}
|
||||
|
||||
if (indexExists) {
|
||||
return { success: true, indexExists: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
indexExists: false,
|
||||
...(resp.errorMessage !== undefined ? { errorMessage: resp.errorMessage } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export const createKibanaDataView = async (
|
||||
destinationIndex: string,
|
||||
dataViewsService: DataViewsContract,
|
||||
timeFieldName?: string,
|
||||
callback?: (response: FormMessage) => void
|
||||
) => {
|
||||
const response: CreateKibanaDataViewResponse = { success: false, message: '' };
|
||||
const dataViewName = destinationIndex;
|
||||
const exists = await retryIndexExistsCheck(destinationIndex);
|
||||
if (exists?.success === true) {
|
||||
// index exists - create data view
|
||||
if (exists?.indexExists === true) {
|
||||
try {
|
||||
const dataView = await dataViewsService.createAndSave(
|
||||
{
|
||||
title: dataViewName,
|
||||
...(timeFieldName ? { timeFieldName } : {}),
|
||||
},
|
||||
false,
|
||||
true
|
||||
);
|
||||
response.success = true;
|
||||
response.message = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.createDataViewSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Kibana data view {dataViewName} created.',
|
||||
values: { dataViewName },
|
||||
}
|
||||
);
|
||||
response.dataViewId = dataView.id;
|
||||
} catch (e) {
|
||||
// handle data view creation error
|
||||
if (e instanceof DuplicateDataViewError) {
|
||||
response.error = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.duplicateDataViewErrorMessageError',
|
||||
{
|
||||
defaultMessage: 'The data view {dataViewName} already exists.',
|
||||
values: { dataViewName },
|
||||
}
|
||||
);
|
||||
response.message = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.duplicateDataViewErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred creating the Kibana data view:',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response.error = extractErrorMessage(e);
|
||||
response.message = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.createDataViewErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred creating the Kibana data view:',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ran out of retries or there was a problem checking index exists
|
||||
if (exists?.errorMessage) {
|
||||
response.error = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.errorCheckingDestinationIndexDataFrameAnalyticsJob',
|
||||
{
|
||||
defaultMessage: '{errorMessage}',
|
||||
values: { errorMessage: exists.errorMessage },
|
||||
}
|
||||
);
|
||||
response.message = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.errorOccurredCheckingDestinationIndexDataFrameAnalyticsJob',
|
||||
{
|
||||
defaultMessage: 'An error occurred checking destination index exists.',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
response.error = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.destinationIndexNotCreatedForDataFrameAnalyticsJob',
|
||||
{
|
||||
defaultMessage: 'Destination index has not yet been created.',
|
||||
}
|
||||
);
|
||||
response.message = i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.unableToCreateDataViewForDataFrameAnalyticsJob',
|
||||
{
|
||||
defaultMessage: 'Unable to create data view.',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
if (callback !== undefined) {
|
||||
callback({ error: response.error, message: response.message });
|
||||
}
|
||||
return response;
|
||||
};
|
|
@ -9,7 +9,6 @@ import { useReducer } from 'react';
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { DuplicateDataViewError } from '@kbn/data-plugin/public';
|
||||
import { extractErrorMessage } from '@kbn/ml-error-utils';
|
||||
import type { DataFrameAnalyticsConfig } from '@kbn/ml-data-frame-analytics-utils';
|
||||
|
||||
|
@ -19,6 +18,7 @@ import { ml } from '../../../../../services/ml_api_service';
|
|||
|
||||
import { useRefreshAnalyticsList } from '../../../../common';
|
||||
import { extractCloningConfig, isAdvancedConfig } from '../../components/action_clone';
|
||||
import { createKibanaDataView } from '../../../../../components/ml_inference/retry_create_data_view';
|
||||
|
||||
import { ActionDispatchers, ACTION } from './actions';
|
||||
import { reducer } from './reducer';
|
||||
|
@ -49,49 +49,6 @@ export interface CreateAnalyticsStepProps extends CreateAnalyticsFormProps {
|
|||
stepActivated?: boolean;
|
||||
}
|
||||
|
||||
async function checkIndexExists(destinationIndex: string) {
|
||||
let resp;
|
||||
let errorMessage;
|
||||
try {
|
||||
resp = await ml.checkIndicesExists({ indices: [destinationIndex] });
|
||||
} catch (e) {
|
||||
errorMessage = extractErrorMessage(e);
|
||||
}
|
||||
return { resp, errorMessage };
|
||||
}
|
||||
|
||||
async function retryIndexExistsCheck(
|
||||
destinationIndex: string
|
||||
): Promise<{ success: boolean; indexExists: boolean; errorMessage?: string }> {
|
||||
let retryCount = 15;
|
||||
|
||||
let resp = await checkIndexExists(destinationIndex);
|
||||
let indexExists = resp.resp && resp.resp[destinationIndex] && resp.resp[destinationIndex].exists;
|
||||
|
||||
while (retryCount > 1 && !indexExists) {
|
||||
retryCount--;
|
||||
await delay(1000);
|
||||
resp = await checkIndexExists(destinationIndex);
|
||||
indexExists = resp.resp && resp.resp[destinationIndex] && resp.resp[destinationIndex].exists;
|
||||
}
|
||||
|
||||
if (indexExists) {
|
||||
return { success: true, indexExists: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
indexExists: false,
|
||||
...(resp.errorMessage !== undefined ? { errorMessage: resp.errorMessage } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function delay(ms = 1000) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
|
||||
const {
|
||||
services: {
|
||||
|
@ -154,7 +111,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
|
|||
});
|
||||
setIsJobCreated(true);
|
||||
if (createIndexPattern) {
|
||||
createKibanaIndexPattern();
|
||||
createKibanaDataView(destinationIndex, dataViews, form.timeFieldName, addRequestMessage);
|
||||
}
|
||||
refresh();
|
||||
return true;
|
||||
|
@ -172,98 +129,6 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => {
|
|||
}
|
||||
};
|
||||
|
||||
const createKibanaIndexPattern = async () => {
|
||||
const dataViewName = destinationIndex;
|
||||
const exists = await retryIndexExistsCheck(destinationIndex);
|
||||
if (exists?.success === true) {
|
||||
// index exists - create data view
|
||||
if (exists?.indexExists === true) {
|
||||
try {
|
||||
await dataViews.createAndSave(
|
||||
{
|
||||
title: dataViewName,
|
||||
...(form.timeFieldName ? { timeFieldName: form.timeFieldName } : {}),
|
||||
},
|
||||
false,
|
||||
true
|
||||
);
|
||||
addRequestMessage({
|
||||
message: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.createDataViewSuccessMessage',
|
||||
{
|
||||
defaultMessage: 'Kibana data view {dataViewName} created.',
|
||||
values: { dataViewName },
|
||||
}
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
// handle data view creation error
|
||||
if (e instanceof DuplicateDataViewError) {
|
||||
addRequestMessage({
|
||||
error: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.duplicateDataViewErrorMessageError',
|
||||
{
|
||||
defaultMessage: 'The data view {dataViewName} already exists.',
|
||||
values: { dataViewName },
|
||||
}
|
||||
),
|
||||
message: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.duplicateDataViewErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred creating the Kibana data view:',
|
||||
}
|
||||
),
|
||||
});
|
||||
} else {
|
||||
addRequestMessage({
|
||||
error: extractErrorMessage(e),
|
||||
message: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.createDataViewErrorMessage',
|
||||
{
|
||||
defaultMessage: 'An error occurred creating the Kibana data view:',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ran out of retries or there was a problem checking index exists
|
||||
if (exists?.errorMessage) {
|
||||
addRequestMessage({
|
||||
error: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.errorCheckingDestinationIndexDataFrameAnalyticsJob',
|
||||
{
|
||||
defaultMessage: '{errorMessage}',
|
||||
values: { errorMessage: exists.errorMessage },
|
||||
}
|
||||
),
|
||||
message: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.errorOccurredCheckingDestinationIndexDataFrameAnalyticsJob',
|
||||
{
|
||||
defaultMessage: 'An error occurred checking destination index exists.',
|
||||
}
|
||||
),
|
||||
});
|
||||
} else {
|
||||
addRequestMessage({
|
||||
error: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.destinationIndexNotCreatedForDataFrameAnalyticsJob',
|
||||
{
|
||||
defaultMessage: 'Destination index has not yet been created.',
|
||||
}
|
||||
),
|
||||
message: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.create.unableToCreateDataViewForDataFrameAnalyticsJob',
|
||||
{
|
||||
defaultMessage: 'Unable to create data view.',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const prepareFormValidation = async () => {
|
||||
try {
|
||||
// Set the existing data view names.
|
||||
|
|
|
@ -147,7 +147,10 @@ export const canDeleteIndex = async (
|
|||
if (!privilege) {
|
||||
return false;
|
||||
}
|
||||
return privilege.securityDisabled === true || privilege.has_all_requested === true;
|
||||
|
||||
return (
|
||||
privilege.hasPrivileges === undefined || privilege.hasPrivileges.has_all_requested === true
|
||||
);
|
||||
} catch (e) {
|
||||
const error = extractErrorMessage(e);
|
||||
toastNotificationService.displayDangerToast(
|
||||
|
|
|
@ -60,7 +60,7 @@ export function useModelActions({
|
|||
},
|
||||
} = useMlKibana();
|
||||
|
||||
const [canManageIngestPipelines, setCanManageIngestPipelines] = useState(false);
|
||||
const [canManageIngestPipelines, setCanManageIngestPipelines] = useState<boolean>(false);
|
||||
|
||||
const startModelDeploymentDocUrl = docLinks.links.ml.startTrainedModelsDeployment;
|
||||
|
||||
|
@ -83,9 +83,11 @@ export function useModelActions({
|
|||
cluster: ['manage_ingest_pipelines'],
|
||||
})
|
||||
.then((result) => {
|
||||
const canManagePipelines = result.cluster?.manage_ingest_pipelines;
|
||||
if (isMounted) {
|
||||
setCanManageIngestPipelines(canManagePipelines);
|
||||
setCanManageIngestPipelines(
|
||||
result.hasPrivileges === undefined ||
|
||||
result.hasPrivileges.cluster?.manage_ingest_pipelines === true
|
||||
);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
|
|
|
@ -53,6 +53,11 @@ import { savedObjectsApiProvider } from './saved_objects';
|
|||
import { trainedModelsApiProvider } from './trained_models';
|
||||
import { notificationsProvider } from './notifications';
|
||||
|
||||
export interface MlHasPrivilegesResponse {
|
||||
hasPrivileges?: estypes.SecurityHasPrivilegesResponse;
|
||||
upgradeInProgress: boolean;
|
||||
}
|
||||
|
||||
export interface MlInfoResponse {
|
||||
defaults: MlServerDefaults;
|
||||
limits: MlServerLimits;
|
||||
|
@ -408,7 +413,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
|
|||
|
||||
hasPrivileges(obj: any) {
|
||||
const body = JSON.stringify(obj);
|
||||
return httpService.http<any>({
|
||||
return httpService.http<MlHasPrivilegesResponse>({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/_has_privileges`,
|
||||
method: 'POST',
|
||||
body,
|
||||
|
@ -778,6 +783,23 @@ export function mlApiServicesProvider(httpService: HttpService) {
|
|||
});
|
||||
},
|
||||
|
||||
reindexWithPipeline(pipelineName: string, sourceIndex: string, destinationIndex: string) {
|
||||
return httpService.http<estypes.ReindexResponse>({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/reindex_with_pipeline`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
source: {
|
||||
index: sourceIndex,
|
||||
},
|
||||
dest: {
|
||||
index: destinationIndex,
|
||||
pipeline: pipelineName,
|
||||
},
|
||||
}),
|
||||
version: '1',
|
||||
});
|
||||
},
|
||||
|
||||
annotations: annotationsApiProvider(httpService),
|
||||
dataFrameAnalytics: dataFrameAnalyticsApiProvider(httpService),
|
||||
filters: filtersApiProvider(httpService),
|
||||
|
|
|
@ -123,6 +123,7 @@
|
|||
"MlInfo",
|
||||
"MlEsSearch",
|
||||
"MlIndexExists",
|
||||
"MlReindexWithPipeline",
|
||||
"MlSpecificIndexExists",
|
||||
|
||||
"JobAuditMessages",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
import { ML_INTERNAL_BASE_PATH } from '../../common/constants/app';
|
||||
import { wrapError } from '../client/error_wrapper';
|
||||
|
@ -73,7 +74,6 @@ export function systemRoutes(
|
|||
// return that security is disabled and don't call the privilegeCheck endpoint
|
||||
return response.ok({
|
||||
body: {
|
||||
securityDisabled: true,
|
||||
upgradeInProgress,
|
||||
},
|
||||
});
|
||||
|
@ -81,7 +81,7 @@ export function systemRoutes(
|
|||
const body = await asCurrentUser.security.hasPrivileges({ body: request.body });
|
||||
return response.ok({
|
||||
body: {
|
||||
...body,
|
||||
hasPrivileges: body,
|
||||
upgradeInProgress,
|
||||
},
|
||||
});
|
||||
|
@ -277,6 +277,47 @@ export function systemRoutes(
|
|||
return acc;
|
||||
}, {} as Record<string, { exists: boolean }>);
|
||||
|
||||
return response.ok({
|
||||
body: result,
|
||||
});
|
||||
} catch (error) {
|
||||
return response.customError(wrapError(error));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @apiGroup SystemRoutes
|
||||
*
|
||||
* @api {post} /internal/ml/reindex_with_pipeline ES reindex wrapper to reindex with pipeline
|
||||
* @apiName MlReindexWithPipeline
|
||||
*/
|
||||
router.versioned
|
||||
.post({
|
||||
path: `${ML_INTERNAL_BASE_PATH}/reindex_with_pipeline`,
|
||||
access: 'internal',
|
||||
options: {
|
||||
tags: ['access:ml:canCreateTrainedModels'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: '1',
|
||||
validate: {
|
||||
request: {
|
||||
body: schema.any(),
|
||||
},
|
||||
},
|
||||
},
|
||||
routeGuard.fullLicenseAPIGuard(async ({ client, request, response }) => {
|
||||
const reindexRequest = {
|
||||
body: request.body,
|
||||
// Create a task and return task id instead of blocking until complete
|
||||
wait_for_completion: false,
|
||||
} as estypes.ReindexRequest;
|
||||
try {
|
||||
const result = await client.asCurrentUser.reindex(reindexRequest);
|
||||
|
||||
return response.ok({
|
||||
body: result,
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue