Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Transforms: Support to set destination ingest pipeline. #123911

Merged
merged 10 commits into from
Feb 1, 2022
5 changes: 5 additions & 0 deletions x-pack/plugins/transform/common/api_schemas/type_guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';

import type { EsIndex } from '../types/es_index';
import type { EsIngestPipeline } from '../types/es_ingest_pipeline';
import { isPopulatedObject } from '../shared_imports';

// To be able to use the type guards on the client side, we need to make sure we don't import
Expand Down Expand Up @@ -60,6 +61,10 @@ export const isEsIndices = (arg: unknown): arg is EsIndex[] => {
return Array.isArray(arg);
};

export const isEsIngestPipelines = (arg: unknown): arg is EsIngestPipeline[] => {
return Array.isArray(arg) && arg.every((d) => isPopulatedObject(d, ['name']));
};

export const isEsSearchResponse = (arg: unknown): arg is estypes.SearchResponse => {
return isPopulatedObject(arg, ['hits']);
};
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/transform/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const API_BASE_PATH = '/api/transform/';
// In the UI additional privileges are required:
// - kibana_admin (builtin)
// - dest index: monitor (applied to df-*)
// - cluster: monitor
// - cluster: monitor, read_pipeline
//
// Note that users with kibana_admin can see all Kibana data views and saved searches
// in the source selection modal when creating a transform, but the wizard will trigger
Expand All @@ -69,7 +69,7 @@ export const APP_GET_TRANSFORM_CLUSTER_PRIVILEGES = [
'cluster.cluster:monitor/transform/stats/get',
];

// Equivalent of capabilities.canGetTransform
// Equivalent of capabilities.canCreateTransform
export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [
'cluster.cluster:monitor/transform/get',
'cluster.cluster:monitor/transform/stats/get',
Expand Down
13 changes: 13 additions & 0 deletions x-pack/plugins/transform/common/types/es_ingest_pipeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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.
*/

// This interface doesn't cover a full ingest pipeline spec,
// just what's necessary to make it work in the transform creation wizard.
// The full interface can be found in x-pack/plugins/ingest_pipelines/common/types.ts
export interface EsIngestPipeline {
name: string;
}
6 changes: 4 additions & 2 deletions x-pack/plugins/transform/public/app/common/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ describe('Transform: Common', () => {
transformSettingsMaxPageSearchSize: 100,
transformSettingsDocsPerSecond: 400,
destinationIndex: 'the-destination-index',
destinationIngestPipeline: 'the-destination-ingest-pipeline',
touched: true,
valid: true,
};
Expand All @@ -249,7 +250,7 @@ describe('Transform: Common', () => {

expect(request).toEqual({
description: 'the-transform-description',
dest: { index: 'the-destination-index' },
dest: { index: 'the-destination-index', pipeline: 'the-destination-ingest-pipeline' },
frequency: '1m',
pivot: {
aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } },
Expand Down Expand Up @@ -315,6 +316,7 @@ describe('Transform: Common', () => {
transformSettingsMaxPageSearchSize: 100,
transformSettingsDocsPerSecond: 400,
destinationIndex: 'the-destination-index',
destinationIngestPipeline: 'the-destination-ingest-pipeline',
touched: true,
valid: true,
};
Expand All @@ -327,7 +329,7 @@ describe('Transform: Common', () => {

expect(request).toEqual({
description: 'the-transform-description',
dest: { index: 'the-destination-index' },
dest: { index: 'the-destination-index', pipeline: 'the-destination-ingest-pipeline' },
frequency: '1m',
pivot: {
aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } },
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/transform/public/app/common/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ export const getCreateTransformRequestBody = (
: {}),
dest: {
index: transformDetailsState.destinationIndex,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider adding the pipeline to the details summary?

image

// conditionally add optional ingest pipeline
...(transformDetailsState.destinationIngestPipeline !== ''
? { pipeline: transformDetailsState.destinationIngestPipeline }
: {}),
},
// conditionally add continuous mode config
...(transformDetailsState.isContinuousModeEnabled
Expand Down
14 changes: 11 additions & 3 deletions x-pack/plugins/transform/public/app/hooks/use_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useMemo } from 'react';

import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';

import { HttpFetchError } from 'kibana/public';
import type { HttpFetchError } from 'kibana/public';

import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public';

Expand Down Expand Up @@ -43,9 +43,10 @@ import type {
PostTransformsUpdateResponseSchema,
} from '../../../common/api_schemas/update_transforms';
import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats';
import { TransformId } from '../../../common/types/transform';
import type { TransformId } from '../../../common/types/transform';
import { API_BASE_PATH } from '../../../common/constants';
import { EsIndex } from '../../../common/types/es_index';
import type { EsIndex } from '../../../common/types/es_index';
import type { EsIngestPipeline } from '../../../common/types/es_ingest_pipeline';

import { useAppDependencies } from '../app_dependencies';

Expand Down Expand Up @@ -202,6 +203,13 @@ export const useApi = () => {
return e;
}
},
async getEsIngestPipelines(): Promise<EsIngestPipeline[] | HttpFetchError> {
try {
return await http.get('/api/ingest_pipelines');
} catch (e) {
return e;
}
},
async getHistogramsForFields(
indexPatternTitle: string,
fields: FieldHistogramRequestConfig[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
import type { TransformConfigUnion, TransformId } from '../../../../../../common/types/transform';

export type EsIndexName = string;
export type EsIngestPipelineName = string;
export type IndexPatternTitle = string;

export interface StepDetailsExposedState {
continuousModeDateField: string;
continuousModeDelay: string;
createIndexPattern: boolean;
destinationIndex: EsIndexName;
destinationIngestPipeline: EsIngestPipelineName;
isContinuousModeEnabled: boolean;
isRetentionPolicyEnabled: boolean;
retentionPolicyDateField: string;
Expand Down Expand Up @@ -48,6 +50,7 @@ export function getDefaultStepDetailsState(): StepDetailsExposedState {
transformFrequency: defaultTransformFrequency,
transformSettingsMaxPageSearchSize: defaultTransformSettingsMaxPageSearchSize,
destinationIndex: '',
destinationIngestPipeline: '',
touched: false,
valid: false,
indexPatternTimeField: undefined,
Expand All @@ -73,6 +76,11 @@ export function applyTransformConfigToDetailsState(
state.transformDescription = transformConfig.description;
}

// Ingest Pipeline
if (transformConfig.dest.pipeline !== undefined) {
state.destinationIngestPipeline = transformConfig.dest.pipeline;
}

// Frequency
if (transformConfig.frequency !== undefined) {
state.transformFrequency = transformConfig.frequency;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';

import {
EuiAccordion,
EuiComboBox,
EuiLink,
EuiSwitch,
EuiFieldText,
Expand All @@ -28,6 +29,7 @@ import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_reac

import {
isEsIndices,
isEsIngestPipelines,
isPostTransformsPreviewResponseSchema,
} from '../../../../../../common/api_schemas/type_guards';
import { TransformId } from '../../../../../../common/types/transform';
Expand Down Expand Up @@ -82,8 +84,12 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
const [destinationIndex, setDestinationIndex] = useState<EsIndexName>(
defaults.destinationIndex
);
const [destinationIngestPipeline, setDestinationIngestPipeline] = useState<string>(
defaults.destinationIngestPipeline
);
const [transformIds, setTransformIds] = useState<TransformId[]>([]);
const [indexNames, setIndexNames] = useState<EsIndexName[]>([]);
const [ingestPipelineNames, setIngestPipelineNames] = useState<string[]>([]);

const canCreateDataView = useMemo(
() =>
Expand Down Expand Up @@ -180,7 +186,10 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
setTransformIds(resp.transforms.map((transform) => transform.id));
}

const indices = await api.getEsIndices();
const [indices, ingestPipelines] = await Promise.all([
api.getEsIndices(),
api.getEsIngestPipelines(),
]);

if (isEsIndices(indices)) {
setIndexNames(indices.map((index) => index.name));
Expand All @@ -200,6 +209,24 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
});
}

if (isEsIngestPipelines(ingestPipelines)) {
setIngestPipelineNames(ingestPipelines.map(({ name }) => name));
} else {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIngestPipelines', {
defaultMessage: 'An error occurred getting the existing ingest pipeline names:',
}),
text: toMountPoint(
<ToastNotificationText
overlays={overlays}
theme={theme}
text={getErrorMessage(ingestPipelines)}
/>,
{ theme$: theme.theme$ }
),
});
}

try {
setIndexPatternTitles(await deps.data.indexPatterns.getTitles());
} catch (e) {
Expand Down Expand Up @@ -311,6 +338,7 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
transformSettingsMaxPageSearchSize,
transformSettingsDocsPerSecond,
destinationIndex,
destinationIngestPipeline,
touched: true,
valid,
indexPatternTimeField,
Expand All @@ -331,6 +359,7 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
transformFrequency,
transformSettingsMaxPageSearchSize,
destinationIndex,
destinationIngestPipeline,
valid,
indexPatternTimeField,
/* eslint-enable react-hooks/exhaustive-deps */
Expand Down Expand Up @@ -443,6 +472,37 @@ export const StepDetailsForm: FC<StepDetailsFormProps> = React.memo(
/>
</EuiFormRow>

{ingestPipelineNames.length > 0 && (
<EuiFormRow
label={i18n.translate(
'xpack.transform.stepDetailsForm.destinationIngestPipelineLabel',
{
defaultMessage: 'Destination ingest pipeline',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, if I add a pipeline, hit Next, and then Previous, the pipeline is being forgotten. It also isn't being saved in the config.

}
)}
>
<EuiComboBox
data-test-subj="transformDestinationPipelineSelect"
aria-label={i18n.translate(
'xpack.transform.stepDetailsForm.destinationIngestPipelineAriaLabel',
{
defaultMessage: 'Select an ingest pipeline',
}
)}
placeholder={i18n.translate(
'xpack.transform.stepDetailsForm.destinationIngestPipelineComboBoxPlaceholder',
{
defaultMessage: 'Select an ingest pipeline',
}
)}
singleSelection={{ asPlainText: true }}
options={ingestPipelineNames.map((label: string) => ({ label }))}
selectedOptions={[{ label: destinationIngestPipeline }]}
onChange={(options) => setDestinationIngestPipeline(options[0]?.label ?? '')}
/>
</EuiFormRow>
)}

{stepDefineState.transformFunction === TRANSFORM_FUNCTION.LATEST ? (
<>
<EuiSpacer size={'m'} />
Expand Down
Loading