mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Streams 🌊] Enrichment UX redesign (#208141)
## 📓 Summary Closes https://github.com/elastic/streams-program/issues/67 Closes https://github.com/elastic/streams-program/issues/69 Closes https://github.com/elastic/streams-program/issues/93 Closes https://github.com/elastic/streams-program/issues/75 This work heavily changes the initial prototype of the stream enrichment section. - Update the design into a unified split view. - Introduce auto-simulation for real-time changes - Differentiate between saved and staged processors, with multi-processor simulation for the draft ones. A downgrade versus the previous experience is the removal of the field mapping selectors and simulation. This is a temporary change, as we want to set a detected fields tab in the simulation panel that embeds the schema editor, which is not ready for this yet. https://github.com/user-attachments/assets/6ea172b1-087f-4fd0-a850-b6dddc5ca311
This commit is contained in:
parent
0d9ce86d0b
commit
8d0f3544f1
46 changed files with 1440 additions and 1596 deletions
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
export * from './ingest';
|
||||
export * from './legacy';
|
||||
|
||||
export * from './api';
|
||||
export * from './core';
|
||||
|
|
|
@ -11,14 +11,15 @@ import { Condition, conditionSchema } from '../conditions';
|
|||
import { createIsNarrowSchema } from '../../../helpers';
|
||||
|
||||
export interface ProcessorBase {
|
||||
description?: string;
|
||||
if: Condition;
|
||||
ignore_failure?: boolean;
|
||||
}
|
||||
|
||||
export interface GrokProcessorConfig extends ProcessorBase {
|
||||
field: string;
|
||||
patterns: string[];
|
||||
pattern_definitions?: Record<string, string>;
|
||||
ignore_failure?: boolean;
|
||||
ignore_missing?: boolean;
|
||||
}
|
||||
|
||||
|
@ -27,7 +28,9 @@ export interface GrokProcessorDefinition {
|
|||
}
|
||||
|
||||
const processorBaseSchema = z.object({
|
||||
description: z.optional(z.string()),
|
||||
if: conditionSchema,
|
||||
ignore_failure: z.optional(z.boolean()),
|
||||
});
|
||||
|
||||
export const grokProcessorDefinitionSchema: z.Schema<GrokProcessorDefinition> = z.strictObject({
|
||||
|
@ -35,9 +38,8 @@ export const grokProcessorDefinitionSchema: z.Schema<GrokProcessorDefinition> =
|
|||
processorBaseSchema,
|
||||
z.object({
|
||||
field: NonEmptyString,
|
||||
patterns: z.array(NonEmptyString),
|
||||
patterns: z.array(NonEmptyString).nonempty(),
|
||||
pattern_definitions: z.optional(z.record(z.string())),
|
||||
ignore_failure: z.optional(z.boolean()),
|
||||
ignore_missing: z.optional(z.boolean()),
|
||||
})
|
||||
),
|
||||
|
@ -47,7 +49,6 @@ export interface DissectProcessorConfig extends ProcessorBase {
|
|||
field: string;
|
||||
pattern: string;
|
||||
append_separator?: string;
|
||||
ignore_failure?: boolean;
|
||||
ignore_missing?: boolean;
|
||||
}
|
||||
|
||||
|
@ -63,7 +64,6 @@ export const dissectProcessorDefinitionSchema: z.Schema<DissectProcessorDefiniti
|
|||
field: NonEmptyString,
|
||||
pattern: NonEmptyString,
|
||||
append_separator: z.optional(NonEmptyString),
|
||||
ignore_failure: z.optional(z.boolean()),
|
||||
ignore_missing: z.optional(z.boolean()),
|
||||
})
|
||||
),
|
||||
|
@ -78,7 +78,7 @@ export type ProcessorConfig = BodyOf<ProcessorDefinition>;
|
|||
|
||||
export type ProcessorType = UnionKeysOf<ProcessorDefinition>;
|
||||
|
||||
type ProcessorTypeOf<TProcessorDefinition extends ProcessorDefinition> =
|
||||
export type ProcessorTypeOf<TProcessorDefinition extends ProcessorDefinition> =
|
||||
UnionKeysOf<TProcessorDefinition> & ProcessorType;
|
||||
|
||||
export const processorDefinitionSchema: z.ZodType<ProcessorDefinition> = z.union([
|
||||
|
@ -96,15 +96,20 @@ export const isDissectProcessorDefinition = createIsNarrowSchema(
|
|||
dissectProcessorDefinitionSchema
|
||||
);
|
||||
|
||||
const processorTypes: ProcessorType[] = (processorDefinitionSchema as z.ZodUnion<any>).options.map(
|
||||
(option: z.ZodUnion<any>['options'][number]) => Object.keys(option.shape)[0]
|
||||
);
|
||||
|
||||
export function getProcessorType<TProcessorDefinition extends ProcessorDefinition>(
|
||||
processor: TProcessorDefinition
|
||||
): ProcessorTypeOf<TProcessorDefinition> {
|
||||
return Object.keys(processor)[0] as ProcessorTypeOf<TProcessorDefinition>;
|
||||
return processorTypes.find((type) => type in processor) as ProcessorTypeOf<TProcessorDefinition>;
|
||||
}
|
||||
|
||||
export function getProcessorConfig(processor: ProcessorDefinition): ProcessorConfig {
|
||||
if ('grok' in processor) {
|
||||
return processor.grok;
|
||||
}
|
||||
return processor.dissect;
|
||||
export function getProcessorConfig<TProcessorDefinition extends ProcessorDefinition>(
|
||||
processor: TProcessorDefinition
|
||||
): ProcessorConfig {
|
||||
const type = getProcessorType(processor);
|
||||
|
||||
return processor[type as keyof TProcessorDefinition];
|
||||
}
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* 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 { NonEmptyString } from '@kbn/zod-helpers';
|
||||
import {
|
||||
InheritedFieldDefinition,
|
||||
UnwiredIngestStreamEffectiveLifecycle,
|
||||
UnwiredStreamDefinition,
|
||||
WiredIngestStreamEffectiveLifecycle,
|
||||
WiredStreamDefinition,
|
||||
inheritedFieldDefinitionSchema,
|
||||
unwiredIngestStreamEffectiveLifecycleSchema,
|
||||
unwiredStreamDefinitionSchema,
|
||||
wiredIngestStreamEffectiveLifecycleSchema,
|
||||
wiredStreamDefinitionSchema,
|
||||
} from './ingest';
|
||||
import { ElasticsearchAsset, elasticsearchAssetSchema } from './ingest/common';
|
||||
import { createIsNarrowSchema } from '../helpers';
|
||||
|
||||
/**
|
||||
* These are deprecated types, they should be migrated to the updated types
|
||||
*/
|
||||
|
||||
interface ReadStreamDefinitionBase {
|
||||
name: string;
|
||||
dashboards: string[];
|
||||
elasticsearch_assets: ElasticsearchAsset[];
|
||||
inherited_fields: InheritedFieldDefinition;
|
||||
}
|
||||
|
||||
interface WiredReadStreamDefinition extends ReadStreamDefinitionBase {
|
||||
stream: WiredStreamDefinition;
|
||||
effective_lifecycle: WiredIngestStreamEffectiveLifecycle;
|
||||
}
|
||||
|
||||
interface UnwiredReadStreamDefinition extends ReadStreamDefinitionBase {
|
||||
stream: UnwiredStreamDefinition;
|
||||
data_stream_exists: boolean;
|
||||
effective_lifecycle: UnwiredIngestStreamEffectiveLifecycle;
|
||||
}
|
||||
|
||||
type ReadStreamDefinition = WiredReadStreamDefinition | UnwiredReadStreamDefinition;
|
||||
|
||||
const readStreamDefinitionSchemaBase: z.Schema<ReadStreamDefinitionBase> = z.object({
|
||||
name: z.string(),
|
||||
dashboards: z.array(NonEmptyString),
|
||||
elasticsearch_assets: z.array(elasticsearchAssetSchema),
|
||||
inherited_fields: inheritedFieldDefinitionSchema,
|
||||
});
|
||||
|
||||
const wiredReadStreamDefinitionSchema: z.Schema<WiredReadStreamDefinition> = z.intersection(
|
||||
readStreamDefinitionSchemaBase,
|
||||
z.object({
|
||||
stream: wiredStreamDefinitionSchema,
|
||||
effective_lifecycle: wiredIngestStreamEffectiveLifecycleSchema,
|
||||
})
|
||||
);
|
||||
|
||||
const unwiredReadStreamDefinitionSchema: z.Schema<UnwiredReadStreamDefinition> = z.intersection(
|
||||
readStreamDefinitionSchemaBase,
|
||||
z.object({
|
||||
stream: unwiredStreamDefinitionSchema,
|
||||
data_stream_exists: z.boolean(),
|
||||
effective_lifecycle: unwiredIngestStreamEffectiveLifecycleSchema,
|
||||
})
|
||||
);
|
||||
|
||||
const readStreamSchema: z.Schema<ReadStreamDefinition> = z.union([
|
||||
wiredReadStreamDefinitionSchema,
|
||||
unwiredReadStreamDefinitionSchema,
|
||||
]);
|
||||
|
||||
const isReadStream = createIsNarrowSchema(z.unknown(), readStreamSchema);
|
||||
const isWiredReadStream = createIsNarrowSchema(readStreamSchema, wiredReadStreamDefinitionSchema);
|
||||
const isUnwiredReadStream = createIsNarrowSchema(
|
||||
readStreamSchema,
|
||||
unwiredReadStreamDefinitionSchema
|
||||
);
|
||||
|
||||
export {
|
||||
readStreamSchema,
|
||||
type ReadStreamDefinition,
|
||||
type WiredReadStreamDefinition,
|
||||
type UnwiredReadStreamDefinition,
|
||||
isReadStream,
|
||||
isWiredReadStream,
|
||||
isUnwiredReadStream,
|
||||
wiredReadStreamDefinitionSchema,
|
||||
};
|
|
@ -28,25 +28,32 @@ import React, { useEffect } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/css';
|
||||
import { CodeEditor } from '@kbn/code-editor';
|
||||
import { EMPTY_EQUALS_CONDITION } from '../../util/condition';
|
||||
import {
|
||||
EMPTY_EQUALS_CONDITION,
|
||||
alwaysToEmptyEquals,
|
||||
emptyEqualsToAlways,
|
||||
} from '../../util/condition';
|
||||
|
||||
export function ConditionEditor(props: {
|
||||
condition: Condition;
|
||||
readonly?: boolean;
|
||||
onConditionChange?: (condition: Condition) => void;
|
||||
}) {
|
||||
const normalizedCondition = alwaysToEmptyEquals(props.condition);
|
||||
|
||||
const handleConditionChange = (condition: Condition) => {
|
||||
props.onConditionChange?.(emptyEqualsToAlways(condition));
|
||||
};
|
||||
|
||||
if (props.readonly) {
|
||||
return (
|
||||
<EuiPanel color="subdued" borderRadius="none" hasShadow={false} paddingSize="xs">
|
||||
<ConditionDisplay condition={props.condition} />
|
||||
<ConditionDisplay condition={normalizedCondition} />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ConditionForm
|
||||
condition={props.condition}
|
||||
onConditionChange={props.onConditionChange || (() => {})}
|
||||
/>
|
||||
<ConditionForm condition={normalizedCondition} onConditionChange={handleConditionChange} />
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBottomBar, EuiButton, EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlexGroup } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDiscardConfirm } from '../../hooks/use_discard_confirm';
|
||||
|
||||
|
@ -25,37 +25,40 @@ export function ManagementBottomBar({
|
|||
onCancel,
|
||||
onConfirm,
|
||||
}: ManagementBottomBarProps) {
|
||||
const handleCancel = useDiscardConfirm(onCancel);
|
||||
const handleCancel = useDiscardConfirm(onCancel, {
|
||||
title: discardUnsavedChangesTitle,
|
||||
message: discardUnsavedChangesMessage,
|
||||
confirmButtonText: discardUnsavedChangesLabel,
|
||||
cancelButtonText: keepEditingLabel,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiBottomBar>
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" responsive={false} gutterSize="s">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="streamsAppManagementBottomBarCancelChangesButton"
|
||||
color="text"
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{i18n.translate('xpack.streams.streamDetailView.managementTab.bottomBar.cancel', {
|
||||
defaultMessage: 'Cancel changes',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppManagementBottomBarButton"
|
||||
disabled={disabled}
|
||||
color="primary"
|
||||
fill
|
||||
size="s"
|
||||
iconType="check"
|
||||
onClick={onConfirm}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiBottomBar>
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" responsive={false} gutterSize="s">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="streamsAppManagementBottomBarCancelChangesButton"
|
||||
color="text"
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={handleCancel}
|
||||
disabled={disabled}
|
||||
>
|
||||
{i18n.translate('xpack.streams.streamDetailView.managementTab.bottomBar.cancel', {
|
||||
defaultMessage: 'Cancel changes',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppManagementBottomBarButton"
|
||||
disabled={disabled}
|
||||
color="primary"
|
||||
fill
|
||||
size="s"
|
||||
iconType="check"
|
||||
onClick={onConfirm}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{confirmButtonText}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -63,3 +66,26 @@ const defaultConfirmButtonText = i18n.translate(
|
|||
'xpack.streams.streamDetailView.managementTab.bottomBar.confirm',
|
||||
{ defaultMessage: 'Save changes' }
|
||||
);
|
||||
|
||||
const discardUnsavedChangesLabel = i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.discardUnsavedChangesLabel',
|
||||
{ defaultMessage: 'Discard unsaved changes' }
|
||||
);
|
||||
|
||||
const keepEditingLabel = i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.discardUnsavedChangesKeepEditing',
|
||||
{ defaultMessage: 'Keep editing' }
|
||||
);
|
||||
|
||||
const discardUnsavedChangesTitle = i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.discardUnsavedChangesTitle',
|
||||
{ defaultMessage: 'Unsaved changes' }
|
||||
);
|
||||
|
||||
const discardUnsavedChangesMessage = i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.discardUnsavedChangesMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'You are about to leave this view without saving. All changes will be lost. Do you really want to leave without saving?',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,28 +6,18 @@
|
|||
*/
|
||||
import { EuiDataGrid } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { CSSProperties, useEffect, useMemo, useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
export function PreviewTable({
|
||||
documents,
|
||||
displayColumns,
|
||||
height,
|
||||
}: {
|
||||
documents: unknown[];
|
||||
displayColumns?: string[];
|
||||
height?: CSSProperties['height'];
|
||||
}) {
|
||||
const [computedHeight, setComputedHeight] = 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(() => {
|
||||
setComputedHeight(`100%`);
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
if (displayColumns) return displayColumns;
|
||||
if (displayColumns && !isEmpty(displayColumns)) return displayColumns;
|
||||
|
||||
const cols = new Set<string>();
|
||||
documents.forEach((doc) => {
|
||||
|
@ -42,9 +32,10 @@ export function PreviewTable({
|
|||
}, [displayColumns, documents]);
|
||||
|
||||
const gridColumns = useMemo(() => {
|
||||
return Array.from(columns).map((column) => ({
|
||||
return columns.map((column) => ({
|
||||
id: column,
|
||||
displayAsText: column,
|
||||
initialWidth: columns.length > 10 ? 250 : undefined,
|
||||
}));
|
||||
}, [columns]);
|
||||
|
||||
|
@ -61,7 +52,6 @@ export function PreviewTable({
|
|||
}}
|
||||
toolbarVisibility={false}
|
||||
rowCount={documents.length}
|
||||
height={height ?? computedHeight}
|
||||
renderCellValue={({ rowIndex, columnId }) => {
|
||||
const doc = documents[rowIndex];
|
||||
if (!doc || typeof doc !== 'object') {
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiButton } from '@elastic/eui';
|
||||
import { EuiButtonPropsForButton } from '@elastic/eui/src/components/button/button';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export function AddProcessorButton(props: EuiButtonPropsForButton) {
|
||||
return (
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppAddProcessorButtonAddAProcessorButton"
|
||||
iconType="plusInCircle"
|
||||
{...props}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichmentEmptyPrompt.addProcessorAction',
|
||||
{ defaultMessage: 'Add a processor' }
|
||||
)}
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* 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 { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AssetImage } from '../asset_image';
|
||||
import { AddProcessorButton } from './add_processor_button';
|
||||
|
||||
interface EnrichmentEmptyPromptProps {
|
||||
onAddProcessor: () => void;
|
||||
}
|
||||
|
||||
export const EnrichmentEmptyPrompt = ({ onAddProcessor }: EnrichmentEmptyPromptProps) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
titleSize="xs"
|
||||
icon={<AssetImage />}
|
||||
title={title}
|
||||
body={body}
|
||||
actions={[<AddProcessorButton onClick={onAddProcessor} />]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const title = (
|
||||
<h2>
|
||||
{i18n.translate('xpack.streams.streamDetailView.managementTab.enrichmentEmptyPrompt.title', {
|
||||
defaultMessage: 'Start extracting useful fields from your data',
|
||||
})}
|
||||
</h2>
|
||||
);
|
||||
|
||||
const body = (
|
||||
<p>
|
||||
{i18n.translate('xpack.streams.streamDetailView.managementTab.enrichmentEmptyPrompt.body', {
|
||||
defaultMessage: 'Use processors to transform data before indexing',
|
||||
})}
|
||||
</p>
|
||||
);
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
useGeneratedHtmlId,
|
||||
EuiButton,
|
||||
EuiConfirmModal,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
|
||||
export const DangerZone = ({
|
||||
onDeleteProcessor,
|
||||
}: Pick<DeleteProcessorButtonProps, 'onDeleteProcessor'>) => {
|
||||
return (
|
||||
<EuiPanel hasShadow={false} paddingSize="none">
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dangerAreaTitle',
|
||||
{ defaultMessage: 'Danger area' }
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<DeleteProcessorButton onDeleteProcessor={onDeleteProcessor} />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
interface DeleteProcessorButtonProps {
|
||||
onDeleteProcessor: () => void;
|
||||
}
|
||||
|
||||
const DeleteProcessorButton = ({ onDeleteProcessor }: DeleteProcessorButtonProps) => {
|
||||
const [isConfirmModalOpen, { on: openConfirmModal, off: closeConfirmModal }] = useBoolean();
|
||||
const confirmModalId = useGeneratedHtmlId();
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppDeleteProcessorButtonDeleteProcessorButton"
|
||||
color="danger"
|
||||
onClick={openConfirmModal}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dangerAreaTitle',
|
||||
{ defaultMessage: 'Delete processor' }
|
||||
)}
|
||||
</EuiButton>
|
||||
{isConfirmModalOpen && (
|
||||
<EuiConfirmModal
|
||||
aria-labelledby={confirmModalId}
|
||||
title={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.deleteProcessorModalTitle',
|
||||
{ defaultMessage: 'Delete processor' }
|
||||
)}
|
||||
titleProps={{ id: confirmModalId }}
|
||||
onCancel={closeConfirmModal}
|
||||
onConfirm={onDeleteProcessor}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.deleteProcessorModalCancel',
|
||||
{ defaultMessage: 'Keep processor' }
|
||||
)}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.deleteProcessorModalConfirm',
|
||||
{ defaultMessage: 'Delete processor' }
|
||||
)}
|
||||
buttonColor="danger"
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.deleteProcessorModalBody',
|
||||
{
|
||||
defaultMessage:
|
||||
'You can still reset this until the changes are confirmed on the processors list.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,204 +0,0 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { EuiCallOut, EuiForm, EuiButton, EuiSpacer, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ReadStreamDefinition } from '@kbn/streams-schema';
|
||||
import { isEqual } from 'lodash';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import { ProcessorTypeSelector } from './processor_type_selector';
|
||||
import { ProcessorFlyoutTemplate } from './processor_flyout_template';
|
||||
import {
|
||||
DetectedField,
|
||||
EnrichmentUIProcessorDefinition,
|
||||
ProcessingDefinition,
|
||||
ProcessorFormState,
|
||||
} from '../types';
|
||||
import { DangerZone } from './danger_zone';
|
||||
import { DissectProcessorForm } from './dissect';
|
||||
import { GrokProcessorForm } from './grok';
|
||||
import { convertFormStateToProcessing, getDefaultFormState } from '../utils';
|
||||
import { useProcessingSimulator } from '../hooks/use_processing_simulator';
|
||||
|
||||
const ProcessorOutcomePreview = dynamic(() =>
|
||||
import(/* webpackChunkName: "management_processor_outcome" */ './processor_outcome_preview').then(
|
||||
(mod) => ({
|
||||
default: mod.ProcessorOutcomePreview,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
export interface ProcessorFlyoutProps {
|
||||
definition: ReadStreamDefinition;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface AddProcessorFlyoutProps extends ProcessorFlyoutProps {
|
||||
onAddProcessor: (newProcessing: ProcessingDefinition, newFields?: DetectedField[]) => void;
|
||||
}
|
||||
export interface EditProcessorFlyoutProps extends ProcessorFlyoutProps {
|
||||
processor: EnrichmentUIProcessorDefinition;
|
||||
onDeleteProcessor: (id: string) => void;
|
||||
onUpdateProcessor: (id: string, processor: EnrichmentUIProcessorDefinition) => void;
|
||||
}
|
||||
|
||||
export function AddProcessorFlyout({
|
||||
definition,
|
||||
onAddProcessor,
|
||||
onClose,
|
||||
}: AddProcessorFlyoutProps) {
|
||||
const defaultValues = useMemo(() => getDefaultFormState('grok'), []);
|
||||
|
||||
const methods = useForm<ProcessorFormState>({ defaultValues, mode: 'onChange' });
|
||||
|
||||
const formFields = methods.watch();
|
||||
|
||||
const hasChanges = useMemo(
|
||||
() => !isEqual(defaultValues, formFields),
|
||||
[defaultValues, formFields]
|
||||
);
|
||||
|
||||
const { error, isLoading, refreshSamples, simulation, samples, simulate } =
|
||||
useProcessingSimulator({
|
||||
definition,
|
||||
condition: { field: formFields.field, operator: 'exists' },
|
||||
});
|
||||
|
||||
const handleSubmit: SubmitHandler<ProcessorFormState> = async (data) => {
|
||||
const processingDefinition = convertFormStateToProcessing(data);
|
||||
|
||||
simulate(processingDefinition, data.detected_fields).then((responseBody) => {
|
||||
if (responseBody instanceof Error) return;
|
||||
|
||||
onAddProcessor(processingDefinition, data.detected_fields);
|
||||
onClose();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ProcessorFlyoutTemplate
|
||||
shouldConfirm={hasChanges}
|
||||
onClose={onClose}
|
||||
title={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.titleAdd',
|
||||
{ defaultMessage: 'Add processor' }
|
||||
)}
|
||||
confirmButton={
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppAddProcessorFlyoutAddProcessorButton"
|
||||
onClick={methods.handleSubmit(handleSubmit)}
|
||||
disabled={!methods.formState.isValid && methods.formState.isSubmitted}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.confirmAddProcessor',
|
||||
{ defaultMessage: 'Add processor' }
|
||||
)}
|
||||
</EuiButton>
|
||||
}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<EuiForm component="form" fullWidth onSubmit={methods.handleSubmit(handleSubmit)}>
|
||||
<ProcessorTypeSelector />
|
||||
<EuiSpacer size="m" />
|
||||
{formFields.type === 'grok' && <GrokProcessorForm />}
|
||||
{formFields.type === 'dissect' && <DissectProcessorForm />}
|
||||
</EuiForm>
|
||||
<EuiHorizontalRule />
|
||||
<ProcessorOutcomePreview
|
||||
definition={definition}
|
||||
formFields={formFields}
|
||||
simulation={simulation}
|
||||
samples={samples}
|
||||
onSimulate={simulate}
|
||||
onRefreshSamples={refreshSamples}
|
||||
simulationError={error}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</FormProvider>
|
||||
</ProcessorFlyoutTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditProcessorFlyout({
|
||||
onClose,
|
||||
onDeleteProcessor,
|
||||
onUpdateProcessor,
|
||||
processor,
|
||||
}: EditProcessorFlyoutProps) {
|
||||
const processorType = 'grok' in processor.config ? 'grok' : 'dissect';
|
||||
|
||||
const defaultValues = useMemo(
|
||||
() => getDefaultFormState(processorType, processor),
|
||||
[processor, processorType]
|
||||
);
|
||||
|
||||
const methods = useForm<ProcessorFormState>({ defaultValues, mode: 'onChange' });
|
||||
|
||||
const formFields = methods.watch();
|
||||
|
||||
const hasChanges = useMemo(
|
||||
() => !isEqual(defaultValues, formFields),
|
||||
[defaultValues, formFields]
|
||||
);
|
||||
|
||||
const handleSubmit: SubmitHandler<ProcessorFormState> = (data) => {
|
||||
const processingDefinition = convertFormStateToProcessing(data);
|
||||
|
||||
onUpdateProcessor(processor.id, { id: processor.id, ...processingDefinition });
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleProcessorDelete = () => {
|
||||
onDeleteProcessor(processor.id);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ProcessorFlyoutTemplate
|
||||
shouldConfirm={hasChanges}
|
||||
onClose={onClose}
|
||||
title={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.titleEdit',
|
||||
{ defaultMessage: 'Edit processor' }
|
||||
)}
|
||||
banner={
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.calloutEdit',
|
||||
{ defaultMessage: 'Outcome preview is not available during edition' }
|
||||
)}
|
||||
iconType="iInCircle"
|
||||
/>
|
||||
}
|
||||
confirmButton={
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppEditProcessorFlyoutUpdateProcessorButton"
|
||||
onClick={methods.handleSubmit(handleSubmit)}
|
||||
disabled={!methods.formState.isValid}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.confirmEditProcessor',
|
||||
{ defaultMessage: 'Update processor' }
|
||||
)}
|
||||
</EuiButton>
|
||||
}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<EuiForm component="form" fullWidth onSubmit={methods.handleSubmit(handleSubmit)}>
|
||||
<ProcessorTypeSelector disabled />
|
||||
<EuiSpacer size="m" />
|
||||
{formFields.type === 'grok' && <GrokProcessorForm />}
|
||||
{formFields.type === 'dissect' && <DissectProcessorForm />}
|
||||
<EuiHorizontalRule />
|
||||
<DangerZone onDeleteProcessor={handleProcessorDelete} />
|
||||
</EuiForm>
|
||||
</FormProvider>
|
||||
</ProcessorFlyoutTemplate>
|
||||
);
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* 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, { PropsWithChildren } from 'react';
|
||||
import {
|
||||
EuiFlyoutResizable,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlexGroup,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDiscardConfirm } from '../../../hooks/use_discard_confirm';
|
||||
|
||||
interface ProcessorFlyoutTemplateProps {
|
||||
banner?: React.ReactNode;
|
||||
confirmButton?: React.ReactNode;
|
||||
onClose: () => void;
|
||||
shouldConfirm?: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function ProcessorFlyoutTemplate({
|
||||
banner,
|
||||
children,
|
||||
confirmButton,
|
||||
onClose,
|
||||
shouldConfirm = false,
|
||||
title,
|
||||
}: PropsWithChildren<ProcessorFlyoutTemplateProps>) {
|
||||
const handleClose = useDiscardConfirm(onClose);
|
||||
|
||||
const closeHandler = shouldConfirm ? handleClose : onClose;
|
||||
|
||||
return (
|
||||
<EuiFlyoutResizable onClose={closeHandler}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>{title}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody banner={banner}>{children}</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="streamsAppProcessorFlyoutTemplateCancelButton"
|
||||
iconType="cross"
|
||||
onClick={closeHandler}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.cancel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
{confirmButton}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyoutResizable>
|
||||
);
|
||||
}
|
|
@ -1,416 +0,0 @@
|
|||
/*
|
||||
* 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, { useEffect, useMemo, useState } from 'react';
|
||||
import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingLogo,
|
||||
EuiButton,
|
||||
EuiFormRow,
|
||||
EuiSuperSelectOption,
|
||||
EuiSuperSelect,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { FieldIcon } from '@kbn/react-field';
|
||||
import {
|
||||
FIELD_DEFINITION_TYPES,
|
||||
ReadStreamDefinition,
|
||||
isWiredReadStream,
|
||||
} from '@kbn/streams-schema';
|
||||
import { UseControllerProps, useController, useFieldArray } from 'react-hook-form';
|
||||
import { css } from '@emotion/react';
|
||||
import { flattenObject } from '@kbn/object-utils';
|
||||
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { StreamsAppSearchBar, StreamsAppSearchBarProps } from '../../streams_app_search_bar';
|
||||
import { PreviewTable } from '../../preview_table';
|
||||
import { convertFormStateToProcessing } from '../utils';
|
||||
import { DetectedField, ProcessorFormState } from '../types';
|
||||
import { UseProcessingSimulatorReturnType } from '../hooks/use_processing_simulator';
|
||||
|
||||
interface ProcessorOutcomePreviewProps {
|
||||
definition: ReadStreamDefinition;
|
||||
formFields: ProcessorFormState;
|
||||
isLoading: UseProcessingSimulatorReturnType['isLoading'];
|
||||
simulation: UseProcessingSimulatorReturnType['simulation'];
|
||||
samples: UseProcessingSimulatorReturnType['samples'];
|
||||
onRefreshSamples: UseProcessingSimulatorReturnType['refreshSamples'];
|
||||
onSimulate: UseProcessingSimulatorReturnType['simulate'];
|
||||
simulationError: UseProcessingSimulatorReturnType['error'];
|
||||
}
|
||||
|
||||
export const ProcessorOutcomePreview = ({
|
||||
definition,
|
||||
formFields,
|
||||
isLoading,
|
||||
simulation,
|
||||
samples,
|
||||
onRefreshSamples,
|
||||
onSimulate,
|
||||
simulationError,
|
||||
}: ProcessorOutcomePreviewProps) => {
|
||||
const { dependencies } = useKibana();
|
||||
const { data } = dependencies.start;
|
||||
|
||||
const { timeRange, setTimeRange } = useDateRange({ data });
|
||||
|
||||
const [selectedDocsFilter, setSelectedDocsFilter] =
|
||||
useState<DocsFilterOption>('outcome_filter_all');
|
||||
|
||||
const simulationDocuments = useMemo(() => {
|
||||
if (!simulation?.documents) {
|
||||
return samples.map((doc) => flattenObject(doc));
|
||||
}
|
||||
|
||||
const filterDocuments = (filter: DocsFilterOption) => {
|
||||
switch (filter) {
|
||||
case 'outcome_filter_matched':
|
||||
return simulation.documents.filter((doc) => doc.isMatch);
|
||||
case 'outcome_filter_unmatched':
|
||||
return simulation.documents.filter((doc) => !doc.isMatch);
|
||||
case 'outcome_filter_all':
|
||||
default:
|
||||
return simulation.documents;
|
||||
}
|
||||
};
|
||||
|
||||
return filterDocuments(selectedDocsFilter).map((doc) => doc.value);
|
||||
}, [samples, simulation?.documents, selectedDocsFilter]);
|
||||
|
||||
const detectedFieldsColumns = useMemo(
|
||||
() =>
|
||||
simulation?.detected_fields ? simulation.detected_fields.map((field) => field.name) : [],
|
||||
[simulation?.detected_fields]
|
||||
);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
switch (selectedDocsFilter) {
|
||||
case 'outcome_filter_unmatched':
|
||||
return [formFields.field];
|
||||
case 'outcome_filter_matched':
|
||||
case 'outcome_filter_all':
|
||||
default:
|
||||
return [formFields.field, ...detectedFieldsColumns];
|
||||
}
|
||||
}, [formFields.field, detectedFieldsColumns, selectedDocsFilter]);
|
||||
|
||||
const detectedFieldsEnabled =
|
||||
isWiredReadStream(definition) &&
|
||||
((simulation && !isEmpty(simulation.detected_fields)) || !isEmpty(formFields.detected_fields));
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} paddingSize="none">
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeTitle',
|
||||
{ defaultMessage: 'Outcome' }
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppProcessorOutcomePreviewRunSimulationButton"
|
||||
iconType="play"
|
||||
color="accentSecondary"
|
||||
size="s"
|
||||
onClick={() => {
|
||||
onSimulate(convertFormStateToProcessing(formFields), formFields.detected_fields);
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.runSimulation',
|
||||
{ defaultMessage: 'Run simulation' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{detectedFieldsEnabled && <DetectedFields detectedFields={simulation?.detected_fields} />}
|
||||
<OutcomeControls
|
||||
docsFilter={selectedDocsFilter}
|
||||
onDocsFilterChange={setSelectedDocsFilter}
|
||||
timeRange={timeRange}
|
||||
onTimeRangeChange={setTimeRange}
|
||||
onTimeRangeRefresh={onRefreshSamples}
|
||||
simulationFailureRate={simulation?.failure_rate}
|
||||
simulationSuccessRate={simulation?.success_rate}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<OutcomePreviewTable
|
||||
documents={simulationDocuments}
|
||||
columns={tableColumns}
|
||||
error={simulationError}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
const docsFilterOptions = {
|
||||
outcome_filter_all: {
|
||||
id: 'outcome_filter_all',
|
||||
label: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeControls.all',
|
||||
{ defaultMessage: 'All samples' }
|
||||
),
|
||||
},
|
||||
outcome_filter_matched: {
|
||||
id: 'outcome_filter_matched',
|
||||
label: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeControls.matched',
|
||||
{ defaultMessage: 'Matched' }
|
||||
),
|
||||
},
|
||||
outcome_filter_unmatched: {
|
||||
id: 'outcome_filter_unmatched',
|
||||
label: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeControls.unmatched',
|
||||
{ defaultMessage: 'Unmatched' }
|
||||
),
|
||||
},
|
||||
} as const;
|
||||
|
||||
type DocsFilterOption = keyof typeof docsFilterOptions;
|
||||
|
||||
interface OutcomeControlsProps {
|
||||
docsFilter: DocsFilterOption;
|
||||
timeRange: TimeRange;
|
||||
onDocsFilterChange: (filter: DocsFilterOption) => void;
|
||||
onTimeRangeChange: (timeRange: TimeRange) => void;
|
||||
onTimeRangeRefresh: () => void;
|
||||
simulationFailureRate?: number;
|
||||
simulationSuccessRate?: number;
|
||||
}
|
||||
|
||||
const OutcomeControls = ({
|
||||
docsFilter,
|
||||
timeRange,
|
||||
onDocsFilterChange,
|
||||
onTimeRangeChange,
|
||||
onTimeRangeRefresh,
|
||||
simulationFailureRate,
|
||||
simulationSuccessRate,
|
||||
}: OutcomeControlsProps) => {
|
||||
const handleQuerySubmit: StreamsAppSearchBarProps['onQuerySubmit'] = (
|
||||
{ dateRange },
|
||||
isUpdate
|
||||
) => {
|
||||
if (!isUpdate) {
|
||||
return onTimeRangeRefresh();
|
||||
}
|
||||
|
||||
if (dateRange) {
|
||||
onTimeRangeChange({
|
||||
from: dateRange.from,
|
||||
to: dateRange?.to,
|
||||
mode: dateRange.mode,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" wrap>
|
||||
<EuiFilterGroup
|
||||
aria-label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomeControlsAriaLabel',
|
||||
{ defaultMessage: 'Filter for all, matching or unmatching previewed documents.' }
|
||||
)}
|
||||
>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={docsFilter === docsFilterOptions.outcome_filter_all.id}
|
||||
onClick={() => onDocsFilterChange(docsFilterOptions.outcome_filter_all.id)}
|
||||
>
|
||||
{docsFilterOptions.outcome_filter_all.label}
|
||||
</EuiFilterButton>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={docsFilter === docsFilterOptions.outcome_filter_matched.id}
|
||||
onClick={() => onDocsFilterChange(docsFilterOptions.outcome_filter_matched.id)}
|
||||
badgeColor="success"
|
||||
numActiveFilters={
|
||||
simulationSuccessRate ? parseFloat((simulationSuccessRate * 100).toFixed(2)) : undefined
|
||||
}
|
||||
>
|
||||
{docsFilterOptions.outcome_filter_matched.label}
|
||||
</EuiFilterButton>
|
||||
<EuiFilterButton
|
||||
hasActiveFilters={docsFilter === docsFilterOptions.outcome_filter_unmatched.id}
|
||||
onClick={() => onDocsFilterChange(docsFilterOptions.outcome_filter_unmatched.id)}
|
||||
badgeColor="accent"
|
||||
numActiveFilters={
|
||||
simulationFailureRate ? parseFloat((simulationFailureRate * 100).toFixed(2)) : undefined
|
||||
}
|
||||
>
|
||||
{docsFilterOptions.outcome_filter_unmatched.label}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
<StreamsAppSearchBar
|
||||
onQuerySubmit={handleQuerySubmit}
|
||||
onRefresh={onTimeRangeRefresh}
|
||||
dateRangeFrom={timeRange.from}
|
||||
dateRangeTo={timeRange.to}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const DetectedFields = ({ detectedFields }: { detectedFields?: DetectedField[] }) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { fields, replace } = useFieldArray<{ detected_fields: DetectedField[] }>({
|
||||
name: 'detected_fields',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (detectedFields) replace(detectedFields);
|
||||
}, [detectedFields, replace]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.detectedFieldsLabel',
|
||||
{ defaultMessage: 'Detected fields' }
|
||||
)}
|
||||
css={css`
|
||||
margin-bottom: ${euiTheme.size.l};
|
||||
`}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" wrap>
|
||||
{fields.map((field, id) => (
|
||||
<DetectedFieldSelector key={field.name} name={`detected_fields.${id}`} />
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
const DetectedFieldSelector = (
|
||||
props: UseControllerProps<ProcessorFormState, `detected_fields.${number}`>
|
||||
) => {
|
||||
const { field } = useController(props);
|
||||
|
||||
const options = useMemo(() => getDetectedFieldSelectOptions(field.value), [field.value]);
|
||||
|
||||
return (
|
||||
<EuiSuperSelect
|
||||
options={options}
|
||||
valueOfSelected={field.value.type}
|
||||
onChange={(type) => field.onChange({ ...field.value, type })}
|
||||
css={css`
|
||||
min-inline-size: 180px;
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getDetectedFieldSelectOptions = (
|
||||
fieldValue: DetectedField
|
||||
): Array<EuiSuperSelectOption<string>> =>
|
||||
[...FIELD_DEFINITION_TYPES, 'unmapped'].map((type) => ({
|
||||
value: type,
|
||||
inputDisplay: (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<FieldIcon type={fieldValue.type} size="s" />
|
||||
{fieldValue.name}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
dropdownDisplay: (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<FieldIcon type={type} size="s" />
|
||||
{type}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
}));
|
||||
|
||||
interface OutcomePreviewTableProps {
|
||||
documents?: Array<Record<PropertyKey, unknown>>;
|
||||
columns: string[];
|
||||
error?: IHttpFetchError<ResponseErrorBody>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const OutcomePreviewTable = ({
|
||||
documents = [],
|
||||
columns,
|
||||
error,
|
||||
isLoading,
|
||||
}: OutcomePreviewTableProps) => {
|
||||
if (error) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="error"
|
||||
color="danger"
|
||||
title={
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomePreviewTable.errorTitle',
|
||||
{ defaultMessage: 'Unable to display the simulation outcome for this processor.' }
|
||||
)}
|
||||
</h3>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomePreviewTable.errorBody',
|
||||
{ defaultMessage: 'The processor did not run correctly.' }
|
||||
)}
|
||||
</p>
|
||||
{error.body?.message ? <p>{error.body.message}</p> : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={<EuiLoadingLogo logo="logoLogging" size="l" />}
|
||||
title={
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomePreviewTable.loadingTitle',
|
||||
{ defaultMessage: 'Running processor simulation' }
|
||||
)}
|
||||
</h3>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (documents?.length === 0) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="dataVisualizer"
|
||||
body={
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.outcomePreviewTable.noDataTitle',
|
||||
{
|
||||
defaultMessage:
|
||||
'There are no simulation outcome documents for the current selection.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <PreviewTable documents={documents} displayColumns={columns} height={500} />;
|
||||
};
|
|
@ -5,26 +5,44 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useState, useMemo, useEffect, useRef, useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAbortController } from '@kbn/observability-utils-browser/hooks/use_abort_controller';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
import {
|
||||
ReadStreamDefinition,
|
||||
isWiredReadStream,
|
||||
IngestStreamGetResponse,
|
||||
isWiredStreamGetResponse,
|
||||
FieldDefinition,
|
||||
WiredReadStreamDefinition,
|
||||
ProcessorDefinition,
|
||||
getProcessorConfig,
|
||||
WiredStreamGetResponse,
|
||||
IngestUpsertRequest,
|
||||
ProcessorDefinition,
|
||||
getProcessorType,
|
||||
} from '@kbn/streams-schema';
|
||||
import { htmlIdGenerator } from '@elastic/eui';
|
||||
import { isEqual, omit } from 'lodash';
|
||||
import { DetectedField, EnrichmentUIProcessorDefinition, ProcessingDefinition } from '../types';
|
||||
import { DetectedField, ProcessorDefinitionWithUIAttributes } from '../types';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { alwaysToEmptyEquals, emptyEqualsToAlways } from '../../../util/condition';
|
||||
import { processorConverter } from '../utils';
|
||||
|
||||
export const useDefinition = (definition: ReadStreamDefinition, refreshDefinition: () => void) => {
|
||||
export interface UseDefinitionReturn {
|
||||
processors: ProcessorDefinitionWithUIAttributes[];
|
||||
hasChanges: boolean;
|
||||
isSavingChanges: boolean;
|
||||
addProcessor: (newProcessor: ProcessorDefinition, newFields?: DetectedField[]) => void;
|
||||
updateProcessor: (
|
||||
id: string,
|
||||
processor: ProcessorDefinition,
|
||||
status?: ProcessorDefinitionWithUIAttributes['status']
|
||||
) => void;
|
||||
deleteProcessor: (id: string) => void;
|
||||
reorderProcessors: (processors: ProcessorDefinitionWithUIAttributes[]) => void;
|
||||
saveChanges: () => Promise<void>;
|
||||
setProcessors: (processors: ProcessorDefinitionWithUIAttributes[]) => void;
|
||||
resetChanges: () => void;
|
||||
}
|
||||
|
||||
export const useDefinition = (
|
||||
definition: IngestStreamGetResponse,
|
||||
refreshDefinition: () => void
|
||||
): UseDefinitionReturn => {
|
||||
const { core, dependencies } = useKibana();
|
||||
|
||||
const { toasts } = core.notifications;
|
||||
|
@ -37,46 +55,79 @@ export const useDefinition = (definition: ReadStreamDefinition, refreshDefinitio
|
|||
const [processors, setProcessors] = useState(() =>
|
||||
createProcessorsList(existingProcessorDefinitions)
|
||||
);
|
||||
|
||||
const initialProcessors = useRef(processors);
|
||||
|
||||
const [fields, setFields] = useState(() =>
|
||||
isWiredReadStream(definition) ? definition.stream.ingest.wired.fields : {}
|
||||
isWiredStreamGetResponse(definition) ? definition.stream.ingest.wired.fields : {}
|
||||
);
|
||||
|
||||
const nextProcessorDefinitions = useMemo(
|
||||
() => processors.map(convertUiDefinitionIntoApiDefinition),
|
||||
() => processors.map(processorConverter.toAPIDefinition),
|
||||
[processors]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset processors when definition refreshes
|
||||
setProcessors(createProcessorsList(definition.stream.ingest.processing));
|
||||
const resetProcessors = createProcessorsList(definition.stream.ingest.processing);
|
||||
setProcessors(resetProcessors);
|
||||
initialProcessors.current = resetProcessors;
|
||||
}, [definition]);
|
||||
|
||||
const hasChanges = useMemo(
|
||||
() => !isEqual(existingProcessorDefinitions, nextProcessorDefinitions),
|
||||
[existingProcessorDefinitions, nextProcessorDefinitions]
|
||||
() =>
|
||||
processors.length !== initialProcessors.current.length || // Processor count changed, a processor might be deleted
|
||||
processors.some((proc) => proc.status === 'draft' || proc.status === 'updated') || // New or updated processors
|
||||
hasOrderChanged(processors, initialProcessors.current), // Processor order changed
|
||||
[processors]
|
||||
);
|
||||
|
||||
const addProcessor = (newProcessing: ProcessingDefinition, newFields?: DetectedField[]) => {
|
||||
setProcessors((prevProcs) => prevProcs.concat({ ...newProcessing, id: createId() }));
|
||||
const addProcessor = useCallback(
|
||||
(newProcessor: ProcessorDefinition, newFields?: DetectedField[]) => {
|
||||
setProcessors((prevProcs) =>
|
||||
prevProcs.concat(processorConverter.toUIDefinition(newProcessor, { status: 'draft' }))
|
||||
);
|
||||
|
||||
if (isWiredReadStream(definition) && newFields) {
|
||||
setFields((currentFields) => mergeFields(definition, currentFields, newFields));
|
||||
}
|
||||
};
|
||||
if (isWiredStreamGetResponse(definition) && newFields) {
|
||||
setFields((currentFields) => mergeFields(definition, currentFields, newFields));
|
||||
}
|
||||
},
|
||||
[definition]
|
||||
);
|
||||
|
||||
const updateProcessor = (id: string, processorUpdate: EnrichmentUIProcessorDefinition) => {
|
||||
setProcessors((prevProcs) =>
|
||||
prevProcs.map((proc) => (proc.id === id ? processorUpdate : proc))
|
||||
);
|
||||
};
|
||||
const updateProcessor = useCallback(
|
||||
(
|
||||
id: string,
|
||||
processorUpdate: ProcessorDefinition,
|
||||
status: ProcessorDefinitionWithUIAttributes['status'] = 'updated'
|
||||
) => {
|
||||
setProcessors((prevProcs) =>
|
||||
prevProcs.map((proc) =>
|
||||
proc.id === id
|
||||
? {
|
||||
...processorUpdate,
|
||||
id,
|
||||
type: getProcessorType(processorUpdate),
|
||||
status,
|
||||
}
|
||||
: proc
|
||||
)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const deleteProcessor = (id: string) => {
|
||||
const reorderProcessors = setProcessors;
|
||||
|
||||
const deleteProcessor = useCallback((id: string) => {
|
||||
setProcessors((prevProcs) => prevProcs.filter((proc) => proc.id !== id));
|
||||
};
|
||||
}, []);
|
||||
|
||||
const resetChanges = () => {
|
||||
setProcessors(createProcessorsList(existingProcessorDefinitions));
|
||||
setFields(isWiredReadStream(definition) ? definition.stream.ingest.wired.fields : {});
|
||||
const resetProcessors = createProcessorsList(existingProcessorDefinitions);
|
||||
setProcessors(resetProcessors);
|
||||
initialProcessors.current = resetProcessors;
|
||||
setFields(isWiredStreamGetResponse(definition) ? definition.stream.ingest.wired.fields : {});
|
||||
};
|
||||
|
||||
const saveChanges = async () => {
|
||||
|
@ -86,13 +137,13 @@ export const useDefinition = (definition: ReadStreamDefinition, refreshDefinitio
|
|||
signal: abortController.signal,
|
||||
params: {
|
||||
path: {
|
||||
id: definition.name,
|
||||
id: definition.stream.name,
|
||||
},
|
||||
body: {
|
||||
ingest: {
|
||||
...definition.stream.ingest,
|
||||
processing: nextProcessorDefinitions,
|
||||
...(isWiredReadStream(definition) && { wired: { fields } }),
|
||||
...(isWiredStreamGetResponse(definition) && { wired: { fields } }),
|
||||
},
|
||||
} as IngestUpsertRequest,
|
||||
},
|
||||
|
@ -104,8 +155,10 @@ export const useDefinition = (definition: ReadStreamDefinition, refreshDefinitio
|
|||
{ defaultMessage: "Stream's processors updated" }
|
||||
)
|
||||
);
|
||||
|
||||
refreshDefinition();
|
||||
} catch (error) {
|
||||
toasts.addError(error, {
|
||||
toasts.addError(new Error(error.body.message), {
|
||||
title: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.saveChangesError',
|
||||
{ defaultMessage: "An issue occurred saving processors' changes." }
|
||||
|
@ -113,7 +166,6 @@ export const useDefinition = (definition: ReadStreamDefinition, refreshDefinitio
|
|||
toastMessage: error.body.message,
|
||||
});
|
||||
} finally {
|
||||
await refreshDefinition();
|
||||
endsSaving();
|
||||
}
|
||||
};
|
||||
|
@ -125,6 +177,7 @@ export const useDefinition = (definition: ReadStreamDefinition, refreshDefinitio
|
|||
addProcessor,
|
||||
updateProcessor,
|
||||
deleteProcessor,
|
||||
reorderProcessors,
|
||||
resetChanges,
|
||||
saveChanges,
|
||||
setProcessors,
|
||||
|
@ -134,46 +187,19 @@ export const useDefinition = (definition: ReadStreamDefinition, refreshDefinitio
|
|||
};
|
||||
};
|
||||
|
||||
const createId = htmlIdGenerator();
|
||||
const createProcessorsList = (
|
||||
processors: ProcessorDefinition[]
|
||||
): EnrichmentUIProcessorDefinition[] => processors.map(createProcessorWithId);
|
||||
const createProcessorsList = (processors: ProcessorDefinition[]) => {
|
||||
return processors.map((processor) => processorConverter.toUIDefinition(processor));
|
||||
};
|
||||
|
||||
const createProcessorWithId = (
|
||||
processor: ProcessorDefinition
|
||||
): EnrichmentUIProcessorDefinition => ({
|
||||
condition: alwaysToEmptyEquals(getProcessorConfig(processor).if),
|
||||
config: {
|
||||
...('grok' in processor
|
||||
? { grok: omit(processor.grok, 'if') }
|
||||
: { dissect: omit(processor.dissect, 'if') }),
|
||||
},
|
||||
id: createId(),
|
||||
});
|
||||
|
||||
const convertUiDefinitionIntoApiDefinition = (
|
||||
processor: EnrichmentUIProcessorDefinition
|
||||
): ProcessorDefinition => {
|
||||
const { id: _id, config, condition } = processor;
|
||||
|
||||
if ('grok' in config) {
|
||||
return {
|
||||
grok: {
|
||||
...config.grok,
|
||||
if: emptyEqualsToAlways(condition),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
dissect: {
|
||||
...config.dissect,
|
||||
if: emptyEqualsToAlways(condition),
|
||||
},
|
||||
};
|
||||
const hasOrderChanged = (
|
||||
processors: ProcessorDefinitionWithUIAttributes[],
|
||||
initialProcessors: ProcessorDefinitionWithUIAttributes[]
|
||||
) => {
|
||||
return processors.some((processor, index) => processor.id !== initialProcessors[index].id);
|
||||
};
|
||||
|
||||
const mergeFields = (
|
||||
definition: WiredReadStreamDefinition,
|
||||
definition: WiredStreamGetResponse,
|
||||
currentFields: FieldDefinition,
|
||||
newFields: DetectedField[]
|
||||
) => {
|
||||
|
|
|
@ -5,49 +5,53 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { useAbortController } from '@kbn/observability-utils-browser/hooks/use_abort_controller';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { debounce, isEmpty, uniq, uniqBy } from 'lodash';
|
||||
import {
|
||||
DissectProcessorDefinition,
|
||||
ReadStreamDefinition,
|
||||
IngestStreamGetResponse,
|
||||
getProcessorConfig,
|
||||
UnaryOperator,
|
||||
Condition,
|
||||
ProcessorDefinition,
|
||||
GrokProcessorDefinition,
|
||||
processorDefinitionSchema,
|
||||
isSchema,
|
||||
} from '@kbn/streams-schema';
|
||||
import useAsyncFn from 'react-use/lib/useAsyncFn';
|
||||
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range';
|
||||
import { APIReturnType, StreamsAPIClientRequestParamsOf } from '@kbn/streams-plugin/public/api';
|
||||
import { APIReturnType } from '@kbn/streams-plugin/public/api';
|
||||
import { useStreamsAppFetch } from '../../../hooks/use_streams_app_fetch';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { ProcessingDefinition } from '../types';
|
||||
import { DetectedField } from '../types';
|
||||
import { emptyEqualsToAlways } from '../../../util/condition';
|
||||
import { DetectedField, ProcessorDefinitionWithUIAttributes } from '../types';
|
||||
import { processorConverter } from '../utils';
|
||||
|
||||
type Simulation = APIReturnType<'POST /api/streams/{id}/processing/_simulate'>;
|
||||
type SimulationRequestBody =
|
||||
StreamsAPIClientRequestParamsOf<'POST /api/streams/{id}/processing/_simulate'>['params']['body'];
|
||||
|
||||
export interface UseProcessingSimulatorReturnType {
|
||||
export interface TableColumn {
|
||||
name: string;
|
||||
origin: 'processor' | 'detected';
|
||||
}
|
||||
|
||||
export interface UseProcessingSimulatorProps {
|
||||
definition: IngestStreamGetResponse;
|
||||
processors: ProcessorDefinitionWithUIAttributes[];
|
||||
}
|
||||
|
||||
export interface UseProcessingSimulatorReturn {
|
||||
hasLiveChanges: boolean;
|
||||
error?: IHttpFetchError<ResponseErrorBody>;
|
||||
isLoading: boolean;
|
||||
refreshSamples: () => void;
|
||||
samples: Array<Record<PropertyKey, unknown>>;
|
||||
simulate: (
|
||||
processing: ProcessingDefinition,
|
||||
detectedFields?: DetectedField[]
|
||||
) => Promise<Simulation | null>;
|
||||
simulation?: Simulation | null;
|
||||
tableColumns: TableColumn[];
|
||||
refreshSamples: () => void;
|
||||
watchProcessor: (
|
||||
processor: ProcessorDefinitionWithUIAttributes | { id: string; deleteIfExists: true }
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const useProcessingSimulator = ({
|
||||
definition,
|
||||
condition,
|
||||
}: {
|
||||
definition: ReadStreamDefinition;
|
||||
condition?: Condition;
|
||||
}): UseProcessingSimulatorReturnType => {
|
||||
processors,
|
||||
}: UseProcessingSimulatorProps): UseProcessingSimulatorReturn => {
|
||||
const { dependencies } = useKibana();
|
||||
const {
|
||||
data,
|
||||
|
@ -58,9 +62,57 @@ export const useProcessingSimulator = ({
|
|||
absoluteTimeRange: { start, end },
|
||||
} = useDateRange({ data });
|
||||
|
||||
const abortController = useAbortController();
|
||||
const draftProcessors = useMemo(
|
||||
() => processors.filter((processor) => processor.status === 'draft'),
|
||||
[processors]
|
||||
);
|
||||
|
||||
const serializedCondition = JSON.stringify(condition);
|
||||
const [liveDraftProcessors, setLiveDraftProcessors] = useState(draftProcessors);
|
||||
|
||||
useEffect(() => {
|
||||
setLiveDraftProcessors((prevLiveProcessors) => {
|
||||
const inProgressDraft = prevLiveProcessors.find((proc) => proc.id === 'draft');
|
||||
return inProgressDraft ? [...draftProcessors, inProgressDraft] : draftProcessors;
|
||||
});
|
||||
}, [draftProcessors]);
|
||||
|
||||
const watchProcessor = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
(processor: ProcessorDefinitionWithUIAttributes | { id: string; deleteIfExists: true }) => {
|
||||
if ('deleteIfExists' in processor) {
|
||||
return setLiveDraftProcessors((prevLiveDraftProcessors) =>
|
||||
prevLiveDraftProcessors.filter((proc) => proc.id !== processor.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (processor.status === 'draft') {
|
||||
setLiveDraftProcessors((prevLiveDraftProcessors) => {
|
||||
const newLiveDraftProcessors = prevLiveDraftProcessors.slice();
|
||||
|
||||
const existingIndex = prevLiveDraftProcessors.findIndex(
|
||||
(proc) => proc.id === processor.id
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
newLiveDraftProcessors[existingIndex] = processor;
|
||||
} else {
|
||||
newLiveDraftProcessors.push(processor);
|
||||
}
|
||||
|
||||
return newLiveDraftProcessors;
|
||||
});
|
||||
}
|
||||
},
|
||||
500
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const samplingCondition = useMemo(
|
||||
() => composeSamplingCondition(liveDraftProcessors),
|
||||
[liveDraftProcessors]
|
||||
);
|
||||
|
||||
const {
|
||||
loading: isLoadingSamples,
|
||||
|
@ -75,9 +127,9 @@ export const useProcessingSimulator = ({
|
|||
return streamsRepositoryClient.fetch('POST /api/streams/{id}/_sample', {
|
||||
signal,
|
||||
params: {
|
||||
path: { id: definition.name },
|
||||
path: { id: definition.stream.name },
|
||||
body: {
|
||||
if: condition ? emptyEqualsToAlways(condition) : { always: {} },
|
||||
if: samplingCondition,
|
||||
start: start?.valueOf(),
|
||||
end: end?.valueOf(),
|
||||
size: 100,
|
||||
|
@ -85,69 +137,103 @@ export const useProcessingSimulator = ({
|
|||
},
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[definition, streamsRepositoryClient, start, end, serializedCondition],
|
||||
[definition, streamsRepositoryClient, start, end, samplingCondition],
|
||||
{ disableToastOnError: true }
|
||||
);
|
||||
|
||||
const sampleDocs = (samples?.documents ?? []) as Array<Record<PropertyKey, unknown>>;
|
||||
const sampleDocs = samples?.documents as Array<Record<PropertyKey, unknown>>;
|
||||
|
||||
const [{ loading: isLoadingSimulation, error, value }, simulate] = useAsyncFn(
|
||||
(processingDefinition: ProcessingDefinition, detectedFields?: DetectedField[]) => {
|
||||
if (!definition) {
|
||||
const {
|
||||
loading: isLoadingSimulation,
|
||||
value: simulation,
|
||||
error: simulationError,
|
||||
} = useStreamsAppFetch(
|
||||
({ signal }) => {
|
||||
if (!definition || isEmpty(sampleDocs) || isEmpty(liveDraftProcessors)) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const processorDefinition: ProcessorDefinition =
|
||||
'grok' in processingDefinition.config
|
||||
? ({
|
||||
grok: {
|
||||
field: processingDefinition.config.grok.field,
|
||||
ignore_failure: processingDefinition.config.grok.ignore_failure,
|
||||
ignore_missing: processingDefinition.config.grok.ignore_missing,
|
||||
if: emptyEqualsToAlways(processingDefinition.condition),
|
||||
patterns: processingDefinition.config.grok.patterns,
|
||||
pattern_definitions: processingDefinition.config.grok.pattern_definitions,
|
||||
},
|
||||
} satisfies GrokProcessorDefinition)
|
||||
: ({
|
||||
dissect: {
|
||||
field: processingDefinition.config.dissect.field,
|
||||
ignore_failure: processingDefinition.config.dissect.ignore_failure,
|
||||
ignore_missing: processingDefinition.config.dissect.ignore_missing,
|
||||
if: emptyEqualsToAlways(processingDefinition.condition),
|
||||
pattern: processingDefinition.config.dissect.pattern,
|
||||
append_separator: processingDefinition.config.dissect.append_separator,
|
||||
},
|
||||
} satisfies DissectProcessorDefinition);
|
||||
const processing = liveDraftProcessors.map(processorConverter.toAPIDefinition);
|
||||
|
||||
const detected_fields = detectedFields
|
||||
? (detectedFields.filter(
|
||||
(field) => field.type !== 'unmapped'
|
||||
) as SimulationRequestBody['detected_fields'])
|
||||
: undefined;
|
||||
const hasValidProcessors = processing.every((processor) =>
|
||||
isSchema(processorDefinitionSchema, processor)
|
||||
);
|
||||
|
||||
// Each processor should meet the minimum schema requirements to run the simulation
|
||||
if (!hasValidProcessors) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return streamsRepositoryClient.fetch('POST /api/streams/{id}/processing/_simulate', {
|
||||
signal: abortController.signal,
|
||||
signal,
|
||||
params: {
|
||||
path: { id: definition.name },
|
||||
path: { id: definition.stream.name },
|
||||
body: {
|
||||
documents: sampleDocs,
|
||||
processing: [processorDefinition],
|
||||
detected_fields,
|
||||
processing: liveDraftProcessors.map(processorConverter.toAPIDefinition),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[definition, sampleDocs]
|
||||
[definition, sampleDocs, liveDraftProcessors, streamsRepositoryClient],
|
||||
{ disableToastOnError: true }
|
||||
);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
// If there is an error, we only want the source fields
|
||||
const detectedFields = simulationError ? [] : simulation?.detected_fields ?? [];
|
||||
|
||||
return getTableColumns(liveDraftProcessors, detectedFields);
|
||||
}, [liveDraftProcessors, simulation, simulationError]);
|
||||
|
||||
const hasLiveChanges = !isEmpty(liveDraftProcessors);
|
||||
|
||||
return {
|
||||
hasLiveChanges,
|
||||
isLoading: isLoadingSamples || isLoadingSimulation,
|
||||
error: error as IHttpFetchError<ResponseErrorBody> | undefined,
|
||||
error: simulationError as IHttpFetchError<ResponseErrorBody> | undefined,
|
||||
refreshSamples,
|
||||
simulate,
|
||||
simulation: value,
|
||||
samples: sampleDocs,
|
||||
simulation,
|
||||
samples: sampleDocs ?? [],
|
||||
tableColumns,
|
||||
watchProcessor,
|
||||
};
|
||||
};
|
||||
|
||||
const composeSamplingCondition = (
|
||||
processors: ProcessorDefinitionWithUIAttributes[]
|
||||
): Condition | undefined => {
|
||||
if (isEmpty(processors)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const uniqueFields = uniq(getSourceFields(processors));
|
||||
|
||||
const conditions = uniqueFields.map((field) => ({
|
||||
field,
|
||||
operator: 'exists' as UnaryOperator,
|
||||
}));
|
||||
|
||||
return { or: conditions };
|
||||
};
|
||||
|
||||
const getSourceFields = (processors: ProcessorDefinitionWithUIAttributes[]): string[] => {
|
||||
return processors.map((processor) => getProcessorConfig(processor).field);
|
||||
};
|
||||
|
||||
const getTableColumns = (
|
||||
processors: ProcessorDefinitionWithUIAttributes[],
|
||||
fields: DetectedField[]
|
||||
) => {
|
||||
const uniqueProcessorsFields = getSourceFields(processors).map((name) => ({
|
||||
name,
|
||||
origin: 'processor',
|
||||
}));
|
||||
|
||||
const uniqueDetectedFields = fields.map((field) => ({
|
||||
name: field.name,
|
||||
origin: 'detected',
|
||||
}));
|
||||
|
||||
return uniqBy([...uniqueProcessorsFields, ...uniqueDetectedFields], 'name') as TableColumn[];
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import { ReadStreamDefinition } from '@kbn/streams-schema';
|
||||
import { IngestStreamGetResponse } from '@kbn/streams-schema';
|
||||
|
||||
const StreamDetailEnrichmentContent = dynamic(() =>
|
||||
import(/* webpackChunkName: "management_enrichment" */ './page_content').then((mod) => ({
|
||||
|
@ -15,7 +15,7 @@ const StreamDetailEnrichmentContent = dynamic(() =>
|
|||
);
|
||||
|
||||
interface StreamDetailEnrichmentProps {
|
||||
definition?: ReadStreamDefinition;
|
||||
definition?: IngestStreamGetResponse;
|
||||
refreshDefinition: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,31 +5,40 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
DragDropContextProps,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiResizableContainer,
|
||||
EuiSplitPanel,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
euiDragDropReorder,
|
||||
useEuiShadow,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ReadStreamDefinition, isRootStreamDefinition } from '@kbn/streams-schema';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
import { IngestStreamGetResponse, isRootStreamDefinition } from '@kbn/streams-schema';
|
||||
import { useUnsavedChangesPrompt } from '@kbn/unsaved-changes-prompt';
|
||||
import { EnrichmentEmptyPrompt } from './enrichment_empty_prompt';
|
||||
import { AddProcessorButton } from './add_processor_button';
|
||||
import { AddProcessorFlyout } from './flyout';
|
||||
import { DraggableProcessorListItem } from './processors_list';
|
||||
import { ManagementBottomBar } from '../management_bottom_bar';
|
||||
import { SortableList } from './sortable_list';
|
||||
import { useDefinition } from './hooks/use_definition';
|
||||
import { css } from '@emotion/react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { UseDefinitionReturn, useDefinition } from './hooks/use_definition';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { RootStreamEmptyPrompt } from './root_stream_empty_prompt';
|
||||
import { DraggableProcessorListItem } from './processors_list';
|
||||
import { SortableList } from './sortable_list';
|
||||
import { ManagementBottomBar } from '../management_bottom_bar';
|
||||
import { AddProcessorPanel } from './processors';
|
||||
import { SimulationPlayground } from './simulation_playground';
|
||||
import {
|
||||
UseProcessingSimulatorReturn,
|
||||
useProcessingSimulator,
|
||||
} from './hooks/use_processing_simulator';
|
||||
|
||||
const MemoSimulationPlayground = React.memo(SimulationPlayground);
|
||||
|
||||
interface StreamDetailEnrichmentContentProps {
|
||||
definition: ReadStreamDefinition;
|
||||
definition: IngestStreamGetResponse;
|
||||
refreshDefinition: () => void;
|
||||
}
|
||||
|
||||
|
@ -39,9 +48,6 @@ export function StreamDetailEnrichmentContent({
|
|||
}: StreamDetailEnrichmentContentProps) {
|
||||
const { appParams, core } = useKibana();
|
||||
|
||||
const [isBottomBarOpen, { on: openBottomBar, off: closeBottomBar }] = useBoolean();
|
||||
const [isAddProcessorOpen, { on: openAddProcessor, off: closeAddProcessor }] = useBoolean();
|
||||
|
||||
const {
|
||||
processors,
|
||||
addProcessor,
|
||||
|
@ -49,110 +55,196 @@ export function StreamDetailEnrichmentContent({
|
|||
deleteProcessor,
|
||||
resetChanges,
|
||||
saveChanges,
|
||||
setProcessors,
|
||||
reorderProcessors,
|
||||
hasChanges,
|
||||
isSavingChanges,
|
||||
} = useDefinition(definition, refreshDefinition);
|
||||
|
||||
const handlerItemDrag: DragDropContextProps['onDragEnd'] = ({ source, destination }) => {
|
||||
if (source && destination) {
|
||||
const items = euiDragDropReorder(processors, source.index, destination.index);
|
||||
setProcessors(items);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasChanges) openBottomBar();
|
||||
else closeBottomBar();
|
||||
}, [closeBottomBar, hasChanges, openBottomBar]);
|
||||
const {
|
||||
hasLiveChanges,
|
||||
isLoading,
|
||||
refreshSamples,
|
||||
samples,
|
||||
simulation,
|
||||
tableColumns,
|
||||
watchProcessor,
|
||||
} = useProcessingSimulator({ definition, processors });
|
||||
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: hasChanges,
|
||||
hasUnsavedChanges: hasChanges || hasLiveChanges,
|
||||
history: appParams.history,
|
||||
http: core.http,
|
||||
navigateToUrl: core.application.navigateToUrl,
|
||||
openConfirm: core.overlays.openConfirm,
|
||||
});
|
||||
|
||||
const handleSaveChanges = async () => {
|
||||
await saveChanges();
|
||||
closeBottomBar();
|
||||
};
|
||||
|
||||
const handleDiscardChanges = async () => {
|
||||
await resetChanges();
|
||||
closeBottomBar();
|
||||
};
|
||||
|
||||
const bottomBar = isBottomBarOpen && (
|
||||
<ManagementBottomBar
|
||||
onCancel={handleDiscardChanges}
|
||||
onConfirm={handleSaveChanges}
|
||||
isLoading={isSavingChanges}
|
||||
/>
|
||||
);
|
||||
|
||||
const addProcessorFlyout = isAddProcessorOpen && (
|
||||
<AddProcessorFlyout
|
||||
key="add-processor"
|
||||
definition={definition}
|
||||
onClose={closeAddProcessor}
|
||||
onAddProcessor={addProcessor}
|
||||
/>
|
||||
);
|
||||
|
||||
const hasProcessors = processors.length > 0;
|
||||
|
||||
if (isRootStreamDefinition(definition.stream)) {
|
||||
return <RootStreamEmptyPrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasProcessors ? (
|
||||
<EuiPanel paddingSize="none">
|
||||
<ProcessorsHeader />
|
||||
<EuiSpacer size="l" />
|
||||
<SortableList onDragItem={handlerItemDrag}>
|
||||
{processors.map((processor, idx) => (
|
||||
<DraggableProcessorListItem
|
||||
key={processor.id}
|
||||
idx={idx}
|
||||
definition={definition}
|
||||
processor={processor}
|
||||
onUpdateProcessor={updateProcessor}
|
||||
onDeleteProcessor={deleteProcessor}
|
||||
/>
|
||||
))}
|
||||
</SortableList>
|
||||
<EuiSpacer size="m" />
|
||||
<AddProcessorButton onClick={openAddProcessor} />
|
||||
</EuiPanel>
|
||||
) : (
|
||||
<EnrichmentEmptyPrompt onAddProcessor={openAddProcessor} />
|
||||
)}
|
||||
{addProcessorFlyout}
|
||||
{bottomBar}
|
||||
</>
|
||||
<EuiSplitPanel.Outer grow hasBorder hasShadow={false}>
|
||||
<EuiSplitPanel.Inner
|
||||
paddingSize="none"
|
||||
css={css`
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
`}
|
||||
>
|
||||
<EuiResizableContainer>
|
||||
{(EuiResizablePanel, EuiResizableButton) => (
|
||||
<>
|
||||
<EuiResizablePanel
|
||||
initialSize={25}
|
||||
minSize="400px"
|
||||
tabIndex={0}
|
||||
paddingSize="none"
|
||||
css={verticalFlexCss}
|
||||
>
|
||||
<ProcessorsEditor
|
||||
definition={definition}
|
||||
processors={processors}
|
||||
onUpdateProcessor={updateProcessor}
|
||||
onDeleteProcessor={deleteProcessor}
|
||||
onWatchProcessor={watchProcessor}
|
||||
onAddProcessor={addProcessor}
|
||||
onReorderProcessor={reorderProcessors}
|
||||
/>
|
||||
</EuiResizablePanel>
|
||||
|
||||
<EuiResizableButton indicator="border" accountForScrollbars="both" />
|
||||
|
||||
<EuiResizablePanel
|
||||
initialSize={75}
|
||||
minSize="300px"
|
||||
tabIndex={0}
|
||||
paddingSize="s"
|
||||
css={verticalFlexCss}
|
||||
>
|
||||
<MemoSimulationPlayground
|
||||
definition={definition}
|
||||
columns={tableColumns}
|
||||
simulation={simulation}
|
||||
samples={samples}
|
||||
onRefreshSamples={refreshSamples}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</EuiResizableContainer>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner grow={false} color="subdued">
|
||||
<ManagementBottomBar
|
||||
onCancel={resetChanges}
|
||||
onConfirm={saveChanges}
|
||||
isLoading={isSavingChanges}
|
||||
disabled={!hasChanges}
|
||||
/>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
);
|
||||
}
|
||||
|
||||
const ProcessorsHeader = () => {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.streams.streamDetailView.managementTab.enrichment.headingTitle', {
|
||||
defaultMessage: 'Processors for field extraction',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiText component="p" size="s">
|
||||
{i18n.translate('xpack.streams.streamDetailView.managementTab.enrichment.headingSubtitle', {
|
||||
defaultMessage:
|
||||
'Use processors to transform data before indexing. Drag and drop existing processors to update their execution order.',
|
||||
})}
|
||||
</EuiText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
interface ProcessorsEditorProps {
|
||||
definition: IngestStreamGetResponse;
|
||||
processors: UseDefinitionReturn['processors'];
|
||||
onAddProcessor: UseDefinitionReturn['addProcessor'];
|
||||
onDeleteProcessor: UseDefinitionReturn['deleteProcessor'];
|
||||
onReorderProcessor: UseDefinitionReturn['reorderProcessors'];
|
||||
onUpdateProcessor: UseDefinitionReturn['updateProcessor'];
|
||||
onWatchProcessor: UseProcessingSimulatorReturn['watchProcessor'];
|
||||
}
|
||||
|
||||
const ProcessorsEditor = React.memo(
|
||||
({
|
||||
definition,
|
||||
processors,
|
||||
onAddProcessor,
|
||||
onDeleteProcessor,
|
||||
onReorderProcessor,
|
||||
onUpdateProcessor,
|
||||
onWatchProcessor,
|
||||
}: ProcessorsEditorProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const handlerItemDrag: DragDropContextProps['onDragEnd'] = ({ source, destination }) => {
|
||||
if (source && destination) {
|
||||
const items = euiDragDropReorder(processors, source.index, destination.index);
|
||||
onReorderProcessor(items);
|
||||
}
|
||||
};
|
||||
|
||||
const hasProcessors = !isEmpty(processors);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPanel
|
||||
paddingSize="m"
|
||||
hasShadow={false}
|
||||
borderRadius="none"
|
||||
grow={false}
|
||||
css={css`
|
||||
z-index: ${euiTheme.levels.maskBelowHeader};
|
||||
${useEuiShadow('xs')};
|
||||
`}
|
||||
>
|
||||
<EuiTitle size="xxs">
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.headingTitle',
|
||||
{
|
||||
defaultMessage: 'Processors for field extraction',
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiText component="p" size="xs">
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.headingSubtitle',
|
||||
{
|
||||
defaultMessage:
|
||||
'Drag and drop existing processors to update their execution order.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
<EuiPanel
|
||||
paddingSize="m"
|
||||
hasShadow={false}
|
||||
borderRadius="none"
|
||||
css={css`
|
||||
overflow: auto;
|
||||
`}
|
||||
>
|
||||
{hasProcessors && (
|
||||
<SortableList onDragItem={handlerItemDrag}>
|
||||
{processors.map((processor, idx) => (
|
||||
<DraggableProcessorListItem
|
||||
key={processor.id}
|
||||
idx={idx}
|
||||
definition={definition}
|
||||
processor={processor}
|
||||
onDeleteProcessor={onDeleteProcessor}
|
||||
onUpdateProcessor={onUpdateProcessor}
|
||||
onWatchProcessor={onWatchProcessor}
|
||||
/>
|
||||
))}
|
||||
</SortableList>
|
||||
)}
|
||||
<AddProcessorPanel
|
||||
key={processors.length} // Used to force reset the inner form state once a new processor is added
|
||||
definition={definition}
|
||||
onAddProcessor={onAddProcessor}
|
||||
onWatchProcessor={onWatchProcessor}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const verticalFlexCss = css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* 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, { useMemo, useState } from 'react';
|
||||
import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiProgress,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TimeRange } from '@kbn/es-query';
|
||||
import { flattenObject } from '@kbn/object-utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { StreamsAppSearchBar, StreamsAppSearchBarProps } from '../streams_app_search_bar';
|
||||
import { PreviewTable } from '../preview_table';
|
||||
import { TableColumn, UseProcessingSimulatorReturn } from './hooks/use_processing_simulator';
|
||||
import { AssetImage } from '../asset_image';
|
||||
|
||||
interface ProcessorOutcomePreviewProps {
|
||||
columns: TableColumn[];
|
||||
isLoading: UseProcessingSimulatorReturn['isLoading'];
|
||||
simulation: UseProcessingSimulatorReturn['simulation'];
|
||||
samples: UseProcessingSimulatorReturn['samples'];
|
||||
onRefreshSamples: UseProcessingSimulatorReturn['refreshSamples'];
|
||||
}
|
||||
|
||||
export const ProcessorOutcomePreview = ({
|
||||
columns,
|
||||
isLoading,
|
||||
simulation,
|
||||
samples,
|
||||
onRefreshSamples,
|
||||
}: ProcessorOutcomePreviewProps) => {
|
||||
const { dependencies } = useKibana();
|
||||
const { data } = dependencies.start;
|
||||
|
||||
const { timeRange, setTimeRange } = useDateRange({ data });
|
||||
|
||||
const [selectedDocsFilter, setSelectedDocsFilter] =
|
||||
useState<DocsFilterOption>('outcome_filter_all');
|
||||
|
||||
const simulationDocuments = useMemo(() => {
|
||||
if (!simulation?.documents) {
|
||||
return samples.map((doc) => flattenObject(doc));
|
||||
}
|
||||
|
||||
const filterDocuments = (filter: DocsFilterOption) => {
|
||||
switch (filter) {
|
||||
case 'outcome_filter_matched':
|
||||
return simulation.documents.filter((doc) => doc.isMatch);
|
||||
case 'outcome_filter_unmatched':
|
||||
return simulation.documents.filter((doc) => !doc.isMatch);
|
||||
case 'outcome_filter_all':
|
||||
default:
|
||||
return simulation.documents;
|
||||
}
|
||||
};
|
||||
|
||||
return filterDocuments(selectedDocsFilter).map((doc) => doc.value);
|
||||
}, [samples, simulation?.documents, selectedDocsFilter]);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
switch (selectedDocsFilter) {
|
||||
case 'outcome_filter_unmatched':
|
||||
return columns
|
||||
.filter((column) => column.origin === 'processor')
|
||||
.map((column) => column.name);
|
||||
case 'outcome_filter_matched':
|
||||
case 'outcome_filter_all':
|
||||
default:
|
||||
return columns.map((column) => column.name);
|
||||
}
|
||||
}, [columns, selectedDocsFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<OutcomeControls
|
||||
docsFilter={selectedDocsFilter}
|
||||
onDocsFilterChange={setSelectedDocsFilter}
|
||||
timeRange={timeRange}
|
||||
onTimeRangeChange={setTimeRange}
|
||||
onTimeRangeRefresh={onRefreshSamples}
|
||||
simulationFailureRate={simulation?.failure_rate}
|
||||
simulationSuccessRate={simulation?.success_rate}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="m" />
|
||||
<OutcomePreviewTable documents={simulationDocuments} columns={tableColumns} />
|
||||
{isLoading && <EuiProgress size="xs" color="accent" position="absolute" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const docsFilterOptions = {
|
||||
outcome_filter_all: {
|
||||
id: 'outcome_filter_all',
|
||||
label: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.outcomeControls.all',
|
||||
{ defaultMessage: 'All samples' }
|
||||
),
|
||||
},
|
||||
outcome_filter_matched: {
|
||||
id: 'outcome_filter_matched',
|
||||
label: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.outcomeControls.matched',
|
||||
{ defaultMessage: 'Matched' }
|
||||
),
|
||||
},
|
||||
outcome_filter_unmatched: {
|
||||
id: 'outcome_filter_unmatched',
|
||||
label: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.outcomeControls.unmatched',
|
||||
{ defaultMessage: 'Unmatched' }
|
||||
),
|
||||
},
|
||||
} as const;
|
||||
|
||||
type DocsFilterOption = keyof typeof docsFilterOptions;
|
||||
|
||||
interface OutcomeControlsProps {
|
||||
docsFilter: DocsFilterOption;
|
||||
timeRange: TimeRange;
|
||||
onDocsFilterChange: (filter: DocsFilterOption) => void;
|
||||
onTimeRangeChange: (timeRange: TimeRange) => void;
|
||||
onTimeRangeRefresh: () => void;
|
||||
simulationFailureRate?: number;
|
||||
simulationSuccessRate?: number;
|
||||
}
|
||||
|
||||
const OutcomeControls = ({
|
||||
docsFilter,
|
||||
timeRange,
|
||||
onDocsFilterChange,
|
||||
onTimeRangeChange,
|
||||
onTimeRangeRefresh,
|
||||
simulationFailureRate,
|
||||
simulationSuccessRate,
|
||||
}: OutcomeControlsProps) => {
|
||||
const handleQuerySubmit: StreamsAppSearchBarProps['onQuerySubmit'] = (
|
||||
{ dateRange },
|
||||
isUpdate
|
||||
) => {
|
||||
if (!isUpdate) {
|
||||
return onTimeRangeRefresh();
|
||||
}
|
||||
|
||||
if (dateRange) {
|
||||
onTimeRangeChange({
|
||||
from: dateRange.from,
|
||||
to: dateRange?.to,
|
||||
mode: dateRange.mode,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getFilterButtonPropsFor = (filterId: DocsFilterOption) => ({
|
||||
hasActiveFilters: docsFilter === filterId,
|
||||
onClick: () => onDocsFilterChange(filterId),
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" wrap>
|
||||
<EuiFilterGroup
|
||||
aria-label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.outcomeControlsAriaLabel',
|
||||
{ defaultMessage: 'Filter for all, matching or unmatching previewed documents.' }
|
||||
)}
|
||||
>
|
||||
<EuiFilterButton {...getFilterButtonPropsFor(docsFilterOptions.outcome_filter_all.id)}>
|
||||
{docsFilterOptions.outcome_filter_all.label}
|
||||
</EuiFilterButton>
|
||||
<EuiFilterButton
|
||||
{...getFilterButtonPropsFor(docsFilterOptions.outcome_filter_matched.id)}
|
||||
badgeColor="success"
|
||||
numActiveFilters={
|
||||
simulationSuccessRate ? parseFloat((simulationSuccessRate * 100).toFixed(2)) : undefined
|
||||
}
|
||||
>
|
||||
{docsFilterOptions.outcome_filter_matched.label}
|
||||
</EuiFilterButton>
|
||||
<EuiFilterButton
|
||||
{...getFilterButtonPropsFor(docsFilterOptions.outcome_filter_unmatched.id)}
|
||||
badgeColor="accent"
|
||||
numActiveFilters={
|
||||
simulationFailureRate ? parseFloat((simulationFailureRate * 100).toFixed(2)) : undefined
|
||||
}
|
||||
>
|
||||
{docsFilterOptions.outcome_filter_unmatched.label}
|
||||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
<StreamsAppSearchBar
|
||||
onQuerySubmit={handleQuerySubmit}
|
||||
onRefresh={onTimeRangeRefresh}
|
||||
dateRangeFrom={timeRange.from}
|
||||
dateRangeTo={timeRange.to}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface OutcomePreviewTableProps {
|
||||
documents: Array<Record<PropertyKey, unknown>>;
|
||||
columns: string[];
|
||||
}
|
||||
|
||||
const OutcomePreviewTable = ({ documents, columns }: OutcomePreviewTableProps) => {
|
||||
if (isEmpty(documents)) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
titleSize="xs"
|
||||
icon={<AssetImage type="noResults" />}
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.rootStreamEmptyPrompt.noDataTitle',
|
||||
{ defaultMessage: 'Unable to generate a preview' }
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.outcomePreviewTable.noDataBody',
|
||||
{
|
||||
defaultMessage:
|
||||
"There are no sample documents to test the processors. Try updating the time range or ingesting more data, it might be possible we could not find any matching documents with the processors' source fields.",
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <PreviewTable documents={documents} displayColumns={columns} />;
|
||||
};
|
|
@ -18,12 +18,12 @@ export const DissectAppendSeparator = () => {
|
|||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternSeparatorLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.dissectPatternSeparatorLabel',
|
||||
{ defaultMessage: 'Append separator' }
|
||||
)}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternSeparatorHelpText"
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processor.dissectPatternSeparatorHelpText"
|
||||
defaultMessage="If you specify a key modifier, this character separates the fields when appending results. Defaults to {value}."
|
||||
values={{ value: <EuiCode>""</EuiCode> }}
|
||||
/>
|
|
@ -22,7 +22,7 @@ export const DissectPatternDefinition = () => {
|
|||
name: 'pattern',
|
||||
rules: {
|
||||
required: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternRequiredError',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.dissectPatternRequiredError',
|
||||
{ defaultMessage: 'A pattern is required.' }
|
||||
),
|
||||
},
|
||||
|
@ -33,12 +33,12 @@ export const DissectPatternDefinition = () => {
|
|||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternDefinitionsLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.dissectPatternDefinitionsLabel',
|
||||
{ defaultMessage: 'Pattern' }
|
||||
)}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternDefinitionsHelpText"
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processor.dissectPatternDefinitionsHelpText"
|
||||
defaultMessage="Pattern used to dissect the specified field. The pattern is defined by the parts of the string to discard. Use a {keyModifier} to alter the dissection behavior."
|
||||
values={{
|
||||
keyModifier: (
|
||||
|
@ -49,7 +49,7 @@ export const DissectPatternDefinition = () => {
|
|||
href={esDocUrl}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternDefinitionsLink',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.dissectPatternDefinitionsLink',
|
||||
{ defaultMessage: 'key modifier' }
|
||||
)}
|
||||
</EuiLink>
|
||||
|
@ -68,7 +68,7 @@ export const DissectPatternDefinition = () => {
|
|||
height={75}
|
||||
options={{ minimap: { enabled: false } }}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectPatternDefinitionsAriaLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.dissectPatternDefinitionsAriaLabel',
|
||||
{ defaultMessage: 'Pattern editor' }
|
||||
)}
|
||||
/>
|
|
@ -20,11 +20,11 @@ export const GrokPatternDefinition = () => {
|
|||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokPatternDefinitionsLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.grokPatternDefinitionsLabel',
|
||||
{ defaultMessage: 'Pattern definitions' }
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokPatternDefinitionsHelpText',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.grokPatternDefinitionsHelpText',
|
||||
{
|
||||
defaultMessage:
|
||||
'A map of pattern-name and pattern tuples defining custom patterns. Patterns matching existing names will override the pre-existing definition.',
|
||||
|
@ -39,7 +39,7 @@ export const GrokPatternDefinition = () => {
|
|||
languageId="xjson"
|
||||
height={200}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokPatternDefinitionsAriaLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.grokPatternDefinitionsAriaLabel',
|
||||
{ defaultMessage: 'Pattern definitions editor' }
|
||||
)}
|
||||
/>
|
|
@ -17,7 +17,6 @@ import {
|
|||
DragDropContextProps,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
EuiDraggable,
|
||||
EuiFlexGroup,
|
||||
|
@ -62,11 +61,11 @@ export const GrokPatternsEditor = () => {
|
|||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokEditorLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.grokEditorLabel',
|
||||
{ defaultMessage: 'Grok patterns editor' }
|
||||
)}
|
||||
>
|
||||
<EuiPanel color="subdued" paddingSize="m">
|
||||
<EuiPanel color="subdued" paddingSize="s">
|
||||
<SortableList onDragItem={handlerPatternDrag}>
|
||||
{fieldsWithError.map((field, idx) => (
|
||||
<DraggablePatternInput
|
||||
|
@ -76,22 +75,20 @@ export const GrokPatternsEditor = () => {
|
|||
onRemove={getRemovePatternHandler(idx)}
|
||||
inputProps={register(`patterns.${idx}.value`, {
|
||||
required: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokEditorRequiredError',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.grokEditorRequiredError',
|
||||
{ defaultMessage: 'A pattern is required.' }
|
||||
),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</SortableList>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="streamsAppGrokPatternsEditorAddPatternButton"
|
||||
onClick={handleAddPattern}
|
||||
iconType="plusInCircle"
|
||||
flush="left"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokEditor.addPattern',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.grokEditor.addPattern',
|
||||
{ defaultMessage: 'Add pattern' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
|
@ -132,13 +129,13 @@ const DraggablePatternInput = ({
|
|||
>
|
||||
{(provided) => (
|
||||
<EuiFormRow isInvalid={isInvalid} error={error?.message}>
|
||||
<EuiFlexGroup gutterSize="m" responsive={false} alignItems="center">
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
|
||||
<EuiPanel
|
||||
color="transparent"
|
||||
paddingSize="xs"
|
||||
{...provided.dragHandleProps}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokEditor.dragHandleLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.grokEditor.dragHandleLabel',
|
||||
{ defaultMessage: 'Drag Handle' }
|
||||
)}
|
||||
>
|
||||
|
@ -158,7 +155,7 @@ const DraggablePatternInput = ({
|
|||
color="danger"
|
||||
onClick={() => onRemove(idx)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokEditor.removePattern',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.grokEditor.removePattern',
|
||||
{ defaultMessage: 'Remove grok pattern' }
|
||||
)}
|
||||
/>
|
|
@ -19,14 +19,14 @@ export const IgnoreFailureToggle = () => {
|
|||
<ToggleField
|
||||
name="ignore_failure"
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.ignoreFailuresLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.ignoreFailuresLabel',
|
||||
{ defaultMessage: 'Ignore failures for this processor' }
|
||||
)}
|
||||
helpText={
|
||||
!value ? (
|
||||
<EuiText component="span" size="relative" color="warning">
|
||||
<FormattedMessage
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.ignoreFailuresWarning"
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processor.ignoreFailuresWarning"
|
||||
defaultMessage="Disabling the {ignoreField} option could lead to unexpected pipeline failures."
|
||||
values={{
|
||||
ignoreField: <EuiCode>ignore_failure</EuiCode>,
|
||||
|
@ -44,11 +44,11 @@ export const IgnoreMissingToggle = () => {
|
|||
<ToggleField
|
||||
name="ignore_missing"
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.ignoreMissingLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.ignoreMissingLabel',
|
||||
{ defaultMessage: 'Ignore missing' }
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.ignoreMissingHelpText',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.ignoreMissingHelpText',
|
||||
{ defaultMessage: 'Ignore documents with a missing field.' }
|
||||
)}
|
||||
/>
|
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiPanel,
|
||||
useEuiTheme,
|
||||
EuiHorizontalRule,
|
||||
EuiAccordion,
|
||||
EuiButtonIcon,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiBadge,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ProcessorType, IngestStreamGetResponse } from '@kbn/streams-schema';
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useForm, SubmitHandler, FormProvider, useWatch } from 'react-hook-form';
|
||||
import { css } from '@emotion/react';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
import { DissectProcessorForm } from './dissect';
|
||||
import { GrokProcessorForm } from './grok';
|
||||
import { ProcessorTypeSelector } from './processor_type_selector';
|
||||
import { ProcessorFormState, ProcessorDefinitionWithUIAttributes } from '../types';
|
||||
import {
|
||||
getDefaultFormState,
|
||||
convertFormStateToProcessor,
|
||||
isGrokProcessor,
|
||||
isDissectProcessor,
|
||||
} from '../utils';
|
||||
import { useDiscardConfirm } from '../../../hooks/use_discard_confirm';
|
||||
import { UseDefinitionReturn } from '../hooks/use_definition';
|
||||
import { UseProcessingSimulatorReturn } from '../hooks/use_processing_simulator';
|
||||
|
||||
export interface ProcessorPanelProps {
|
||||
definition: IngestStreamGetResponse;
|
||||
onWatchProcessor: UseProcessingSimulatorReturn['watchProcessor'];
|
||||
}
|
||||
|
||||
export interface AddProcessorPanelProps extends ProcessorPanelProps {
|
||||
isInitiallyOpen?: boolean;
|
||||
onAddProcessor: UseDefinitionReturn['addProcessor'];
|
||||
}
|
||||
|
||||
export interface EditProcessorPanelProps extends ProcessorPanelProps {
|
||||
processor: ProcessorDefinitionWithUIAttributes;
|
||||
onDeleteProcessor: UseDefinitionReturn['deleteProcessor'];
|
||||
onUpdateProcessor: UseDefinitionReturn['updateProcessor'];
|
||||
}
|
||||
|
||||
export function AddProcessorPanel({ onAddProcessor, onWatchProcessor }: AddProcessorPanelProps) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [isOpen, { on: openPanel, off: closePanel }] = useBoolean(false);
|
||||
|
||||
const defaultValues = useMemo(() => getDefaultFormState('grok'), []);
|
||||
|
||||
const methods = useForm<ProcessorFormState>({ defaultValues, mode: 'onChange' });
|
||||
|
||||
const type = useWatch({ control: methods.control, name: 'type' });
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const { unsubscribe } = methods.watch((value) => {
|
||||
const draftProcessor = createDraftProcessorFromForm(value as ProcessorFormState);
|
||||
onWatchProcessor(draftProcessor);
|
||||
setHasChanges(!isEqual(defaultValues, value));
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}
|
||||
}, [defaultValues, isOpen, methods, onWatchProcessor]);
|
||||
|
||||
const handleSubmit: SubmitHandler<ProcessorFormState> = async (data) => {
|
||||
const processingDefinition = convertFormStateToProcessor(data);
|
||||
|
||||
onWatchProcessor({ id: 'draft', deleteIfExists: true });
|
||||
onAddProcessor(processingDefinition, data.detected_fields);
|
||||
closePanel();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
methods.reset();
|
||||
onWatchProcessor({ id: 'draft', deleteIfExists: true });
|
||||
closePanel();
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
const draftProcessor = createDraftProcessorFromForm(defaultValues);
|
||||
onWatchProcessor(draftProcessor);
|
||||
openPanel();
|
||||
};
|
||||
|
||||
const confirmDiscardAndClose = useDiscardConfirm(handleCancel);
|
||||
|
||||
const buttonContent = isOpen ? (
|
||||
i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorPanel.addingProcessor',
|
||||
{ defaultMessage: 'Adding processor' }
|
||||
)
|
||||
) : (
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
|
||||
<EuiIcon type="plus" />
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.addProcessorAction',
|
||||
{ defaultMessage: 'Add a processor' }
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
color={isOpen ? 'subdued' : undefined}
|
||||
hasBorder
|
||||
css={css`
|
||||
border: ${euiTheme.border.thin};
|
||||
padding: ${euiTheme.size.m};
|
||||
`}
|
||||
>
|
||||
<EuiAccordion
|
||||
id="add-processor-accordion"
|
||||
arrowProps={{
|
||||
css: { display: 'none' },
|
||||
}}
|
||||
buttonContent={buttonContent}
|
||||
buttonElement="div"
|
||||
forceState={isOpen ? 'open' : 'closed'}
|
||||
onToggle={handleOpen}
|
||||
extraAction={
|
||||
isOpen ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="streamsAppAddProcessorPanelCancelButton"
|
||||
onClick={hasChanges ? confirmDiscardAndClose : handleCancel}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorPanel.cancel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppAddProcessorPanelAddProcessorButton"
|
||||
size="s"
|
||||
onClick={methods.handleSubmit(handleSubmit)}
|
||||
disabled={!methods.formState.isValid && methods.formState.isSubmitted}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorPanel.confirmAddProcessor',
|
||||
{ defaultMessage: 'Add processor' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<FormProvider {...methods}>
|
||||
<EuiForm component="form" fullWidth onSubmit={methods.handleSubmit(handleSubmit)}>
|
||||
<ProcessorTypeSelector />
|
||||
<EuiSpacer size="m" />
|
||||
{type === 'grok' && <GrokProcessorForm />}
|
||||
{type === 'dissect' && <DissectProcessorForm />}
|
||||
</EuiForm>
|
||||
</FormProvider>
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const createDraftProcessorFromForm = (
|
||||
formState: ProcessorFormState
|
||||
): ProcessorDefinitionWithUIAttributes => {
|
||||
const processingDefinition = convertFormStateToProcessor(formState);
|
||||
|
||||
return {
|
||||
id: 'draft',
|
||||
status: 'draft',
|
||||
type: formState.type,
|
||||
...processingDefinition,
|
||||
};
|
||||
};
|
||||
|
||||
export function EditProcessorPanel({
|
||||
onDeleteProcessor,
|
||||
onUpdateProcessor,
|
||||
onWatchProcessor,
|
||||
processor,
|
||||
}: EditProcessorPanelProps) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [isOpen, { on: openPanel, off: closePanel }] = useBoolean();
|
||||
|
||||
const processorDescription = getProcessorDescription(processor);
|
||||
|
||||
const isDraft = processor.status === 'draft';
|
||||
const isUnsaved = isDraft || processor.status === 'updated';
|
||||
|
||||
const defaultValues = useMemo(() => getDefaultFormState(processor.type, processor), [processor]);
|
||||
|
||||
const methods = useForm<ProcessorFormState>({ defaultValues, mode: 'onChange' });
|
||||
|
||||
const type = useWatch({ control: methods.control, name: 'type' });
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = methods.watch((value) => {
|
||||
const processingDefinition = convertFormStateToProcessor(value as ProcessorFormState);
|
||||
onWatchProcessor({
|
||||
id: processor.id,
|
||||
status: processor.status,
|
||||
type: value.type as ProcessorType,
|
||||
...processingDefinition,
|
||||
});
|
||||
setHasChanges(!isEqual(defaultValues, value));
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, [defaultValues, methods, onWatchProcessor, processor.id, processor.status]);
|
||||
|
||||
const handleSubmit: SubmitHandler<ProcessorFormState> = (data) => {
|
||||
const processorDefinition = convertFormStateToProcessor(data);
|
||||
|
||||
onUpdateProcessor(processor.id, processorDefinition, isDraft ? 'draft' : 'updated');
|
||||
closePanel();
|
||||
};
|
||||
|
||||
const handleProcessorDelete = () => {
|
||||
onDeleteProcessor(processor.id);
|
||||
closePanel();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
methods.reset();
|
||||
closePanel();
|
||||
};
|
||||
|
||||
const confirmDiscardAndClose = useDiscardConfirm(handleCancel);
|
||||
const confirmDeletionAndClose = useDiscardConfirm(handleProcessorDelete, {
|
||||
title: deleteProcessorTitle,
|
||||
message: deleteProcessorMessage,
|
||||
confirmButtonText: deleteProcessorLabel,
|
||||
cancelButtonText: deleteProcessorCancelLabel,
|
||||
});
|
||||
|
||||
const buttonContent = isOpen ? (
|
||||
<strong>{processor.type.toUpperCase()}</strong>
|
||||
) : (
|
||||
<EuiFlexGroup gutterSize="s" responsive={false} alignItems="center">
|
||||
<EuiIcon type="grab" />
|
||||
<strong>{processor.type.toUpperCase()}</strong>
|
||||
<EuiText component="span" size="s" color="subdued" className="eui-textTruncate">
|
||||
{processorDescription}
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
hasBorder
|
||||
color={isDraft ? 'subdued' : undefined}
|
||||
css={css`
|
||||
border: ${euiTheme.border.thin};
|
||||
padding: ${euiTheme.size.m};
|
||||
`}
|
||||
>
|
||||
<EuiAccordion
|
||||
id="edit-processor-accordion"
|
||||
arrowProps={{
|
||||
css: { display: 'none' },
|
||||
}}
|
||||
buttonContent={buttonContent}
|
||||
buttonContentClassName="eui-textTruncate"
|
||||
buttonElement="div"
|
||||
buttonProps={{
|
||||
/* Allow text ellipsis in flex child nodes */
|
||||
css: css`
|
||||
min-width: 0;
|
||||
&:is(:hover, :focus) {
|
||||
cursor: grab;
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
}}
|
||||
forceState={isOpen ? 'open' : 'closed'}
|
||||
extraAction={
|
||||
isOpen ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="streamsAppEditProcessorPanelCancelButton"
|
||||
onClick={hasChanges ? confirmDiscardAndClose : handleCancel}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorPanel.cancel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppEditProcessorPanelUpdateProcessorButton"
|
||||
size="s"
|
||||
onClick={methods.handleSubmit(handleSubmit)}
|
||||
disabled={!methods.formState.isValid}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorPanel.confirmEditProcessor',
|
||||
{ defaultMessage: 'Update processor' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
{isUnsaved && (
|
||||
<EuiBadge>
|
||||
{i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorPanel.unsavedBadge',
|
||||
{ defaultMessage: 'Unsaved' }
|
||||
)}
|
||||
</EuiBadge>
|
||||
)}
|
||||
<EuiButtonIcon
|
||||
data-test-subj="streamsAppEditProcessorPanelButton"
|
||||
onClick={openPanel}
|
||||
iconType="pencil"
|
||||
color="text"
|
||||
size="xs"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.editProcessorAction',
|
||||
{ defaultMessage: 'Edit {type} processor', values: { type: processor.type } }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
)
|
||||
}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<FormProvider {...methods}>
|
||||
<EuiForm component="form" fullWidth onSubmit={methods.handleSubmit(handleSubmit)}>
|
||||
<ProcessorTypeSelector disabled />
|
||||
<EuiSpacer size="m" />
|
||||
{type === 'grok' && <GrokProcessorForm />}
|
||||
{type === 'dissect' && <DissectProcessorForm />}
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiButton
|
||||
data-test-subj="streamsAppEditProcessorPanelButton"
|
||||
color="danger"
|
||||
onClick={confirmDeletionAndClose}
|
||||
>
|
||||
{deleteProcessorLabel}
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
</FormProvider>
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const deleteProcessorLabel = i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.deleteProcessorLabel',
|
||||
{ defaultMessage: 'Delete processor' }
|
||||
);
|
||||
|
||||
const deleteProcessorCancelLabel = i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.deleteProcessorCancelLabel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
);
|
||||
|
||||
const deleteProcessorTitle = i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.deleteProcessorTitle',
|
||||
{ defaultMessage: 'Are you sure you want to delete this processor?' }
|
||||
);
|
||||
|
||||
const deleteProcessorMessage = i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.deleteProcessorMessage',
|
||||
{ defaultMessage: 'Deleting this processor will permanently impact the field configuration.' }
|
||||
);
|
||||
|
||||
const getProcessorDescription = (processor: ProcessorDefinitionWithUIAttributes) => {
|
||||
if (isGrokProcessor(processor)) {
|
||||
return processor.grok.patterns.join(' • ');
|
||||
} else if (isDissectProcessor(processor)) {
|
||||
return processor.dissect.pattern;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
|
@ -19,7 +19,7 @@ export const OptionalFieldsAccordion = ({ children }: PropsWithChildren) => {
|
|||
id="optionalFieldsAccordion"
|
||||
paddingSize="none"
|
||||
buttonContent={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.optionalFields',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.optionalFields',
|
||||
{ defaultMessage: 'Optional fields' }
|
||||
)}
|
||||
>
|
|
@ -11,7 +11,7 @@ import { ConditionEditor } from '../../condition_editor';
|
|||
import { ProcessorFormState } from '../types';
|
||||
|
||||
export const ProcessorConditionEditor = () => {
|
||||
const { field } = useController<ProcessorFormState, 'condition'>({ name: 'condition' });
|
||||
const { field } = useController<ProcessorFormState, 'if'>({ name: 'if' });
|
||||
|
||||
return <ConditionEditor condition={field.value} onConditionChange={field.onChange} />;
|
||||
};
|
|
@ -16,7 +16,7 @@ export const ProcessorFieldSelector = () => {
|
|||
name: 'field',
|
||||
rules: {
|
||||
required: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.fieldSelectorRequiredError',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.fieldSelectorRequiredError',
|
||||
{ defaultMessage: 'A field value is required.' }
|
||||
),
|
||||
},
|
||||
|
@ -28,11 +28,11 @@ export const ProcessorFieldSelector = () => {
|
|||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.fieldSelectorLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.fieldSelectorLabel',
|
||||
{ defaultMessage: 'Field' }
|
||||
)}
|
||||
helpText={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.fieldSelectorHelpText',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.fieldSelectorHelpText',
|
||||
{ defaultMessage: 'Field to search for matches.' }
|
||||
)}
|
||||
isInvalid={invalid}
|
|
@ -46,7 +46,7 @@ export const ProcessorTypeSelector = ({
|
|||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.typeSelectorLabel',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.typeSelectorLabel',
|
||||
{ defaultMessage: 'Processor' }
|
||||
)}
|
||||
helpText={getProcessorDescription(esDocUrl)(processorType)}
|
||||
|
@ -59,7 +59,7 @@ export const ProcessorTypeSelector = ({
|
|||
onChange={handleChange}
|
||||
fullWidth
|
||||
placeholder={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.typeSelectorPlaceholder',
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.processor.typeSelectorPlaceholder',
|
||||
{ defaultMessage: 'Grok, Dissect ...' }
|
||||
)}
|
||||
/>
|
||||
|
@ -73,7 +73,7 @@ const availableProcessors: TAvailableProcessors = {
|
|||
inputDisplay: 'Dissect',
|
||||
getDocUrl: (esDocUrl: string) => (
|
||||
<FormattedMessage
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.dissectHelpText"
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processor.dissectHelpText"
|
||||
defaultMessage="Uses {dissectLink} patterns to extract matches from a field."
|
||||
values={{
|
||||
dissectLink: (
|
||||
|
@ -97,7 +97,7 @@ const availableProcessors: TAvailableProcessors = {
|
|||
inputDisplay: 'Grok',
|
||||
getDocUrl: (esDocUrl: string) => (
|
||||
<FormattedMessage
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processorFlyout.grokHelpText"
|
||||
id="xpack.streams.streamDetailView.managementTab.enrichment.processor.grokHelpText"
|
||||
defaultMessage="Uses {grokLink} expressions to extract matches from a field."
|
||||
values={{
|
||||
grokLink: (
|
|
@ -6,28 +6,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiDraggable,
|
||||
EuiPanelProps,
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ReadStreamDefinition } from '@kbn/streams-schema';
|
||||
import { useBoolean } from '@kbn/react-hooks';
|
||||
import { css } from '@emotion/react';
|
||||
import { EditProcessorFlyout, EditProcessorFlyoutProps } from './flyout';
|
||||
import { EnrichmentUIProcessorDefinition, isDissectProcessor, isGrokProcessor } from './types';
|
||||
import { EuiDraggable } from '@elastic/eui';
|
||||
import { EditProcessorPanel, type EditProcessorPanelProps } from './processors';
|
||||
|
||||
export const DraggableProcessorListItem = ({
|
||||
processor,
|
||||
idx,
|
||||
...props
|
||||
}: Omit<ProcessorListItemProps, 'hasShadow'> & { idx: number }) => (
|
||||
}: EditProcessorPanelProps & { idx: number }) => (
|
||||
<EuiDraggable
|
||||
index={idx}
|
||||
spacing="m"
|
||||
|
@ -38,81 +24,6 @@ export const DraggableProcessorListItem = ({
|
|||
paddingRight: 0,
|
||||
}}
|
||||
>
|
||||
{(_provided, state) => (
|
||||
<ProcessorListItem processor={processor} hasShadow={state.isDragging} {...props} />
|
||||
)}
|
||||
{() => <EditProcessorPanel processor={processor} {...props} />}
|
||||
</EuiDraggable>
|
||||
);
|
||||
|
||||
interface ProcessorListItemProps {
|
||||
definition: ReadStreamDefinition;
|
||||
processor: EnrichmentUIProcessorDefinition;
|
||||
hasShadow: EuiPanelProps['hasShadow'];
|
||||
onUpdateProcessor: EditProcessorFlyoutProps['onUpdateProcessor'];
|
||||
onDeleteProcessor: EditProcessorFlyoutProps['onDeleteProcessor'];
|
||||
}
|
||||
|
||||
const ProcessorListItem = ({
|
||||
definition,
|
||||
processor,
|
||||
hasShadow = false,
|
||||
onUpdateProcessor,
|
||||
onDeleteProcessor,
|
||||
}: ProcessorListItemProps) => {
|
||||
const [isEditProcessorOpen, { on: openEditProcessor, off: closeEditProcessor }] = useBoolean();
|
||||
|
||||
const type = 'grok' in processor.config ? 'grok' : 'dissect';
|
||||
const description = getProcessorDescription(processor);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder hasShadow={hasShadow} paddingSize="s">
|
||||
<EuiFlexGroup gutterSize="m" responsive={false} alignItems="center">
|
||||
<EuiIcon type="grab" />
|
||||
<EuiText component="span" size="s">
|
||||
{type.toUpperCase()}
|
||||
</EuiText>
|
||||
<EuiFlexItem
|
||||
/* Allow text to overflow in flex child nodes */
|
||||
css={css`
|
||||
min-width: 0;
|
||||
`}
|
||||
>
|
||||
<EuiText component="span" size="s" color="subdued" className="eui-textTruncate">
|
||||
{description}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="streamsAppProcessorListItemButton"
|
||||
onClick={openEditProcessor}
|
||||
iconType="pencil"
|
||||
color="text"
|
||||
size="s"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.editProcessorAction',
|
||||
{ defaultMessage: 'Edit {type} processor', values: { type } }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
{isEditProcessorOpen && (
|
||||
<EditProcessorFlyout
|
||||
key={`edit-processor`}
|
||||
definition={definition}
|
||||
processor={processor}
|
||||
onClose={closeEditProcessor}
|
||||
onUpdateProcessor={onUpdateProcessor}
|
||||
onDeleteProcessor={onDeleteProcessor}
|
||||
/>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
const getProcessorDescription = (processor: EnrichmentUIProcessorDefinition) => {
|
||||
if (isGrokProcessor(processor.config)) {
|
||||
return processor.config.grok.patterns.join(' • ');
|
||||
} else if (isDissectProcessor(processor.config)) {
|
||||
return processor.config.dissect.pattern;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexItem, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { IngestStreamGetResponse, isWiredStreamGetResponse } from '@kbn/streams-schema';
|
||||
import { ProcessorOutcomePreview } from './processor_outcome_preview';
|
||||
import { TableColumn, UseProcessingSimulatorReturn } from './hooks/use_processing_simulator';
|
||||
|
||||
interface SimulationPlaygroundProps {
|
||||
definition: IngestStreamGetResponse;
|
||||
columns: TableColumn[];
|
||||
isLoading: UseProcessingSimulatorReturn['isLoading'];
|
||||
simulation: UseProcessingSimulatorReturn['simulation'];
|
||||
samples: UseProcessingSimulatorReturn['samples'];
|
||||
onRefreshSamples: UseProcessingSimulatorReturn['refreshSamples'];
|
||||
}
|
||||
|
||||
export const SimulationPlayground = (props: SimulationPlaygroundProps) => {
|
||||
const { definition, columns, isLoading, simulation, samples, onRefreshSamples } = props;
|
||||
|
||||
const tabs = {
|
||||
dataPreview: {
|
||||
name: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.simulationPlayground.dataPreview',
|
||||
{ defaultMessage: 'Data preview' }
|
||||
),
|
||||
},
|
||||
...(isWiredStreamGetResponse(definition) && {
|
||||
detectedFields: {
|
||||
name: i18n.translate(
|
||||
'xpack.streams.streamDetailView.managementTab.enrichment.simulationPlayground.detectedFields',
|
||||
{ defaultMessage: 'Detected fields' }
|
||||
),
|
||||
},
|
||||
}),
|
||||
} as const;
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState<keyof typeof tabs>('dataPreview');
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTabs bottomBorder={false}>
|
||||
{Object.entries(tabs).map(([tabId, tab]) => (
|
||||
<EuiTab
|
||||
key={tabId}
|
||||
isSelected={selectedTabId === tabId}
|
||||
onClick={() => setSelectedTabId(tabId as keyof typeof tabs)}
|
||||
>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="m" />
|
||||
{selectedTabId === 'dataPreview' && (
|
||||
<ProcessorOutcomePreview
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
simulation={simulation}
|
||||
samples={samples}
|
||||
onRefreshSamples={onRefreshSamples}
|
||||
/>
|
||||
)}
|
||||
{selectedTabId === 'detectedFields' &&
|
||||
i18n.translate('xpack.streams.simulationPlayground.div.detectedFieldsLabel', {
|
||||
defaultMessage: 'WIP',
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -29,6 +29,7 @@ export const SortableList = ({ onDragItem, children }: SortableListProps) => {
|
|||
droppableId="droppable-area"
|
||||
css={css`
|
||||
background-color: ${euiTheme.colors.backgroundTransparent};
|
||||
margin-bottom: ${euiTheme.size.s};
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -6,65 +6,36 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
Condition,
|
||||
DissectProcessorConfig,
|
||||
FieldDefinitionConfig,
|
||||
FieldDefinitionType,
|
||||
GrokProcessorConfig,
|
||||
ProcessorDefinition,
|
||||
ProcessorTypeOf,
|
||||
} from '@kbn/streams-schema';
|
||||
|
||||
export interface DissectProcessingDefinition {
|
||||
dissect: Omit<DissectProcessorConfig, 'if'>;
|
||||
}
|
||||
|
||||
export interface GrokProcessingDefinition {
|
||||
grok: Omit<GrokProcessorConfig, 'if'>;
|
||||
}
|
||||
|
||||
export interface ProcessingDefinition {
|
||||
condition: Condition;
|
||||
config: DissectProcessingDefinition | GrokProcessingDefinition;
|
||||
}
|
||||
|
||||
export interface EnrichmentUIProcessorDefinition extends ProcessingDefinition {
|
||||
export type WithUIAttributes<T extends ProcessorDefinition> = T & {
|
||||
id: string;
|
||||
type: ProcessorTypeOf<T>;
|
||||
status: 'draft' | 'saved' | 'updated';
|
||||
};
|
||||
|
||||
export type ProcessorDefinitionWithUIAttributes = WithUIAttributes<ProcessorDefinition>;
|
||||
|
||||
export interface DetectedField {
|
||||
name: string;
|
||||
type: FieldDefinitionType | 'unmapped';
|
||||
}
|
||||
|
||||
export interface ProcessingDefinitionGrok extends Pick<ProcessingDefinition, 'condition'> {
|
||||
config: GrokProcessingDefinition;
|
||||
}
|
||||
|
||||
export interface ProcessingDefinitionDissect extends Pick<ProcessingDefinition, 'condition'> {
|
||||
config: DissectProcessingDefinition;
|
||||
}
|
||||
|
||||
interface BaseFormState extends Pick<ProcessingDefinition, 'condition'> {
|
||||
interface BaseFormState {
|
||||
detected_fields?: DetectedField[];
|
||||
}
|
||||
|
||||
export type GrokFormState = BaseFormState &
|
||||
Omit<GrokProcessingDefinition['grok'], 'patterns'> & {
|
||||
Omit<GrokProcessorConfig, 'patterns'> & {
|
||||
type: 'grok';
|
||||
patterns: Array<{ value: string }>;
|
||||
};
|
||||
|
||||
export type DissectFormState = DissectProcessingDefinition['dissect'] &
|
||||
BaseFormState & { type: 'dissect' };
|
||||
export type DissectFormState = BaseFormState & DissectProcessorConfig & { type: 'dissect' };
|
||||
|
||||
export type ProcessorFormState = GrokFormState | DissectFormState;
|
||||
|
||||
export interface DetectedField {
|
||||
name: string;
|
||||
type: FieldDefinitionConfig['type'] | 'unmapped';
|
||||
}
|
||||
|
||||
export function isGrokProcessor(
|
||||
config: ProcessingDefinition['config']
|
||||
): config is GrokProcessingDefinition {
|
||||
return 'grok' in config;
|
||||
}
|
||||
|
||||
export function isDissectProcessor(
|
||||
config: ProcessingDefinition['config']
|
||||
): config is DissectProcessingDefinition {
|
||||
return 'dissect' in config;
|
||||
}
|
||||
|
|
|
@ -7,21 +7,17 @@
|
|||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import { ProcessorType, conditionSchema, createIsNarrowSchema } from '@kbn/streams-schema';
|
||||
import { ProcessorDefinition, ProcessorType, getProcessorType } from '@kbn/streams-schema';
|
||||
import { htmlIdGenerator } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { z } from '@kbn/zod';
|
||||
import {
|
||||
DissectFormState,
|
||||
DissectProcessingDefinition,
|
||||
EnrichmentUIProcessorDefinition,
|
||||
ProcessorDefinitionWithUIAttributes,
|
||||
GrokFormState,
|
||||
GrokProcessingDefinition,
|
||||
ProcessingDefinition,
|
||||
ProcessorFormState,
|
||||
isDissectProcessor,
|
||||
isGrokProcessor,
|
||||
WithUIAttributes,
|
||||
} from './types';
|
||||
import { EMPTY_EQUALS_CONDITION } from '../../util/condition';
|
||||
import { ALWAYS_CONDITION } from '../../util/condition';
|
||||
|
||||
const defaultGrokProcessorFormState: GrokFormState = {
|
||||
type: 'grok',
|
||||
|
@ -30,7 +26,7 @@ const defaultGrokProcessorFormState: GrokFormState = {
|
|||
pattern_definitions: {},
|
||||
ignore_failure: true,
|
||||
ignore_missing: true,
|
||||
condition: EMPTY_EQUALS_CONDITION,
|
||||
if: ALWAYS_CONDITION,
|
||||
};
|
||||
|
||||
const defaultDissectProcessorFormState: DissectFormState = {
|
||||
|
@ -39,7 +35,7 @@ const defaultDissectProcessorFormState: DissectFormState = {
|
|||
pattern: '',
|
||||
ignore_failure: true,
|
||||
ignore_missing: true,
|
||||
condition: EMPTY_EQUALS_CONDITION,
|
||||
if: ALWAYS_CONDITION,
|
||||
};
|
||||
|
||||
const defaultProcessorFormStateByType: Record<ProcessorType, ProcessorFormState> = {
|
||||
|
@ -49,67 +45,59 @@ const defaultProcessorFormStateByType: Record<ProcessorType, ProcessorFormState>
|
|||
|
||||
export const getDefaultFormState = (
|
||||
type: ProcessorType,
|
||||
processor?: EnrichmentUIProcessorDefinition
|
||||
processor?: ProcessorDefinitionWithUIAttributes
|
||||
): ProcessorFormState => {
|
||||
if (!processor) return defaultProcessorFormStateByType[type];
|
||||
|
||||
if (isGrokProcessor(processor.config)) {
|
||||
const { grok } = processor.config;
|
||||
if (isGrokProcessor(processor)) {
|
||||
const { grok } = processor;
|
||||
|
||||
return structuredClone({
|
||||
...grok,
|
||||
condition: processor.condition,
|
||||
type: 'grok',
|
||||
patterns: grok.patterns.map((pattern) => ({ value: pattern })),
|
||||
});
|
||||
}
|
||||
|
||||
const { dissect } = processor.config;
|
||||
if (isDissectProcessor(processor)) {
|
||||
const { dissect } = processor;
|
||||
|
||||
return structuredClone({
|
||||
...dissect,
|
||||
condition: processor.condition,
|
||||
type: 'dissect',
|
||||
});
|
||||
return structuredClone({
|
||||
...dissect,
|
||||
type: 'dissect',
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Default state not found for unsupported processor type: ${type}`);
|
||||
};
|
||||
|
||||
export const convertFormStateToProcessing = (
|
||||
formState: ProcessorFormState
|
||||
): ProcessingDefinition => {
|
||||
export const convertFormStateToProcessor = (formState: ProcessorFormState): ProcessorDefinition => {
|
||||
if (formState.type === 'grok') {
|
||||
const { condition, patterns, field, pattern_definitions, ignore_failure, ignore_missing } =
|
||||
formState;
|
||||
const { patterns, field, pattern_definitions, ignore_failure, ignore_missing } = formState;
|
||||
|
||||
return {
|
||||
condition,
|
||||
config: {
|
||||
grok: {
|
||||
patterns: patterns
|
||||
.filter(({ value }) => value.trim().length > 0)
|
||||
.map(({ value }) => value),
|
||||
field,
|
||||
pattern_definitions,
|
||||
ignore_failure,
|
||||
ignore_missing,
|
||||
},
|
||||
grok: {
|
||||
if: formState.if,
|
||||
patterns: patterns.filter(({ value }) => value.trim().length > 0).map(({ value }) => value),
|
||||
field,
|
||||
pattern_definitions,
|
||||
ignore_failure,
|
||||
ignore_missing,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (formState.type === 'dissect') {
|
||||
const { condition, field, pattern, append_separator, ignore_failure, ignore_missing } =
|
||||
formState;
|
||||
const { field, pattern, append_separator, ignore_failure, ignore_missing } = formState;
|
||||
|
||||
return {
|
||||
condition,
|
||||
config: {
|
||||
dissect: {
|
||||
field,
|
||||
pattern,
|
||||
append_separator,
|
||||
ignore_failure,
|
||||
ignore_missing,
|
||||
},
|
||||
dissect: {
|
||||
if: formState.if,
|
||||
field,
|
||||
pattern,
|
||||
append_separator: isEmpty(append_separator) ? undefined : append_separator,
|
||||
ignore_failure,
|
||||
ignore_missing,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -117,27 +105,36 @@ export const convertFormStateToProcessing = (
|
|||
throw new Error('Cannot convert form state to processing: unknown type.');
|
||||
};
|
||||
|
||||
export const isCompleteCondition = createIsNarrowSchema(z.unknown(), conditionSchema);
|
||||
const createProcessorGuardByType =
|
||||
<TProcessorType extends ProcessorType>(type: TProcessorType) =>
|
||||
(
|
||||
processor: ProcessorDefinitionWithUIAttributes
|
||||
): processor is WithUIAttributes<
|
||||
Extract<ProcessorDefinition, { [K in TProcessorType]: unknown }>
|
||||
> =>
|
||||
processor.type === type;
|
||||
|
||||
export const isCompleteGrokDefinition = (processing: GrokProcessingDefinition) => {
|
||||
const { patterns } = processing.grok;
|
||||
export const isGrokProcessor = createProcessorGuardByType('grok');
|
||||
export const isDissectProcessor = createProcessorGuardByType('dissect');
|
||||
|
||||
return !isEmpty(patterns);
|
||||
const createId = htmlIdGenerator();
|
||||
const toUIDefinition = <TProcessorDefinition extends ProcessorDefinition>(
|
||||
processor: TProcessorDefinition,
|
||||
uiAttributes: Partial<Pick<WithUIAttributes<TProcessorDefinition>, 'status'>> = {}
|
||||
): ProcessorDefinitionWithUIAttributes => ({
|
||||
id: createId(),
|
||||
status: 'saved',
|
||||
type: getProcessorType(processor),
|
||||
...uiAttributes,
|
||||
...processor,
|
||||
});
|
||||
|
||||
const toAPIDefinition = (processor: ProcessorDefinitionWithUIAttributes): ProcessorDefinition => {
|
||||
const { id, status, type, ...processorConfig } = processor;
|
||||
return processorConfig;
|
||||
};
|
||||
|
||||
export const isCompleteDissectDefinition = (processing: DissectProcessingDefinition) => {
|
||||
const { pattern } = processing.dissect;
|
||||
|
||||
return !isEmpty(pattern);
|
||||
};
|
||||
|
||||
export const isCompleteProcessingDefinition = (processing: ProcessingDefinition) => {
|
||||
if (isGrokProcessor(processing.config)) {
|
||||
return isCompleteGrokDefinition(processing.config);
|
||||
}
|
||||
if (isDissectProcessor(processing.config)) {
|
||||
return isCompleteDissectDefinition(processing.config);
|
||||
}
|
||||
|
||||
return false;
|
||||
export const processorConverter = {
|
||||
toAPIDefinition,
|
||||
toUIDefinition,
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UnwiredStreamGetResponse } from '@kbn/streams-schema';
|
||||
import { EuiCallOut, EuiFlexGroup, EuiListGroup, EuiText } from '@elastic/eui';
|
||||
|
@ -31,23 +31,6 @@ export function ClassicStreamDetailManagement({
|
|||
path: { key, subtab },
|
||||
} = useStreamsAppParams('/{key}/management/{subtab}');
|
||||
|
||||
const legacyDefinition = useMemo(() => {
|
||||
if (!definition) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
dashboards: definition.dashboards,
|
||||
elasticsearch_assets: definition.elasticsearch_assets,
|
||||
inherited_fields: {},
|
||||
effective_lifecycle: definition.effective_lifecycle,
|
||||
name: definition.stream.name,
|
||||
data_stream_exists: definition.data_stream_exists,
|
||||
stream: {
|
||||
...definition.stream,
|
||||
},
|
||||
};
|
||||
}, [definition]);
|
||||
|
||||
const tabs: ManagementTabs = {
|
||||
overview: {
|
||||
content: <UnmanagedStreamOverview definition={definition} />,
|
||||
|
@ -60,10 +43,7 @@ export function ClassicStreamDetailManagement({
|
|||
if (definition.data_stream_exists) {
|
||||
tabs.enrich = {
|
||||
content: (
|
||||
<StreamDetailEnrichment
|
||||
definition={legacyDefinition}
|
||||
refreshDefinition={refreshDefinition}
|
||||
/>
|
||||
<StreamDetailEnrichment definition={definition} refreshDefinition={refreshDefinition} />
|
||||
),
|
||||
label: i18n.translate('xpack.streams.streamDetailView.enrichmentTab', {
|
||||
defaultMessage: 'Extract field',
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { WiredStreamGetResponse } from '@kbn/streams-schema';
|
||||
import { useStreamsAppParams } from '../../hooks/use_streams_app_params';
|
||||
|
@ -33,22 +33,6 @@ export function WiredStreamDetailManagement({
|
|||
path: { key, subtab },
|
||||
} = useStreamsAppParams('/{key}/management/{subtab}');
|
||||
|
||||
const legacyDefinition = useMemo(() => {
|
||||
if (!definition) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
dashboards: definition.dashboards,
|
||||
inherited_fields: definition.inherited_fields,
|
||||
elasticsearch_assets: [],
|
||||
effective_lifecycle: definition.effective_lifecycle,
|
||||
name: definition.stream.name,
|
||||
stream: {
|
||||
...definition.stream,
|
||||
},
|
||||
};
|
||||
}, [definition]);
|
||||
|
||||
const tabs = {
|
||||
route: {
|
||||
content: (
|
||||
|
@ -60,10 +44,7 @@ export function WiredStreamDetailManagement({
|
|||
},
|
||||
enrich: {
|
||||
content: (
|
||||
<StreamDetailEnrichment
|
||||
definition={legacyDefinition}
|
||||
refreshDefinition={refreshDefinition}
|
||||
/>
|
||||
<StreamDetailEnrichment definition={definition} refreshDefinition={refreshDefinition} />
|
||||
),
|
||||
label: i18n.translate('xpack.streams.streamDetailView.enrichmentTab', {
|
||||
defaultMessage: 'Extract field',
|
||||
|
|
|
@ -306,7 +306,7 @@ function ChildStreamList({ definition }: { definition?: IngestStreamGetResponse
|
|||
[streamsRepositoryClient]
|
||||
);
|
||||
|
||||
const childDefinitions = useMemo(() => {
|
||||
const childrenStreams = useMemo(() => {
|
||||
if (!definition) {
|
||||
return [];
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ function ChildStreamList({ definition }: { definition?: IngestStreamGetResponse
|
|||
);
|
||||
}, [definition, streamsListFetch.value?.streams]);
|
||||
|
||||
if (definition && childDefinitions?.length === 1) {
|
||||
if (definition && childrenStreams?.length === 1) {
|
||||
return (
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
|
@ -361,5 +361,5 @@ function ChildStreamList({ definition }: { definition?: IngestStreamGetResponse
|
|||
);
|
||||
}
|
||||
|
||||
return <StreamsList definitions={childDefinitions} showControls={false} />;
|
||||
return <StreamsList streams={childrenStreams} showControls={false} />;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ import { useAbortController } from '@kbn/observability-utils-browser/hooks/use_a
|
|||
import { useDateRange } from '@kbn/observability-utils-browser/hooks/use_date_range';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import {
|
||||
ReadStreamDefinition,
|
||||
isRoot,
|
||||
isDescendantOf,
|
||||
RoutingDefinition,
|
||||
|
@ -93,9 +92,9 @@ function useRoutingState({
|
|||
);
|
||||
|
||||
// Child streams: either represents the child streams as they are, or the new order from drag and drop.
|
||||
const [childStreams, setChildStreams] = React.useState<
|
||||
ReadStreamDefinition['stream']['ingest']['routing']
|
||||
>(definition?.stream.ingest.routing ?? []);
|
||||
const [childStreams, setChildStreams] = React.useState<RoutingDefinition[]>(
|
||||
definition?.stream.ingest.routing ?? []
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setChildStreams(definition?.stream.ingest.routing ?? []);
|
||||
|
|
|
@ -61,7 +61,7 @@ export function StreamListView() {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<StreamsList definitions={streamsListFetch.value?.streams} query={query} showControls />
|
||||
<StreamsList streams={streamsListFetch.value?.streams} query={query} showControls />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</StreamsAppPageBody>
|
||||
|
|
|
@ -35,31 +35,29 @@ import { getIndexPatterns } from '../../util/hierarchy_helpers';
|
|||
export interface StreamTree {
|
||||
name: string;
|
||||
type: 'wired' | 'root' | 'classic';
|
||||
definition: StreamDefinition;
|
||||
stream: StreamDefinition;
|
||||
children: StreamTree[];
|
||||
}
|
||||
|
||||
function asTrees(definitions: StreamDefinition[]) {
|
||||
function asTrees(streams: StreamDefinition[]) {
|
||||
const trees: StreamTree[] = [];
|
||||
const wiredDefinitions = definitions.filter((definition) => isWiredStreamDefinition(definition));
|
||||
wiredDefinitions.sort((a, b) => getSegments(a.name).length - getSegments(b.name).length);
|
||||
const wiredStreams = streams.filter(isWiredStreamDefinition);
|
||||
wiredStreams.sort((a, b) => getSegments(a.name).length - getSegments(b.name).length);
|
||||
|
||||
wiredDefinitions.forEach((definition) => {
|
||||
wiredStreams.forEach((stream) => {
|
||||
let currentTree = trees;
|
||||
let existingNode: StreamTree | undefined;
|
||||
const segments = getSegments(definition.name);
|
||||
const segments = getSegments(stream.name);
|
||||
// traverse the tree following the prefix of the current id.
|
||||
// once we reach the leaf, the current id is added as child - this works because the ids are sorted by depth
|
||||
while (
|
||||
(existingNode = currentTree.find((node) => isDescendantOf(node.name, definition.name)))
|
||||
) {
|
||||
while ((existingNode = currentTree.find((node) => isDescendantOf(node.name, stream.name)))) {
|
||||
currentTree = existingNode.children;
|
||||
}
|
||||
if (!existingNode) {
|
||||
const newNode: StreamTree = {
|
||||
name: definition.name,
|
||||
name: stream.name,
|
||||
children: [],
|
||||
definition,
|
||||
stream,
|
||||
type: segments.length === 1 ? 'root' : 'wired',
|
||||
};
|
||||
currentTree.push(newNode);
|
||||
|
@ -70,19 +68,19 @@ function asTrees(definitions: StreamDefinition[]) {
|
|||
}
|
||||
|
||||
export function StreamsList({
|
||||
definitions,
|
||||
streams,
|
||||
query,
|
||||
showControls,
|
||||
}: {
|
||||
definitions: StreamDefinition[] | undefined;
|
||||
streams: StreamDefinition[] | undefined;
|
||||
query?: string;
|
||||
showControls: boolean;
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = React.useState<Record<string, boolean>>({});
|
||||
const [showClassic, setShowClassic] = React.useState(true);
|
||||
const items = useMemo(() => {
|
||||
return definitions ?? [];
|
||||
}, [definitions]);
|
||||
return streams ?? [];
|
||||
}, [streams]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
return items
|
||||
|
@ -96,10 +94,10 @@ export function StreamsList({
|
|||
|
||||
const treeView = useMemo(() => {
|
||||
const trees = asTrees(filteredItems);
|
||||
const classicList = classicStreams.map((definition) => ({
|
||||
name: definition.name,
|
||||
const classicList = classicStreams.map((stream) => ({
|
||||
name: stream.name,
|
||||
type: 'classic' as const,
|
||||
definition,
|
||||
stream,
|
||||
children: [],
|
||||
}));
|
||||
return [...trees, ...classicList];
|
||||
|
@ -190,7 +188,7 @@ function StreamNode({
|
|||
);
|
||||
|
||||
const discoverUrl = useMemo(() => {
|
||||
const indexPatterns = getIndexPatterns(node.definition);
|
||||
const indexPatterns = getIndexPatterns(node.stream);
|
||||
|
||||
if (!discoverLocator || !indexPatterns) {
|
||||
return undefined;
|
||||
|
|
|
@ -6,30 +6,35 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { OverlayModalConfirmOptions } from '@kbn/core/public';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
export const useDiscardConfirm = (handler: () => void) => {
|
||||
const defaultMessage = i18n.translate('xpack.streams.cancelModal.message', {
|
||||
defaultMessage: 'Are you sure you want to discard your changes?',
|
||||
});
|
||||
|
||||
export const useDiscardConfirm = <THandler extends (..._args: any[]) => any>(
|
||||
handler: THandler,
|
||||
options: OverlayModalConfirmOptions & { message?: string } = {}
|
||||
) => {
|
||||
const { core } = useKibana();
|
||||
const { message = defaultMessage, ...optionsOverride } = options;
|
||||
|
||||
return async () => {
|
||||
const hasCancelled = await core.overlays.openConfirm(
|
||||
i18n.translate('xpack.streams.cancelModal.message', {
|
||||
defaultMessage: 'Are you sure you want to discard your changes?',
|
||||
return async (...args: Parameters<THandler>) => {
|
||||
const hasCancelled = await core.overlays.openConfirm(message, {
|
||||
buttonColor: 'danger',
|
||||
title: i18n.translate('xpack.streams.cancelModal.title', {
|
||||
defaultMessage: 'Discard changes?',
|
||||
}),
|
||||
{
|
||||
buttonColor: 'danger',
|
||||
title: i18n.translate('xpack.streams.cancelModal.title', {
|
||||
defaultMessage: 'Discard changes?',
|
||||
}),
|
||||
confirmButtonText: i18n.translate('xpack.streams.cancelModal.confirm', {
|
||||
defaultMessage: 'Discard',
|
||||
}),
|
||||
cancelButtonText: i18n.translate('xpack.streams.cancelModal.cancel', {
|
||||
defaultMessage: 'Keep editing',
|
||||
}),
|
||||
}
|
||||
);
|
||||
confirmButtonText: i18n.translate('xpack.streams.cancelModal.confirm', {
|
||||
defaultMessage: 'Discard',
|
||||
}),
|
||||
cancelButtonText: i18n.translate('xpack.streams.cancelModal.cancel', {
|
||||
defaultMessage: 'Keep editing',
|
||||
}),
|
||||
...optionsOverride,
|
||||
});
|
||||
|
||||
if (hasCancelled) handler();
|
||||
if (hasCancelled) handler(...args);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -19,6 +19,8 @@ export const EMPTY_EQUALS_CONDITION: BinaryFilterCondition = Object.freeze({
|
|||
value: '',
|
||||
});
|
||||
|
||||
export const ALWAYS_CONDITION: AlwaysCondition = Object.freeze({ always: {} });
|
||||
|
||||
export function alwaysToEmptyEquals<T extends Condition>(condition: T): Exclude<T, AlwaysCondition>;
|
||||
|
||||
export function alwaysToEmptyEquals(condition: Condition) {
|
||||
|
@ -30,7 +32,7 @@ export function alwaysToEmptyEquals(condition: Condition) {
|
|||
|
||||
export function emptyEqualsToAlways(condition: Condition) {
|
||||
if (isEqual(condition, EMPTY_EQUALS_CONDITION)) {
|
||||
return { always: {} };
|
||||
return ALWAYS_CONDITION;
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
|
||||
import { StreamDefinition, isUnwiredStreamDefinition } from '@kbn/streams-schema';
|
||||
|
||||
export function getIndexPatterns(definition: StreamDefinition | undefined) {
|
||||
if (!definition) {
|
||||
export function getIndexPatterns(stream: StreamDefinition | undefined) {
|
||||
if (!stream) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isUnwiredStreamDefinition(definition)) {
|
||||
return [definition.name];
|
||||
if (!isUnwiredStreamDefinition(stream)) {
|
||||
return [stream.name];
|
||||
}
|
||||
const isRoot = definition.name.indexOf('.') === -1;
|
||||
const dataStreamOfDefinition = definition.name;
|
||||
const isRoot = stream.name.indexOf('.') === -1;
|
||||
const dataStreamOfDefinition = stream.name;
|
||||
return isRoot
|
||||
? [dataStreamOfDefinition, `${dataStreamOfDefinition}.*`]
|
||||
: [`${dataStreamOfDefinition}*`];
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
"@kbn/deeplinks-analytics",
|
||||
"@kbn/dashboard-plugin",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/fields-metadata-plugin",
|
||||
"@kbn/zod"
|
||||
"@kbn/fields-metadata-plugin"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
IngestStreamEffectiveLifecycle,
|
||||
IngestStreamLifecycle,
|
||||
IngestStreamUpsertRequest,
|
||||
WiredReadStreamDefinition,
|
||||
WiredStreamGetResponse,
|
||||
asIngestStreamGetResponse,
|
||||
isDslLifecycle,
|
||||
|
@ -104,12 +103,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
expect(response).to.have.property('acknowledged', true);
|
||||
|
||||
const updatedRootDefinition = await getStream(apiClient, 'logs');
|
||||
expect((updatedRootDefinition as WiredReadStreamDefinition).stream.ingest.lifecycle).to.eql(
|
||||
{
|
||||
dsl: { data_retention: '999d' },
|
||||
}
|
||||
);
|
||||
expect((updatedRootDefinition as WiredReadStreamDefinition).effective_lifecycle).to.eql({
|
||||
expect((updatedRootDefinition as WiredStreamGetResponse).stream.ingest.lifecycle).to.eql({
|
||||
dsl: { data_retention: '999d' },
|
||||
});
|
||||
expect((updatedRootDefinition as WiredStreamGetResponse).effective_lifecycle).to.eql({
|
||||
dsl: { data_retention: '999d' },
|
||||
from: 'logs',
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue