[ML] Adds Pattern analysis embeddable for dashboards (#186539)

Adds a new embeddable version of the pattern analysis component.


![image](https://github.com/user-attachments/assets/984921b5-cd33-4f7f-9207-c3182c97d015)

The options to configure it are the same as the menu available in the
Patterns tab in Discover.


![image](https://github.com/user-attachments/assets/33e3b537-c4e2-475b-89ad-8fdfef347fdc)

**Actions**
The user can choose to filter a pattern in or out in in the current
dashboard, or jump to Discover.

![image](https://github.com/user-attachments/assets/dfd9dc3d-932c-463b-8ea5-e93fa6caf518)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
James Gowdy 2024-08-20 12:09:20 +01:00 committed by GitHub
parent 8316cbf019
commit 9ff78cf08b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 2442 additions and 539 deletions

View file

@ -78,6 +78,17 @@ export const DiscoverMainContent = ({
trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK);
}
}
return new Promise<VIEW_MODE>((resolve, reject) => {
// return a promise to report when the view mode has been updated
stateContainer.appState.subscribe((state) => {
if (state.viewMode === mode) {
resolve(mode);
} else {
reject(mode);
}
});
});
},
[trackUiMetric, stateContainer]
);

View file

@ -7,24 +7,16 @@
*/
import React, { memo, type FC } from 'react';
import { useQuerySubscriber } from '@kbn/unified-field-list/src/hooks/use_query_subscriber';
import { useSavedSearch } from '../../state_management/discover_state_provider';
import { PatternAnalysisTable, type PatternAnalysisTableProps } from './pattern_analysis_table';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
export const PatternAnalysisTab: FC<Omit<PatternAnalysisTableProps, 'query' | 'filters'>> = memo(
(props) => {
const services = useDiscoverServices();
const querySubscriberResult = useQuerySubscriber({
data: services.data,
});
const savedSearch = useSavedSearch();
return (
<PatternAnalysisTable
dataView={props.dataView}
filters={querySubscriberResult.filters}
query={querySubscriberResult.query}
switchToDocumentView={props.switchToDocumentView}
savedSearch={savedSearch}
stateContainer={props.stateContainer}

View file

@ -10,7 +10,7 @@ import React, { useEffect, useState, useMemo } from 'react';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import { type EmbeddablePatternAnalysisInput } from '@kbn/aiops-log-pattern-analysis/embeddable';
import { pick } from 'lodash';
import type { LogCategorizationEmbeddableProps } from '@kbn/aiops-plugin/public/components/log_categorization/log_categorization_for_embeddable/log_categorization_for_embeddable';
import type { LogCategorizationEmbeddableProps } from '@kbn/aiops-plugin/public/components/log_categorization/log_categorization_for_embeddable/log_categorization_for_discover';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import type { DiscoverStateContainer } from '../../state_management/discover_state';
import { PATTERN_ANALYSIS_LOADED } from './constants';
@ -47,11 +47,9 @@ export const PatternAnalysisTable = (props: PatternAnalysisTableProps) => {
const patternAnalysisComponentProps: LogCategorizationEmbeddableProps = useMemo(
() => ({
input: Object.assign(
{},
pick(props, ['dataView', 'savedSearch', 'query', 'filters', 'switchToDocumentView']),
{ lastReloadRequestTime }
),
input: Object.assign({}, pick(props, ['dataView', 'savedSearch', 'switchToDocumentView']), {
lastReloadRequestTime,
}),
renderViewModeToggle: props.renderViewModeToggle,
}),
[lastReloadRequestTime, props]

View file

@ -31,7 +31,7 @@ export const DocumentViewModeToggle = ({
isEsqlMode: boolean;
prepend?: ReactElement;
stateContainer: DiscoverStateContainer;
setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
setDiscoverViewMode: (viewMode: VIEW_MODE) => Promise<VIEW_MODE>;
patternCount?: number;
dataView: DataView;
}) => {
@ -86,8 +86,7 @@ export const DocumentViewModeToggle = ({
}
}, [showPatternAnalysisTab, viewMode, setDiscoverViewMode]);
const includesNormalTabsStyle =
viewMode === VIEW_MODE.AGGREGATED_LEVEL || viewMode === VIEW_MODE.PATTERN_LEVEL || isLegacy;
const includesNormalTabsStyle = viewMode === VIEW_MODE.AGGREGATED_LEVEL || isLegacy;
const containerPadding = includesNormalTabsStyle ? euiTheme.size.s : 0;
const containerCss = css`

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export const EMBEDDABLE_PATTERN_ANALYSIS_TYPE = 'aiopsPatternAnalysisEmbeddable' as const;
export const PATTERN_ANALYSIS_DATA_VIEW_REF_NAME = 'aiopsPatternAnalysisEmbeddableDataViewId';

View file

@ -5,16 +5,13 @@
* 2.0.
*/
import type { Query, AggregateQuery, Filter } from '@kbn/es-query';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import type { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
export interface EmbeddablePatternAnalysisInput<T = Query | AggregateQuery> {
export interface EmbeddablePatternAnalysisInput {
dataView: DataView;
savedSearch?: SavedSearch | null;
query?: T;
filters?: Filter[];
embeddingOrigin?: string;
switchToDocumentView?: () => void;
switchToDocumentView?: () => Promise<VIEW_MODE>;
lastReloadRequestTime?: number;
}

View file

@ -20,7 +20,6 @@
"@kbn/config-schema",
"@kbn/i18n",
"@kbn/ml-runtime-field-utils",
"@kbn/es-query",
"@kbn/saved-search-plugin",
"@kbn/data-views-plugin",
"@kbn/ml-is-populated-object",

View file

@ -6,7 +6,7 @@
*/
import type { FC } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { EuiBasicTableColumn, EuiTableSelectionType } from '@elastic/eui';
import {
@ -16,12 +16,12 @@ import {
EuiToolTip,
EuiIcon,
} from '@elastic/eui';
import type { Action } from '@elastic/eui/src/components/basic_table/action_types';
import { i18n } from '@kbn/i18n';
import type { UseTableState } from '@kbn/ml-in-memory-table';
import { css } from '@emotion/react';
import { QUERY_MODE } from '@kbn/aiops-log-pattern-analysis/get_category_query';
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
import { useEuiTheme } from '../../../hooks/use_eui_theme';
@ -32,34 +32,36 @@ import type { EventRate } from '../use_categorize_request';
import { ExpandedRow } from './expanded_row';
import { FormattedPatternExamples, FormattedTokens } from '../format_category';
import type { OpenInDiscover } from './use_open_in_discover';
interface Props {
categories: Category[];
eventRate: EventRate;
pinnedCategory: Category | null;
setPinnedCategory: (category: Category | null) => void;
highlightedCategory: Category | null;
setHighlightedCategory: (category: Category | null) => void;
mouseOver?: {
pinnedCategory: Category | null;
setPinnedCategory: (category: Category | null) => void;
highlightedCategory: Category | null;
setHighlightedCategory: (category: Category | null) => void;
};
setSelectedCategories: (category: Category[]) => void;
openInDiscover: OpenInDiscover;
tableState: UseTableState<Category>;
actions: Array<Action<Category>>;
enableRowActions?: boolean;
displayExamples?: boolean;
selectable?: boolean;
onRenderComplete?: () => void;
}
export const CategoryTable: FC<Props> = ({
categories,
eventRate,
pinnedCategory,
setPinnedCategory,
highlightedCategory,
setHighlightedCategory,
mouseOver,
setSelectedCategories,
openInDiscover,
tableState,
actions,
enableRowActions = true,
displayExamples = true,
selectable = true,
onRenderComplete,
}) => {
const euiTheme = useEuiTheme();
const primaryBackgroundColor = useEuiBackgroundColor('primary');
@ -73,8 +75,6 @@ export const CategoryTable: FC<Props> = ({
return categories.some((category) => category.sparkline !== undefined);
}, [categories]);
const { labels: openInDiscoverLabels, openFunction: openInDiscoverFunction } = openInDiscover;
const toggleDetails = useCallback(
(category: Category) => {
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
@ -134,24 +134,7 @@ export const CategoryTable: FC<Props> = ({
}),
sortable: false,
width: '65px',
actions: [
{
name: openInDiscoverLabels.singleSelect.in,
description: openInDiscoverLabels.singleSelect.in,
icon: 'plusInCircle',
type: 'icon',
'data-test-subj': 'aiopsLogPatternsActionFilterInButton',
onClick: (category) => openInDiscoverFunction(QUERY_MODE.INCLUDE, category),
},
{
name: openInDiscoverLabels.singleSelect.out,
description: openInDiscoverLabels.singleSelect.out,
icon: 'minusInCircle',
type: 'icon',
'data-test-subj': 'aiopsLogPatternsActionFilterOutButton',
onClick: (category) => openInDiscoverFunction(QUERY_MODE.EXCLUDE, category),
},
],
actions,
},
] as Array<EuiBasicTableColumn<Category>>;
@ -214,23 +197,29 @@ export const CategoryTable: FC<Props> = ({
});
}
const selectionValue: EuiTableSelectionType<Category> | undefined = {
selectable: () => true,
onSelectionChange: (selectedItems) => setSelectedCategories(selectedItems),
};
const selectionValue: EuiTableSelectionType<Category> | undefined = selectable
? {
selectable: () => true,
onSelectionChange: (selectedItems) => setSelectedCategories(selectedItems),
}
: undefined;
const getRowStyle = (category: Category) => {
if (mouseOver === undefined) {
return {};
}
if (
pinnedCategory &&
pinnedCategory.key === category.key &&
pinnedCategory.key === category.key
mouseOver.pinnedCategory &&
mouseOver.pinnedCategory.key === category.key &&
mouseOver.pinnedCategory.key === category.key
) {
return {
backgroundColor: primaryBackgroundColor,
};
}
if (highlightedCategory && highlightedCategory.key === category.key) {
if (mouseOver.highlightedCategory && mouseOver.highlightedCategory.key === category.key) {
return {
backgroundColor: euiTheme.euiColorLightestShade,
};
@ -251,39 +240,66 @@ export const CategoryTable: FC<Props> = ({
},
});
const chartWrapperRef = useRef<HTMLDivElement>(null);
const renderCompleteListener = useCallback(
(event: Event) => {
if (event.target !== chartWrapperRef.current) {
return;
}
if (typeof onRenderComplete === 'function') {
onRenderComplete();
}
},
[onRenderComplete]
);
useEffect(() => {
if (!chartWrapperRef.current) {
throw new Error('Reference to the chart wrapper is not set');
}
const chartWrapper = chartWrapperRef.current;
chartWrapper.addEventListener('renderComplete', renderCompleteListener);
return () => {
chartWrapper.removeEventListener('renderComplete', renderCompleteListener);
};
}, [renderCompleteListener]);
return (
<EuiInMemoryTable<Category>
compressed
items={categories}
columns={columns}
selection={selectionValue}
itemId="key"
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="aiopsLogPatternsTable"
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
css={tableStyle}
rowProps={(category) => {
return enableRowActions
? {
onClick: () => {
if (category.key === pinnedCategory?.key) {
setPinnedCategory(null);
} else {
setPinnedCategory(category);
}
},
onMouseEnter: () => {
setHighlightedCategory(category);
},
onMouseLeave: () => {
setHighlightedCategory(null);
},
style: getRowStyle(category),
}
: undefined;
}}
/>
<div ref={chartWrapperRef}>
<EuiInMemoryTable<Category>
compressed
items={categories}
columns={columns}
selection={selectionValue}
itemId="key"
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="aiopsLogPatternsTable"
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
css={tableStyle}
rowProps={(category) => {
return mouseOver
? {
onClick: () => {
if (category.key === mouseOver.pinnedCategory?.key) {
mouseOver.setPinnedCategory(null);
} else {
mouseOver.setPinnedCategory(category);
}
},
onMouseEnter: () => {
mouseOver.setHighlightedCategory(category);
},
onMouseLeave: () => {
mouseOver.setHighlightedCategory(null);
},
style: getRowStyle(category),
}
: undefined;
}}
/>
</div>
);
};

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
export function getLabels(isFlyout: boolean) {
export function getLabels(inDiscover: boolean) {
const flyoutFilterIn = (single: boolean) =>
i18n.translate('xpack.aiops.logCategorization.flyout.filterIn', {
defaultMessage: 'Filter for {values, plural, one {pattern} other {patterns}}',
@ -38,18 +38,8 @@ export function getLabels(isFlyout: boolean) {
},
});
return isFlyout
return inDiscover
? {
multiSelect: {
in: flyoutFilterIn(false),
out: flyoutFilterOut(false),
},
singleSelect: {
in: flyoutFilterIn(true),
out: flyoutFilterOut(true),
},
}
: {
multiSelect: {
in: aiopsFilterIn(false),
out: aiopsFilterOut(false),
@ -58,5 +48,15 @@ export function getLabels(isFlyout: boolean) {
in: aiopsFilterIn(true),
out: aiopsFilterOut(true),
},
}
: {
multiSelect: {
in: flyoutFilterIn(false),
out: flyoutFilterOut(false),
},
singleSelect: {
in: flyoutFilterIn(true),
out: flyoutFilterOut(true),
},
};
}

View file

@ -58,7 +58,8 @@ export const OpenInDiscoverButtons: FC<{ openInDiscover: OpenInDiscover; showTex
openInDiscover,
showText = true,
}) => {
const { labels, openFunction } = openInDiscover;
const { getLabels, openFunction } = openInDiscover;
const labels = getLabels(false);
return (
<EuiFlexGroup gutterSize="none" alignItems="center">
@ -67,7 +68,7 @@ export const OpenInDiscoverButtons: FC<{ openInDiscover: OpenInDiscover; showTex
<EuiButtonEmpty
data-test-subj="aiopsLogPatternAnalysisOpenInDiscoverIncludeButton"
size="s"
onClick={() => openFunction(QUERY_MODE.INCLUDE)}
onClick={() => openFunction(QUERY_MODE.INCLUDE, true)}
iconType="plusInCircle"
iconSide="left"
>
@ -80,7 +81,7 @@ export const OpenInDiscoverButtons: FC<{ openInDiscover: OpenInDiscover; showTex
<EuiButtonEmpty
data-test-subj="aiopsLogPatternAnalysisOpenInDiscoverExcludeButton"
size="s"
onClick={() => openFunction(QUERY_MODE.EXCLUDE)}
onClick={() => openFunction(QUERY_MODE.EXCLUDE, true)}
iconType="minusInCircle"
iconSide="left"
>

View file

@ -0,0 +1,78 @@
/*
* 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 { useCallback } from 'react';
import type { Action } from '@elastic/eui/src/components/basic_table/action_types';
import { QUERY_MODE } from '@kbn/aiops-log-pattern-analysis/get_category_query';
import type { Filter } from '@kbn/es-query';
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
import type { DataViewField } from '@kbn/data-views-plugin/common';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import type { CategorizationAdditionalFilter } from '@kbn/aiops-log-pattern-analysis/create_category_request';
import type { LogCategorizationAppState } from '../../../application/url_state/log_pattern_analysis';
import type { OpenInDiscover } from './use_open_in_discover';
import { useOpenInDiscover } from './use_open_in_discover';
export interface UseActions {
getActions: (navigateToDiscover: boolean) => Array<Action<Category>>;
openInDiscover: OpenInDiscover;
}
export function useActions(
dataViewId: string,
selectedField: DataViewField | string | undefined,
selectedCategories: Category[],
aiopsListState: LogCategorizationAppState,
timefilter: TimefilterContract,
onAddFilter?: (values: Filter, alias?: string) => void,
additionalFilter?: CategorizationAdditionalFilter,
onClose: () => void = () => {}
): UseActions {
const openInDiscover = useOpenInDiscover(
dataViewId,
selectedField ?? undefined,
selectedCategories,
aiopsListState,
timefilter,
onAddFilter,
additionalFilter
);
const { getLabels: getOpenInDiscoverLabels, openFunction: openInDiscoverFunction } =
openInDiscover;
const getActions = useCallback(
(navigateToDiscover: boolean): Array<Action<Category>> => {
const openInDiscoverLabels = getOpenInDiscoverLabels(navigateToDiscover);
return [
{
name: openInDiscoverLabels.singleSelect.in,
description: openInDiscoverLabels.singleSelect.in,
icon: 'plusInCircle',
type: 'icon',
'data-test-subj': 'aiopsLogPatternsActionFilterInButton',
onClick: (category) =>
openInDiscoverFunction(QUERY_MODE.INCLUDE, navigateToDiscover, category),
},
{
name: openInDiscoverLabels.singleSelect.out,
description: openInDiscoverLabels.singleSelect.out,
icon: 'minusInCircle',
type: 'icon',
'data-test-subj': 'aiopsLogPatternsActionFilterOutButton',
onClick: (category) =>
openInDiscoverFunction(QUERY_MODE.EXCLUDE, navigateToDiscover, category),
},
];
},
[getOpenInDiscoverLabels, openInDiscoverFunction]
);
return { getActions, openInDiscover };
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import moment from 'moment';
@ -20,8 +20,8 @@ import type { LogCategorizationAppState } from '../../../application/url_state/l
import { getLabels } from './labels';
export interface OpenInDiscover {
openFunction: (mode: QueryMode, category?: Category) => void;
labels: ReturnType<typeof getLabels>;
openFunction: (mode: QueryMode, navigateToDiscover: boolean, category?: Category) => void;
getLabels: (navigateToDiscover: boolean) => ReturnType<typeof getLabels>;
count: number;
}
@ -31,7 +31,6 @@ export function useOpenInDiscover(
selectedCategories: Category[],
aiopsListState: LogCategorizationAppState,
timefilter: TimefilterContract,
navigateToDiscover?: boolean,
onAddFilter?: (values: Filter, alias?: string) => void,
additionalFilter?: CategorizationAdditionalFilter,
onClose: () => void = () => {}
@ -39,7 +38,7 @@ export function useOpenInDiscover(
const { openInDiscoverWithFilter } = useDiscoverLinks();
const openFunction = useCallback(
(mode: QueryMode, category?: Category) => {
(mode: QueryMode, navigateToDiscover: boolean, category?: Category) => {
if (
onAddFilter !== undefined &&
selectedField !== undefined &&
@ -80,7 +79,6 @@ export function useOpenInDiscover(
[
onAddFilter,
selectedField,
navigateToDiscover,
additionalFilter,
timefilter,
openInDiscoverWithFilter,
@ -91,10 +89,5 @@ export function useOpenInDiscover(
]
);
const labels = useMemo(() => {
const isFlyout = onAddFilter !== undefined && onClose !== undefined;
return getLabels(isFlyout && navigateToDiscover === false);
}, [navigateToDiscover, onAddFilter, onClose]);
return { openFunction, labels, count: selectedCategories.length };
return { openFunction, getLabels, count: selectedCategories.length };
}

View file

@ -29,6 +29,8 @@ export const FieldValidationCallout: FC<Props> = ({ validationResults }) => {
return (
<EuiCallOut
color="warning"
size="s"
iconType={'alert'}
title={i18n.translate('xpack.aiops.logCategorization.fieldValidationTitle', {
defaultMessage: 'The selected field is possibly not suitable for pattern analysis',
})}

View file

@ -8,7 +8,7 @@
import type { FC } from 'react';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
@ -67,48 +67,45 @@ export const DiscoverTabs: FC<Props> = ({
<EuiFlexItem grow={false}>{renderViewModeToggle(data?.categories.length)}</EuiFlexItem>
<EuiFlexItem />
<EuiFlexItem grow={false}>
<>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="s" responsive={false}>
{selectedCategories.length > 0 ? (
<EuiFlexItem>
<SelectedPatterns openInDiscover={openInDiscover} />
</EuiFlexItem>
) : null}
<EuiFlexGroup gutterSize="s" responsive={false}>
{selectedCategories.length > 0 ? (
<EuiFlexItem>
<SelectedField
fields={fields}
setSelectedField={setSelectedField}
selectedField={selectedField}
/>
<SelectedPatterns openInDiscover={openInDiscover} />
</EuiFlexItem>
<EuiFlexItem>
<div className="unifiedDataTableToolbarControlGroup" css={{ marginRight: '8px' }}>
) : null}
<EuiFlexItem>
<SelectedField
fields={fields}
setSelectedField={setSelectedField}
selectedField={selectedField}
/>
</EuiFlexItem>
<EuiFlexItem>
<div className="unifiedDataTableToolbarControlGroup" css={{ marginRight: '8px' }}>
<div className="unifiedDataTableToolbarControlIconButton">
<EmbeddableMenu
randomSampler={randomSampler}
reload={() => loadCategories()}
minimumTimeRangeOption={minimumTimeRangeOption}
setMinimumTimeRangeOption={setMinimumTimeRangeOption}
categoryCount={data?.totalCategories}
/>
</div>
{selectedField !== null && earliest !== undefined && latest !== undefined ? (
<div className="unifiedDataTableToolbarControlIconButton">
<EmbeddableMenu
randomSampler={randomSampler}
reload={() => loadCategories()}
minimumTimeRangeOption={minimumTimeRangeOption}
setMinimumTimeRangeOption={setMinimumTimeRangeOption}
categoryCount={data?.totalCategories}
<CreateCategorizationJobButton
dataView={dataview}
field={selectedField}
query={query}
earliest={earliest}
latest={latest}
iconOnly={true}
/>
</div>
{selectedField !== null && earliest !== undefined && latest !== undefined ? (
<div className="unifiedDataTableToolbarControlIconButton">
<CreateCategorizationJobButton
dataView={dataview}
field={selectedField}
query={query}
earliest={earliest}
latest={latest}
iconOnly={true}
/>
</div>
) : null}
</div>
</EuiFlexItem>
</EuiFlexGroup>
</>
) : null}
</div>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -36,9 +36,9 @@ interface Props {
reload: () => void;
}
const minimumTimeRangeOptions = Object.keys(MINIMUM_TIME_RANGE).map((value) => ({
inputDisplay: value,
value: value as MinimumTimeRangeOption,
const minimumTimeRangeOptions = Object.entries(MINIMUM_TIME_RANGE).map(([key, { label }]) => ({
inputDisplay: label,
value: key as MinimumTimeRangeOption,
}));
export const EmbeddableMenu: FC<Props> = ({
@ -62,7 +62,7 @@ export const EmbeddableMenu: FC<Props> = ({
size="s"
iconType="controlsHorizontal"
onClick={() => togglePopover()}
// @ts-ignore - subdued does work
// @ts-expect-error - subdued does work
color="subdued"
aria-label={i18n.translate('xpack.aiops.logCategorization.embeddableMenu.aria', {
defaultMessage: 'Pattern analysis options',
@ -91,54 +91,11 @@ export const EmbeddableMenu: FC<Props> = ({
</EuiTitle>
<EuiSpacer size="s" />
<EuiFormRow
data-test-subj="aiopsRandomSamplerOptionsFormRow"
label={
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem css={{ textWrap: 'nowrap' }}>
{i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.minimumTimeRangeOptionsRowLabel',
{
defaultMessage: 'Minimum time range',
}
)}
</EuiFlexItem>
<EuiFlexItem>
<EuiToolTip
content={i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.minimumTimeRange.tooltip',
{
defaultMessage:
'Adds a wider time range to the analysis to improve pattern accuracy.',
}
)}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
}
helpText={
<>
{categoryCount !== undefined && minimumTimeRangeOption !== 'No minimum' ? (
<>
<FormattedMessage
id="xpack.aiops.logCategorization.embeddableMenu.totalPatternsMessage"
defaultMessage="Total patterns in {minimumTimeRangeOption}: {categoryCount}"
values={{ minimumTimeRangeOption, categoryCount }}
/>
</>
) : null}
</>
}
>
<EuiSuperSelect
aria-label="Select a minimum time range"
options={minimumTimeRangeOptions}
valueOfSelected={minimumTimeRangeOption}
onChange={setMinimumTimeRangeOption}
/>
</EuiFormRow>
<PatternAnalysisSettings
minimumTimeRangeOption={minimumTimeRangeOption}
setMinimumTimeRangeOption={setMinimumTimeRangeOption}
categoryCount={categoryCount}
/>
<EuiHorizontalRule margin="m" />
@ -147,3 +104,78 @@ export const EmbeddableMenu: FC<Props> = ({
</EuiPopover>
);
};
interface PatternAnalysisSettingsProps {
minimumTimeRangeOption: MinimumTimeRangeOption;
setMinimumTimeRangeOption: (w: MinimumTimeRangeOption) => void;
categoryCount: number | undefined;
compressed?: boolean;
}
export const PatternAnalysisSettings: FC<PatternAnalysisSettingsProps> = ({
minimumTimeRangeOption,
setMinimumTimeRangeOption,
categoryCount,
compressed = false,
}) => {
return (
<>
<EuiFormRow
fullWidth
data-test-subj="aiopsRandomSamplerOptionsFormRow"
label={
<EuiFlexGroup gutterSize="s" responsive={false}>
<EuiFlexItem css={{ textWrap: 'nowrap' }}>
{i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.minimumTimeRangeOptionsRowLabel',
{
defaultMessage: 'Minimum time range',
}
)}
</EuiFlexItem>
<EuiFlexItem>
<EuiToolTip
content={i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.minimumTimeRange.tooltip',
{
defaultMessage:
'Adds a wider time range to the analysis to improve pattern accuracy.',
}
)}
>
<EuiIcon type="questionInCircle" color="subdued" />
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
}
helpText={
<>
{categoryCount !== undefined && minimumTimeRangeOption !== 'No minimum' ? (
<>
<FormattedMessage
id="xpack.aiops.logCategorization.embeddableMenu.totalPatternsMessage"
defaultMessage="Total patterns in {minimumTimeRangeOption}: {categoryCount}"
values={{ minimumTimeRangeOption, categoryCount }}
/>
</>
) : null}
</>
}
>
<EuiSuperSelect
fullWidth
aria-label={i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.minimumTimeRangeOptionsRowAriaLabel',
{
defaultMessage: 'Select a minimum time range',
}
)}
options={minimumTimeRangeOptions}
valueOfSelected={minimumTimeRangeOption}
onChange={setMinimumTimeRangeOption}
compressed={compressed}
/>
</EuiFormRow>
</>
);
};

View file

@ -25,17 +25,13 @@ interface Props {
fields: DataViewField[];
selectedField: DataViewField | null;
setSelectedField: (field: DataViewField) => void;
WarningComponent?: FC;
}
export const SelectedField: FC<Props> = ({ fields, selectedField, setSelectedField }) => {
const [showPopover, setShowPopover] = useState(false);
const togglePopover = () => setShowPopover(!showPopover);
const fieldOptions = useMemo(
() => fields.map((field) => ({ inputDisplay: field.name, value: field })),
[fields]
);
const button = (
<EuiDataGridToolbarControl
data-test-subj="aiopsEmbeddableSelectFieldButton"
@ -57,23 +53,48 @@ export const SelectedField: FC<Props> = ({ fields, selectedField, setSelectedFie
button={button}
className="unifiedDataTableToolbarControlButton"
>
<EuiFormRow
data-test-subj="aiopsEmbeddableMenuSelectedFieldFormRow"
label={i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.selectedFieldRowLabel',
{
defaultMessage: 'Selected field',
}
)}
>
<EuiSuperSelect
aria-label="Select a field"
options={fieldOptions}
valueOfSelected={selectedField ?? undefined}
onChange={setSelectedField}
css={{ minWidth: '300px' }}
/>
</EuiFormRow>
<FieldSelector
fields={fields}
selectedField={selectedField}
setSelectedField={setSelectedField}
/>
</EuiPopover>
);
};
export const FieldSelector: FC<Props> = ({
fields,
selectedField,
setSelectedField,
WarningComponent,
}) => {
const fieldOptions = useMemo(
() => fields.map((field) => ({ inputDisplay: field.name, value: field })),
[fields]
);
const label = i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.selectedFieldRowLabel',
{
defaultMessage: 'Selected field',
}
);
return (
<>
{WarningComponent !== undefined ? <WarningComponent /> : null}
<EuiFormRow fullWidth data-test-subj="aiopsEmbeddableMenuSelectedFieldFormRow" label={label}>
<EuiSuperSelect
fullWidth
compressed
aria-label={label}
options={fieldOptions}
disabled={fields.length === 0}
valueOfSelected={selectedField ?? undefined}
onChange={setSelectedField}
/>
</EuiFormRow>
</>
);
};

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export { LogCategorizationEmbeddable } from './log_categorization_for_embeddable';
export { LogCategorizationWrapper } from './log_categorization_wrapper';
export { LogCategorizationDiscover } from './log_categorization_for_discover';
export { LogCategorizationDiscoverWrapper } from './log_categorization_for_discover_wrapper';

View file

@ -0,0 +1,470 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FC } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, useEuiPaddingSize } from '@elastic/eui';
import type { DataViewField } from '@kbn/data-views-plugin/public';
import { i18n } from '@kbn/i18n';
import type { Filter } from '@kbn/es-query';
import { buildEmptyFilter } from '@kbn/es-query';
import { usePageUrlState } from '@kbn/ml-url-state';
import type { FieldValidationResults } from '@kbn/ml-category-validator';
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
import type { CategorizationAdditionalFilter } from '@kbn/aiops-log-pattern-analysis/create_category_request';
import { AIOPS_TELEMETRY_ID } from '@kbn/aiops-common/constants';
import type { EmbeddablePatternAnalysisInput } from '@kbn/aiops-log-pattern-analysis/embeddable';
import { css } from '@emotion/react';
import { useTableState } from '@kbn/ml-in-memory-table/hooks/use_table_state';
import useMountedState from 'react-use/lib/useMountedState';
import {
type LogCategorizationPageUrlState,
getDefaultLogCategorizationAppState,
} from '../../../application/url_state/log_pattern_analysis';
import { createMergedEsQuery } from '../../../application/utils/search_utils';
import { useData } from '../../../hooks/use_data';
import { useSearch } from '../../../hooks/use_search';
import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context';
import { useCategorizeRequest } from '../use_categorize_request';
import type { EventRate } from '../use_categorize_request';
import { CategoryTable } from '../category_table';
import { InformationText } from '../information_text';
import { LoadingCategorization } from '../loading_categorization';
import { useValidateFieldRequest } from '../use_validate_category_field';
import { FieldValidationCallout } from '../category_validation_callout';
import { useMinimumTimeRange } from './use_minimum_time_range';
import { createAdditionalConfigHash, createDocumentStatsHash, getMessageField } from '../utils';
import { DiscoverTabs } from './discover_tabs';
import { useRandomSamplerStorage } from '../sampling_menu';
import { useActions } from '../category_table/use_actions';
export interface LogCategorizationEmbeddableProps {
input: Readonly<EmbeddablePatternAnalysisInput>;
renderViewModeToggle: (patternCount?: number) => React.ReactElement;
}
const BAR_TARGET = 20;
export const LogCategorizationDiscover: FC<LogCategorizationEmbeddableProps> = ({
input,
renderViewModeToggle,
}) => {
const {
notifications: { toasts },
data: {
query: { getState, filterManager },
},
uiSettings,
embeddingOrigin,
} = useAiopsAppContext();
const tablePadding = useEuiPaddingSize('xs');
const { dataView, savedSearch } = input;
const { runValidateFieldRequest, cancelRequest: cancelValidationRequest } =
useValidateFieldRequest();
const {
getMinimumTimeRange,
cancelRequest: cancelWiderTimeRangeRequest,
minimumTimeRangeOption,
setMinimumTimeRangeOption,
} = useMinimumTimeRange();
const { filters, query } = useMemo(() => getState(), [getState]);
const isMounted = useMountedState();
const randomSamplerStorage = useRandomSamplerStorage();
const {
runCategorizeRequest,
cancelRequest: cancelCategorizationRequest,
randomSampler,
} = useCategorizeRequest(randomSamplerStorage);
const [stateFromUrl] = usePageUrlState<LogCategorizationPageUrlState>(
'logCategorization',
getDefaultLogCategorizationAppState({
searchQuery: createMergedEsQuery(query, filters, dataView, uiSettings),
})
);
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
const [selectedField, setSelectedField] = useState<DataViewField | null>(null);
const [fields, setFields] = useState<DataViewField[]>([]);
const [currentDocumentStatsHash, setCurrentDocumentStatsHash] = useState<number | null>(null);
const [previousDocumentStatsHash, setPreviousDocumentStatsHash] = useState<number>(0);
const [currentAdditionalConfigsHash, setCurrentAdditionalConfigsHash] = useState<number | null>(
null
);
const [previousAdditionalConfigsHash, setPreviousAdditionalConfigsHash] = useState<number | null>(
null
);
const [loading, setLoading] = useState<boolean | null>(null);
const [eventRate, setEventRate] = useState<EventRate>([]);
const [data, setData] = useState<{
categories: Category[];
displayExamples: boolean;
totalCategories: number;
} | null>(null);
const [fieldValidationResult, setFieldValidationResult] = useState<FieldValidationResults | null>(
null
);
const tableState = useTableState<Category>([], 'key');
useEffect(
function initFields() {
setCurrentDocumentStatsHash(null);
setSelectedField(null);
setLoading(null);
const { dataViewFields, messageField } = getMessageField(dataView);
setFields(dataViewFields);
setSelectedField(messageField);
},
[dataView]
);
const cancelRequest = useCallback(() => {
cancelWiderTimeRangeRequest();
cancelValidationRequest();
cancelCategorizationRequest();
}, [cancelCategorizationRequest, cancelValidationRequest, cancelWiderTimeRangeRequest]);
useEffect(
function cancelRequestOnLeave() {
return () => {
cancelRequest();
};
},
[cancelRequest]
);
const { searchQuery } = useSearch(
{ dataView, savedSearch: savedSearch ?? null },
stateFromUrl,
true
);
const { documentStats, timefilter, earliest, latest, intervalMs, forceRefresh } = useData(
dataView,
'log_categorization',
searchQuery,
() => {},
undefined,
undefined,
BAR_TARGET,
false
);
const onAddFilter = useCallback(
(values: Filter, alias?: string) => {
const filter = buildEmptyFilter(false, dataView.id);
if (alias) {
filter.meta.alias = alias;
}
filter.query = values.query;
if (typeof input.switchToDocumentView === 'function') {
input.switchToDocumentView().finally(() => {
filterManager.addFilters([filter]);
});
}
},
[dataView.id, filterManager, input]
);
const { getActions, openInDiscover } = useActions(
dataView.id!,
selectedField ?? undefined,
selectedCategories,
stateFromUrl,
timefilter,
onAddFilter,
undefined
);
useEffect(
function createDocumentStatHash() {
if (documentStats.documentCountStats === undefined) {
return;
}
const hash = createDocumentStatsHash(documentStats);
if (hash !== previousDocumentStatsHash) {
setCurrentDocumentStatsHash(hash);
setData(null);
setFieldValidationResult(null);
}
},
[documentStats, previousDocumentStatsHash]
);
useEffect(
function createAdditionalConfigHash2() {
if (!selectedField?.name) {
return;
}
const hash = createAdditionalConfigHash([selectedField.name, minimumTimeRangeOption]);
if (hash !== previousAdditionalConfigsHash) {
setCurrentAdditionalConfigsHash(hash);
setData(null);
setFieldValidationResult(null);
}
},
[minimumTimeRangeOption, previousAdditionalConfigsHash, selectedField]
);
const loadCategories = useCallback(async () => {
const { getIndexPattern, timeFieldName: timeField } = dataView;
const index = getIndexPattern();
if (
loading === true ||
selectedField === null ||
timeField === undefined ||
earliest === undefined ||
latest === undefined ||
minimumTimeRangeOption === undefined ||
isMounted() !== true
) {
return;
}
cancelRequest();
setLoading(true);
setData(null);
setFieldValidationResult(null);
const additionalFilter: CategorizationAdditionalFilter = {
from: earliest,
to: latest,
};
const runtimeMappings = dataView.getRuntimeMappings();
try {
const timeRange = await getMinimumTimeRange(
index,
timeField,
additionalFilter,
minimumTimeRangeOption,
searchQuery,
runtimeMappings
);
if (isMounted() === false) {
return;
}
const [validationResult, categorizationResult] = await Promise.all([
runValidateFieldRequest(
index,
selectedField.name,
timeField,
timeRange,
searchQuery,
runtimeMappings,
{
[AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN]: embeddingOrigin,
}
),
runCategorizeRequest(
index,
selectedField.name,
timeField,
{ to: timeRange.to, from: timeRange.from },
searchQuery,
runtimeMappings,
intervalMs,
timeRange.useSubAgg ? additionalFilter : undefined
),
]);
if (isMounted() === false) {
return;
}
setFieldValidationResult(validationResult);
const { categories, hasExamples } = categorizationResult;
if (timeRange.useSubAgg) {
const categoriesInBucket = categorizationResult.categories
.map((category) => ({
...category,
count: category.subFieldCount ?? category.subTimeRangeCount!,
examples: category.subFieldExamples!,
sparkline: category.subFieldSparkline,
}))
.filter((category) => category.count > 0)
.sort((a, b) => b.count - a.count);
setData({
categories: categoriesInBucket,
displayExamples: hasExamples,
totalCategories: categories.length,
});
} else {
setData({
categories,
displayExamples: hasExamples,
totalCategories: categories.length,
});
}
} catch (error) {
if (error.name !== 'AbortError') {
toasts.addError(error, {
title: i18n.translate('xpack.aiops.logCategorization.errorLoadingCategories', {
defaultMessage: 'Error loading categories',
}),
});
}
}
if (isMounted() === true) {
setLoading(false);
}
}, [
dataView,
loading,
selectedField,
earliest,
latest,
minimumTimeRangeOption,
isMounted,
cancelRequest,
getMinimumTimeRange,
searchQuery,
runValidateFieldRequest,
embeddingOrigin,
runCategorizeRequest,
intervalMs,
toasts,
]);
useEffect(
function triggerAnalysis() {
const buckets = documentStats.documentCountStats?.buckets;
if (buckets === undefined || currentDocumentStatsHash === null) {
return;
}
if (
currentDocumentStatsHash !== previousDocumentStatsHash ||
(currentAdditionalConfigsHash !== previousAdditionalConfigsHash &&
currentDocumentStatsHash !== null)
) {
randomSampler.setDocCount(documentStats.totalCount);
setEventRate(
Object.entries(buckets).map(([key, docCount]) => ({
key: +key,
docCount,
}))
);
loadCategories();
setPreviousDocumentStatsHash(currentDocumentStatsHash);
setPreviousAdditionalConfigsHash(currentAdditionalConfigsHash);
}
},
[
loadCategories,
randomSampler,
previousDocumentStatsHash,
fieldValidationResult,
currentDocumentStatsHash,
currentAdditionalConfigsHash,
documentStats.documentCountStats?.buckets,
documentStats.totalCount,
previousAdditionalConfigsHash,
]
);
useEffect(
function refreshTriggeredFromButton() {
if (input.lastReloadRequestTime !== undefined) {
setPreviousDocumentStatsHash(0);
setPreviousAdditionalConfigsHash(null);
forceRefresh();
}
},
// stop infinite loop from forceRefresh dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
[input.lastReloadRequestTime]
);
const style = css({
overflowY: 'auto',
'.kbnDocTableWrapper': {
overflowX: 'hidden',
},
});
const actions = getActions(false);
return (
<>
<DiscoverTabs
data={data}
fields={fields}
loadCategories={loadCategories}
minimumTimeRangeOption={minimumTimeRangeOption}
openInDiscover={openInDiscover}
randomSampler={randomSampler}
selectedCategories={selectedCategories}
selectedField={selectedField}
setMinimumTimeRangeOption={setMinimumTimeRangeOption}
setSelectedField={setSelectedField}
renderViewModeToggle={renderViewModeToggle}
dataview={dataView}
earliest={earliest}
latest={latest}
query={searchQuery}
/>
<EuiSpacer size="s" />
<EuiFlexItem css={style}>
<EuiFlexGroup
className="eui-fullHeight"
direction="column"
gutterSize="none"
responsive={false}
>
<EuiFlexItem css={{ position: 'relative', overflowY: 'auto', marginLeft: tablePadding }}>
<>
<FieldValidationCallout validationResults={fieldValidationResult} />
{(loading ?? true) === true ? (
<LoadingCategorization onCancel={cancelRequest} />
) : null}
<InformationText
loading={loading ?? true}
categoriesLength={data?.categories?.length ?? null}
eventRateLength={eventRate.length}
fields={fields}
/>
{loading === false &&
data !== null &&
data.categories.length > 0 &&
selectedField !== null ? (
<CategoryTable
categories={data.categories}
eventRate={eventRate}
enableRowActions={false}
displayExamples={data.displayExamples}
setSelectedCategories={setSelectedCategories}
tableState={tableState}
actions={actions}
/>
) : null}
</>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</>
);
};
// eslint-disable-next-line import/no-default-export
export default LogCategorizationDiscover;

View file

@ -24,8 +24,8 @@ import { Storage } from '@kbn/kibana-utils-plugin/public';
import { AIOPS_STORAGE_KEYS } from '../../../types/storage';
import type { AiopsAppDependencies } from '../../../hooks/use_aiops_app_context';
import { AiopsAppContext } from '../../../hooks/use_aiops_app_context';
import type { LogCategorizationEmbeddableProps } from './log_categorization_for_embeddable';
import { LogCategorizationEmbeddable } from './log_categorization_for_embeddable';
import type { LogCategorizationEmbeddableProps } from './log_categorization_for_discover';
import { LogCategorizationDiscover } from './log_categorization_for_discover';
export interface EmbeddableLogCategorizationDeps {
theme: ThemeServiceStart;
@ -49,7 +49,7 @@ export interface LogCategorizationEmbeddableWrapperProps {
const localStorage = new Storage(window.localStorage);
export const LogCategorizationWrapper: FC<LogCategorizationEmbeddableWrapperProps> = ({
export const LogCategorizationDiscoverWrapper: FC<LogCategorizationEmbeddableWrapperProps> = ({
deps,
props,
embeddingOrigin,
@ -71,7 +71,7 @@ export const LogCategorizationWrapper: FC<LogCategorizationEmbeddableWrapperProp
<DatePickerContextProvider {...datePickerDeps}>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<Suspense fallback={null}>
<LogCategorizationEmbeddable
<LogCategorizationDiscover
input={props.input}
renderViewModeToggle={props.renderViewModeToggle}
/>
@ -84,4 +84,4 @@ export const LogCategorizationWrapper: FC<LogCategorizationEmbeddableWrapperProp
};
// eslint-disable-next-line import/no-default-export
export default LogCategorizationWrapper;
export default LogCategorizationDiscoverWrapper;

View file

@ -5,106 +5,96 @@
* 2.0.
*/
import type { FC } from 'react';
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useMemo } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, useEuiPaddingSize } from '@elastic/eui';
import type { DataViewField } from '@kbn/data-views-plugin/public';
import { i18n } from '@kbn/i18n';
import type { Filter } from '@kbn/es-query';
import { buildEmptyFilter } from '@kbn/es-query';
import { usePageUrlState } from '@kbn/ml-url-state';
import type { FieldValidationResults } from '@kbn/ml-category-validator';
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
import type { CategorizationAdditionalFilter } from '@kbn/aiops-log-pattern-analysis/create_category_request';
import { AIOPS_TELEMETRY_ID } from '@kbn/aiops-common/constants';
import type { EmbeddablePatternAnalysisInput } from '@kbn/aiops-log-pattern-analysis/embeddable';
import { css } from '@emotion/react';
import { useTableState } from '@kbn/ml-in-memory-table/hooks/use_table_state';
import {
type LogCategorizationPageUrlState,
getDefaultLogCategorizationAppState,
} from '../../../application/url_state/log_pattern_analysis';
import { AIOPS_TELEMETRY_ID } from '@kbn/aiops-common/constants';
import datemath from '@elastic/datemath';
import useMountedState from 'react-use/lib/useMountedState';
import { useFilterQueryUpdates } from '../../../hooks/use_filters_query';
import type { PatternAnalysisProps } from '../../../shared_components/pattern_analysis';
import { useSearch } from '../../../hooks/use_search';
import { getDefaultLogCategorizationAppState } from '../../../application/url_state/log_pattern_analysis';
import { createMergedEsQuery } from '../../../application/utils/search_utils';
import { useData } from '../../../hooks/use_data';
import { useSearch } from '../../../hooks/use_search';
import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context';
import { useCategorizeRequest } from '../use_categorize_request';
import type { EventRate } from '../use_categorize_request';
import { CategoryTable } from '../category_table';
import { InformationText } from '../information_text';
import { LoadingCategorization } from '../loading_categorization';
import { useValidateFieldRequest } from '../use_validate_category_field';
import { FieldValidationCallout } from '../category_validation_callout';
import { useMinimumTimeRange } from './use_minimum_time_range';
import { createAdditionalConfigHash, createDocumentStatsHash, getMessageField } from '../utils';
import { useOpenInDiscover } from '../category_table/use_open_in_discover';
import { DiscoverTabs } from './discover_tabs';
import { FieldValidationCallout } from '../category_validation_callout';
import { useActions } from '../category_table/use_actions';
import { InformationText } from '../information_text';
export interface LogCategorizationEmbeddableProps {
input: Readonly<EmbeddablePatternAnalysisInput>;
renderViewModeToggle: (patternCount?: number) => React.ReactElement;
}
export type LogCategorizationEmbeddableProps = Readonly<
EmbeddablePatternAnalysisInput & PatternAnalysisProps
>;
const BAR_TARGET = 20;
export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> = ({
input,
renderViewModeToggle,
dataView,
savedSearch,
fieldName,
minimumTimeRangeOption,
randomSamplerMode,
randomSamplerProbability,
onChange,
onRenderComplete,
timeRange,
lastReloadRequestTime,
}) => {
const {
notifications: { toasts },
data: {
query: { getState, filterManager },
query: { filterManager },
},
uiSettings,
embeddingOrigin,
} = useAiopsAppContext();
const tablePadding = useEuiPaddingSize('xs');
const { dataView, savedSearch } = input;
const { filters, query } = useFilterQueryUpdates();
const { runValidateFieldRequest, cancelRequest: cancelValidationRequest } =
useValidateFieldRequest();
const {
getMinimumTimeRange,
cancelRequest: cancelWiderTimeRangeRequest,
minimumTimeRangeOption,
setMinimumTimeRangeOption,
} = useMinimumTimeRange();
const { filters, query } = useMemo(() => getState(), [getState]);
const { getMinimumTimeRange, cancelRequest: cancelWiderTimeRangeRequest } = useMinimumTimeRange();
const mounted = useRef(false);
const isMounted = useMountedState();
const {
runCategorizeRequest,
cancelRequest: cancelCategorizationRequest,
randomSampler,
} = useCategorizeRequest();
const [stateFromUrl] = usePageUrlState<LogCategorizationPageUrlState>(
'logCategorization',
getDefaultLogCategorizationAppState({
searchQuery: createMergedEsQuery(query, filters, dataView, uiSettings),
})
);
const [highlightedCategory, setHighlightedCategory] = useState<Category | null>(null);
} = useCategorizeRequest({
randomSamplerMode,
setRandomSamplerMode: () => {},
randomSamplerProbability,
setRandomSamplerProbability: () => {},
});
const appState = getDefaultLogCategorizationAppState({
searchQuery: createMergedEsQuery(query, filters, dataView, uiSettings),
filters,
});
const { searchQuery } = useSearch({ dataView, savedSearch: savedSearch ?? null }, appState, true);
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
const [selectedField, setSelectedField] = useState<DataViewField | null>(null);
const [fields, setFields] = useState<DataViewField[]>([]);
const [currentDocumentStatsHash, setCurrentDocumentStatsHash] = useState<number | null>(null);
const [previousDocumentStatsHash, setPreviousDocumentStatsHash] = useState<number>(0);
const [currentAdditionalConfigsHash, setCurrentAdditionalConfigsHash] = useState<number | null>(
null
);
const [previousAdditionalConfigsHash, setPreviousAdditionalConfigsHash] = useState<number | null>(
null
);
const [loading, setLoading] = useState<boolean | null>(null);
const [eventRate, setEventRate] = useState<EventRate>([]);
const [pinnedCategory, setPinnedCategory] = useState<Category | null>(null);
const [data, setData] = useState<{
categories: Category[];
displayExamples: boolean;
@ -117,12 +107,7 @@ export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> =
useEffect(
function initFields() {
setCurrentDocumentStatsHash(null);
setSelectedField(null);
setLoading(null);
const { dataViewFields, messageField } = getMessageField(dataView);
setFields(dataViewFields);
setSelectedField(messageField);
},
[dataView]
);
@ -131,24 +116,27 @@ export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> =
cancelWiderTimeRangeRequest();
cancelValidationRequest();
cancelCategorizationRequest();
setLoading(false);
}, [cancelCategorizationRequest, cancelValidationRequest, cancelWiderTimeRangeRequest]);
useEffect(
function cancelRequestOnLeave() {
mounted.current = true;
return () => {
mounted.current = false;
cancelRequest();
};
},
[cancelRequest, mounted]
[cancelRequest]
);
const { searchQuery } = useSearch(
{ dataView, savedSearch: savedSearch ?? null },
stateFromUrl,
true
);
const timeRangeParsed = useMemo(() => {
if (timeRange) {
const min = datemath.parse(timeRange.from);
const max = datemath.parse(timeRange.to);
if (min && max) {
return { min, max };
}
}
}, [timeRange]);
const { documentStats, timefilter, earliest, latest, intervalMs, forceRefresh } = useData(
dataView,
@ -158,81 +146,45 @@ export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> =
undefined,
undefined,
BAR_TARGET,
false
false,
timeRangeParsed
);
const onAddFilter = useCallback(
(values: Filter, alias?: string) => {
if (input.switchToDocumentView === undefined) {
return;
}
const filter = buildEmptyFilter(false, dataView.id);
if (alias) {
filter.meta.alias = alias;
}
filter.query = values.query;
input.switchToDocumentView();
filterManager.addFilters([filter]);
},
[dataView.id, filterManager, input]
[dataView.id, filterManager]
);
const openInDiscover = useOpenInDiscover(
const { getActions } = useActions(
dataView.id!,
selectedField ?? undefined,
dataView.fields.find((field) => field.name === fieldName),
selectedCategories,
stateFromUrl,
appState,
timefilter,
false,
onAddFilter,
undefined
);
useEffect(
function createDocumentStatHash() {
if (documentStats.documentCountStats === undefined) {
return;
}
const hash = createDocumentStatsHash(documentStats);
if (hash !== previousDocumentStatsHash) {
setCurrentDocumentStatsHash(hash);
setData(null);
setFieldValidationResult(null);
}
},
[documentStats, previousDocumentStatsHash]
);
useEffect(
function createAdditionalConfigHash2() {
if (!selectedField?.name) {
return;
}
const hash = createAdditionalConfigHash([selectedField.name, minimumTimeRangeOption]);
if (hash !== previousAdditionalConfigsHash) {
setCurrentAdditionalConfigsHash(hash);
setData(null);
setFieldValidationResult(null);
}
},
[minimumTimeRangeOption, previousAdditionalConfigsHash, selectedField]
);
const loadCategories = useCallback(async () => {
const { getIndexPattern, timeFieldName: timeField } = dataView;
const index = getIndexPattern();
if (
loading === true ||
selectedField === null ||
fieldName === null ||
fieldName === undefined ||
timeField === undefined ||
earliest === undefined ||
latest === undefined ||
minimumTimeRangeOption === undefined ||
mounted.current !== true
isMounted() !== true
) {
return;
}
@ -250,7 +202,7 @@ export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> =
const runtimeMappings = dataView.getRuntimeMappings();
try {
const timeRange = await getMinimumTimeRange(
const minTimeRange = await getMinimumTimeRange(
index,
timeField,
additionalFilter,
@ -259,16 +211,16 @@ export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> =
runtimeMappings
);
if (mounted.current !== true) {
if (isMounted() !== true) {
return;
}
const [validationResult, categorizationResult] = await Promise.all([
runValidateFieldRequest(
index,
selectedField.name,
fieldName,
timeField,
timeRange,
minTimeRange,
searchQuery,
runtimeMappings,
{
@ -277,24 +229,24 @@ export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> =
),
runCategorizeRequest(
index,
selectedField.name,
fieldName,
timeField,
{ to: timeRange.to, from: timeRange.from },
{ to: minTimeRange.to, from: minTimeRange.from },
searchQuery,
runtimeMappings,
intervalMs,
timeRange.useSubAgg ? additionalFilter : undefined
minTimeRange.useSubAgg ? additionalFilter : undefined
),
]);
if (mounted.current !== true) {
if (isMounted() !== true) {
return;
}
setFieldValidationResult(validationResult);
const { categories, hasExamples } = categorizationResult;
if (timeRange.useSubAgg) {
if (minTimeRange.useSubAgg) {
const categoriesInBucket = categorizationResult.categories
.map((category) => ({
...category,
@ -317,9 +269,7 @@ export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> =
});
}
} catch (error) {
if (error.name === 'AbortError') {
// ignore error
} else {
if (error.name !== 'AbortError') {
toasts.addError(error, {
title: i18n.translate('xpack.aiops.logCategorization.errorLoadingCategories', {
defaultMessage: 'Error loading categories',
@ -328,144 +278,103 @@ export const LogCategorizationEmbeddable: FC<LogCategorizationEmbeddableProps> =
}
}
if (mounted.current === true) {
if (isMounted() === true) {
setLoading(false);
}
}, [
dataView,
loading,
selectedField,
earliest,
latest,
minimumTimeRangeOption,
cancelRequest,
getMinimumTimeRange,
searchQuery,
runValidateFieldRequest,
dataView,
earliest,
embeddingOrigin,
runCategorizeRequest,
fieldName,
getMinimumTimeRange,
intervalMs,
isMounted,
latest,
loading,
minimumTimeRangeOption,
runCategorizeRequest,
runValidateFieldRequest,
searchQuery,
toasts,
]);
useEffect(
function setOnChange() {
if (typeof onChange === 'function') {
onChange(data?.categories ?? []);
}
},
[data, onChange]
);
useEffect(
function triggerAnalysis() {
const buckets = documentStats.documentCountStats?.buckets;
if (buckets === undefined || currentDocumentStatsHash === null) {
if (buckets === undefined) {
return;
}
if (
currentDocumentStatsHash !== previousDocumentStatsHash ||
(currentAdditionalConfigsHash !== previousAdditionalConfigsHash &&
currentDocumentStatsHash !== null)
) {
randomSampler.setDocCount(documentStats.totalCount);
setEventRate(
Object.entries(buckets).map(([key, docCount]) => ({
key: +key,
docCount,
}))
);
loadCategories();
setPreviousDocumentStatsHash(currentDocumentStatsHash);
setPreviousAdditionalConfigsHash(currentAdditionalConfigsHash);
}
randomSampler.setDocCount(documentStats.totalCount);
setEventRate(
Object.entries(buckets).map(([key, docCount]) => ({
key: +key,
docCount,
}))
);
loadCategories();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
loadCategories,
randomSampler,
previousDocumentStatsHash,
fieldValidationResult,
currentDocumentStatsHash,
currentAdditionalConfigsHash,
documentStats.documentCountStats?.buckets,
documentStats.totalCount,
previousAdditionalConfigsHash,
dataView.name,
fieldName,
minimumTimeRangeOption,
randomSamplerMode,
randomSamplerProbability,
]
);
useEffect(
function refreshTriggeredFromButton() {
if (input.lastReloadRequestTime !== undefined) {
setPreviousDocumentStatsHash(0);
setPreviousAdditionalConfigsHash(null);
if (lastReloadRequestTime !== undefined) {
cancelRequest();
forceRefresh();
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[input.lastReloadRequestTime]
[lastReloadRequestTime]
);
const style = css({
overflowY: 'auto',
'.kbnDocTableWrapper': {
overflowX: 'hidden',
},
});
const actions = [...getActions(false), ...getActions(true)];
return (
<>
<DiscoverTabs
data={data}
fields={fields}
loadCategories={loadCategories}
minimumTimeRangeOption={minimumTimeRangeOption}
openInDiscover={openInDiscover}
randomSampler={randomSampler}
selectedCategories={selectedCategories}
selectedField={selectedField}
setMinimumTimeRangeOption={setMinimumTimeRangeOption}
setSelectedField={setSelectedField}
renderViewModeToggle={renderViewModeToggle}
dataview={dataView}
earliest={earliest}
latest={latest}
query={searchQuery}
<FieldValidationCallout validationResults={fieldValidationResult} />
{(loading ?? true) === true ? <LoadingCategorization onCancel={cancelRequest} /> : null}
<InformationText
loading={loading ?? true}
categoriesLength={data?.categories?.length ?? null}
eventRateLength={eventRate.length}
/>
<EuiSpacer size="s" />
<EuiFlexItem css={style}>
<EuiFlexGroup
className="eui-fullHeight"
direction="column"
gutterSize="none"
responsive={false}
>
<EuiFlexItem css={{ position: 'relative', overflowY: 'auto', marginLeft: tablePadding }}>
<>
<FieldValidationCallout validationResults={fieldValidationResult} />
{(loading ?? true) === true ? (
<LoadingCategorization onCancel={cancelRequest} />
) : null}
<InformationText
loading={loading ?? true}
categoriesLength={data?.categories?.length ?? null}
eventRateLength={eventRate.length}
fields={fields}
/>
{loading === false &&
data !== null &&
data.categories.length > 0 &&
selectedField !== null ? (
<CategoryTable
categories={data.categories}
eventRate={eventRate}
pinnedCategory={pinnedCategory}
setPinnedCategory={setPinnedCategory}
highlightedCategory={highlightedCategory}
setHighlightedCategory={setHighlightedCategory}
enableRowActions={false}
displayExamples={data.displayExamples}
setSelectedCategories={setSelectedCategories}
openInDiscover={openInDiscover}
tableState={tableState}
/>
) : null}
</>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{loading === false && data !== null && data.categories.length > 0 && fieldName !== null ? (
<CategoryTable
categories={data.categories}
eventRate={eventRate}
enableRowActions={false}
displayExamples={data.displayExamples}
setSelectedCategories={setSelectedCategories}
tableState={tableState}
selectable={false}
actions={actions}
onRenderComplete={onRenderComplete}
/>
) : null}
</>
);
};

View file

@ -4,16 +4,50 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import type { unitOfTime } from 'moment';
export type MinimumTimeRangeOption = 'No minimum' | '1 week' | '1 month' | '3 months' | '6 months';
type MinimumTimeRange = Record<MinimumTimeRangeOption, { factor: number; unit: unitOfTime.Base }>;
type MinimumTimeRange = Record<
MinimumTimeRangeOption,
{ label: string; factor: number; unit: unitOfTime.Base }
>;
export const MINIMUM_TIME_RANGE: MinimumTimeRange = {
'No minimum': { factor: 0, unit: 'w' },
'1 week': { factor: 1, unit: 'w' },
'1 month': { factor: 1, unit: 'M' },
'3 months': { factor: 3, unit: 'M' },
'6 months': { factor: 6, unit: 'M' },
'No minimum': {
label: i18n.translate('xpack.aiops.logCategorization.minimumTimeRange.noMin', {
defaultMessage: 'No minimum',
}),
factor: 0,
unit: 'w',
},
'1 week': {
label: i18n.translate('xpack.aiops.logCategorization.minimumTimeRange.1week', {
defaultMessage: '1 week',
}),
factor: 1,
unit: 'w',
},
'1 month': {
label: i18n.translate('xpack.aiops.logCategorization.minimumTimeRange.1month', {
defaultMessage: '1 month',
}),
factor: 1,
unit: 'M',
},
'3 months': {
label: i18n.translate('xpack.aiops.logCategorization.minimumTimeRange.3months', {
defaultMessage: '3 months',
}),
factor: 3,
unit: 'M',
},
'6 months': {
label: i18n.translate('xpack.aiops.logCategorization.minimumTimeRange.6months', {
defaultMessage: '6 months',
}),
factor: 6,
unit: 'M',
},
};

View file

@ -19,7 +19,8 @@ import { QUERY_MODE } from '@kbn/aiops-log-pattern-analysis/get_category_query';
import type { OpenInDiscover } from '../category_table/use_open_in_discover';
export const SelectedPatterns: FC<{ openInDiscover: OpenInDiscover }> = ({ openInDiscover }) => {
const { labels, openFunction } = openInDiscover;
const { getLabels, openFunction } = openInDiscover;
const labels = getLabels(false);
const [showMenu, setShowMenu] = useState(false);
const togglePopover = () => setShowMenu(!showMenu);
@ -51,14 +52,14 @@ export const SelectedPatterns: FC<{ openInDiscover: OpenInDiscover }> = ({ openI
<EuiContextMenuItem
key="in"
icon="plusInCircle"
onClick={() => openFunction(QUERY_MODE.INCLUDE)}
onClick={() => openFunction(QUERY_MODE.INCLUDE, false)}
>
{labels.multiSelect.in}
</EuiContextMenuItem>,
<EuiContextMenuItem
key="out"
icon="minusInCircle"
onClick={() => openFunction(QUERY_MODE.EXCLUDE)}
onClick={() => openFunction(QUERY_MODE.EXCLUDE, false)}
>
{labels.multiSelect.out}
</EuiContextMenuItem>,

View file

@ -54,6 +54,11 @@ export function useMinimumTimeRange() {
signal: abortController.current.signal,
});
if (resp.end.epoch === null || resp.start.epoch === null) {
// epoch can be null if no data can be found.
return { ...timeRange, useSubAgg: false };
}
// the index isn't big enough to get a wider time range
const indexTimeRangeMs = resp.end.epoch - resp.start.epoch;
if (indexTimeRangeMs < minimumTimeRangeMs) {

View file

@ -46,13 +46,13 @@ import { useCategorizeRequest } from './use_categorize_request';
import type { EventRate } from './use_categorize_request';
import { CategoryTable } from './category_table';
import { InformationText } from './information_text';
import { SamplingMenu } from './sampling_menu';
import { SamplingMenu, useRandomSamplerStorage } from './sampling_menu';
import { LoadingCategorization } from './loading_categorization';
import { useValidateFieldRequest } from './use_validate_category_field';
import { FieldValidationCallout } from './category_validation_callout';
import { CreateCategorizationJobButton } from './create_categorization_job';
import { TableHeader } from './category_table/table_header';
import { useOpenInDiscover } from './category_table/use_open_in_discover';
import { useActions } from './category_table/use_actions';
enum SELECTED_TAB {
BUCKET,
@ -93,23 +93,21 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
const { filters, query } = useMemo(() => getState(), [getState]);
const mounted = useRef(false);
const randomSamplerStorage = useRandomSamplerStorage();
const {
runCategorizeRequest,
cancelRequest: cancelCategorizationRequest,
randomSampler,
} = useCategorizeRequest();
} = useCategorizeRequest(randomSamplerStorage);
const [stateFromUrl] = usePageUrlState<LogCategorizationPageUrlState>(
'logCategorization',
getDefaultLogCategorizationAppState({
searchQuery: createMergedEsQuery(query, filters, dataView, uiSettings),
})
);
const [highlightedCategory, setHighlightedCategory] = useState<Category | null>(null);
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
const [selectedSavedSearch /* , setSelectedSavedSearch*/] = useState(savedSearch);
const [loading, setLoading] = useState(true);
const [eventRate, setEventRate] = useState<EventRate>([]);
const [pinnedCategory, setPinnedCategory] = useState<Category | null>(null);
const [data, setData] = useState<{
categories: Category[];
categoriesInBucket: Category[] | null;
@ -139,7 +137,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
);
const { searchQueryLanguage, searchString, searchQuery } = useSearch(
{ dataView, savedSearch: selectedSavedSearch },
{ dataView, savedSearch },
stateFromUrl,
true
);
@ -154,13 +152,12 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
BAR_TARGET
);
const openInDiscover = useOpenInDiscover(
const { getActions, openInDiscover } = useActions(
dataView.id!,
selectedField,
selectedCategories,
stateFromUrl,
timefilter,
true,
undefined,
undefined
);
@ -292,6 +289,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
randomSampler,
]);
const actions = getActions(true);
const infoIconCss = { marginTop: euiTheme.size.m, marginLeft: euiTheme.size.xxs };
return (
@ -417,15 +415,11 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
: data.categories
}
eventRate={eventRate}
pinnedCategory={pinnedCategory}
setPinnedCategory={setPinnedCategory}
highlightedCategory={highlightedCategory}
setHighlightedCategory={setHighlightedCategory}
enableRowActions={false}
displayExamples={data.displayExamples}
setSelectedCategories={setSelectedCategories}
openInDiscover={openInDiscover}
tableState={tableState}
actions={actions}
/>
</>
) : null}

View file

@ -49,12 +49,12 @@ import { useCategorizeRequest } from './use_categorize_request';
import { CategoryTable } from './category_table';
import { DocumentCountChart } from './document_count_chart';
import { InformationText } from './information_text';
import { SamplingMenu } from './sampling_menu';
import { SamplingMenu, useRandomSamplerStorage } from './sampling_menu';
import { useValidateFieldRequest } from './use_validate_category_field';
import { FieldValidationCallout } from './category_validation_callout';
import { createDocumentStatsHash } from './utils';
import { TableHeader } from './category_table/table_header';
import { useOpenInDiscover } from './category_table/use_open_in_discover';
import { useActions } from './category_table/use_actions';
const BAR_TARGET = 20;
const DEFAULT_SELECTED_FIELD = 'message';
@ -70,11 +70,12 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
} = useAiopsAppContext();
const { dataView, savedSearch } = useDataSource();
const randomSamplerStorage = useRandomSamplerStorage();
const {
runCategorizeRequest,
cancelRequest: cancelCategorizationRequest,
randomSampler,
} = useCategorizeRequest();
} = useCategorizeRequest(randomSamplerStorage);
const { runValidateFieldRequest, cancelRequest: cancelValidationRequest } =
useValidateFieldRequest();
const [stateFromUrl, setUrlState] = usePageUrlState<LogCategorizationPageUrlState>(
@ -159,13 +160,12 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
BAR_TARGET
);
const openInDiscover = useOpenInDiscover(
const { getActions, openInDiscover } = useActions(
dataView.id!,
selectedField,
selectedCategories,
stateFromUrl,
timefilter,
true,
undefined,
undefined
);
@ -336,6 +336,8 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
setUrlState({ field });
};
const actions = getActions(true);
return (
<EuiPageBody data-test-subj="aiopsLogPatternAnalysisPage" paddingSize="none" panelled={false}>
<PageHeader />
@ -435,14 +437,16 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
<CategoryTable
categories={data.categories}
eventRate={eventRate}
pinnedCategory={pinnedCategory}
setPinnedCategory={setPinnedCategory}
highlightedCategory={highlightedCategory}
setHighlightedCategory={setHighlightedCategory}
mouseOver={{
pinnedCategory,
setPinnedCategory,
highlightedCategory,
setHighlightedCategory,
}}
displayExamples={data.displayExamples}
setSelectedCategories={setSelectedCategories}
openInDiscover={openInDiscover}
tableState={tableState}
actions={actions}
/>
</>
) : null}

View file

@ -7,3 +7,4 @@
export { SamplingMenu } from './sampling_menu';
export { RandomSampler } from './random_sampler';
export { useRandomSamplerStorage, type RandomSamplerStorage } from './use_random_sampler_storage';

View file

@ -8,6 +8,7 @@
import { BehaviorSubject } from 'rxjs';
import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
import { i18n } from '@kbn/i18n';
import type { RandomSamplerStorage } from './use_random_sampler_storage';
export const RANDOM_SAMPLER_PROBABILITIES = [
0.00001, 0.00005, 0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5,
@ -68,12 +69,12 @@ export class RandomSampler {
private setRandomSamplerModeInStorage: (mode: RandomSamplerOption) => void;
private setRandomSamplerProbabilityInStorage: (prob: RandomSamplerProbability) => void;
constructor(
randomSamplerMode: RandomSamplerOption,
setRandomSamplerMode: (mode: RandomSamplerOption) => void,
randomSamplerProbability: RandomSamplerProbability,
setRandomSamplerProbability: (prob: RandomSamplerProbability) => void
) {
constructor({
randomSamplerMode,
randomSamplerProbability,
setRandomSamplerMode,
setRandomSamplerProbability,
}: RandomSamplerStorage) {
this.mode$.next(randomSamplerMode);
this.setRandomSamplerModeInStorage = setRandomSamplerMode;
this.probability$.next(randomSamplerProbability);

View file

@ -19,11 +19,19 @@ import { RANDOM_SAMPLER_OPTION, RANDOM_SAMPLER_SELECT_OPTIONS } from './random_s
interface Props {
randomSampler: RandomSampler;
displayProbability?: boolean;
calloutPosition?: 'top' | 'bottom';
compressed?: boolean;
reload: () => void;
}
export const SamplingPanel: FC<Props> = ({ randomSampler, reload, calloutPosition = 'top' }) => {
export const SamplingPanel: FC<Props> = ({
randomSampler,
reload,
displayProbability = true,
calloutPosition = 'top',
compressed = false,
}) => {
const samplingProbability = useObservable(
randomSampler.getProbability$(),
randomSampler.getProbability()
@ -60,6 +68,7 @@ export const SamplingPanel: FC<Props> = ({ randomSampler, reload, calloutPositio
) : null}
<EuiFormRow
fullWidth
data-test-subj="aiopsRandomSamplerOptionsFormRow"
label={i18n.translate(
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.randomSamplerRowLabel',
@ -68,12 +77,14 @@ export const SamplingPanel: FC<Props> = ({ randomSampler, reload, calloutPositio
}
)}
helpText={
randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_AUTOMATIC ? (
displayProbability && randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_AUTOMATIC ? (
<ProbabilityUsedMessage samplingProbability={samplingProbability} />
) : null
}
>
<EuiSuperSelect
fullWidth
compressed={compressed}
data-test-subj="aiopsRandomSamplerOptionsSelect"
options={RANDOM_SAMPLER_SELECT_OPTIONS}
valueOfSelected={randomSamplerPreference}

View file

@ -0,0 +1,35 @@
/*
* 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 { useStorage } from '@kbn/ml-local-storage';
import { RANDOM_SAMPLER_OPTION, DEFAULT_PROBABILITY } from '@kbn/ml-random-sampler-utils';
import type { AiOpsKey, AiOpsStorageMapped } from '../../../types/storage';
import {
AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE,
AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE,
} from '../../../types/storage';
export type RandomSamplerStorage = ReturnType<typeof useRandomSamplerStorage>;
export function useRandomSamplerStorage() {
const [randomSamplerMode, setRandomSamplerMode] = useStorage<
AiOpsKey,
AiOpsStorageMapped<typeof AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE>
>(AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE, RANDOM_SAMPLER_OPTION.ON_AUTOMATIC);
const [randomSamplerProbability, setRandomSamplerProbability] = useStorage<
AiOpsKey,
AiOpsStorageMapped<typeof AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE>
>(AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE, DEFAULT_PROBABILITY);
return {
randomSamplerMode,
setRandomSamplerMode,
randomSamplerProbability,
setRandomSamplerProbability,
};
}

View file

@ -10,7 +10,6 @@ import { useRef, useCallback, useMemo } from 'react';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { isRunningResponse } from '@kbn/data-plugin/public';
import { useStorage } from '@kbn/ml-local-storage';
import {
type CategorizationAdditionalFilter,
@ -21,44 +20,22 @@ import type { CatResponse } from '@kbn/aiops-log-pattern-analysis/types';
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import {
type AiOpsKey,
type AiOpsStorageMapped,
AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE,
AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE,
} from '../../types/storage';
import type { RandomSamplerStorage } from './sampling_menu';
import { RandomSampler } from './sampling_menu';
import { RANDOM_SAMPLER_OPTION, DEFAULT_PROBABILITY } from './sampling_menu/random_sampler';
export type EventRate = Array<{
key: number;
docCount: number;
}>;
export function useCategorizeRequest() {
const [randomSamplerMode, setRandomSamplerMode] = useStorage<
AiOpsKey,
AiOpsStorageMapped<typeof AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE>
>(AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE, RANDOM_SAMPLER_OPTION.ON_AUTOMATIC);
const [randomSamplerProbability, setRandomSamplerProbability] = useStorage<
AiOpsKey,
AiOpsStorageMapped<typeof AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE>
>(AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE, DEFAULT_PROBABILITY);
export function useCategorizeRequest(randomSamplerStorage: RandomSamplerStorage) {
const { data } = useAiopsAppContext();
const abortController = useRef(new AbortController());
const randomSampler = useMemo(
() =>
new RandomSampler(
randomSamplerMode,
setRandomSamplerMode,
randomSamplerProbability,
setRandomSamplerProbability
),
() => new RandomSampler(randomSamplerStorage),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

View file

@ -8,6 +8,7 @@
import type { CoreSetup } from '@kbn/core-lifecycle-browser';
import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '@kbn/aiops-change-point-detection/constants';
import { EMBEDDABLE_PATTERN_ANALYSIS_TYPE } from '@kbn/aiops-log-pattern-analysis/constants';
import type { AiopsPluginStart, AiopsPluginStartDeps } from '../types';
export const registerEmbeddables = (
@ -18,4 +19,8 @@ export const registerEmbeddables = (
const { getChangePointChartEmbeddableFactory } = await import('./change_point_chart');
return getChangePointChartEmbeddableFactory(core.getStartServices);
});
embeddable.registerReactEmbeddableFactory(EMBEDDABLE_PATTERN_ANALYSIS_TYPE, async () => {
const { getPatternAnalysisEmbeddableFactory } = await import('./pattern_analysis');
return getPatternAnalysisEmbeddableFactory(core.getStartServices);
});
};

View file

@ -0,0 +1,265 @@
/*
* 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 {
EMBEDDABLE_PATTERN_ANALYSIS_TYPE,
PATTERN_ANALYSIS_DATA_VIEW_REF_NAME,
} from '@kbn/aiops-log-pattern-analysis/constants';
import type { Reference } from '@kbn/content-management-utils';
import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser';
import { type DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
import type { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { i18n } from '@kbn/i18n';
import {
apiHasExecutionContext,
fetch$,
initializeTimeRange,
initializeTitles,
useBatchedPublishingSubjects,
} from '@kbn/presentation-publishing';
import fastIsEqual from 'fast-deep-equal';
import { cloneDeep } from 'lodash';
import React, { useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { BehaviorSubject, distinctUntilChanged, map, skipWhile } from 'rxjs';
import { getPatternAnalysisComponent } from '../../shared_components';
import type { AiopsPluginStart, AiopsPluginStartDeps } from '../../types';
import { initializePatternAnalysisControls } from './initialize_pattern_analysis_controls';
import type {
PatternAnalysisEmbeddableApi,
PatternAnalysisEmbeddableRuntimeState,
PatternAnalysisEmbeddableState,
} from './types';
export interface EmbeddablePatternAnalysisStartServices {
data: DataPublicPluginStart;
}
export type EmbeddablePatternAnalysisType = typeof EMBEDDABLE_PATTERN_ANALYSIS_TYPE;
export const getDependencies = async (
getStartServices: StartServicesAccessor<AiopsPluginStartDeps, AiopsPluginStart>
) => {
const [
{ http, uiSettings, notifications, ...startServices },
{ lens, data, usageCollection, fieldFormats },
] = await getStartServices();
return {
http,
uiSettings,
data,
notifications,
lens,
usageCollection,
fieldFormats,
...startServices,
};
};
export const getPatternAnalysisEmbeddableFactory = (
getStartServices: StartServicesAccessor<AiopsPluginStartDeps, AiopsPluginStart>
) => {
const factory: ReactEmbeddableFactory<
PatternAnalysisEmbeddableState,
PatternAnalysisEmbeddableRuntimeState,
PatternAnalysisEmbeddableApi
> = {
type: EMBEDDABLE_PATTERN_ANALYSIS_TYPE,
deserializeState: (state) => {
const serializedState = cloneDeep(state.rawState);
// inject the reference
const dataViewIdRef = state.references?.find(
(ref) => ref.name === PATTERN_ANALYSIS_DATA_VIEW_REF_NAME
);
// if the serializedState already contains a dataViewId, we don't want to overwrite it. (Unsaved state can cause this)
if (dataViewIdRef && serializedState && !serializedState.dataViewId) {
serializedState.dataViewId = dataViewIdRef?.id;
}
return serializedState;
},
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
const [coreStart, pluginStart] = await getStartServices();
const { http, uiSettings, notifications, ...startServices } = coreStart;
const { lens, data, usageCollection, fieldFormats } = pluginStart;
const deps = {
http,
uiSettings,
data,
notifications,
lens,
usageCollection,
fieldFormats,
...startServices,
};
const {
api: timeRangeApi,
comparators: timeRangeComparators,
serialize: serializeTimeRange,
} = initializeTimeRange(state);
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
const {
patternAnalysisControlsApi,
serializePatternAnalysisChartState,
patternAnalysisControlsComparators,
} = initializePatternAnalysisControls(state);
const dataLoading = new BehaviorSubject<boolean | undefined>(true);
const blockingError = new BehaviorSubject<Error | undefined>(undefined);
const dataViews$ = new BehaviorSubject<DataView[] | undefined>([
await deps.data.dataViews.get(
state.dataViewId ?? (await deps.data.dataViews.getDefaultId())
),
]);
const api = buildApi(
{
...timeRangeApi,
...titlesApi,
...patternAnalysisControlsApi,
getTypeDisplayName: () =>
i18n.translate('xpack.aiops.patternAnalysis.typeDisplayName', {
defaultMessage: 'pattern analysis',
}),
isEditingEnabled: () => true,
onEdit: async () => {
try {
const { resolveEmbeddablePatternAnalysisUserInput } = await import(
'./resolve_pattern_analysis_config_input'
);
const result = await resolveEmbeddablePatternAnalysisUserInput(
coreStart,
pluginStart,
parentApi,
uuid,
false,
patternAnalysisControlsApi,
undefined,
serializePatternAnalysisChartState()
);
patternAnalysisControlsApi.updateUserInput(result);
} catch (e) {
return Promise.reject();
}
},
dataLoading,
blockingError,
dataViews: dataViews$,
serializeState: () => {
const dataViewId = patternAnalysisControlsApi.dataViewId.getValue();
const references: Reference[] = dataViewId
? [
{
type: DATA_VIEW_SAVED_OBJECT_TYPE,
name: PATTERN_ANALYSIS_DATA_VIEW_REF_NAME,
id: dataViewId,
},
]
: [];
return {
rawState: {
timeRange: undefined,
...serializeTitles(),
...serializeTimeRange(),
...serializePatternAnalysisChartState(),
},
references,
};
},
},
{
...timeRangeComparators,
...titleComparators,
...patternAnalysisControlsComparators,
}
);
const PatternAnalysisComponent = getPatternAnalysisComponent(coreStart, pluginStart);
const onLoading = (v: boolean) => dataLoading.next(v);
const onRenderComplete = () => dataLoading.next(false);
const onError = (error: Error) => blockingError.next(error);
return {
api,
Component: () => {
if (!apiHasExecutionContext(parentApi)) {
throw new Error('Parent API does not have execution context');
}
const [
dataViewId,
fieldName,
minimumTimeRangeOption,
randomSamplerMode,
randomSamplerProbability,
] = useBatchedPublishingSubjects(
api.dataViewId,
api.fieldName,
api.minimumTimeRangeOption,
api.randomSamplerMode,
api.randomSamplerProbability
);
const reload$ = useMemo(
() =>
fetch$(api).pipe(
skipWhile((fetchContext) => !fetchContext.isReload),
map((fetchContext) => Date.now())
),
[]
);
const timeRange$ = useMemo(
() =>
fetch$(api).pipe(
map((fetchContext) => fetchContext.timeRange),
distinctUntilChanged(fastIsEqual)
),
[]
);
const lastReloadRequestTime = useObservable(reload$, Date.now());
const timeRange = useObservable(timeRange$, undefined);
let embeddingOrigin;
if (apiHasExecutionContext(parentApi)) {
embeddingOrigin = parentApi.executionContext.type;
}
return (
<PatternAnalysisComponent
dataViewId={dataViewId}
fieldName={fieldName}
minimumTimeRangeOption={minimumTimeRangeOption}
randomSamplerMode={randomSamplerMode}
randomSamplerProbability={randomSamplerProbability}
timeRange={timeRange}
onLoading={onLoading}
onRenderComplete={onRenderComplete}
onError={onError}
embeddingOrigin={embeddingOrigin}
lastReloadRequestTime={lastReloadRequestTime}
/>
);
},
};
},
};
return factory;
};

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { getPatternAnalysisEmbeddableFactory } from './embeddable_pattern_analysis_factory';

View file

@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { StateComparators } from '@kbn/presentation-publishing';
import { BehaviorSubject } from 'rxjs';
import type { PatternAnalysisComponentApi, PatternAnalysisEmbeddableState } from './types';
type PatternAnalysisEmbeddableCustomState = Omit<
PatternAnalysisEmbeddableState,
'timeRange' | 'title' | 'description' | 'hidePanelTitles'
>;
export const initializePatternAnalysisControls = (rawState: PatternAnalysisEmbeddableState) => {
const dataViewId = new BehaviorSubject(rawState.dataViewId);
const fieldName = new BehaviorSubject(rawState.fieldName);
const minimumTimeRangeOption = new BehaviorSubject(rawState.minimumTimeRangeOption);
const randomSamplerMode = new BehaviorSubject(rawState.randomSamplerMode);
const randomSamplerProbability = new BehaviorSubject(rawState.randomSamplerProbability);
const updateUserInput = (update: PatternAnalysisEmbeddableCustomState) => {
dataViewId.next(update.dataViewId);
fieldName.next(update.fieldName);
minimumTimeRangeOption.next(update.minimumTimeRangeOption);
randomSamplerMode.next(update.randomSamplerMode);
randomSamplerProbability.next(update.randomSamplerProbability);
};
const serializePatternAnalysisChartState = (): PatternAnalysisEmbeddableCustomState => {
return {
dataViewId: dataViewId.getValue(),
fieldName: fieldName.getValue(),
minimumTimeRangeOption: minimumTimeRangeOption.getValue(),
randomSamplerMode: randomSamplerMode.getValue(),
randomSamplerProbability: randomSamplerProbability.getValue(),
};
};
const patternAnalysisControlsComparators: StateComparators<PatternAnalysisEmbeddableCustomState> =
{
dataViewId: [dataViewId, (arg) => dataViewId.next(arg)],
fieldName: [fieldName, (arg) => fieldName.next(arg)],
minimumTimeRangeOption: [minimumTimeRangeOption, (arg) => minimumTimeRangeOption.next(arg)],
randomSamplerMode: [randomSamplerMode, (arg) => randomSamplerMode.next(arg)],
randomSamplerProbability: [
randomSamplerProbability,
(arg) => randomSamplerProbability.next(arg),
],
};
return {
patternAnalysisControlsApi: {
dataViewId,
fieldName,
minimumTimeRangeOption,
randomSamplerMode,
randomSamplerProbability,
updateUserInput,
} as unknown as PatternAnalysisComponentApi,
serializePatternAnalysisChartState,
patternAnalysisControlsComparators,
};
};

View file

@ -0,0 +1,420 @@
/*
* 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 {
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiForm,
EuiFormRow,
EuiSpacer,
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutFooter,
EuiCallOut,
} from '@elastic/eui';
import { euiThemeVars } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
import type { FC } from 'react';
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { pick } from 'lodash';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import useObservable from 'react-use/lib/useObservable';
import type { DataViewField } from '@kbn/data-views-plugin/public';
import useMountedState from 'react-use/lib/useMountedState';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { DataSourceContextProvider } from '../../hooks/use_data_source';
import type { PatternAnalysisEmbeddableRuntimeState } from './types';
import { PatternAnalysisSettings } from '../../components/log_categorization/log_categorization_for_embeddable/embeddable_menu';
import { RandomSampler } from '../../components/log_categorization/sampling_menu';
import {
DEFAULT_PROBABILITY,
RANDOM_SAMPLER_OPTION,
} from '../../components/log_categorization/sampling_menu/random_sampler';
import type { MinimumTimeRangeOption } from '../../components/log_categorization/log_categorization_for_embeddable/minimum_time_range';
import { getMessageField } from '../../components/log_categorization/utils';
import { FieldSelector } from '../../components/log_categorization/log_categorization_for_embeddable/field_selector';
import { SamplingPanel } from '../../components/log_categorization/sampling_menu/sampling_panel';
export interface PatternAnalysisInitializerProps {
initialInput?: Partial<PatternAnalysisEmbeddableRuntimeState>;
onCreate: (props: PatternAnalysisEmbeddableRuntimeState) => void;
onCancel: () => void;
onPreview: (update: PatternAnalysisEmbeddableRuntimeState) => Promise<void>;
isNewPanel: boolean;
}
export const PatternAnalysisEmbeddableInitializer: FC<PatternAnalysisInitializerProps> = ({
initialInput,
onCreate,
onCancel,
onPreview,
isNewPanel,
}) => {
const {
unifiedSearch: {
ui: { IndexPatternSelect },
},
} = useAiopsAppContext();
const [formInput, setFormInput] = useState<PatternAnalysisEmbeddableRuntimeState>(
pick(
initialInput ?? {
minimumTimeRangeOption: '1 week',
randomSamplerMode: RANDOM_SAMPLER_OPTION.ON_AUTOMATIC,
randomSamplerProbability: DEFAULT_PROBABILITY,
},
[
'dataViewId',
'fieldName',
'minimumTimeRangeOption',
'randomSamplerMode',
'randomSamplerProbability',
]
) as PatternAnalysisEmbeddableRuntimeState
);
const [isFormValid, setIsFormValid] = useState(true);
const updatedProps = useMemo(() => {
return {
...formInput,
title: isPopulatedObject(formInput)
? i18n.translate('xpack.aiops.embeddablePatternAnalysis.attachmentTitle', {
defaultMessage: 'Pattern analysis: {fieldName}',
values: {
fieldName: formInput.fieldName,
},
})
: '',
};
}, [formInput]);
useEffect(
function previewChanges() {
if (isFormValid && updatedProps.fieldName !== undefined) {
onPreview(updatedProps);
}
},
[isFormValid, onPreview, updatedProps]
);
const setDataViewId = useCallback(
(dataViewId: string | undefined) => {
setFormInput({
...formInput,
dataViewId: dataViewId ?? '',
fieldName: undefined,
});
setIsFormValid(false);
},
[formInput]
);
return (
<>
<EuiFlyoutHeader
hasBorder={true}
css={{
pointerEvents: 'auto',
backgroundColor: euiThemeVars.euiColorEmptyShade,
}}
>
<EuiTitle size="s" data-test-subj="inlineEditingFlyoutLabel">
<h2>
{isNewPanel
? i18n.translate('xpack.aiops.embeddablePatternAnalysis.config.title.new', {
defaultMessage: 'Create pattern analysis',
})
: i18n.translate('xpack.aiops.embeddablePatternAnalysis.config.title.edit', {
defaultMessage: 'Edit pattern analysis',
})}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiForm>
<EuiFormRow
fullWidth
label={i18n.translate('xpack.aiops.embeddablePatternAnalysis.config.dataViewLabel', {
defaultMessage: 'Data view',
})}
>
<IndexPatternSelect
autoFocus={!formInput.dataViewId}
fullWidth
compressed
indexPatternId={formInput.dataViewId}
placeholder={i18n.translate(
'xpack.aiops.embeddablePatternAnalysis.config.dataViewSelectorPlaceholder',
{
defaultMessage: 'Select data view',
}
)}
onChange={(newId) => {
setDataViewId(newId ?? '');
}}
/>
</EuiFormRow>
<DataSourceContextProvider dataViewId={formInput.dataViewId}>
<EuiSpacer />
<FormControls
formInput={formInput}
onChange={setFormInput}
onValidationChange={setIsFormValid}
/>
</DataSourceContextProvider>
</EuiForm>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
color="primary"
size="m"
onClick={onCancel}
data-test-subj="aiopsPatternAnalysisCancelButton"
>
<FormattedMessage
id="xpack.aiops.embeddablePatternAnalysis.config.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onCreate.bind(null, updatedProps)}
fill
aria-label={i18n.translate(
'xpack.aiops.embeddablePatternAnalysis.config.applyFlyoutAriaLabel',
{
defaultMessage: 'Apply changes',
}
)}
isDisabled={!isFormValid}
iconType="check"
data-test-subj="aiopsPatternAnalysisConfirmButton"
>
<FormattedMessage
id="xpack.aiops.embeddablePatternAnalysis.config.applyAndCloseLabel"
defaultMessage="Apply and close"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
);
};
export const FormControls: FC<{
formInput: PatternAnalysisEmbeddableRuntimeState;
onChange: (update: PatternAnalysisEmbeddableRuntimeState) => void;
onValidationChange: (isValid: boolean) => void;
}> = ({ formInput, onChange, onValidationChange }) => {
const dataViewId = formInput.dataViewId;
const {
data: { dataViews },
} = useAiopsAppContext();
const [fields, setFields] = useState<DataViewField[]>([]);
const [selectedField, setSelectedField] = useState<DataViewField | null>(null);
const [isDataViewTimeBased, setIsDataViewTimeBased] = useState(true);
const randomSampler = useMemo(() => {
return new RandomSampler({
randomSamplerMode: formInput.randomSamplerMode ?? RANDOM_SAMPLER_OPTION.ON_AUTOMATIC,
setRandomSamplerMode: () => {},
randomSamplerProbability: formInput.randomSamplerProbability ?? DEFAULT_PROBABILITY,
setRandomSamplerProbability: () => {},
});
}, [formInput.randomSamplerMode, formInput.randomSamplerProbability]);
const randomSamplerMode = useObservable(randomSampler.getMode$(), randomSampler.getMode());
const randomSamplerProbability = useObservable(
randomSampler.getProbability$(),
randomSampler.getProbability()
);
const isMounted = useMountedState();
useEffect(
function initFields() {
if (!dataViewId) {
setFields([]);
setSelectedField(null);
return;
}
dataViews
.get(dataViewId)
.then((dataView) => {
if (!isMounted()) {
return;
}
const isTimeBased = dataView.isTimeBased();
setIsDataViewTimeBased(isTimeBased);
if (isTimeBased === false) {
setFields([]);
setSelectedField(null);
return;
}
const { dataViewFields, messageField } = getMessageField(dataView);
setFields(dataViewFields);
const field = dataViewFields.find((f) => f.name === formInput.fieldName);
if (formInput.fieldName === undefined) {
// form input does not contain a field name, select the found message field
setSelectedField(messageField ?? null);
return;
}
// otherwise, select the field from the form input
setSelectedField(field ?? messageField ?? null);
})
.catch(() => {
setFields([]);
setSelectedField(null);
});
},
[dataViewId, dataViews, formInput, isMounted, onChange]
);
useEffect(
function validateForm() {
onValidationChange(selectedField !== null && formInput.dataViewId !== undefined);
},
[selectedField, formInput, onValidationChange]
);
useEffect(
function samplerChange() {
onChange({
...formInput,
randomSamplerMode,
randomSamplerProbability,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[onChange, randomSamplerMode, randomSamplerProbability]
);
useEffect(
function samplerChange() {
if (selectedField === null) {
return;
}
onChange({
...formInput,
fieldName: selectedField.name,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[onChange, selectedField]
);
const setMinimumTimeRangeOption = (option: MinimumTimeRangeOption) => {
onChange({
...formInput,
minimumTimeRangeOption: option,
});
};
return (
<>
<FieldSelector
fields={fields}
selectedField={selectedField}
setSelectedField={setSelectedField}
WarningComponent={
isDataViewTimeBased === false
? TimeFieldWarning
: fields.length === 0
? TextFieldWarning
: undefined
}
/>
<EuiSpacer />
<PatternAnalysisSettings
categoryCount={undefined}
minimumTimeRangeOption={formInput.minimumTimeRangeOption}
setMinimumTimeRangeOption={setMinimumTimeRangeOption}
compressed={true}
/>
<EuiSpacer />
<SamplingPanel
randomSampler={randomSampler}
reload={() => {}}
calloutPosition="bottom"
displayProbability={false}
compressed={true}
/>
</>
);
};
const TextFieldWarning = () => {
return (
<>
<EuiCallOut
size="s"
title={i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.textFieldWarning.title',
{
defaultMessage: 'The selected data view does not contain any text fields.',
}
)}
color="warning"
iconType="warning"
>
<p>
{i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.textFieldWarning.title.description',
{
defaultMessage: 'Pattern analysis can only be run on data views with a text field.',
}
)}
</p>
</EuiCallOut>
<EuiSpacer />
</>
);
};
const TimeFieldWarning = () => {
return (
<>
<EuiCallOut
size="s"
title={i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.timeFieldWarning.title',
{
defaultMessage: 'The selected data view does not contain a time field.',
}
)}
color="warning"
iconType="warning"
>
<p>
{i18n.translate(
'xpack.aiops.logCategorization.embeddableMenu.timeFieldWarning.title.description',
{
defaultMessage: 'Pattern analysis can only be run on data views with a time field.',
}
)}
</p>
</EuiCallOut>
<EuiSpacer />
</>
);
};

View file

@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FC } from 'react';
import React from 'react';
import { useDataSource } from '../../hooks/use_data_source';
import type { PatternAnalysisProps } from '../../shared_components/pattern_analysis';
import LogCategorizationEmbeddable from '../../components/log_categorization/log_categorization_for_embeddable/log_categorization_for_embeddable';
/**
* Grid component wrapper for embeddable.
*
* @param fieldName
* @param minimumTimeRangeOption
* @param randomSamplerMode
* @param randomSamplerProbability
* @param lastReloadRequestTime
* @param onError
* @param onLoading
* @param onRenderComplete
* @param onChange
* @param emptyState
* @param timeRange
* @constructor
*/
export const PatternAnalysisEmbeddableWrapper: FC<PatternAnalysisProps> = ({
dataViewId,
fieldName,
minimumTimeRangeOption,
randomSamplerMode,
randomSamplerProbability,
lastReloadRequestTime,
onError,
onLoading,
onRenderComplete,
onChange,
emptyState,
timeRange,
}) => {
const { dataView } = useDataSource();
if (dataView.id !== dataViewId) {
return null;
}
return (
<div
css={{
width: '100%',
height: '100%',
overflowY: 'auto',
}}
>
<LogCategorizationEmbeddable
dataView={dataView}
dataViewId={dataView.id!}
fieldName={fieldName}
minimumTimeRangeOption={minimumTimeRangeOption}
randomSamplerProbability={randomSamplerProbability}
randomSamplerMode={randomSamplerMode}
lastReloadRequestTime={lastReloadRequestTime}
timeRange={timeRange}
onError={onError}
onLoading={onLoading}
onRenderComplete={onRenderComplete}
onChange={onChange}
emptyState={emptyState}
/>
</div>
);
};

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { CoreStart } from '@kbn/core/public';
import { tracksOverlays } from '@kbn/presentation-containers';
import { toMountPoint } from '@kbn/react-kibana-mount';
import React from 'react';
import type { AiopsAppDependencies } from '../..';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import type { AiopsPluginStartDeps } from '../../types';
import { PatternAnalysisEmbeddableInitializer } from './pattern_analysis_initializer';
import type { PatternAnalysisComponentApi, PatternAnalysisEmbeddableState } from './types';
export async function resolveEmbeddablePatternAnalysisUserInput(
coreStart: CoreStart,
pluginStart: AiopsPluginStartDeps,
parentApi: unknown,
focusedPanelId: string,
isNewPanel: boolean,
patternAnalysisControlsApi: PatternAnalysisComponentApi,
deletePanel?: () => void,
initialState?: PatternAnalysisEmbeddableState
): Promise<PatternAnalysisEmbeddableState> {
const { overlays } = coreStart;
const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined;
let hasChanged = false;
return new Promise(async (resolve, reject) => {
try {
const cancelChanges = () => {
if (isNewPanel && deletePanel) {
deletePanel();
} else if (hasChanged && patternAnalysisControlsApi && initialState) {
// Reset to initialState in case user has changed the preview state
patternAnalysisControlsApi.updateUserInput(initialState);
}
flyoutSession.close();
overlayTracker?.clearOverlays();
};
const update = async (nextUpdate: PatternAnalysisEmbeddableState) => {
resolve(nextUpdate);
flyoutSession.close();
overlayTracker?.clearOverlays();
};
const preview = async (nextUpdate: PatternAnalysisEmbeddableState) => {
if (patternAnalysisControlsApi) {
patternAnalysisControlsApi.updateUserInput(nextUpdate);
hasChanged = true;
}
};
const flyoutSession = overlays.openFlyout(
toMountPoint(
<AiopsAppContext.Provider
value={
{
...coreStart,
...pluginStart,
} as unknown as AiopsAppDependencies
}
>
<PatternAnalysisEmbeddableInitializer
initialInput={initialState}
onCreate={update}
onCancel={cancelChanges}
onPreview={preview}
isNewPanel={isNewPanel}
/>
</AiopsAppContext.Provider>,
coreStart
),
{
ownFocus: true,
size: 's',
type: 'push',
paddingSize: 'm',
hideCloseButton: true,
'data-test-subj': 'aiopsPatternAnalysisEmbeddableInitializer',
'aria-labelledby': 'patternAnalysisConfig',
onClose: () => {
reject();
flyoutSession.close();
overlayTracker?.clearOverlays();
},
}
);
if (tracksOverlays(parentApi)) {
parentApi.openOverlay(flyoutSession, { focusedPanelId });
}
} catch (error) {
reject(error);
}
});
}

View file

@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
import type {
HasEditCapabilities,
PublishesDataViews,
PublishesTimeRange,
PublishingSubject,
SerializedTimeRange,
SerializedTitles,
} from '@kbn/presentation-publishing';
import type { FC } from 'react';
import type { MinimumTimeRangeOption } from '../../components/log_categorization/log_categorization_for_embeddable/minimum_time_range';
import type {
RandomSamplerOption,
RandomSamplerProbability,
} from '../../components/log_categorization/sampling_menu/random_sampler';
export type ViewComponent = FC<{
interval: string;
onRenderComplete?: () => void;
}>;
export interface PatternAnalysisComponentApi {
dataViewId: PublishingSubject<PatternAnalysisEmbeddableState['dataViewId']>;
fieldName: PublishingSubject<PatternAnalysisEmbeddableState['fieldName']>;
minimumTimeRangeOption: PublishingSubject<
PatternAnalysisEmbeddableState['minimumTimeRangeOption']
>;
randomSamplerMode: PublishingSubject<PatternAnalysisEmbeddableState['randomSamplerMode']>;
randomSamplerProbability: PublishingSubject<
PatternAnalysisEmbeddableState['randomSamplerProbability']
>;
updateUserInput: (update: PatternAnalysisEmbeddableState) => void;
}
export type PatternAnalysisEmbeddableApi = DefaultEmbeddableApi<PatternAnalysisEmbeddableState> &
HasEditCapabilities &
PublishesDataViews &
PublishesTimeRange &
PatternAnalysisComponentApi;
export interface PatternAnalysisEmbeddableState extends SerializedTitles, SerializedTimeRange {
dataViewId: string;
fieldName: string | undefined;
minimumTimeRangeOption: MinimumTimeRangeOption;
randomSamplerMode: RandomSamplerOption;
randomSamplerProbability: RandomSamplerProbability;
}
export interface PatternAnalysisEmbeddableInitialState
extends SerializedTitles,
SerializedTimeRange {
dataViewId?: string;
}
export type PatternAnalysisEmbeddableRuntimeState = PatternAnalysisEmbeddableState;

View file

@ -72,7 +72,7 @@ export class AiopsPlugin
PatternAnalysisComponent: dynamic(
async () =>
import(
'./components/log_categorization/log_categorization_for_embeddable/log_categorization_wrapper'
'./components/log_categorization/log_categorization_for_embeddable/log_categorization_for_discover_wrapper'
)
),
};

View file

@ -10,6 +10,7 @@ import { dynamic } from '@kbn/shared-ux-utility';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import type { AiopsPluginStartDeps } from '../types';
import type { ChangePointDetectionSharedComponent } from './change_point_detection';
import type { PatternAnalysisSharedComponent } from './pattern_analysis';
const ChangePointDetectionLazy = dynamic(async () => import('./change_point_detection'));
@ -23,3 +24,16 @@ export const getChangePointDetectionComponent = (
};
export type { ChangePointDetectionSharedComponent } from './change_point_detection';
const PatternAnalysisLazy = dynamic(async () => import('./pattern_analysis'));
export const getPatternAnalysisComponent = (
coreStart: CoreStart,
pluginStart: AiopsPluginStartDeps
): PatternAnalysisSharedComponent => {
return React.memo((props) => {
return <PatternAnalysisLazy coreStart={coreStart} pluginStart={pluginStart} {...props} />;
});
};
export type { PatternAnalysisSharedComponent } from './pattern_analysis';

View file

@ -0,0 +1,172 @@
/*
* 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 { EMBEDDABLE_ORIGIN } from '@kbn/aiops-common/constants';
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import { UI_SETTINGS } from '@kbn/data-service';
import type { TimeRange } from '@kbn/es-query';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { pick } from 'lodash';
import React, { useEffect, useMemo, useState, type FC } from 'react';
import type { Observable } from 'rxjs';
import { BehaviorSubject, combineLatest, distinctUntilChanged, map } from 'rxjs';
import type { MinimumTimeRangeOption } from '../components/log_categorization/log_categorization_for_embeddable/minimum_time_range';
import type {
RandomSamplerOption,
RandomSamplerProbability,
} from '../components/log_categorization/sampling_menu/random_sampler';
import { PatternAnalysisEmbeddableWrapper } from '../embeddables/pattern_analysis/pattern_analysys_component_wrapper';
import { AiopsAppContext, type AiopsAppDependencies } from '../hooks/use_aiops_app_context';
import { DataSourceContextProvider } from '../hooks/use_data_source';
import { FilterQueryContextProvider } from '../hooks/use_filters_query';
import { ReloadContextProvider } from '../hooks/use_reload';
import type { AiopsPluginStartDeps } from '../types';
/**
* Only used to initialize internally
*/
export type PatternAnalysisPropsWithDeps = PatternAnalysisProps & {
coreStart: CoreStart;
pluginStart: AiopsPluginStartDeps;
};
export type PatternAnalysisSharedComponent = FC<PatternAnalysisProps>;
export interface PatternAnalysisProps {
dataViewId: string;
timeRange: TimeRange;
fieldName: string | undefined;
minimumTimeRangeOption: MinimumTimeRangeOption;
randomSamplerMode: RandomSamplerOption;
randomSamplerProbability: RandomSamplerProbability;
/**
* Component to render if there are no patterns found
*/
emptyState?: React.ReactElement;
/**
* Outputs the most recent patterns data
*/
onChange?: (patterns: Category[]) => void;
/**
* Last reload request time, can be used for manual reload
*/
lastReloadRequestTime?: number;
/** Origin of the embeddable instance */
embeddingOrigin?: string;
onLoading: (isLoading: boolean) => void;
onRenderComplete: () => void;
onError: (error: Error) => void;
}
const PatternAnalysisWrapper: FC<PatternAnalysisPropsWithDeps> = ({
// Component dependencies
coreStart,
pluginStart,
// Component props
dataViewId,
timeRange,
fieldName,
minimumTimeRangeOption,
randomSamplerMode,
randomSamplerProbability,
onLoading,
onError,
onRenderComplete,
embeddingOrigin,
lastReloadRequestTime,
onChange,
}) => {
const deps = useMemo(() => {
const { http, uiSettings, notifications, ...startServices } = coreStart;
const { lens, data, usageCollection, fieldFormats, charts } = pluginStart;
return {
http,
uiSettings,
data,
notifications,
lens,
usageCollection,
fieldFormats,
charts,
...startServices,
};
}, [coreStart, pluginStart]);
const datePickerDeps = {
...pick(deps, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']),
uiSettingsKeys: UI_SETTINGS,
};
const aiopsAppContextValue = useMemo<AiopsAppDependencies>(() => {
return {
embeddingOrigin: embeddingOrigin ?? EMBEDDABLE_ORIGIN,
...deps,
} as unknown as AiopsAppDependencies;
}, [deps, embeddingOrigin]);
const [manualReload$] = useState<BehaviorSubject<number>>(
new BehaviorSubject<number>(lastReloadRequestTime ?? Date.now())
);
useEffect(
function updateManualReloadSubject() {
if (!lastReloadRequestTime) return;
manualReload$.next(lastReloadRequestTime);
},
[lastReloadRequestTime, manualReload$]
);
const resultObservable$ = useMemo<Observable<number>>(() => {
return combineLatest([manualReload$]).pipe(
map(([manualReload]) => Math.max(manualReload)),
distinctUntilChanged()
);
}, [manualReload$]);
// TODO: Remove data-shared-item as part of https://github.com/elastic/kibana/issues/179376>
return (
<div
data-shared-item=""
data-test-subj="aiopsEmbeddablePatternAnalysis"
css={{
width: '100%',
padding: '10px',
}}
>
<KibanaRenderContextProvider {...coreStart}>
<AiopsAppContext.Provider value={aiopsAppContextValue}>
<DatePickerContextProvider {...datePickerDeps}>
<ReloadContextProvider reload$={resultObservable$}>
<DataSourceContextProvider dataViewId={dataViewId}>
<FilterQueryContextProvider timeRange={timeRange}>
<PatternAnalysisEmbeddableWrapper
dataViewId={dataViewId}
timeRange={timeRange}
fieldName={fieldName}
minimumTimeRangeOption={minimumTimeRangeOption}
randomSamplerMode={randomSamplerMode}
randomSamplerProbability={randomSamplerProbability}
lastReloadRequestTime={lastReloadRequestTime}
onLoading={onLoading}
onRenderComplete={onRenderComplete}
onError={onError}
onChange={onChange}
/>
</FilterQueryContextProvider>
</DataSourceContextProvider>
</ReloadContextProvider>
</DatePickerContextProvider>
</AiopsAppContext.Provider>
</KibanaRenderContextProvider>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default PatternAnalysisWrapper;

View file

@ -22,7 +22,7 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { ChangePointDetectionSharedComponent } from '../shared_components';
import type { LogCategorizationEmbeddableWrapperProps } from '../components/log_categorization/log_categorization_for_embeddable/log_categorization_wrapper';
import type { LogCategorizationEmbeddableWrapperProps } from '../components/log_categorization/log_categorization_for_embeddable/log_categorization_for_discover_wrapper';
export interface AiopsPluginSetupDeps {
embeddable: EmbeddableSetup;

View file

@ -0,0 +1,97 @@
/*
* 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 { i18n } from '@kbn/i18n';
import type { PresentationContainer } from '@kbn/presentation-containers';
import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public';
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import type { CoreStart } from '@kbn/core-lifecycle-browser';
import { EMBEDDABLE_PATTERN_ANALYSIS_TYPE } from '@kbn/aiops-log-pattern-analysis/constants';
import type { AiopsPluginStartDeps } from '../types';
import type { PatternAnalysisActionContext } from './pattern_analysis_action_context';
import type {
PatternAnalysisEmbeddableApi,
PatternAnalysisEmbeddableInitialState,
} from '../embeddables/pattern_analysis/types';
const parentApiIsCompatible = async (
parentApi: unknown
): Promise<PresentationContainer | undefined> => {
const { apiIsPresentationContainer } = await import('@kbn/presentation-containers');
// we cannot have an async type check, so return the casted parentApi rather than a boolean
return apiIsPresentationContainer(parentApi) ? (parentApi as PresentationContainer) : undefined;
};
export function createAddPatternAnalysisEmbeddableAction(
coreStart: CoreStart,
pluginStart: AiopsPluginStartDeps
): UiActionsActionDefinition<PatternAnalysisActionContext> {
return {
id: 'create-pattern-analysis-embeddable',
grouping: [
{
id: 'ml',
getDisplayName: () =>
i18n.translate('xpack.aiops.navMenu.mlAppNameText', {
defaultMessage: 'Machine Learning and Analytics',
}),
getIconType: () => 'logPatternAnalysis',
},
],
getIconType: () => 'logPatternAnalysis',
getDisplayName: () =>
i18n.translate('xpack.aiops.embeddablePatternAnalysisDisplayName', {
defaultMessage: 'Pattern analysis',
}),
async isCompatible(context: EmbeddableApiContext) {
return Boolean(await parentApiIsCompatible(context.embeddable));
},
async execute(context) {
const presentationContainerParent = await parentApiIsCompatible(context.embeddable);
if (!presentationContainerParent) throw new IncompatibleActionError();
try {
const { resolveEmbeddablePatternAnalysisUserInput } = await import(
'../embeddables/pattern_analysis/resolve_pattern_analysis_config_input'
);
const initialState: PatternAnalysisEmbeddableInitialState = {
dataViewId: undefined,
};
const embeddable = await presentationContainerParent.addNewPanel<
object,
PatternAnalysisEmbeddableApi
>({
panelType: EMBEDDABLE_PATTERN_ANALYSIS_TYPE,
initialState,
});
if (!embeddable) {
return;
}
const deletePanel = () => {
presentationContainerParent.removePanel(embeddable.uuid);
};
resolveEmbeddablePatternAnalysisUserInput(
coreStart,
pluginStart,
context.embeddable,
embeddable.uuid,
true,
embeddable,
deletePanel
);
} catch (e) {
return Promise.reject();
}
},
};
}

View file

@ -17,6 +17,7 @@ import { createAddChangePointChartAction } from './create_change_point_chart';
import { createOpenChangePointInMlAppAction } from './open_change_point_ml';
import type { AiopsPluginStartDeps } from '../types';
import { createCategorizeFieldAction } from '../components/log_categorization';
import { createAddPatternAnalysisEmbeddableAction } from './create_pattern_analysis_action';
export function registerAiopsUiActions(
uiActions: UiActionsSetup,
@ -25,7 +26,9 @@ export function registerAiopsUiActions(
) {
const openChangePointInMlAppAction = createOpenChangePointInMlAppAction(coreStart, pluginStart);
const addChangePointChartAction = createAddChangePointChartAction(coreStart, pluginStart);
const addPatternAnalysisAction = createAddPatternAnalysisEmbeddableAction(coreStart, pluginStart);
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addPatternAnalysisAction);
uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addChangePointChartAction);
uiActions.registerTrigger(categorizeFieldTrigger);

View file

@ -0,0 +1,24 @@
/*
* 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 { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { apiIsOfType, type EmbeddableApiContext } from '@kbn/presentation-publishing';
import { EMBEDDABLE_PATTERN_ANALYSIS_TYPE } from '@kbn/aiops-log-pattern-analysis/constants';
import type { ChangePointEmbeddableApi } from '../embeddables/change_point_chart/types';
export interface PatternAnalysisActionContext extends EmbeddableApiContext {
embeddable: ChangePointEmbeddableApi;
}
export function isPatternAnalysisEmbeddableContext(
arg: unknown
): arg is PatternAnalysisActionContext {
return (
isPopulatedObject(arg, ['embeddable']) &&
apiIsOfType(arg.embeddable, EMBEDDABLE_PATTERN_ANALYSIS_TYPE)
);
}

View file

@ -78,6 +78,7 @@
"@kbn/usage-collection-plugin",
"@kbn/utility-types",
"@kbn/observability-ai-assistant-plugin",
"@kbn/ui-theme",
"@kbn/apm-utils",
],
"exclude": [