mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ML] Adds Pattern analysis embeddable for dashboards (#186539)
Adds a new embeddable version of the pattern analysis component.  The options to configure it are the same as the menu available in the Patterns tab in Discover.  **Actions** The user can choose to filter a pattern in or out in in the current dashboard, or jump to Discover.  --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8316cbf019
commit
9ff78cf08b
46 changed files with 2442 additions and 539 deletions
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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`
|
||||
|
|
10
x-pack/packages/ml/aiops_log_pattern_analysis/constants.ts
Normal file
10
x-pack/packages/ml/aiops_log_pattern_analysis/constants.ts
Normal 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';
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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 };
|
||||
}
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export { SamplingMenu } from './sampling_menu';
|
||||
export { RandomSampler } from './random_sampler';
|
||||
export { useRandomSamplerStorage, type RandomSamplerStorage } from './use_random_sampler_storage';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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
|
||||
[]
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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;
|
|
@ -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'
|
||||
)
|
||||
),
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
|
@ -78,6 +78,7 @@
|
|||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/apm-utils",
|
||||
],
|
||||
"exclude": [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue