Skip to content

Commit

Permalink
[ML] Transforms: Create optional data view on server side as part of …
Browse files Browse the repository at this point in the history
…transform create API call. (elastic#160513)

When creating a transform, users have the option to create a Kibana data
view as part of the creation process. So far we've done that as a
separate call from the client side. This PR adds query param options to
the API endpoint to optionally create the data view server side.
API integration tests have been extended to test the new feature.
  • Loading branch information
walterra authored Nov 16, 2023
1 parent 4f2d1db commit 8f129de
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 138 deletions.
19 changes: 16 additions & 3 deletions x-pack/plugins/transform/common/api_schemas/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,29 @@ export interface PutTransformsPivotRequestSchema

export type PutTransformsLatestRequestSchema = Omit<PutTransformsRequestSchema, 'pivot'>;

export const putTransformsQuerySchema = schema.object({
createDataView: schema.boolean({ defaultValue: false }),
timeFieldName: schema.maybe(schema.string()),
});

export type PutTransformsQuerySchema = TypeOf<typeof putTransformsQuerySchema>;

interface TransformCreated {
transform: TransformId;
}
interface TransformCreatedError {
id: TransformId;
interface DataViewCreated {
id: string;
}
interface CreatedError {
id: string;
error: any;
}

export interface PutTransformsResponseSchema {
transformsCreated: TransformCreated[];
errors: TransformCreatedError[];
dataViewsCreated: DataViewCreated[];
dataViewsErrors: CreatedError[];
errors: CreatedError[];
}

// POST transforms/_preview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { useRefreshTransformList } from './use_refresh_transform_list';
interface CreateTransformArgs {
transformId: TransformId;
transformConfig: PutTransformsRequestSchema;
createDataView: boolean;
timeFieldName?: string;
}

export const useCreateTransform = () => {
Expand All @@ -48,10 +50,16 @@ export const useCreateTransform = () => {
}

const mutation = useMutation({
mutationFn: ({ transformId, transformConfig }: CreateTransformArgs) => {
mutationFn: ({
transformId,
transformConfig,
createDataView = false,
timeFieldName,
}: CreateTransformArgs) => {
return http.put<PutTransformsResponseSchema>(
addInternalBasePath(`transforms/${transformId}`),
{
query: { createDataView, timeFieldName },
body: JSON.stringify(transformConfig),
version: '1',
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ import { toMountPoint } from '@kbn/react-kibana-mount';

import { DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common';

import { DuplicateDataViewError } from '@kbn/data-plugin/public';
import type { RuntimeField } from '@kbn/data-views-plugin/common';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants';

import { getErrorMessage } from '../../../../../../common/utils/errors';
Expand All @@ -44,7 +41,7 @@ import {
PutTransformsLatestRequestSchema,
PutTransformsPivotRequestSchema,
} from '../../../../../../common/api_schemas/transforms';
import { isContinuousTransform, isLatestTransform } from '../../../../../../common/types/transform';
import { isContinuousTransform } from '../../../../../../common/types/transform';
import { TransformAlertFlyout } from '../../../../../alerting/transform_alerting_flyout';

export interface StepDetailsExposedState {
Expand Down Expand Up @@ -87,8 +84,7 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
const [discoverLink, setDiscoverLink] = useState<string>();

const toastNotifications = useToastNotifications();
const { application, data, i18n: i18nStart, share, theme } = useAppDependencies();
const dataViews = data.dataViews;
const { application, i18n: i18nStart, share, theme } = useAppDependencies();
const isDiscoverAvailable = application.capabilities.discover?.show ?? false;

useEffect(() => {
Expand Down Expand Up @@ -128,13 +124,13 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
setLoading(true);

createTransform(
{ transformId, transformConfig },
{ transformId, transformConfig, createDataView, timeFieldName },
{
onError: () => setCreated(false),
onSuccess: () => {
onSuccess: (resp) => {
setCreated(true);
if (createDataView) {
createKibanaDataView();
if (resp.dataViewsCreated.length === 1) {
setDataViewId(resp.dataViewsCreated[0].id);
}
if (startAfterCreation) {
startTransform();
Expand All @@ -155,57 +151,6 @@ export const StepCreateForm: FC<StepCreateFormProps> = React.memo(
});
}

const createKibanaDataView = async () => {
setLoading(true);
const dataViewName = transformConfig.dest.index;
const runtimeMappings = transformConfig.source.runtime_mappings as Record<
string,
RuntimeField
>;

try {
const newDataView = await dataViews.createAndSave(
{
title: dataViewName,
timeFieldName,
...(isPopulatedObject(runtimeMappings) && isLatestTransform(transformConfig)
? { runtimeFieldMap: runtimeMappings }
: {}),
allowNoIndex: true,
},
false,
true
);

setDataViewId(newDataView.id);
setLoading(false);
return true;
} catch (e) {
if (e instanceof DuplicateDataViewError) {
toastNotifications.addDanger(
i18n.translate('xpack.transform.stepCreateForm.duplicateDataViewErrorMessage', {
defaultMessage:
'An error occurred creating the Kibana data view {dataViewName}: The data view already exists.',
values: { dataViewName },
})
);
} else {
toastNotifications.addDanger({
title: i18n.translate('xpack.transform.stepCreateForm.createDataViewErrorMessage', {
defaultMessage: 'An error occurred creating the Kibana data view {dataViewName}:',
values: { dataViewName },
}),
text: toMountPoint(<ToastNotificationText text={getErrorMessage(e)} />, {
theme,
i18n: i18nStart,
}),
});
setLoading(false);
return false;
}
}
};

const isBatchTransform = typeof transformConfig.sync === 'undefined';

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@ import {
} from '../../../../common/api_schemas/common';
import {
putTransformsRequestSchema,
putTransformsQuerySchema,
type PutTransformsRequestSchema,
type PutTransformsQuerySchema,
} from '../../../../common/api_schemas/transforms';
import { addInternalBasePath } from '../../../../common/constants';

import type { RouteDependencies } from '../../../types';

import { routeHandler } from './route_handler';
import { routeHandlerFactory } from './route_handler_factory';

export function registerRoute(routeDependencies: RouteDependencies) {
const { router, license } = routeDependencies;

export function registerRoute({ router, license }: RouteDependencies) {
/**
* @apiGroup Transforms
*
Expand All @@ -28,25 +32,29 @@ export function registerRoute({ router, license }: RouteDependencies) {
* @apiDescription Creates a transform
*
* @apiSchema (params) transformIdParamSchema
* @apiSchema (query) transformIdParamSchema
* @apiSchema (body) putTransformsRequestSchema
*/
router.versioned
.put({
path: addInternalBasePath('transforms/{transformId}'),
access: 'internal',
})
.addVersion<TransformIdParamSchema, undefined, PutTransformsRequestSchema>(
.addVersion<TransformIdParamSchema, PutTransformsQuerySchema, PutTransformsRequestSchema>(
{
version: '1',
validate: {
request: {
params: transformIdParamSchema,
query: putTransformsQuerySchema,
body: putTransformsRequestSchema,
},
},
},
license.guardApiRoute<TransformIdParamSchema, undefined, PutTransformsRequestSchema>(
routeHandler
)
license.guardApiRoute<
TransformIdParamSchema,
PutTransformsQuerySchema,
PutTransformsRequestSchema
>(routeHandlerFactory(routeDependencies))
);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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 { RequestHandler } from '@kbn/core/server';
import type { RuntimeField } from '@kbn/data-views-plugin/common';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';

import type { TransformIdParamSchema } from '../../../../common/api_schemas/common';
import type {
PutTransformsRequestSchema,
PutTransformsQuerySchema,
PutTransformsResponseSchema,
} from '../../../../common/api_schemas/transforms';
import { isLatestTransform } from '../../../../common/types/transform';

import type { RouteDependencies } from '../../../types';
import type { TransformRequestHandlerContext } from '../../../services/license';

import { wrapEsError } from '../../utils/error_utils';

export const routeHandlerFactory: (
routeDependencies: RouteDependencies
) => RequestHandler<
TransformIdParamSchema,
PutTransformsQuerySchema,
PutTransformsRequestSchema,
TransformRequestHandlerContext
> = (routeDependencies) => async (ctx, req, res) => {
const { coreStart, dataViews } = routeDependencies;
const { transformId } = req.params;
const { createDataView, timeFieldName } = req.query;

const response: PutTransformsResponseSchema = {
dataViewsCreated: [],
dataViewsErrors: [],
transformsCreated: [],
errors: [],
};

const esClient = (await ctx.core).elasticsearch.client;

try {
const resp = await esClient.asCurrentUser.transform.putTransform({
// @ts-expect-error @elastic/elasticsearch group_by is expected to be optional in TransformPivot
body: req.body,
transform_id: transformId,
});

if (resp.acknowledged) {
response.transformsCreated.push({ transform: transformId });
} else {
response.errors.push({
id: transformId,
error: wrapEsError(resp),
});
}
} catch (e) {
response.errors.push({
id: transformId,
error: wrapEsError(e),
});
}

if (createDataView) {
const { savedObjects, elasticsearch } = coreStart;
const dataViewsService = await dataViews.dataViewsServiceFactory(
savedObjects.getScopedClient(req),
elasticsearch.client.asScoped(req).asCurrentUser,
req
);

const dataViewName = req.body.dest.index;
const runtimeMappings = req.body.source.runtime_mappings as Record<string, RuntimeField>;

try {
const dataViewsResp = await dataViewsService.createAndSave(
{
title: dataViewName,
timeFieldName,
// Adding runtime mappings for transforms of type latest only here
// since only they will want to replicate the source index mapping.
// Pivot type transforms have index mappings that cannot be
// inferred from the source index.
...(isPopulatedObject(runtimeMappings) && isLatestTransform(req.body)
? { runtimeFieldMap: runtimeMappings }
: {}),
allowNoIndex: true,
},
false,
true
);

if (dataViewsResp.id) {
response.dataViewsCreated = [{ id: dataViewsResp.id }];
}
} catch (error) {
// For the error id we use the transform id
// because in case of an error we don't get a data view id.
response.dataViewsErrors = [{ id: transformId, error }];
}
}

return res.ok({ body: response });
};
2 changes: 0 additions & 2 deletions x-pack/plugins/translations/translations/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -40449,9 +40449,7 @@
"xpack.transform.forceDeleteTransformMessage": "Supprimer {count} {count, plural, one {transformation} many {transformations} other {transformations}}",
"xpack.transform.managedTransformsWarningCallout": "{count, plural, one {Cette transformation} many {Au moins l''une de ces transformations} other {Au moins l''une de ces transformations}} est préconfigurée par Elastic. Le fait de {action} {count, plural, one {la} many {les} other {les}} avec une heure de fin spécifique peut avoir un impact sur d'autres éléments du produit.",
"xpack.transform.multiTransformActionsMenu.transformsCount": "Sélection effectuée de {count} {count, plural, one {transformation} many {transformations} other {transformations}}",
"xpack.transform.stepCreateForm.createDataViewErrorMessage": "Une erreur est survenue lors de la création de la vue de données Kibana {dataViewName} :",
"xpack.transform.stepCreateForm.createTransformErrorMessage": "Une erreur s'est produite lors de la création de la transformation {transformId} :",
"xpack.transform.stepCreateForm.duplicateDataViewErrorMessage": "Une erreur est survenue lors de la création de la vue de données Kibana {dataViewName} : La vue de données existe déjà.",
"xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar": "Requête non valide : {queryErrorMessage}",
"xpack.transform.stepDefineForm.queryPlaceholderKql": "Par exemple, {example}",
"xpack.transform.stepDefineForm.queryPlaceholderLucene": "Par exemple, {example}",
Expand Down
Loading

0 comments on commit 8f129de

Please sign in to comment.