Skip to content

Commit 56acf16

Browse files
[8.x] [Streams] Schema Editor advanced field mapping options (#210667) (#211220)
# Backport This will backport the following commits from `main` to `8.x`: - [[Streams] Schema Editor advanced field mapping options (#210667)](#210667) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Kerry Gallagher","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-02-14T13:52:18Z","message":"[Streams] Schema Editor advanced field mapping options (#210667)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/streams-program/issues/88\r\n\r\nAdds JSON advanced field mapping parameters to the Schema Editor.\r\n\r\nMain questions here are around the types and data structure. In this PR\r\nthese are added as an `additionalProperties` property, but we may also\r\nwant to have all of these parameters top level (like `type` and\r\n`format`). This version makes separating concerns easier in the UI and\r\nseparating \"first class\" options vs advanced options, I could see pros\r\nand cons to both, and also things might be \"upgraded\" from advanced to\r\nfirst class later on. Also an open question on whether the\r\n`MappingProperty` type needs to be explicitly redefined for Zod (ES will\r\nobviously reject anything that isn't supported here).\r\n\r\n![Screenshot 2025-02-12 at 13 01\r\n35](https://github.com/user-attachments/assets/7082fed7-f445-476f-abb7-8f41d693d378)\r\n\r\n\r\n![json_params](https://github.com/user-attachments/assets/521df9bf-cbd4-468a-9385-5787cdd5f906)","sha":"c9dfe9aab1ac2146f3c6ec3810ad35f8b753e6c3","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:version","Feature:Streams","v9.1.0","v8.19.0"],"title":"[Streams] Schema Editor advanced field mapping options","number":210667,"url":"https://github.com/elastic/kibana/pull/210667","mergeCommit":{"message":"[Streams] Schema Editor advanced field mapping options (#210667)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/streams-program/issues/88\r\n\r\nAdds JSON advanced field mapping parameters to the Schema Editor.\r\n\r\nMain questions here are around the types and data structure. In this PR\r\nthese are added as an `additionalProperties` property, but we may also\r\nwant to have all of these parameters top level (like `type` and\r\n`format`). This version makes separating concerns easier in the UI and\r\nseparating \"first class\" options vs advanced options, I could see pros\r\nand cons to both, and also things might be \"upgraded\" from advanced to\r\nfirst class later on. Also an open question on whether the\r\n`MappingProperty` type needs to be explicitly redefined for Zod (ES will\r\nobviously reject anything that isn't supported here).\r\n\r\n![Screenshot 2025-02-12 at 13 01\r\n35](https://github.com/user-attachments/assets/7082fed7-f445-476f-abb7-8f41d693d378)\r\n\r\n\r\n![json_params](https://github.com/user-attachments/assets/521df9bf-cbd4-468a-9385-5787cdd5f906)","sha":"c9dfe9aab1ac2146f3c6ec3810ad35f8b753e6c3"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/210667","number":210667,"mergeCommit":{"message":"[Streams] Schema Editor advanced field mapping options (#210667)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/streams-program/issues/88\r\n\r\nAdds JSON advanced field mapping parameters to the Schema Editor.\r\n\r\nMain questions here are around the types and data structure. In this PR\r\nthese are added as an `additionalProperties` property, but we may also\r\nwant to have all of these parameters top level (like `type` and\r\n`format`). This version makes separating concerns easier in the UI and\r\nseparating \"first class\" options vs advanced options, I could see pros\r\nand cons to both, and also things might be \"upgraded\" from advanced to\r\nfirst class later on. Also an open question on whether the\r\n`MappingProperty` type needs to be explicitly redefined for Zod (ES will\r\nobviously reject anything that isn't supported here).\r\n\r\n![Screenshot 2025-02-12 at 13 01\r\n35](https://github.com/user-attachments/assets/7082fed7-f445-476f-abb7-8f41d693d378)\r\n\r\n\r\n![json_params](https://github.com/user-attachments/assets/521df9bf-cbd4-468a-9385-5787cdd5f906)","sha":"c9dfe9aab1ac2146f3c6ec3810ad35f8b753e6c3"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Kerry Gallagher <[email protected]>
1 parent 4071c0b commit 56acf16

File tree

12 files changed

+192
-25
lines changed

12 files changed

+192
-25
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { omit } from 'lodash';
9+
import { FieldDefinitionConfig } from '../models';
10+
11+
// Parameters that we consider first class and provide a curated experience for
12+
const FIRST_CLASS_PARAMETERS = ['type', 'format'];
13+
14+
// Advanced parameters that we provide a generic experience (JSON blob) for
15+
export const getAdvancedParameters = (fieldName: string, fieldConfig: FieldDefinitionConfig) => {
16+
// @timestamp can't ignore malformed dates as it's used for sorting in logsdb
17+
const additionalOmissions = fieldName === '@timestamp' ? ['ignore_malformed'] : [];
18+
return omit(fieldConfig, FIRST_CLASS_PARAMETERS.concat(additionalOmissions));
19+
};

x-pack/solutions/observability/packages/kbn-streams-schema/src/helpers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from './hierarchy';
1010
export * from './lifecycle';
1111
export * from './condition_fields';
1212
export * from './condition_to_query_dsl';
13+
export * from './field_definition';

x-pack/solutions/observability/packages/kbn-streams-schema/src/models/ingest/fields/index.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* 2.0.
66
*/
77

8+
import { MappingProperty } from '@elastic/elasticsearch/lib/api/types';
89
import { z } from '@kbn/zod';
910
import { NonEmptyString } from '@kbn/zod-helpers';
1011

@@ -20,15 +21,25 @@ export const FIELD_DEFINITION_TYPES = [
2021

2122
export type FieldDefinitionType = (typeof FIELD_DEFINITION_TYPES)[number];
2223

23-
export interface FieldDefinitionConfig {
24+
// We redefine "first class" parameters
25+
export type FieldDefinitionConfig = MappingProperty & {
2426
type: FieldDefinitionType;
2527
format?: string;
26-
}
28+
};
29+
30+
// Parameters that we provide a generic (JSON blob) experience for
31+
export type FieldDefinitionConfigAdvancedParameters = Omit<
32+
FieldDefinitionConfig,
33+
'type' | 'format'
34+
>;
2735

28-
export const fieldDefinitionConfigSchema: z.Schema<FieldDefinitionConfig> = z.object({
29-
type: z.enum(FIELD_DEFINITION_TYPES),
30-
format: z.optional(NonEmptyString),
31-
});
36+
export const fieldDefinitionConfigSchema: z.Schema<FieldDefinitionConfig> = z.intersection(
37+
z.record(z.string(), z.unknown()),
38+
z.object({
39+
type: z.enum(FIELD_DEFINITION_TYPES),
40+
format: z.optional(NonEmptyString),
41+
})
42+
);
3243

3344
export interface FieldDefinition {
3445
[x: string]: FieldDefinitionConfig;
@@ -39,9 +50,9 @@ export const fieldDefinitionSchema: z.Schema<FieldDefinition> = z.record(
3950
fieldDefinitionConfigSchema
4051
);
4152

42-
export interface InheritedFieldDefinitionConfig extends FieldDefinitionConfig {
53+
export type InheritedFieldDefinitionConfig = FieldDefinitionConfig & {
4354
from: string;
44-
}
55+
};
4556

4657
export interface InheritedFieldDefinition {
4758
[x: string]: InheritedFieldDefinitionConfig;
@@ -52,9 +63,9 @@ export const inheritedFieldDefinitionSchema: z.Schema<InheritedFieldDefinition>
5263
z.intersection(fieldDefinitionConfigSchema, z.object({ from: NonEmptyString }))
5364
);
5465

55-
export interface NamedFieldDefinitionConfig extends FieldDefinitionConfig {
66+
export type NamedFieldDefinitionConfig = FieldDefinitionConfig & {
5667
name: string;
57-
}
68+
};
5869

5970
export const namedFieldDefinitionConfigSchema: z.Schema<NamedFieldDefinitionConfig> =
6071
z.intersection(

x-pack/solutions/observability/plugins/streams/server/lib/streams/component_templates/generate_layer.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import {
1010
MappingDateProperty,
1111
MappingProperty,
1212
} from '@elastic/elasticsearch/lib/api/types';
13-
import { WiredStreamDefinition, isDslLifecycle, isIlmLifecycle, isRoot } from '@kbn/streams-schema';
13+
import {
14+
WiredStreamDefinition,
15+
getAdvancedParameters,
16+
isDslLifecycle,
17+
isIlmLifecycle,
18+
isRoot,
19+
} from '@kbn/streams-schema';
1420
import { ASSET_VERSION } from '../../../../common/constants';
1521
import { logsSettings } from './logs_layer';
1622
import { getComponentTemplateName } from './name';
@@ -25,13 +31,21 @@ export function generateLayer(
2531
const property: MappingProperty = {
2632
type: props.type,
2733
};
34+
35+
const advancedParameters = getAdvancedParameters(field, props);
36+
37+
if (Object.keys(advancedParameters).length > 0) {
38+
Object.assign(property, advancedParameters);
39+
}
40+
2841
if (field === '@timestamp') {
2942
// @timestamp can't ignore malformed dates as it's used for sorting in logsdb
3043
(property as MappingDateProperty).ignore_malformed = false;
3144
}
3245
if (props.type === 'date' && props.format) {
3346
(property as MappingDateProperty).format = props.format;
3447
}
48+
3549
properties[field] = property;
3650
});
3751

x-pack/solutions/observability/plugins/streams/server/routes/streams/schema/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ export const schemaFieldsSimulationRoute = createServerRoute({
165165
const propertiesForSimulation = Object.fromEntries(
166166
params.body.field_definitions.map((field) => [
167167
field.name,
168-
{ type: field.type, ...(field.format ? { format: field.format } : {}) },
168+
{
169+
type: field.type,
170+
...(field.format ? { format: field.format } : {}),
171+
},
169172
])
170173
);
171174

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
import {
8+
EuiAccordion,
9+
EuiCodeBlock,
10+
EuiLink,
11+
EuiPanel,
12+
EuiSpacer,
13+
EuiText,
14+
useGeneratedHtmlId,
15+
} from '@elastic/eui';
16+
import { CodeEditor } from '@kbn/code-editor';
17+
import { i18n } from '@kbn/i18n';
18+
import React, { useMemo } from 'react';
19+
import { FormattedMessage } from '@kbn/i18n-react';
20+
import { getAdvancedParameters } from '@kbn/streams-schema';
21+
import { SchemaField } from '../types';
22+
import { useKibana } from '../../../hooks/use_kibana';
23+
24+
const label = i18n.translate('xpack.streams.advancedFieldMappingOptions.label', {
25+
defaultMessage: 'Advanced field mapping parameters',
26+
});
27+
28+
export const AdvancedFieldMappingOptions = ({
29+
field,
30+
onChange,
31+
isEditing,
32+
}: {
33+
field: SchemaField;
34+
onChange: (field: Partial<SchemaField>) => void;
35+
isEditing: boolean;
36+
}) => {
37+
const { core } = useKibana();
38+
39+
const accordionId = useGeneratedHtmlId({ prefix: 'accordionID' });
40+
41+
const jsonOptions = useMemo(
42+
() => (field.additionalParameters ? JSON.stringify(field.additionalParameters, null, 2) : ''),
43+
[field.additionalParameters]
44+
);
45+
46+
return (
47+
<EuiAccordion id={accordionId} buttonContent={label}>
48+
<EuiPanel color="subdued">
49+
<EuiText size="xs">
50+
<FormattedMessage
51+
id="xpack.streams.advancedFieldMappingOptions.docs.label"
52+
defaultMessage="Parameters can be defined with JSON. {link}"
53+
values={{
54+
link: (
55+
<EuiLink
56+
data-test-subj="streamsAppAdvancedFieldMappingOptionsViewDocumentationLink"
57+
href={core.docLinks.links.elasticsearch.docsBase.concat('mapping-params.html')}
58+
target="_blank"
59+
external
60+
>
61+
<FormattedMessage
62+
id="xpack.streams.indexPattern.randomSampling.learnMore"
63+
defaultMessage="View documentation."
64+
/>
65+
</EuiLink>
66+
),
67+
}}
68+
/>
69+
</EuiText>
70+
<EuiSpacer size="s" />
71+
{isEditing ? (
72+
<CodeEditor
73+
height={120}
74+
languageId="json"
75+
value={jsonOptions}
76+
onChange={(value) => {
77+
try {
78+
onChange({
79+
additionalParameters:
80+
value === '' ? undefined : getAdvancedParameters(field.name, JSON.parse(value)),
81+
});
82+
} catch (error: unknown) {
83+
// do nothing
84+
}
85+
}}
86+
/>
87+
) : (
88+
<EuiCodeBlock language="json" isCopyable>
89+
{jsonOptions ?? ''}
90+
</EuiCodeBlock>
91+
)}
92+
</EuiPanel>
93+
</EuiAccordion>
94+
);
95+
};

x-pack/solutions/observability/plugins/streams_app/public/components/schema_editor/flyout/field_summary.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
} from '@elastic/eui';
1717
import React from 'react';
1818
import { i18n } from '@kbn/i18n';
19-
import useToggle from 'react-use/lib/useToggle';
2019
import { WiredStreamDefinition } from '@kbn/streams-schema';
2120
import { useStreamsAppRouter } from '../../../hooks/use_streams_app_router';
2221
import { FieldParent } from '../field_parent';
@@ -56,18 +55,17 @@ const FIELD_SUMMARIES = {
5655

5756
interface FieldSummaryProps {
5857
field: SchemaField;
59-
isEditingByDefault: boolean;
58+
isEditing: boolean;
59+
toggleEditMode: () => void;
6060
stream: WiredStreamDefinition;
6161
onChange: (field: Partial<SchemaField>) => void;
6262
}
6363

6464
export const FieldSummary = (props: FieldSummaryProps) => {
65-
const { field, isEditingByDefault, onChange, stream } = props;
65+
const { field, isEditing, toggleEditMode, onChange, stream } = props;
6666

6767
const router = useStreamsAppRouter();
6868

69-
const [isEditing, toggleEditMode] = useToggle(isEditingByDefault);
70-
7169
return (
7270
<>
7371
<EuiFlexGroup direction="column" gutterSize="s">

x-pack/solutions/observability/plugins/streams_app/public/components/schema_editor/flyout/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ import React, { useReducer } from 'react';
1919
import { i18n } from '@kbn/i18n';
2020
import { WiredStreamDefinition } from '@kbn/streams-schema';
2121
import useAsyncFn from 'react-use/lib/useAsyncFn';
22+
import useToggle from 'react-use/lib/useToggle';
2223
import { SamplePreviewTable } from './sample_preview_table';
2324
import { FieldSummary } from './field_summary';
2425
import { SchemaField } from '../types';
26+
import { AdvancedFieldMappingOptions } from './advanced_field_mapping_options';
2527

2628
export interface SchemaEditorFlyoutProps {
2729
field: SchemaField;
@@ -40,6 +42,8 @@ export const SchemaEditorFlyout = ({
4042
isEditingByDefault = false,
4143
withFieldSimulation = false,
4244
}: SchemaEditorFlyoutProps) => {
45+
const [isEditing, toggleEditMode] = useToggle(isEditingByDefault);
46+
4347
const [nextField, setNextField] = useReducer(
4448
(prev: SchemaField, updated: Partial<SchemaField>) =>
4549
({
@@ -66,10 +70,16 @@ export const SchemaEditorFlyout = ({
6670
<EuiFlexGroup direction="column">
6771
<FieldSummary
6872
field={nextField}
69-
isEditingByDefault={isEditingByDefault}
73+
isEditing={isEditing}
74+
toggleEditMode={toggleEditMode}
7075
onChange={setNextField}
7176
stream={stream}
7277
/>
78+
<AdvancedFieldMappingOptions
79+
field={nextField}
80+
onChange={setNextField}
81+
isEditing={isEditing}
82+
/>
7383
{withFieldSimulation && (
7484
<EuiFlexItem grow={false}>
7585
<SamplePreviewTable stream={stream} nextField={nextField} />

x-pack/solutions/observability/plugins/streams_app/public/components/schema_editor/hooks/use_schema_fields.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77

88
import { i18n } from '@kbn/i18n';
99
import { useAbortController } from '@kbn/observability-utils-browser/hooks/use_abort_controller';
10-
import { NamedFieldDefinitionConfig, WiredStreamGetResponse } from '@kbn/streams-schema';
10+
import {
11+
NamedFieldDefinitionConfig,
12+
WiredStreamGetResponse,
13+
getAdvancedParameters,
14+
} from '@kbn/streams-schema';
1115
import { isEqual, omit } from 'lodash';
1216
import { useMemo, useCallback } from 'react';
1317
import { useStreamsAppFetch } from '../../../hooks/use_streams_app_fetch';
@@ -59,6 +63,7 @@ export const useSchemaFields = ({
5963
name,
6064
type: field.type,
6165
format: field.format,
66+
additionalParameters: getAdvancedParameters(name, field),
6267
parent: field.from,
6368
status: 'inherited',
6469
})
@@ -69,6 +74,7 @@ export const useSchemaFields = ({
6974
name,
7075
type: field.type,
7176
format: field.format,
77+
additionalParameters: getAdvancedParameters(name, field),
7278
parent: definition.stream.name,
7379
status: 'mapped',
7480
})
@@ -132,12 +138,12 @@ export const useSchemaFields = ({
132138

133139
refreshFields();
134140
} catch (error) {
135-
toasts.addError(error, {
141+
toasts.addError(new Error(error.body.message), {
136142
title: i18n.translate('xpack.streams.streamDetailSchemaEditorEditErrorToast', {
137143
defaultMessage: 'Something went wrong editing the {field} field',
138144
values: { field: field.name },
139145
}),
140-
toastMessage: error.message,
146+
toastMessage: error.body.message,
141147
toastLifeTimeMs: 5000,
142148
});
143149
}

x-pack/solutions/observability/plugins/streams_app/public/components/schema_editor/schema_editor_table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const createCellRenderer =
107107
return <FieldStatusBadge status={status} />;
108108
}
109109

110-
return field[columnId as keyof SchemaField] || EMPTY_CONTENT;
110+
return <>{field[columnId as keyof SchemaField] || EMPTY_CONTENT}</>;
111111
};
112112

113113
const createFieldActionsCellRenderer = (fields: SchemaField[]): EuiDataGridControlColumn => ({

0 commit comments

Comments
 (0)