[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:
Melissa Alvarez 2023-09-19 16:55:34 -06:00 committed by GitHub
parent 2234237cd4
commit e5cd6fb493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 707 additions and 151 deletions

View file

@ -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`,

View file

@ -389,6 +389,7 @@ export interface DocLinks {
searchPreference: string;
securityApis: string;
simulatePipeline: string;
tasks: string;
timeUnits: string;
unfreezeIndex: string;
updateTransform: string;

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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 ? (
<>

View file

@ -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;
};

View file

@ -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.

View file

@ -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(

View file

@ -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 () => {

View file

@ -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),

View file

@ -123,6 +123,7 @@
"MlInfo",
"MlEsSearch",
"MlIndexExists",
"MlReindexWithPipeline",
"MlSpecificIndexExists",
"JobAuditMessages",

View file

@ -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,
});