From 4495e745ebd807ee6103b87202e1d50dc91a67d9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 4 Dec 2024 09:40:18 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=8A=20Streams=20routing=20UI=20(#20142?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First iteration of the streams partitioning/routing UI: Screenshot 2024-11-27 at 21 31 13 ## Changes ### Unified search bar An empty `FlexItem` would be rendered even if there is no query input which will stick around as a 320px wide empty element. This change avoids rendering this empty wrapper. ### Streams API * Add logic to extract fields (and their types) from a condition * Add logic to turn the streams condition dialect into query dsl * Add dot expander to the root logs pipeline to normalize incoming docs for consistent access in painless and querydsl (this is a stopgap solution) * Add some additional validation to incoming definitions * Add a sample API which takes a stream and a condition, turns the condition into querydsl and searches for a bunch of docs and returns their sources (also sets runtime mappings so non-mapped fields can be used in the condition) ### Streams app * Adjust page height based on whether the new nav is enabled * Add a condition editor to show and change conditions (can also be reused in other UIs) * Add UI to show child routing conditions, change existing routings, partition new streams and delete streams as well --------- Co-authored-by: Chris Cowan Co-authored-by: Dario Gieselaar Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../query_string_input/query_bar_top_row.tsx | 101 +-- x-pack/plugins/streams/common/index.ts | 2 +- x-pack/plugins/streams/common/types.ts | 26 +- .../lib/streams/helpers/condition_fields.ts | 85 ++ .../lib/streams/helpers/condition_guards.ts | 29 + .../streams/helpers/condition_to_painless.ts | 20 +- .../streams/helpers/condition_to_query_dsl.ts | 63 ++ .../ingest_pipelines/logs_default_pipeline.ts | 5 + .../streams/server/lib/streams/stream_crud.ts | 13 +- x-pack/plugins/streams/server/routes/index.ts | 2 + .../streams/server/routes/streams/edit.ts | 8 +- .../streams/server/routes/streams/fork.ts | 3 + .../streams/server/routes/streams/read.ts | 8 +- .../streams/server/routes/streams/sample.ts | 105 +++ .../get_mock_streams_app_context.tsx | 2 + x-pack/plugins/streams_app/kibana.jsonc | 3 +- .../public/components/assets/illustration.png | Bin 0 -> 58425 bytes .../components/condition_editor/index.tsx | 297 +++++++ .../components/entity_detail_view/index.tsx | 9 +- .../public/components/nested_view/index.tsx | 40 + .../public/components/preview_table/index.tsx | 71 ++ .../components/stream_delete_modal/index.tsx | 121 +++ .../stream_detail_management/index.tsx | 22 +- .../stream_detail_routing/index.tsx | 773 +++++++++++++++++- .../components/stream_detail_view/index.tsx | 8 +- .../streams_app_page_body/index.tsx | 1 + .../streams_app_page_template/index.tsx | 17 +- .../streams_app_search_bar/index.tsx | 57 +- .../streams_app/public/routes/config.tsx | 9 + x-pack/plugins/streams_app/public/types.ts | 2 + .../streams_app/public/util/use_debounce.ts | 23 + x-pack/plugins/streams_app/tsconfig.json | 3 + 32 files changed, 1787 insertions(+), 141 deletions(-) create mode 100644 x-pack/plugins/streams/server/lib/streams/helpers/condition_fields.ts create mode 100644 x-pack/plugins/streams/server/lib/streams/helpers/condition_guards.ts create mode 100644 x-pack/plugins/streams/server/lib/streams/helpers/condition_to_query_dsl.ts create mode 100644 x-pack/plugins/streams/server/routes/streams/sample.ts create mode 100644 x-pack/plugins/streams_app/public/components/assets/illustration.png create mode 100644 x-pack/plugins/streams_app/public/components/condition_editor/index.tsx create mode 100644 x-pack/plugins/streams_app/public/components/nested_view/index.tsx create mode 100644 x-pack/plugins/streams_app/public/components/preview_table/index.tsx create mode 100644 x-pack/plugins/streams_app/public/components/stream_delete_modal/index.tsx create mode 100644 x-pack/plugins/streams_app/public/util/use_debounce.ts diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 037997bbd5c86..d6bab939c1efc 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -673,49 +673,59 @@ export const QueryBarTopRow = React.memo( } function renderQueryInput() { + const filterButtonGroup = !renderFilterMenuOnly() && renderFilterButtonGroup(); + const queryInput = shouldRenderQueryInput() && ( + + + + ); + if (isQueryLangSelected || (!filterButtonGroup && !queryInput)) { + return null; + } return ( - - {!renderFilterMenuOnly() && renderFilterButtonGroup()} - {shouldRenderQueryInput() && ( - - - - )} - + + + {filterButtonGroup} + {queryInput} + + ); } @@ -787,12 +797,7 @@ export const QueryBarTopRow = React.memo( adHocDataview={props.indexPatterns?.[0]} /> )} - - {!isQueryLangSelected ? renderQueryInput() : null} - + {renderQueryInput()} {props.renderQueryInputAppend?.()} {shouldShowDatePickerAsBadge() && props.filterBar} {renderUpdateButton()} diff --git a/x-pack/plugins/streams/common/index.ts b/x-pack/plugins/streams/common/index.ts index 3a7306e46cae2..634994cb87f13 100644 --- a/x-pack/plugins/streams/common/index.ts +++ b/x-pack/plugins/streams/common/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export type { StreamDefinition } from './types'; +export type { StreamDefinition, ReadStreamDefinition } from './types'; diff --git a/x-pack/plugins/streams/common/types.ts b/x-pack/plugins/streams/common/types.ts index c2d99d4ba1d89..59cdd1cf9c4b9 100644 --- a/x-pack/plugins/streams/common/types.ts +++ b/x-pack/plugins/streams/common/types.ts @@ -33,11 +33,11 @@ export interface AndCondition { and: Condition[]; } -export interface RerouteOrCondition { +export interface OrCondition { or: Condition[]; } -export type Condition = FilterCondition | AndCondition | RerouteOrCondition | undefined; +export type Condition = FilterCondition | AndCondition | OrCondition | undefined; export const conditionSchema: z.ZodType = z.lazy(() => z.union([ @@ -77,17 +77,17 @@ export const fieldDefinitionSchema = z.object({ export type FieldDefinition = z.infer; +export const streamChildSchema = z.object({ + id: z.string(), + condition: z.optional(conditionSchema), +}); + +export type StreamChild = z.infer; + export const streamWithoutIdDefinitonSchema = z.object({ processing: z.array(processingDefinitionSchema).default([]), fields: z.array(fieldDefinitionSchema).default([]), - children: z - .array( - z.object({ - id: z.string(), - condition: z.optional(conditionSchema), - }) - ) - .default([]), + children: z.array(streamChildSchema).default([]), }); export type StreamWithoutIdDefinition = z.infer; @@ -110,3 +110,9 @@ export type StreamDefinition = z.infer; export const streamDefinitonWithoutChildrenSchema = streamDefinitonSchema.omit({ children: true }); export type StreamWithoutChildrenDefinition = z.infer; + +export const readStreamDefinitonSchema = streamDefinitonSchema.extend({ + inheritedFields: z.array(fieldDefinitionSchema.extend({ from: z.string() })).default([]), +}); + +export type ReadStreamDefinition = z.infer; diff --git a/x-pack/plugins/streams/server/lib/streams/helpers/condition_fields.ts b/x-pack/plugins/streams/server/lib/streams/helpers/condition_fields.ts new file mode 100644 index 0000000000000..48b06b8ea0701 --- /dev/null +++ b/x-pack/plugins/streams/server/lib/streams/helpers/condition_fields.ts @@ -0,0 +1,85 @@ +/* + * 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 { Condition, FilterCondition } from '../../../../common/types'; +import { isAndCondition, isFilterCondition, isOrCondition } from './condition_guards'; + +export function isComplete(condition: Condition): boolean { + if (isFilterCondition(condition)) { + return condition.field !== undefined && condition.field !== ''; + } + if (isAndCondition(condition)) { + return condition.and.every(isComplete); + } + if (isOrCondition(condition)) { + return condition.or.every(isComplete); + } + return false; +} + +export function getFields( + condition: Condition +): Array<{ name: string; type: 'number' | 'string' }> { + const fields = collectFields(condition); + // deduplicate fields, if mapped as string and number, keep as number + const uniqueFields = new Map(); + fields.forEach((field) => { + const existing = uniqueFields.get(field.name); + if (existing === 'number') { + return; + } + if (existing === 'string' && field.type === 'number') { + uniqueFields.set(field.name, 'number'); + return; + } + uniqueFields.set(field.name, field.type); + }); + + return Array.from(uniqueFields).map(([name, type]) => ({ name, type })); +} + +function collectFields(condition: Condition): Array<{ name: string; type: 'number' | 'string' }> { + if (isFilterCondition(condition)) { + return [{ name: condition.field, type: getFieldTypeForFilterCondition(condition) }]; + } + if (isAndCondition(condition)) { + return condition.and.flatMap(collectFields); + } + if (isOrCondition(condition)) { + return condition.or.flatMap(collectFields); + } + return []; +} + +function getFieldTypeForFilterCondition(condition: FilterCondition): 'number' | 'string' { + switch (condition.operator) { + case 'gt': + case 'gte': + case 'lt': + case 'lte': + return 'number'; + case 'neq': + case 'eq': + case 'exists': + case 'contains': + case 'startsWith': + case 'endsWith': + case 'notExists': + return 'string'; + default: + return 'string'; + } +} + +export function validateCondition(condition: Condition) { + if (isFilterCondition(condition)) { + // check whether a field is specified + if (!condition.field.trim()) { + throw new Error('Field is required in conditions'); + } + } +} diff --git a/x-pack/plugins/streams/server/lib/streams/helpers/condition_guards.ts b/x-pack/plugins/streams/server/lib/streams/helpers/condition_guards.ts new file mode 100644 index 0000000000000..1469471bd8943 --- /dev/null +++ b/x-pack/plugins/streams/server/lib/streams/helpers/condition_guards.ts @@ -0,0 +1,29 @@ +/* + * 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 { + AndCondition, + conditionSchema, + FilterCondition, + filterConditionSchema, + OrCondition, +} from '../../../../common/types'; + +export function isFilterCondition(subject: any): subject is FilterCondition { + const result = filterConditionSchema.safeParse(subject); + return result.success; +} + +export function isAndCondition(subject: any): subject is AndCondition { + const result = conditionSchema.safeParse(subject); + return result.success && subject.and != null; +} + +export function isOrCondition(subject: any): subject is OrCondition { + const result = conditionSchema.safeParse(subject); + return result.success && subject.or != null; +} diff --git a/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.ts b/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.ts index dccc15b2ec8fc..1894ebaa6226d 100644 --- a/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.ts +++ b/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_painless.ts @@ -7,30 +7,12 @@ import { isBoolean, isString } from 'lodash'; import { - AndCondition, BinaryFilterCondition, Condition, - conditionSchema, FilterCondition, - filterConditionSchema, - RerouteOrCondition, UnaryFilterCondition, } from '../../../../common/types'; - -function isFilterCondition(subject: any): subject is FilterCondition { - const result = filterConditionSchema.safeParse(subject); - return result.success; -} - -function isAndCondition(subject: any): subject is AndCondition { - const result = conditionSchema.safeParse(subject); - return result.success && subject.and != null; -} - -function isOrCondition(subject: any): subject is RerouteOrCondition { - const result = conditionSchema.safeParse(subject); - return result.success && subject.or != null; -} +import { isAndCondition, isFilterCondition, isOrCondition } from './condition_guards'; function safePainlessField(condition: FilterCondition) { return `ctx.${condition.field.split('.').join('?.')}`; diff --git a/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_query_dsl.ts b/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_query_dsl.ts new file mode 100644 index 0000000000000..3864639175008 --- /dev/null +++ b/x-pack/plugins/streams/server/lib/streams/helpers/condition_to_query_dsl.ts @@ -0,0 +1,63 @@ +/* + * 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 { Condition, FilterCondition } from '../../../../common/types'; +import { isAndCondition, isFilterCondition, isOrCondition } from './condition_guards'; + +function conditionToClause(condition: FilterCondition) { + switch (condition.operator) { + case 'neq': + return { bool: { must_not: { match: { [condition.field]: condition.value } } } }; + case 'eq': + return { match: { [condition.field]: condition.value } }; + case 'exists': + return { exists: { field: condition.field } }; + case 'gt': + return { range: { [condition.field]: { gt: condition.value } } }; + case 'gte': + return { range: { [condition.field]: { gte: condition.value } } }; + case 'lt': + return { range: { [condition.field]: { lt: condition.value } } }; + case 'lte': + return { range: { [condition.field]: { lte: condition.value } } }; + case 'contains': + return { wildcard: { [condition.field]: `*${condition.value}*` } }; + case 'startsWith': + return { prefix: { [condition.field]: condition.value } }; + case 'endsWith': + return { wildcard: { [condition.field]: `*${condition.value}` } }; + case 'notExists': + return { bool: { must_not: { exists: { field: condition.field } } } }; + default: + return { match_none: {} }; + } +} + +export function conditionToQueryDsl(condition: Condition): any { + if (isFilterCondition(condition)) { + return conditionToClause(condition); + } + if (isAndCondition(condition)) { + const and = condition.and.map((filter) => conditionToQueryDsl(filter)); + return { + bool: { + must: and, + }, + }; + } + if (isOrCondition(condition)) { + const or = condition.or.map((filter) => conditionToQueryDsl(filter)); + return { + bool: { + should: or, + }, + }; + } + return { + match_none: {}, + }; +} diff --git a/x-pack/plugins/streams/server/lib/streams/ingest_pipelines/logs_default_pipeline.ts b/x-pack/plugins/streams/server/lib/streams/ingest_pipelines/logs_default_pipeline.ts index 762155ba5047c..90f941657faf4 100644 --- a/x-pack/plugins/streams/server/lib/streams/ingest_pipelines/logs_default_pipeline.ts +++ b/x-pack/plugins/streams/server/lib/streams/ingest_pipelines/logs_default_pipeline.ts @@ -20,4 +20,9 @@ export const logsDefaultPipelineProcessors = [ ignore_missing_pipeline: true, }, }, + { + dot_expander: { + field: '*', + }, + }, ]; diff --git a/x-pack/plugins/streams/server/lib/streams/stream_crud.ts b/x-pack/plugins/streams/server/lib/streams/stream_crud.ts index 245e06e8b4573..452b0f40cb38e 100644 --- a/x-pack/plugins/streams/server/lib/streams/stream_crud.ts +++ b/x-pack/plugins/streams/server/lib/streams/stream_crud.ts @@ -89,7 +89,6 @@ async function upsertInternalStream({ definition, scopedClusterClient }: BasePar type ListStreamsParams = BaseParams; export interface ListStreamResponse { - total: number; definitions: StreamDefinition[]; } @@ -103,12 +102,14 @@ export async function listStreams({ }); const dataStreams = await listDataStreamsAsStreams({ scopedClusterClient }); - const definitions = response.hits.hits.map((hit) => ({ ...hit._source!, managed: true })); - const total = response.hits.total!; + let definitions = response.hits.hits.map((hit) => ({ ...hit._source!, managed: true })); + const hasAccess = await Promise.all( + definitions.map((definition) => checkReadAccess({ id: definition.id, scopedClusterClient })) + ); + definitions = definitions.filter((_, index) => hasAccess[index]); return { definitions: [...definitions, ...dataStreams], - total: (typeof total === 'number' ? total : total.value) + dataStreams.length, }; } @@ -244,7 +245,9 @@ export async function readAncestors({ return { ancestors: await Promise.all( - ancestorIds.map((ancestorId) => readStream({ scopedClusterClient, id: ancestorId })) + ancestorIds.map((ancestorId) => + readStream({ scopedClusterClient, id: ancestorId, skipAccessCheck: true }) + ) ), }; } diff --git a/x-pack/plugins/streams/server/routes/index.ts b/x-pack/plugins/streams/server/routes/index.ts index 7267dbedeacff..cf130e99db3fc 100644 --- a/x-pack/plugins/streams/server/routes/index.ts +++ b/x-pack/plugins/streams/server/routes/index.ts @@ -14,6 +14,7 @@ import { forkStreamsRoute } from './streams/fork'; import { listStreamsRoute } from './streams/list'; import { readStreamRoute } from './streams/read'; import { resyncStreamsRoute } from './streams/resync'; +import { sampleStreamRoute } from './streams/sample'; import { streamsStatusRoutes } from './streams/settings'; export const streamsRouteRepository = { @@ -27,6 +28,7 @@ export const streamsRouteRepository = { ...streamsStatusRoutes, ...esqlRoutes, ...disableStreamsRoute, + ...sampleStreamRoute, }; export type StreamsRouteRepository = typeof streamsRouteRepository; diff --git a/x-pack/plugins/streams/server/routes/streams/edit.ts b/x-pack/plugins/streams/server/routes/streams/edit.ts index 6125aa2470b94..e280796bc9780 100644 --- a/x-pack/plugins/streams/server/routes/streams/edit.ts +++ b/x-pack/plugins/streams/server/routes/streams/edit.ts @@ -27,6 +27,7 @@ import { import { MalformedStreamId } from '../../lib/streams/errors/malformed_stream_id'; import { getParentId } from '../../lib/streams/helpers/hierarchy'; import { MalformedChildren } from '../../lib/streams/errors/malformed_children'; +import { validateCondition } from '../../lib/streams/helpers/condition_fields'; export const editStreamRoute = createServerRoute({ endpoint: 'PUT /api/streams/{id}', @@ -57,7 +58,7 @@ export const editStreamRoute = createServerRoute({ const parentId = getParentId(params.path.id); let parentDefinition: StreamDefinition | undefined; - const streamDefinition = { ...params.body }; + const streamDefinition = { ...params.body, id: params.path.id }; // always need to go from the leaves to the parent when syncing ingest pipelines, otherwise data // will be routed before the data stream is ready @@ -151,7 +152,7 @@ async function updateParentStream( async function validateStreamChildren( scopedClusterClient: IScopedClusterClient, id: string, - children: Array<{ id: string }> + children: StreamDefinition['children'] ) { try { const { definition: oldDefinition } = await readStream({ @@ -160,6 +161,9 @@ async function validateStreamChildren( }); const oldChildren = oldDefinition.children.map((child) => child.id); const newChildren = new Set(children.map((child) => child.id)); + children.forEach((child) => { + validateCondition(child.condition); + }); if (oldChildren.some((child) => !newChildren.has(child))) { throw new MalformedChildren( 'Cannot remove children from a stream, please delete the stream instead' diff --git a/x-pack/plugins/streams/server/routes/streams/fork.ts b/x-pack/plugins/streams/server/routes/streams/fork.ts index a4d846ceccb35..070dc66b9ab10 100644 --- a/x-pack/plugins/streams/server/routes/streams/fork.ts +++ b/x-pack/plugins/streams/server/routes/streams/fork.ts @@ -18,6 +18,7 @@ import { conditionSchema, streamDefinitonWithoutChildrenSchema } from '../../../ import { syncStream, readStream, validateAncestorFields } from '../../lib/streams/stream_crud'; import { MalformedStreamId } from '../../lib/streams/errors/malformed_stream_id'; import { isChildOf } from '../../lib/streams/helpers/hierarchy'; +import { validateCondition } from '../../lib/streams/helpers/condition_fields'; export const forkStreamsRoute = createServerRoute({ endpoint: 'POST /api/streams/{id}/_fork', @@ -48,6 +49,8 @@ export const forkStreamsRoute = createServerRoute({ throw new ForkConditionMissing('You must provide a condition to fork a stream'); } + validateCondition(params.body.condition); + const { scopedClusterClient } = await getScopedClients({ request }); const { definition: rootDefinition } = await readStream({ diff --git a/x-pack/plugins/streams/server/routes/streams/read.ts b/x-pack/plugins/streams/server/routes/streams/read.ts index 5c503e2b7e625..dbbda8c0dc5de 100644 --- a/x-pack/plugins/streams/server/routes/streams/read.ts +++ b/x-pack/plugins/streams/server/routes/streams/read.ts @@ -7,10 +7,10 @@ import { z } from '@kbn/zod'; import { notFound, internal } from '@hapi/boom'; +import { ReadStreamDefinition } from '../../../common/types'; import { createServerRoute } from '../create_server_route'; import { DefinitionNotFound } from '../../lib/streams/errors'; import { readAncestors, readStream } from '../../lib/streams/stream_crud'; -import { StreamDefinition } from '../../../common'; export const readStreamRoute = createServerRoute({ endpoint: 'GET /api/streams/{id}', @@ -33,11 +33,7 @@ export const readStreamRoute = createServerRoute({ request, logger, getScopedClients, - }): Promise< - StreamDefinition & { - inheritedFields: Array; - } - > => { + }): Promise => { try { const { scopedClusterClient } = await getScopedClients({ request }); const streamEntity = await readStream({ diff --git a/x-pack/plugins/streams/server/routes/streams/sample.ts b/x-pack/plugins/streams/server/routes/streams/sample.ts new file mode 100644 index 0000000000000..cd3a989c29109 --- /dev/null +++ b/x-pack/plugins/streams/server/routes/streams/sample.ts @@ -0,0 +1,105 @@ +/* + * 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 { z } from '@kbn/zod'; +import { notFound, internal } from '@hapi/boom'; +import { conditionSchema } from '../../../common/types'; +import { createServerRoute } from '../create_server_route'; +import { DefinitionNotFound } from '../../lib/streams/errors'; +import { checkReadAccess } from '../../lib/streams/stream_crud'; +import { conditionToQueryDsl } from '../../lib/streams/helpers/condition_to_query_dsl'; +import { getFields, isComplete } from '../../lib/streams/helpers/condition_fields'; + +export const sampleStreamRoute = createServerRoute({ + endpoint: 'POST /api/streams/{id}/_sample', + options: { + access: 'internal', + }, + security: { + authz: { + enabled: false, + reason: + 'This API delegates security to the currently logged in user and their Elasticsearch permissions.', + }, + }, + params: z.object({ + path: z.object({ id: z.string() }), + body: z.object({ + condition: z.optional(conditionSchema), + start: z.optional(z.number()), + end: z.optional(z.number()), + number: z.optional(z.number()), + }), + }), + handler: async ({ + response, + params, + request, + logger, + getScopedClients, + }): Promise<{ documents: unknown[] }> => { + try { + const { scopedClusterClient } = await getScopedClients({ request }); + + const hasAccess = await checkReadAccess({ id: params.path.id, scopedClusterClient }); + if (!hasAccess) { + throw new DefinitionNotFound(`Stream definition for ${params.path.id} not found.`); + } + const searchBody = { + query: { + bool: { + must: [ + isComplete(params.body.condition) + ? conditionToQueryDsl(params.body.condition) + : { match_all: {} }, + { + range: { + '@timestamp': { + gte: params.body.start, + lte: params.body.end, + format: 'epoch_millis', + }, + }, + }, + ], + }, + }, + // Conditions could be using fields which are not indexed or they could use it with other types than they are eventually mapped as. + // Because of this we can't rely on mapped fields to draw a sample, instead we need to use runtime fields to simulate what happens during + // ingest in the painless condition checks. + // This is less efficient than it could be - in some cases, these fields _are_ indexed with the right type and we could use them directly. + // This can be optimized in the future. + runtime_mappings: Object.fromEntries( + getFields(params.body.condition).map((field) => [ + field.name, + { type: field.type === 'string' ? 'keyword' : 'double' }, + ]) + ), + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + size: params.body.number, + }; + const results = await scopedClusterClient.asCurrentUser.search({ + index: params.path.id, + ...searchBody, + }); + + return { documents: results.hits.hits.map((hit) => hit._source) }; + } catch (e) { + if (e instanceof DefinitionNotFound) { + throw notFound(e); + } + + throw internal(e); + } + }, +}); diff --git a/x-pack/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx b/x-pack/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx index 1660042b2cb66..cb3148a7f6644 100644 --- a/x-pack/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx +++ b/x-pack/plugins/streams_app/.storybook/get_mock_streams_app_context.tsx @@ -12,6 +12,7 @@ import type { StreamsPluginStart } from '@kbn/streams-plugin/public'; import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { SharePublicStart } from '@kbn/share-plugin/public/plugin'; +import { NavigationPublicStart } from '@kbn/navigation-plugin/public/types'; import type { StreamsAppKibanaContext } from '../public/hooks/use_kibana'; export function getMockStreamsAppContext(): StreamsAppKibanaContext { @@ -27,6 +28,7 @@ export function getMockStreamsAppContext(): StreamsAppKibanaContext { unifiedSearch: {} as unknown as UnifiedSearchPublicPluginStart, streams: {} as unknown as StreamsPluginStart, share: {} as unknown as SharePublicStart, + navigation: {} as unknown as NavigationPublicStart, }, }, services: { diff --git a/x-pack/plugins/streams_app/kibana.jsonc b/x-pack/plugins/streams_app/kibana.jsonc index c1480aba906de..09f356f0654ba 100644 --- a/x-pack/plugins/streams_app/kibana.jsonc +++ b/x-pack/plugins/streams_app/kibana.jsonc @@ -15,7 +15,8 @@ "data", "dataViews", "unifiedSearch", - "share" + "share", + "navigation" ], "requiredBundles": [ "kibanaReact" diff --git a/x-pack/plugins/streams_app/public/components/assets/illustration.png b/x-pack/plugins/streams_app/public/components/assets/illustration.png new file mode 100644 index 0000000000000000000000000000000000000000..58b0f61a943991ebede1ee686b3931a71a14fe52 GIT binary patch literal 58425 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 JM(J(j{{WPBV(|a~ literal 0 HcmV?d00001 diff --git a/x-pack/plugins/streams_app/public/components/condition_editor/index.tsx b/x-pack/plugins/streams_app/public/components/condition_editor/index.tsx new file mode 100644 index 0000000000000..e7e2a79b59294 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/condition_editor/index.tsx @@ -0,0 +1,297 @@ +/* + * 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 { + EuiBadge, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSelect, + EuiSwitch, + EuiText, +} from '@elastic/eui'; +import { + AndCondition, + BinaryFilterCondition, + Condition, + FilterCondition, + OrCondition, +} from '@kbn/streams-plugin/common/types'; +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/css'; +import { CodeEditor } from '@kbn/code-editor'; + +export function ConditionEditor(props: { + condition: Condition; + readonly?: boolean; + onConditionChange?: (condition: Condition) => void; +}) { + if (!props.condition) { + return null; + } + if (props.readonly) { + return ( + + + + ); + } + return ( + {})} + /> + ); +} + +export function ConditionForm(props: { + condition: Condition; + onConditionChange: (condition: Condition) => void; +}) { + const [syntaxEditor, setSyntaxEditor] = React.useState(() => + Boolean(props.condition && !('operator' in props.condition)) + ); + const [jsonCondition, setJsonCondition] = React.useState(() => + JSON.stringify(props.condition, null, 2) + ); + useEffect(() => { + if (!syntaxEditor && props.condition) { + setJsonCondition(JSON.stringify(props.condition, null, 2)); + } + }, [syntaxEditor, props.condition]); + return ( + + + + + {i18n.translate('xpack.streams.conditionEditor.title', { defaultMessage: 'Condition' })} + + + setSyntaxEditor(!syntaxEditor)} + /> + + {syntaxEditor ? ( + { + setJsonCondition(e); + try { + const condition = JSON.parse(e); + props.onConditionChange(condition); + } catch (error: unknown) { + // do nothing + } + }} + /> + ) : ( + props.condition && + ('operator' in props.condition ? ( + + ) : ( +
{JSON.stringify(props.condition, null, 2)}
+ )) + )} +
+ ); +} + +const operatorMap = { + eq: i18n.translate('xpack.streams.filter.equals', { defaultMessage: 'equals' }), + neq: i18n.translate('xpack.streams.filter.notEquals', { defaultMessage: 'not equals' }), + lt: i18n.translate('xpack.streams.filter.lessThan', { defaultMessage: 'less than' }), + lte: i18n.translate('xpack.streams.filter.lessThanOrEquals', { + defaultMessage: 'less than or equals', + }), + gt: i18n.translate('xpack.streams.filter.greaterThan', { defaultMessage: 'greater than' }), + gte: i18n.translate('xpack.streams.filter.greaterThanOrEquals', { + defaultMessage: 'greater than or equals', + }), + contains: i18n.translate('xpack.streams.filter.contains', { defaultMessage: 'contains' }), + startsWith: i18n.translate('xpack.streams.filter.startsWith', { defaultMessage: 'starts with' }), + endsWith: i18n.translate('xpack.streams.filter.endsWith', { defaultMessage: 'ends with' }), + exists: i18n.translate('xpack.streams.filter.exists', { defaultMessage: 'exists' }), + notExists: i18n.translate('xpack.streams.filter.notExists', { defaultMessage: 'not exists' }), +}; + +function FilterForm(props: { + condition: FilterCondition; + onConditionChange: (condition: FilterCondition) => void; +}) { + return ( + + + { + props.onConditionChange({ ...props.condition, field: e.target.value }); + }} + /> + + + ({ + value, + text, + })) as Array<{ value: FilterCondition['operator']; text: string }> + } + value={props.condition.operator} + compressed + onChange={(e) => { + const newCondition: Partial = { + ...props.condition, + }; + + const newOperator = e.target.value as FilterCondition['operator']; + if ( + 'value' in newCondition && + (newOperator === 'exists' || newOperator === 'notExists') + ) { + delete newCondition.value; + } else if (!('value' in newCondition)) { + (newCondition as BinaryFilterCondition).value = ''; + } + props.onConditionChange({ + ...newCondition, + operator: newOperator, + } as FilterCondition); + }} + /> + + + {'value' in props.condition && ( + + { + props.onConditionChange({ + ...props.condition, + value: e.target.value, + } as BinaryFilterCondition); + }} + /> + + )} + + ); +} + +export function ConditionDisplay(props: { condition: Condition }) { + if (!props.condition) { + return null; + } + return ( + <> + {'or' in props.condition ? ( + + ) : 'and' in props.condition ? ( + + ) : ( + + )} + + ); +} + +function OrDisplay(props: { condition: OrCondition }) { + return ( +
+ {i18n.translate('xpack.streams.orDisplay.orLabel', { defaultMessage: 'Or' })} +
+ {props.condition.or.map((condition, index) => ( + + ))} +
+
+ ); +} + +function AndDisplay(props: { condition: AndCondition }) { + return ( +
+ {i18n.translate('xpack.streams.andDisplay.andLabel', { defaultMessage: 'And' })} +
+ {props.condition.and.map((condition, index) => ( + + ))} +
+
+ ); +} + +function FilterDisplay(props: { condition: FilterCondition }) { + return ( + + + {i18n.translate('xpack.streams.filter.field', { defaultMessage: 'Field' })} + + {props.condition.field} + + {i18n.translate('xpack.streams.filter.operator', { defaultMessage: 'Operator' })} + + {props.condition.operator} + {'value' in props.condition && ( + <> + + {i18n.translate('xpack.streams.filter.value', { defaultMessage: 'Value' })} + + {props.condition.value} + + )} + + ); +} diff --git a/x-pack/plugins/streams_app/public/components/entity_detail_view/index.tsx b/x-pack/plugins/streams_app/public/components/entity_detail_view/index.tsx index 8e423908af27d..4e1ec87866aee 100644 --- a/x-pack/plugins/streams_app/public/components/entity_detail_view/index.tsx +++ b/x-pack/plugins/streams_app/public/components/entity_detail_view/index.tsx @@ -7,6 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiPanel, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { css } from '@emotion/css'; import { StreamDefinition } from '@kbn/streams-plugin/common'; import { useStreamsAppBreadcrumbs } from '../../hooks/use_streams_app_breadcrumbs'; import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; @@ -73,7 +74,13 @@ export function EntityDetailViewWithoutParams({ const selectedTabObject = tabMap[selectedTab]; return ( - + diff --git a/x-pack/plugins/streams_app/public/components/nested_view/index.tsx b/x-pack/plugins/streams_app/public/components/nested_view/index.tsx new file mode 100644 index 0000000000000..e19a97053f080 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/nested_view/index.tsx @@ -0,0 +1,40 @@ +/* + * 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 from 'react'; +import { css } from '@emotion/css'; +import { euiThemeVars } from '@kbn/ui-theme'; + +const borderSpec = `1px solid ${euiThemeVars.euiColorLightShade}`; + +export function NestedView({ children, last }: { children: React.ReactNode; last?: boolean }) { + return ( +
+
+ {children} +
+ ); +} diff --git a/x-pack/plugins/streams_app/public/components/preview_table/index.tsx b/x-pack/plugins/streams_app/public/components/preview_table/index.tsx new file mode 100644 index 0000000000000..22db6ff294079 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/preview_table/index.tsx @@ -0,0 +1,71 @@ +/* + * 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 { EuiDataGrid } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect, useMemo, useState } from 'react'; + +export function PreviewTable({ documents }: { documents: unknown[] }) { + const [height, setHeight] = useState('100px'); + useEffect(() => { + // set height to 100% after a short delay otherwise it doesn't calculate correctly + // TODO: figure out a better way to do this + setTimeout(() => { + setHeight(`100%`); + }, 50); + }, []); + + const columns = useMemo(() => { + const cols = new Set(); + documents.forEach((doc) => { + if (!doc || typeof doc !== 'object') { + return; + } + Object.keys(doc).forEach((key) => { + cols.add(key); + }); + }); + return Array.from(cols); + }, [documents]); + + const gridColumns = useMemo(() => { + return Array.from(columns).map((column) => ({ + id: column, + displayAsText: column, + })); + }, [columns]); + + return ( + {}, + canDragAndDropColumns: false, + }} + toolbarVisibility={false} + rowCount={documents.length} + height={height} + renderCellValue={({ rowIndex, columnId }) => { + const doc = documents[rowIndex]; + if (!doc || typeof doc !== 'object') { + return ''; + } + const value = (doc as Record)[columnId]; + if (value === undefined || value === null) { + return ''; + } + if (typeof value === 'object') { + return JSON.stringify(value); + } + return String(value); + }} + /> + ); +} diff --git a/x-pack/plugins/streams_app/public/components/stream_delete_modal/index.tsx b/x-pack/plugins/streams_app/public/components/stream_delete_modal/index.tsx new file mode 100644 index 0000000000000..5b6c04fa805a9 --- /dev/null +++ b/x-pack/plugins/streams_app/public/components/stream_delete_modal/index.tsx @@ -0,0 +1,121 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, + useGeneratedHtmlId, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useAbortController } from '@kbn/observability-utils-browser/hooks/use_abort_controller'; +import React from 'react'; +import { useKibana } from '../../hooks/use_kibana'; + +export function StreamDeleteModal({ + closeModal, + clearChildUnderEdit, + refreshDefinition, + id, +}: { + closeModal: () => void; + clearChildUnderEdit: () => void; + refreshDefinition: () => void; + id: string; +}) { + const { + core: { notifications }, + dependencies: { + start: { + streams: { streamsRepositoryClient }, + }, + }, + } = useKibana(); + const abortController = useAbortController(); + const [deleteInProgress, setDeleteInProgress] = React.useState(false); + const modalTitleId = useGeneratedHtmlId(); + return ( + + + + {i18n.translate('xpack.streams.streamDetailRouting.deleteModalTitle', { + defaultMessage: 'Are you sure you want to delete this data stream?', + })} + + + + + + {i18n.translate('xpack.streams.streamDetailRouting.deleteModalDescription', { + defaultMessage: + 'Deleting this stream will remove all of its children and the data will no longer be routed. All existing data will be removed as well.', + })} + + + + + + + {i18n.translate('xpack.streams.streamDetailRouting.deleteModalCancel', { + defaultMessage: 'Cancel', + })} + + { + try { + setDeleteInProgress(true); + await streamsRepositoryClient.fetch('DELETE /api/streams/{id}', { + signal: abortController.signal, + params: { + path: { + id, + }, + }, + }); + setDeleteInProgress(false); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.streams.streamDetailRouting.deleted', { + defaultMessage: 'Stream deleted', + }), + }); + clearChildUnderEdit(); + closeModal(); + refreshDefinition(); + } catch (error) { + setDeleteInProgress(false); + notifications.toasts.addError(error, { + title: i18n.translate('xpack.streams.failedToDelete', { + defaultMessage: 'Failed to delete stream {id}', + values: { + id, + }, + }), + }); + } + }} + isLoading={deleteInProgress} + > + {i18n.translate('xpack.streams.streamDetailRouting.delete', { + defaultMessage: 'Delete', + })} + + + + + ); +} diff --git a/x-pack/plugins/streams_app/public/components/stream_detail_management/index.tsx b/x-pack/plugins/streams_app/public/components/stream_detail_management/index.tsx index 749b0e659d659..1e66490bca3c9 100644 --- a/x-pack/plugins/streams_app/public/components/stream_detail_management/index.tsx +++ b/x-pack/plugins/streams_app/public/components/stream_detail_management/index.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { StreamDefinition } from '@kbn/streams-plugin/common'; +import { ReadStreamDefinition, StreamDefinition } from '@kbn/streams-plugin/common'; +import { css } from '@emotion/css'; import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiListGroup, EuiText } from '@elastic/eui'; import { useStreamsAppParams } from '../../hooks/use_streams_app_params'; import { RedirectTo } from '../redirect_to'; @@ -26,7 +27,7 @@ export function StreamDetailManagement({ definition, refreshDefinition, }: { - definition?: StreamDefinition; + definition?: ReadStreamDefinition; refreshDefinition: () => void; }) { const { @@ -91,7 +92,13 @@ export function StreamDetailManagement({ const selectedTabObject = tabs[subtab]; return ( - + - {selectedTabObject.content} + + {selectedTabObject.content} + ); } diff --git a/x-pack/plugins/streams_app/public/components/stream_detail_routing/index.tsx b/x-pack/plugins/streams_app/public/components/stream_detail_routing/index.tsx index af65c7eab4235..ca58051f9db2b 100644 --- a/x-pack/plugins/streams_app/public/components/stream_detail_routing/index.tsx +++ b/x-pack/plugins/streams_app/public/components/stream_detail_routing/index.tsx @@ -4,15 +4,778 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { StreamDefinition } from '@kbn/streams-plugin/common'; +import { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiImage, + EuiLoadingSpinner, + EuiPanel, + EuiResizableContainer, + EuiSpacer, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import { i18n } from '@kbn/i18n'; +import { useAbortController } from '@kbn/observability-utils-browser/hooks/use_abort_controller'; +import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range'; +import { ReadStreamDefinition } from '@kbn/streams-plugin/common'; import React from 'react'; +import { StreamChild } from '@kbn/streams-plugin/common/types'; +import { AbortableAsyncState } from '@kbn/observability-utils-browser/hooks/use_abortable_async'; +import { useKibana } from '../../hooks/use_kibana'; +import { useStreamsAppFetch } from '../../hooks/use_streams_app_fetch'; +import { StreamsAppSearchBar } from '../streams_app_search_bar'; +import { ConditionEditor } from '../condition_editor'; +import { useDebounced } from '../../util/use_debounce'; +import { useStreamsAppRouter } from '../../hooks/use_streams_app_router'; +import { NestedView } from '../nested_view'; +import illustration from '../assets/illustration.png'; +import { PreviewTable } from '../preview_table'; +import { StreamDeleteModal } from '../stream_delete_modal'; + +function useRoutingState() { + const [childUnderEdit, setChildUnderEdit] = React.useState< + { isNew: boolean; child: StreamChild } | undefined + >(); + + const debouncedChildUnderEdit = useDebounced(childUnderEdit, 300); + + const [saveInProgress, setSaveInProgress] = React.useState(false); + const [showDeleteModal, setShowDeleteModal] = React.useState(false); + + return { + debouncedChildUnderEdit, + childUnderEdit, + setChildUnderEdit, + saveInProgress, + setSaveInProgress, + showDeleteModal, + setShowDeleteModal, + }; +} export function StreamDetailRouting({ - definition: _definition, - refreshDefinition: _refreshDefinition, + definition, + refreshDefinition, +}: { + definition?: ReadStreamDefinition; + refreshDefinition: () => void; +}) { + const theme = useEuiTheme().euiTheme; + const routingAppState = useRoutingState(); + + if (!definition) { + return null; + } + + const closeModal = () => routingAppState.setShowDeleteModal(false); + + return ( + <> + {routingAppState.showDeleteModal && routingAppState.childUnderEdit && ( + routingAppState.setChildUnderEdit(undefined)} + refreshDefinition={refreshDefinition} + id={routingAppState.childUnderEdit.child.id} + /> + )} + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + + )} + + + + + + + + ); +} + +function ControlBar({ + definition, + routingAppState, + refreshDefinition, }: { - definition?: StreamDefinition; + definition: ReadStreamDefinition; + routingAppState: ReturnType; refreshDefinition: () => void; }) { - return <>{'TODO'}; + const { + core: { notifications }, + dependencies: { + start: { + streams: { streamsRepositoryClient }, + }, + }, + } = useKibana(); + + const { signal } = useAbortController(); + + if (!routingAppState.childUnderEdit) { + return ( + + + {i18n.translate('xpack.streams.streamDetailRouting.save', { + defaultMessage: 'Save', + })} + + + ); + } + + function forkChild() { + if (!routingAppState.childUnderEdit) { + return; + } + return streamsRepositoryClient.fetch('POST /api/streams/{id}/_fork', { + signal, + params: { + path: { + id: definition.id, + }, + body: { + condition: routingAppState.childUnderEdit.child.condition, + stream: { + id: routingAppState.childUnderEdit.child.id, + processing: [], + fields: [], + }, + }, + }, + }); + } + + function updateChild() { + if (!routingAppState.childUnderEdit) { + return; + } + + const childUnderEdit = routingAppState.childUnderEdit.child; + const { inheritedFields, id, ...definitionToUpdate } = definition; + return streamsRepositoryClient.fetch('PUT /api/streams/{id}', { + signal, + params: { + path: { + id: definition.id, + }, + body: { + ...definitionToUpdate, + children: definition.children.map((child) => + child.id === childUnderEdit.id ? childUnderEdit : child + ), + }, + }, + }); + } + + async function saveOrUpdateChild() { + if (!routingAppState.childUnderEdit) { + return; + } + try { + routingAppState.setSaveInProgress(true); + + if (routingAppState.childUnderEdit.isNew) { + await forkChild(); + } else { + await updateChild(); + } + + routingAppState.setSaveInProgress(false); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.streams.streamDetailRouting.saved', { + defaultMessage: 'Stream saved', + }), + }); + routingAppState.setChildUnderEdit(undefined); + refreshDefinition(); + } catch (error) { + routingAppState.setSaveInProgress(false); + notifications.toasts.addError(error, { + title: i18n.translate('xpack.streams.failedToSave', { + defaultMessage: 'Failed to save', + }), + toastMessage: 'body' in error ? error.body.message : error.message, + }); + } + } + + return ( + + {!routingAppState.childUnderEdit.isNew && ( + <> + { + routingAppState.setShowDeleteModal(true); + }} + > + {i18n.translate('xpack.streams.streamDetailRouting.remove', { + defaultMessage: 'Remove', + })} + + + + )} + { + routingAppState.setChildUnderEdit(undefined); + }} + > + {i18n.translate('xpack.streams.streamDetailRouting.cancel', { + defaultMessage: 'Cancel', + })} + + + {routingAppState.childUnderEdit.isNew + ? i18n.translate('xpack.streams.streamDetailRouting.add', { + defaultMessage: 'Save', + }) + : i18n.translate('xpack.streams.streamDetailRouting.change', { + defaultMessage: 'Change routing', + })} + + + ); +} + +function PreviewPanel({ + definition, + routingAppState, +}: { + definition: ReadStreamDefinition; + routingAppState: ReturnType; +}) { + const { + dependencies: { + start: { + data, + streams: { streamsRepositoryClient }, + }, + }, + } = useKibana(); + + const { + timeRange, + absoluteTimeRange: { start, end }, + setTimeRange, + } = useDateRange({ data }); + + const previewSampleFetch = useStreamsAppFetch( + ({ signal }) => { + if ( + !definition || + !routingAppState.debouncedChildUnderEdit || + !routingAppState.debouncedChildUnderEdit.isNew + ) { + return Promise.resolve({ documents: [] }); + } + return streamsRepositoryClient.fetch('POST /api/streams/{id}/_sample', { + signal, + params: { + path: { + id: definition.id, + }, + body: { + condition: routingAppState.debouncedChildUnderEdit.child.condition, + start: start?.valueOf(), + end: end?.valueOf(), + number: 100, + }, + }, + }); + }, + [definition, routingAppState.debouncedChildUnderEdit, streamsRepositoryClient, start, end], + { + disableToastOnError: true, + } + ); + + let content = ( + + ); + + if (routingAppState.debouncedChildUnderEdit?.isNew) { + if (previewSampleFetch.error) { + content = ( + + + + {i18n.translate('xpack.streams.streamDetail.preview.error', { + defaultMessage: 'Error loading preview', + })} + + + + ); + } else if (previewSampleFetch.value?.documents && previewSampleFetch.value.documents.length) { + content = ( + + + + ); + } + } + + return ( + <> + + + + + + + {i18n.translate('xpack.streams.streamDetail.preview.header', { + defaultMessage: 'Data Preview', + })} + {previewSampleFetch.loading && } + + + + + { + if (!isUpdate) { + previewSampleFetch.refresh(); + return; + } + + if (dateRange) { + setTimeRange({ + from: dateRange.from, + to: dateRange?.to, + mode: dateRange.mode, + }); + } + }} + onRefresh={() => { + previewSampleFetch.refresh(); + }} + dateRangeFrom={timeRange.from} + dateRangeTo={timeRange.to} + /> + + + + + {content} + + ); +} + +function PreviewPanelIllustration({ + previewSampleFetch, + routingAppState, +}: { + routingAppState: ReturnType; + previewSampleFetch: AbortableAsyncState<{ + documents: unknown[]; + }>; +}) { + return ( + + + + {previewSampleFetch.loading ? ( + + + + ) : ( + <> + {routingAppState.debouncedChildUnderEdit && + routingAppState.debouncedChildUnderEdit.isNew && ( + + {i18n.translate('xpack.streams.streamDetail.preview.empty', { + defaultMessage: 'No documents to preview', + })} + + )} + {routingAppState.debouncedChildUnderEdit && + !routingAppState.debouncedChildUnderEdit.isNew && ( + + {i18n.translate('xpack.streams.streamDetail.preview.editPreviewMessage', { + defaultMessage: 'Preview is not available while editing streams', + })} + + )} + {!routingAppState.debouncedChildUnderEdit && ( + <> + + {i18n.translate('xpack.streams.streamDetail.preview.editPreviewMessageEmpty', { + defaultMessage: 'Your preview will appear here', + })} + + + {i18n.translate( + 'xpack.streams.streamDetail.preview.editPreviewMessageEmptyDescription', + { + defaultMessage: + 'Create a new child stream to see what will be routed to it based on the conditions', + } + )} + + + )} + + )} + + + ); +} + +function ChildStreamList({ + definition, + routingAppState: { childUnderEdit, setChildUnderEdit }, +}: { + definition: ReadStreamDefinition; + routingAppState: ReturnType; +}) { + return ( + + + + {i18n.translate('xpack.streams.streamDetailRouting.rules.header', { + defaultMessage: 'Routing rules', + })} + + + + + + {definition.children.map((child, i) => ( + + { + if (child.id === childUnderEdit?.child.id) { + setChildUnderEdit(undefined); + } else { + setChildUnderEdit({ isNew: false, child }); + } + }} + onChildChange={(newChild) => { + setChildUnderEdit({ + isNew: false, + child: newChild, + }); + }} + /> + + ))} + {childUnderEdit?.isNew ? ( + + { + if (!newChild) { + setChildUnderEdit(undefined); + return; + } + setChildUnderEdit({ + isNew: true, + child: newChild, + }); + }} + /> + + ) : ( + + + { + setChildUnderEdit({ + isNew: true, + child: { + id: `${definition.id}.child`, + condition: { + field: '', + operator: 'eq', + value: '', + }, + }, + }); + }} + > + {i18n.translate('xpack.streams.streamDetailRouting.addRule', { + defaultMessage: 'Create a new child stream', + })} + + + + )} + + + ); +} + +function CurrentStreamEntry({ definition }: { definition: ReadStreamDefinition }) { + return ( + + + {definition.id} + + {i18n.translate('xpack.streams.streamDetailRouting.currentStream', { + defaultMessage: 'Current stream', + })} + + + + ); +} + +function PreviousStreamEntry({ definition }: { definition: ReadStreamDefinition }) { + const router = useStreamsAppRouter(); + + const parentId = definition.id.split('.').slice(0, -1).join('.'); + if (parentId === '') { + return null; + } + + return ( + + + + + {i18n.translate('xpack.streams.streamDetailRouting.previousStream', { + defaultMessage: '.. (Previous stream)', + })} + + + + + ); +} + +function RoutingStreamEntry({ + child, + onChildChange, + onEditStateChange, + edit, +}: { + child: StreamChild; + onChildChange: (child: StreamChild) => void; + onEditStateChange: () => void; + edit?: boolean; +}) { + const router = useStreamsAppRouter(); + return ( + + + + {child.id} + + { + onEditStateChange(); + }} + aria-label={i18n.translate('xpack.streams.streamDetailRouting.edit', { + defaultMessage: 'Edit', + })} + /> + + + {child.condition && ( + { + onChildChange({ + ...child, + condition, + }); + }} + /> + )} + {!child.condition && ( + + {i18n.translate('xpack.streams.streamDetailRouting.noCondition', { + defaultMessage: 'No condition, no documents will be routed', + })} + + )} + + ); +} + +function NewRoutingStreamEntry({ + child, + onChildChange, +}: { + child: StreamChild; + onChildChange: (child?: StreamChild) => void; +}) { + return ( + + + + { + onChildChange({ + ...child, + id: e.target.value, + }); + }} + /> + + {child.condition && ( + { + onChildChange({ + ...child, + condition, + }); + }} + /> + )} + {!child.condition && ( + + {i18n.translate('xpack.streams.streamDetailRouting.noCondition', { + defaultMessage: 'No condition, no documents will be routed', + })} + + )} + + + ); } diff --git a/x-pack/plugins/streams_app/public/components/stream_detail_view/index.tsx b/x-pack/plugins/streams_app/public/components/stream_detail_view/index.tsx index d091fb5758a1e..b0a2307f7b2b7 100644 --- a/x-pack/plugins/streams_app/public/components/stream_detail_view/index.tsx +++ b/x-pack/plugins/streams_app/public/components/stream_detail_view/index.tsx @@ -14,10 +14,12 @@ import { StreamDetailOverview } from '../stream_detail_overview'; import { StreamDetailManagement } from '../stream_detail_management'; export function StreamDetailView() { - const { path } = useStreamsAppParams('/{key}/*'); + const params1 = useStreamsAppParams('/{key}/{tab}', true); - const key = path.key; - const tab = 'tab' in path ? path.tab : 'management'; + const params2 = useStreamsAppParams('/{key}/management/{subtab}', true); + + const key = params1?.path?.key || params2.path.key; + const tab = params1?.path?.tab || 'management'; const { dependencies: { diff --git a/x-pack/plugins/streams_app/public/components/streams_app_page_body/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_page_body/index.tsx index 04b44f82df0fd..1718e2e638a51 100644 --- a/x-pack/plugins/streams_app/public/components/streams_app_page_body/index.tsx +++ b/x-pack/plugins/streams_app/public/components/streams_app_page_body/index.tsx @@ -18,6 +18,7 @@ export function StreamsAppPageBody({ children }: { children: React.ReactNode }) border-top: 1px solid ${theme.colors.lightShade}; border-radius: 0px; display: flex; + overflow-y: auto; `} paddingSize="l" > diff --git a/x-pack/plugins/streams_app/public/components/streams_app_page_template/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_page_template/index.tsx index 942d2937dba81..280c32841811a 100644 --- a/x-pack/plugins/streams_app/public/components/streams_app_page_template/index.tsx +++ b/x-pack/plugins/streams_app/public/components/streams_app_page_template/index.tsx @@ -6,23 +6,32 @@ */ import { css } from '@emotion/css'; import React from 'react'; -import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; +import useObservable from 'react-use/lib/useObservable'; import { useKibana } from '../../hooks/use_kibana'; export function StreamsAppPageTemplate({ children }: { children: React.ReactNode }) { const { dependencies: { - start: { observabilityShared }, + start: { observabilityShared, navigation }, }, } = useKibana(); const { PageTemplate } = observabilityShared.navigation; + const isSolutionNavEnabled = useObservable(navigation.isSolutionNavEnabled$); + return ( - {children} diff --git a/x-pack/plugins/streams_app/public/components/streams_app_search_bar/index.tsx b/x-pack/plugins/streams_app/public/components/streams_app_search_bar/index.tsx index 563fb752efbd5..cd07f540f3ba1 100644 --- a/x-pack/plugins/streams_app/public/components/streams_app_search_bar/index.tsx +++ b/x-pack/plugins/streams_app/public/components/streams_app_search_bar/index.tsx @@ -4,17 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { css } from '@emotion/css'; import type { TimeRange } from '@kbn/es-query'; import { SearchBar } from '@kbn/unified-search-plugin/public'; import React, { useMemo } from 'react'; import type { DataView } from '@kbn/data-views-plugin/common'; import { useKibana } from '../../hooks/use_kibana'; -const parentClassName = css` - width: 100%; -`; - interface Props { query?: string; dateRangeFrom?: string; @@ -47,32 +42,30 @@ export function StreamsAppSearchBar({ const showQueryInput = query === undefined; return ( -
- { - onQuerySubmit?.( - { dateRange, query: (nextQuery?.query as string | undefined) ?? '' }, - isUpdate - ); - }} - onQueryChange={({ dateRange, query: nextQuery }) => { - onQueryChange?.({ dateRange, query: (nextQuery?.query as string | undefined) ?? '' }); - }} - query={queryObj} - showQueryInput={showQueryInput} - showFilterBar={false} - showQueryMenu={false} - showDatePicker={Boolean(dateRangeFrom && dateRangeTo)} - showSubmitButton={true} - dateRangeFrom={dateRangeFrom} - dateRangeTo={dateRangeTo} - onRefresh={onRefresh} - displayStyle="inPage" - disableQueryLanguageSwitcher - placeholder={placeholder} - indexPatterns={dataViews} - /> -
+ { + onQuerySubmit?.( + { dateRange, query: (nextQuery?.query as string | undefined) ?? '' }, + isUpdate + ); + }} + onQueryChange={({ dateRange, query: nextQuery }) => { + onQueryChange?.({ dateRange, query: (nextQuery?.query as string | undefined) ?? '' }); + }} + query={queryObj} + showQueryInput={showQueryInput} + showFilterBar={false} + showQueryMenu={false} + showDatePicker={Boolean(dateRangeFrom && dateRangeTo)} + showSubmitButton={true} + dateRangeFrom={dateRangeFrom} + dateRangeTo={dateRangeTo} + onRefresh={onRefresh} + displayStyle="inPage" + disableQueryLanguageSwitcher + placeholder={placeholder} + indexPatterns={dataViews} + /> ); } diff --git a/x-pack/plugins/streams_app/public/routes/config.tsx b/x-pack/plugins/streams_app/public/routes/config.tsx index 444218c6f9769..5887528f07b16 100644 --- a/x-pack/plugins/streams_app/public/routes/config.tsx +++ b/x-pack/plugins/streams_app/public/routes/config.tsx @@ -68,6 +68,15 @@ const streamsAppRoutes = { }), }), }, + '/{key}/{tab}/{subtab}': { + element: , + params: t.type({ + path: t.type({ + tab: t.string, + subtab: t.string, + }), + }), + }, }, }, '/': { diff --git a/x-pack/plugins/streams_app/public/types.ts b/x-pack/plugins/streams_app/public/types.ts index 58d44784fe031..680dd008d2e1f 100644 --- a/x-pack/plugins/streams_app/public/types.ts +++ b/x-pack/plugins/streams_app/public/types.ts @@ -16,6 +16,7 @@ import type { import type { StreamsPluginSetup, StreamsPluginStart } from '@kbn/streams-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { SharePublicSetup, SharePublicStart } from '@kbn/share-plugin/public/plugin'; +import { NavigationPublicStart } from '@kbn/navigation-plugin/public/types'; /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ConfigSchema {} @@ -36,6 +37,7 @@ export interface StreamsAppStartDependencies { observabilityShared: ObservabilitySharedPluginStart; unifiedSearch: UnifiedSearchPublicPluginStart; share: SharePublicStart; + navigation: NavigationPublicStart; } export interface StreamsAppPublicSetup {} diff --git a/x-pack/plugins/streams_app/public/util/use_debounce.ts b/x-pack/plugins/streams_app/public/util/use_debounce.ts new file mode 100644 index 0000000000000..eee9accd2e29e --- /dev/null +++ b/x-pack/plugins/streams_app/public/util/use_debounce.ts @@ -0,0 +1,23 @@ +/* + * 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 useDebounce from 'react-use/lib/useDebounce'; +import { useState } from 'react'; + +export function useDebounced(value: T, debounceDelay: number = 300) { + const [debouncedValue, setValue] = useState(value); + + useDebounce( + () => { + setValue(value); + }, + debounceDelay, + [value, setValue] + ); + + return debouncedValue; +} diff --git a/x-pack/plugins/streams_app/tsconfig.json b/x-pack/plugins/streams_app/tsconfig.json index 39acb94665ae5..cba6a2d993bd4 100644 --- a/x-pack/plugins/streams_app/tsconfig.json +++ b/x-pack/plugins/streams_app/tsconfig.json @@ -33,5 +33,8 @@ "@kbn/streams-plugin", "@kbn/share-plugin", "@kbn/observability-utils-server", + "@kbn/code-editor", + "@kbn/ui-theme", + "@kbn/navigation-plugin", ] }