Skip to content

Commit dcd90e8

Browse files
[8.x] [Streams 🌊] Schema editor UI (#202372) (#204156)
# Backport This will backport the following commits from `main` to `8.x`: - [[Streams 🌊] Schema editor UI (#202372)](#202372) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Kerry Gallagher","email":"kerry.gallagher@elastic.co"},"sourceCommit":{"committedDate":"2024-12-13T00:39:52Z","message":"[Streams 🌊] Schema editor UI (#202372)\n\n## Summary\r\n\r\nImplements https://github.com/elastic/observability-dev/issues/4133.\r\n\r\nOpening this up for a first pass as the PR is getting quite big. I've\r\nlisted below some things that can be improved in further iterations.\r\n\r\n## High level notes \r\n\r\n- Support for `format` has been added to the field definition\r\n- UI: \r\n - View inherited, mapped, unmapped fields.\r\n - Edit mapped and unmapped fields.\r\n - Map unmapped and unmap mapped fields\r\n - Simulation / preview results\r\n - Filtering\r\n\r\n## Followups\r\n\r\n- Filter dropdowns (on the right):\r\n\r\n![Screenshot 2024-12-05 at 19 31\r\n05](https://github.com/user-attachments/assets/31f22cd6-bf39-49bf-ba1c-1e94e42ebbd6)\r\n\r\n- We could potentially use a separate API for the mapping edits, rather\r\nthan the core edit route, to be more performant, but for now this is\r\nused to create less surface area / deviation.\r\n\r\n- State management is rudimentary right now. It could be improved with a\r\n`useReducer` approach to avoid potential `useState` race conditions, and\r\nthen even something like xstate when things are more concrete. No state\r\nsyncs with the URL currently.\r\n- Due to the lack of URL state syncing the \"Edit in parent stream\"\r\nbutton doesn't navigate with things like a pre-selected field. We could\r\npotentially co-ordinate this between the hooks in the schema editor and\r\ndetail view parent, but it's unneeded complexity at the moment.\r\n\r\n- We could provide a lot more assistance with `format`. We could provide\r\na dropdown with options, and then a toggle to do custom. (Actually, it\r\nlooks like in the refined designs this is a dropdown, so I'll probably\r\nswitch this to a select with predefined options)\r\n\r\n\r\n## Issues\r\n\r\n- There seems to be a bug in the Elasticsearch JS library we use, calls\r\nto `simulate.ingest` don't work as `body` is just set to `undefined`\r\n(chasing this up). You can do the following patch in node_modules just\r\nto get things going (run `yarn start` again):\r\n\r\n![Screenshot 2024-12-05 at 19 52\r\n08](https://github.com/user-attachments/assets/73e8e067-ca36-472f-81fc-f8158653f0c8)\r\n\r\n- Runtime mappings don't seem to work with `match_only_text`:\r\n`mapper_parsing_exception: No handler for type [match_only_text]`\r\n\r\n## Open questions\r\n\r\n- We might freeze changes to the root stream\r\n- A failure on simulation doesn't do a hard block on saving changes. I\r\ndon't think it should, but open to other opinions.\r\n\r\n## Screenshots\r\n\r\n![Screenshot 2024-12-05 at 19 50\r\n33](https://github.com/user-attachments/assets/bcccc223-1c65-47c5-8b06-7c79ed4004e6)\r\n![Screenshot 2024-12-05 at 19 50\r\n42](https://github.com/user-attachments/assets/c9cc24d6-738f-4d9a-a8a9-114403548f69)\r\n![Screenshot 2024-12-05 at 19 50\r\n54](https://github.com/user-attachments/assets/c19e5d37-b194-449e-ba46-6bd7eb0784cd)\r\n![Screenshot 2024-12-05 at 19 41\r\n15](https://github.com/user-attachments/assets/f2b4306c-1d6b-4899-914b-8796151ed2c2)\r\n![Screenshot 2024-12-05 at 19 41\r\n27](https://github.com/user-attachments/assets/effea5bd-b0fb-4c16-a758-a37fa25cb965)\r\n![Screenshot 2024-12-05 at 19 49\r\n53](https://github.com/user-attachments/assets/8f963162-9d7e-4fb2-b702-5af0d9c4f6a7)\r\n![Screenshot 2024-12-05 at 19 50\r\n03](https://github.com/user-attachments/assets/2c34b320-b0b2-4c16-8e78-018b461f7969)\r\n\r\n---------\r\n\r\nCo-authored-by: Joe Reuter <johannes.reuter@elastic.co>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"82f9da1a8e734eb24375a281a09de9adfba65d9a","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:version","v8.18.0","Feature:Streams"],"title":"[Streams 🌊] Schema editor UI","number":202372,"url":"https://github.com/elastic/kibana/pull/202372","mergeCommit":{"message":"[Streams 🌊] Schema editor UI (#202372)\n\n## Summary\r\n\r\nImplements https://github.com/elastic/observability-dev/issues/4133.\r\n\r\nOpening this up for a first pass as the PR is getting quite big. I've\r\nlisted below some things that can be improved in further iterations.\r\n\r\n## High level notes \r\n\r\n- Support for `format` has been added to the field definition\r\n- UI: \r\n - View inherited, mapped, unmapped fields.\r\n - Edit mapped and unmapped fields.\r\n - Map unmapped and unmap mapped fields\r\n - Simulation / preview results\r\n - Filtering\r\n\r\n## Followups\r\n\r\n- Filter dropdowns (on the right):\r\n\r\n![Screenshot 2024-12-05 at 19 31\r\n05](https://github.com/user-attachments/assets/31f22cd6-bf39-49bf-ba1c-1e94e42ebbd6)\r\n\r\n- We could potentially use a separate API for the mapping edits, rather\r\nthan the core edit route, to be more performant, but for now this is\r\nused to create less surface area / deviation.\r\n\r\n- State management is rudimentary right now. It could be improved with a\r\n`useReducer` approach to avoid potential `useState` race conditions, and\r\nthen even something like xstate when things are more concrete. No state\r\nsyncs with the URL currently.\r\n- Due to the lack of URL state syncing the \"Edit in parent stream\"\r\nbutton doesn't navigate with things like a pre-selected field. We could\r\npotentially co-ordinate this between the hooks in the schema editor and\r\ndetail view parent, but it's unneeded complexity at the moment.\r\n\r\n- We could provide a lot more assistance with `format`. We could provide\r\na dropdown with options, and then a toggle to do custom. (Actually, it\r\nlooks like in the refined designs this is a dropdown, so I'll probably\r\nswitch this to a select with predefined options)\r\n\r\n\r\n## Issues\r\n\r\n- There seems to be a bug in the Elasticsearch JS library we use, calls\r\nto `simulate.ingest` don't work as `body` is just set to `undefined`\r\n(chasing this up). You can do the following patch in node_modules just\r\nto get things going (run `yarn start` again):\r\n\r\n![Screenshot 2024-12-05 at 19 52\r\n08](https://github.com/user-attachments/assets/73e8e067-ca36-472f-81fc-f8158653f0c8)\r\n\r\n- Runtime mappings don't seem to work with `match_only_text`:\r\n`mapper_parsing_exception: No handler for type [match_only_text]`\r\n\r\n## Open questions\r\n\r\n- We might freeze changes to the root stream\r\n- A failure on simulation doesn't do a hard block on saving changes. I\r\ndon't think it should, but open to other opinions.\r\n\r\n## Screenshots\r\n\r\n![Screenshot 2024-12-05 at 19 50\r\n33](https://github.com/user-attachments/assets/bcccc223-1c65-47c5-8b06-7c79ed4004e6)\r\n![Screenshot 2024-12-05 at 19 50\r\n42](https://github.com/user-attachments/assets/c9cc24d6-738f-4d9a-a8a9-114403548f69)\r\n![Screenshot 2024-12-05 at 19 50\r\n54](https://github.com/user-attachments/assets/c19e5d37-b194-449e-ba46-6bd7eb0784cd)\r\n![Screenshot 2024-12-05 at 19 41\r\n15](https://github.com/user-attachments/assets/f2b4306c-1d6b-4899-914b-8796151ed2c2)\r\n![Screenshot 2024-12-05 at 19 41\r\n27](https://github.com/user-attachments/assets/effea5bd-b0fb-4c16-a758-a37fa25cb965)\r\n![Screenshot 2024-12-05 at 19 49\r\n53](https://github.com/user-attachments/assets/8f963162-9d7e-4fb2-b702-5af0d9c4f6a7)\r\n![Screenshot 2024-12-05 at 19 50\r\n03](https://github.com/user-attachments/assets/2c34b320-b0b2-4c16-8e78-018b461f7969)\r\n\r\n---------\r\n\r\nCo-authored-by: Joe Reuter <johannes.reuter@elastic.co>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"82f9da1a8e734eb24375a281a09de9adfba65d9a"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/202372","number":202372,"mergeCommit":{"message":"[Streams 🌊] Schema editor UI (#202372)\n\n## Summary\r\n\r\nImplements https://github.com/elastic/observability-dev/issues/4133.\r\n\r\nOpening this up for a first pass as the PR is getting quite big. I've\r\nlisted below some things that can be improved in further iterations.\r\n\r\n## High level notes \r\n\r\n- Support for `format` has been added to the field definition\r\n- UI: \r\n - View inherited, mapped, unmapped fields.\r\n - Edit mapped and unmapped fields.\r\n - Map unmapped and unmap mapped fields\r\n - Simulation / preview results\r\n - Filtering\r\n\r\n## Followups\r\n\r\n- Filter dropdowns (on the right):\r\n\r\n![Screenshot 2024-12-05 at 19 31\r\n05](https://github.com/user-attachments/assets/31f22cd6-bf39-49bf-ba1c-1e94e42ebbd6)\r\n\r\n- We could potentially use a separate API for the mapping edits, rather\r\nthan the core edit route, to be more performant, but for now this is\r\nused to create less surface area / deviation.\r\n\r\n- State management is rudimentary right now. It could be improved with a\r\n`useReducer` approach to avoid potential `useState` race conditions, and\r\nthen even something like xstate when things are more concrete. No state\r\nsyncs with the URL currently.\r\n- Due to the lack of URL state syncing the \"Edit in parent stream\"\r\nbutton doesn't navigate with things like a pre-selected field. We could\r\npotentially co-ordinate this between the hooks in the schema editor and\r\ndetail view parent, but it's unneeded complexity at the moment.\r\n\r\n- We could provide a lot more assistance with `format`. We could provide\r\na dropdown with options, and then a toggle to do custom. (Actually, it\r\nlooks like in the refined designs this is a dropdown, so I'll probably\r\nswitch this to a select with predefined options)\r\n\r\n\r\n## Issues\r\n\r\n- There seems to be a bug in the Elasticsearch JS library we use, calls\r\nto `simulate.ingest` don't work as `body` is just set to `undefined`\r\n(chasing this up). You can do the following patch in node_modules just\r\nto get things going (run `yarn start` again):\r\n\r\n![Screenshot 2024-12-05 at 19 52\r\n08](https://github.com/user-attachments/assets/73e8e067-ca36-472f-81fc-f8158653f0c8)\r\n\r\n- Runtime mappings don't seem to work with `match_only_text`:\r\n`mapper_parsing_exception: No handler for type [match_only_text]`\r\n\r\n## Open questions\r\n\r\n- We might freeze changes to the root stream\r\n- A failure on simulation doesn't do a hard block on saving changes. I\r\ndon't think it should, but open to other opinions.\r\n\r\n## Screenshots\r\n\r\n![Screenshot 2024-12-05 at 19 50\r\n33](https://github.com/user-attachments/assets/bcccc223-1c65-47c5-8b06-7c79ed4004e6)\r\n![Screenshot 2024-12-05 at 19 50\r\n42](https://github.com/user-attachments/assets/c9cc24d6-738f-4d9a-a8a9-114403548f69)\r\n![Screenshot 2024-12-05 at 19 50\r\n54](https://github.com/user-attachments/assets/c19e5d37-b194-449e-ba46-6bd7eb0784cd)\r\n![Screenshot 2024-12-05 at 19 41\r\n15](https://github.com/user-attachments/assets/f2b4306c-1d6b-4899-914b-8796151ed2c2)\r\n![Screenshot 2024-12-05 at 19 41\r\n27](https://github.com/user-attachments/assets/effea5bd-b0fb-4c16-a758-a37fa25cb965)\r\n![Screenshot 2024-12-05 at 19 49\r\n53](https://github.com/user-attachments/assets/8f963162-9d7e-4fb2-b702-5af0d9c4f6a7)\r\n![Screenshot 2024-12-05 at 19 50\r\n03](https://github.com/user-attachments/assets/2c34b320-b0b2-4c16-8e78-018b461f7969)\r\n\r\n---------\r\n\r\nCo-authored-by: Joe Reuter <johannes.reuter@elastic.co>\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"82f9da1a8e734eb24375a281a09de9adfba65d9a"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Kerry Gallagher <kerry.gallagher@elastic.co>
1 parent efdca2d commit dcd90e8

29 files changed

Lines changed: 1929 additions & 20 deletions

File tree

β€Žx-pack/solutions/observability/plugins/streams/common/types.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export type ProcessingDefinition = z.infer<typeof processingDefinitionSchema>;
7373
export const fieldDefinitionSchema = z.object({
7474
name: z.string(),
7575
type: z.enum(['keyword', 'match_only_text', 'long', 'double', 'date', 'boolean', 'ip']),
76+
format: z.optional(z.string()),
7677
});
7778

7879
export type FieldDefinition = z.infer<typeof fieldDefinitionSchema>;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export function generateLayer(
2929
// @timestamp can't ignore malformed dates as it's used for sorting in logsdb
3030
(property as MappingDateProperty).ignore_malformed = false;
3131
}
32+
if (field.type === 'date' && field.format) {
33+
(property as MappingDateProperty).format = field.format;
34+
}
3235
properties[field.name] = property;
3336
});
3437
return {

β€Žx-pack/solutions/observability/plugins/streams/server/routes/index.tsβ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { listStreamsRoute } from './streams/list';
1515
import { readStreamRoute } from './streams/read';
1616
import { resyncStreamsRoute } from './streams/resync';
1717
import { sampleStreamRoute } from './streams/sample';
18+
import { schemaFieldsSimulationRoute } from './streams/schema/fields_simulation';
19+
import { unmappedFieldsRoute } from './streams/schema/unmapped_fields';
1820
import { streamsStatusRoutes } from './streams/settings';
1921

2022
export const streamsRouteRepository = {
@@ -29,6 +31,8 @@ export const streamsRouteRepository = {
2931
...esqlRoutes,
3032
...disableStreamsRoute,
3133
...sampleStreamRoute,
34+
...unmappedFieldsRoute,
35+
...schemaFieldsSimulationRoute,
3236
};
3337

3438
export type StreamsRouteRepository = typeof streamsRouteRepository;
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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 { z } from '@kbn/zod';
9+
import { notFound, internal } from '@hapi/boom';
10+
import { getFlattenedObject } from '@kbn/std';
11+
import { fieldDefinitionSchema } from '../../../../common/types';
12+
import { createServerRoute } from '../../create_server_route';
13+
import { DefinitionNotFound } from '../../../lib/streams/errors';
14+
import { checkReadAccess } from '../../../lib/streams/stream_crud';
15+
16+
const SAMPLE_SIZE = 200;
17+
18+
export const schemaFieldsSimulationRoute = createServerRoute({
19+
endpoint: 'POST /api/streams/{id}/schema/fields_simulation',
20+
options: {
21+
access: 'internal',
22+
},
23+
security: {
24+
authz: {
25+
enabled: false,
26+
reason:
27+
'This API delegates security to the currently logged in user and their Elasticsearch permissions.',
28+
},
29+
},
30+
params: z.object({
31+
path: z.object({ id: z.string() }),
32+
body: z.object({
33+
field_definitions: z.array(fieldDefinitionSchema),
34+
}),
35+
}),
36+
handler: async ({
37+
response,
38+
params,
39+
request,
40+
logger,
41+
getScopedClients,
42+
}): Promise<{
43+
status: 'unknown' | 'success' | 'failure';
44+
simulationError: string | null;
45+
documentsWithRuntimeFieldsApplied: unknown[] | null;
46+
}> => {
47+
try {
48+
const { scopedClusterClient } = await getScopedClients({ request });
49+
50+
const hasAccess = await checkReadAccess({ id: params.path.id, scopedClusterClient });
51+
if (!hasAccess) {
52+
throw new DefinitionNotFound(`Stream definition for ${params.path.id} not found.`);
53+
}
54+
55+
const propertiesForSample = Object.fromEntries(
56+
params.body.field_definitions.map((field) => [field.name, { type: 'keyword' }])
57+
);
58+
59+
const documentSamplesSearchBody = {
60+
// Add keyword runtime mappings so we can pair with exists, this is to attempt to "miss" less documents for the simulation.
61+
runtime_mappings: propertiesForSample,
62+
query: {
63+
bool: {
64+
filter: Object.keys(propertiesForSample).map((field) => ({
65+
exists: { field },
66+
})),
67+
},
68+
},
69+
sort: [
70+
{
71+
'@timestamp': {
72+
order: 'desc',
73+
},
74+
},
75+
],
76+
size: SAMPLE_SIZE,
77+
};
78+
79+
const sampleResults = await scopedClusterClient.asCurrentUser.search({
80+
index: params.path.id,
81+
...documentSamplesSearchBody,
82+
});
83+
84+
if (
85+
(typeof sampleResults.hits.total === 'object' && sampleResults.hits.total?.value === 0) ||
86+
sampleResults.hits.total === 0 ||
87+
!sampleResults.hits.total
88+
) {
89+
return {
90+
status: 'unknown',
91+
simulationError: null,
92+
documentsWithRuntimeFieldsApplied: null,
93+
};
94+
}
95+
96+
const propertiesForSimulation = Object.fromEntries(
97+
params.body.field_definitions.map((field) => [
98+
field.name,
99+
{ type: field.type, ...(field.format ? { format: field.format } : {}) },
100+
])
101+
);
102+
103+
const fieldDefinitionKeys = Object.keys(propertiesForSimulation);
104+
105+
const sampleResultsAsSimulationDocs = sampleResults.hits.hits.map((hit) => ({
106+
_index: params.path.id,
107+
_id: hit._id,
108+
_source: Object.fromEntries(
109+
Object.entries(getFlattenedObject(hit._source as Record<string, unknown>)).filter(
110+
([k]) => fieldDefinitionKeys.includes(k) || k === '@timestamp'
111+
)
112+
),
113+
}));
114+
115+
const simulationBody = {
116+
docs: sampleResultsAsSimulationDocs,
117+
component_template_substitutions: {
118+
[`${params.path.id}@stream.layer`]: {
119+
template: {
120+
mappings: {
121+
dynamic: 'strict',
122+
properties: propertiesForSimulation,
123+
},
124+
},
125+
},
126+
},
127+
};
128+
129+
// TODO: We should be using scopedClusterClient.asCurrentUser.simulate.ingest() but the ES JS lib currently has a bug. The types also aren't available yet, so we use any.
130+
const simulation = (await scopedClusterClient.asCurrentUser.transport.request({
131+
method: 'POST',
132+
path: `_ingest/_simulate`,
133+
body: simulationBody,
134+
})) as any;
135+
136+
const hasErrors = simulation.docs.some((doc: any) => doc.doc.error !== undefined);
137+
138+
if (hasErrors) {
139+
const documentWithError = simulation.docs.find((doc: any) => {
140+
return doc.doc.error !== undefined;
141+
});
142+
143+
return {
144+
status: 'failure',
145+
simulationError: JSON.stringify(
146+
// Use the first error as a representative error
147+
documentWithError.doc.error
148+
),
149+
documentsWithRuntimeFieldsApplied: null,
150+
};
151+
}
152+
153+
// Convert the field definitions to a format that can be used in runtime mappings (match_only_text -> keyword)
154+
const propertiesCompatibleWithRuntimeMappings = Object.fromEntries(
155+
params.body.field_definitions.map((field) => [
156+
field.name,
157+
{
158+
type: field.type === 'match_only_text' ? 'keyword' : field.type,
159+
...(field.format ? { format: field.format } : {}),
160+
},
161+
])
162+
);
163+
164+
const runtimeFieldsSearchBody = {
165+
runtime_mappings: propertiesCompatibleWithRuntimeMappings,
166+
sort: [
167+
{
168+
'@timestamp': {
169+
order: 'desc',
170+
},
171+
},
172+
],
173+
size: SAMPLE_SIZE,
174+
fields: params.body.field_definitions.map((field) => field.name),
175+
_source: false,
176+
};
177+
178+
// This gives us a "fields" representation rather than _source from the simulation
179+
const runtimeFieldsResult = await scopedClusterClient.asCurrentUser.search({
180+
index: params.path.id,
181+
...runtimeFieldsSearchBody,
182+
});
183+
184+
return {
185+
status: 'success',
186+
simulationError: null,
187+
documentsWithRuntimeFieldsApplied: runtimeFieldsResult.hits.hits
188+
.map((hit) => {
189+
if (!hit.fields) {
190+
return {};
191+
}
192+
return Object.keys(hit.fields).reduce<Record<string, unknown>>((acc, field) => {
193+
acc[field] = hit.fields![field][0];
194+
return acc;
195+
}, {});
196+
})
197+
.filter((doc) => Object.keys(doc).length > 0),
198+
};
199+
} catch (e) {
200+
if (e instanceof DefinitionNotFound) {
201+
throw notFound(e);
202+
}
203+
204+
throw internal(e);
205+
}
206+
},
207+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 { z } from '@kbn/zod';
9+
import { internal, notFound } from '@hapi/boom';
10+
import { getFlattenedObject } from '@kbn/std';
11+
import { DefinitionNotFound } from '../../../lib/streams/errors';
12+
import { checkReadAccess, readAncestors, readStream } from '../../../lib/streams/stream_crud';
13+
import { createServerRoute } from '../../create_server_route';
14+
15+
const SAMPLE_SIZE = 500;
16+
17+
export const unmappedFieldsRoute = createServerRoute({
18+
endpoint: 'GET /api/streams/{id}/schema/unmapped_fields',
19+
options: {
20+
access: 'internal',
21+
},
22+
security: {
23+
authz: {
24+
enabled: false,
25+
reason:
26+
'This API delegates security to the currently logged in user and their Elasticsearch permissions.',
27+
},
28+
},
29+
params: z.object({
30+
path: z.object({ id: z.string() }),
31+
}),
32+
handler: async ({
33+
response,
34+
params,
35+
request,
36+
logger,
37+
getScopedClients,
38+
}): Promise<{ unmappedFields: string[] }> => {
39+
try {
40+
const { scopedClusterClient } = await getScopedClients({ request });
41+
42+
const hasAccess = await checkReadAccess({ id: params.path.id, scopedClusterClient });
43+
if (!hasAccess) {
44+
throw new DefinitionNotFound(`Stream definition for ${params.path.id} not found.`);
45+
}
46+
47+
const streamEntity = await readStream({
48+
scopedClusterClient,
49+
id: params.path.id,
50+
});
51+
52+
const searchBody = {
53+
sort: [
54+
{
55+
'@timestamp': {
56+
order: 'desc',
57+
},
58+
},
59+
],
60+
size: SAMPLE_SIZE,
61+
};
62+
63+
const results = await scopedClusterClient.asCurrentUser.search({
64+
index: params.path.id,
65+
...searchBody,
66+
});
67+
68+
const sourceFields = new Set<string>();
69+
70+
results.hits.hits.forEach((hit) => {
71+
Object.keys(getFlattenedObject(hit._source as Record<string, unknown>)).forEach((field) => {
72+
sourceFields.add(field);
73+
});
74+
});
75+
76+
// Mapped fields from the stream's definition and inherited from ancestors
77+
const mappedFields = new Set<string>();
78+
79+
streamEntity.definition.fields.forEach((field) => mappedFields.add(field.name));
80+
81+
const { ancestors } = await readAncestors({
82+
id: params.path.id,
83+
scopedClusterClient,
84+
});
85+
86+
for (const ancestor of ancestors) {
87+
ancestor.definition.fields.forEach((field) => mappedFields.add(field.name));
88+
}
89+
90+
const unmappedFields = Array.from(sourceFields)
91+
.filter((field) => !mappedFields.has(field))
92+
.sort();
93+
94+
return { unmappedFields };
95+
} catch (e) {
96+
if (e instanceof DefinitionNotFound) {
97+
throw notFound(e);
98+
}
99+
100+
throw internal(e);
101+
}
102+
},
103+
});

β€Žx-pack/solutions/observability/plugins/streams/tsconfig.jsonβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@kbn/server-route-repository-client",
3131
"@kbn/observability-utils-server",
3232
"@kbn/observability-utils-common",
33+
"@kbn/std",
3334
"@kbn/safer-lodash-set"
3435
]
3536
}

β€Žx-pack/solutions/observability/plugins/streams_app/public/components/preview_table/index.tsxβ€Ž

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import { EuiDataGrid } from '@elastic/eui';
88
import { i18n } from '@kbn/i18n';
99
import React, { useEffect, useMemo, useState } from 'react';
1010

11-
export function PreviewTable({ documents }: { documents: unknown[] }) {
11+
export function PreviewTable({
12+
documents,
13+
displayColumns,
14+
}: {
15+
documents: unknown[];
16+
displayColumns?: string[];
17+
}) {
1218
const [height, setHeight] = useState('100px');
1319
useEffect(() => {
1420
// set height to 100% after a short delay otherwise it doesn't calculate correctly
@@ -19,6 +25,8 @@ export function PreviewTable({ documents }: { documents: unknown[] }) {
1925
}, []);
2026

2127
const columns = useMemo(() => {
28+
if (displayColumns) return displayColumns;
29+
2230
const cols = new Set<string>();
2331
documents.forEach((doc) => {
2432
if (!doc || typeof doc !== 'object') {
@@ -29,7 +37,7 @@ export function PreviewTable({ documents }: { documents: unknown[] }) {
2937
});
3038
});
3139
return Array.from(cols);
32-
}, [documents]);
40+
}, [displayColumns, documents]);
3341

3442
const gridColumns = useMemo(() => {
3543
return Array.from(columns).map((column) => ({

0 commit comments

Comments
Β (0)