mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ML] [AIOps] Pattern analysis tab in Discover (#178916)
Closes https://github.com/elastic/kibana/issues/178534 Replaces the pattern analysis flyout in Discover with a tab which sits alongside the Documents and Field statistics tabs.  **Field selection** Lists all of the text fields in the index. Auto selects `message`, then `error.message`, then `event.original` and if none of these fields are available, it just selects the first field in the list.  The Options menu provides some configuration options: **Minimum time range** Sets the minimum time range used for the pattern analysis search. The pattern matching results results will be more accurate the more data it sees, so if the user has selected e.g. last 15mins in the time picker, this settings will ensure a wider time range is used to improve the accuracy of the patterns. If the time picker has a larger time range than this setting, the larger time range will be used. **Random sampling** Improves the search performance by using a random sampler. This is the same setting as before.  --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: István Zoltán Szabó <istvan.szabo@elastic.co>
This commit is contained in:
parent
9e8bf2d8c6
commit
a69f24b2af
83 changed files with 2655 additions and 1016 deletions
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { Action, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { canCategorize } from './categorize_trigger_utils';
|
||||
|
||||
const textField = {
|
||||
name: 'fieldName',
|
||||
type: 'string',
|
||||
esTypes: ['text'],
|
||||
count: 1,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
visualizable: true,
|
||||
} as DataViewField;
|
||||
|
||||
const numberField = {
|
||||
name: 'fieldName',
|
||||
type: 'number',
|
||||
esTypes: ['double'],
|
||||
count: 1,
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
visualizable: true,
|
||||
} as DataViewField;
|
||||
|
||||
const mockGetActions = jest.fn<Promise<Array<Action<object>>>, [string, { fieldName: string }]>(
|
||||
() => Promise.resolve([])
|
||||
);
|
||||
|
||||
const uiActions = {
|
||||
getTriggerCompatibleActions: mockGetActions,
|
||||
} as unknown as UiActionsStart;
|
||||
|
||||
const action: Action = {
|
||||
id: 'action',
|
||||
type: 'CATEGORIZE_FIELD',
|
||||
getIconType: () => undefined,
|
||||
getDisplayName: () => 'Action',
|
||||
isCompatible: () => Promise.resolve(true),
|
||||
execute: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
const dataViewMock = { id: '1', toSpec: () => ({}), isTimeBased: () => true } as DataView;
|
||||
|
||||
describe('categorize_trigger_utils', () => {
|
||||
afterEach(() => {
|
||||
mockGetActions.mockReset();
|
||||
});
|
||||
|
||||
describe('getCategorizeInformation', () => {
|
||||
it('should return true for a categorizable field with an action', async () => {
|
||||
mockGetActions.mockResolvedValue([action]);
|
||||
const resp = await canCategorize(uiActions, textField, dataViewMock);
|
||||
expect(resp).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a non-categorizable field with an action', async () => {
|
||||
mockGetActions.mockResolvedValue([action]);
|
||||
const resp = await canCategorize(uiActions, numberField, dataViewMock);
|
||||
expect(resp).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { CATEGORIZE_FIELD_TRIGGER, type CategorizeFieldContext } from '@kbn/ml-ui-actions';
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
|
||||
|
||||
async function getCompatibleActions(
|
||||
uiActions: UiActionsStart,
|
||||
field: DataViewField,
|
||||
dataView: DataView,
|
||||
trigger: typeof CATEGORIZE_FIELD_TRIGGER
|
||||
) {
|
||||
const compatibleActions = await uiActions.getTriggerCompatibleActions(trigger, {
|
||||
dataView,
|
||||
field,
|
||||
});
|
||||
return compatibleActions;
|
||||
}
|
||||
|
||||
export function triggerCategorizeActions(
|
||||
uiActions: UiActionsStart,
|
||||
field: DataViewField,
|
||||
originatingApp: string,
|
||||
dataView?: DataView
|
||||
) {
|
||||
if (!dataView) return;
|
||||
const triggerOptions: CategorizeFieldContext = {
|
||||
dataView,
|
||||
field,
|
||||
originatingApp,
|
||||
};
|
||||
uiActions.getTrigger(CATEGORIZE_FIELD_TRIGGER).exec(triggerOptions);
|
||||
}
|
||||
|
||||
export async function canCategorize(
|
||||
uiActions: UiActionsStart,
|
||||
field: DataViewField,
|
||||
dataView: DataView | undefined
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
field.name === '_id' ||
|
||||
!dataView?.id ||
|
||||
!dataView.isTimeBased() ||
|
||||
!field.esTypes?.includes('text')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const actions = await getCompatibleActions(uiActions, field, dataView, CATEGORIZE_FIELD_TRIGGER);
|
||||
|
||||
return actions.length > 0;
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import { ActionInternal } from '@kbn/ui-actions-plugin/public';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { getFieldCategorizeButton } from './field_categorize_button';
|
||||
import {
|
||||
CATEGORIZE_FIELD_TRIGGER,
|
||||
ACTION_CATEGORIZE_FIELD,
|
||||
type CategorizeFieldContext,
|
||||
} from '@kbn/ml-ui-actions';
|
||||
import { TriggerContract } from '@kbn/ui-actions-plugin/public/triggers';
|
||||
|
||||
const ORIGINATING_APP = 'test';
|
||||
const mockExecuteAction = jest.fn();
|
||||
const uiActions = uiActionsPluginMock.createStartContract();
|
||||
const categorizeAction = new ActionInternal({
|
||||
type: ACTION_CATEGORIZE_FIELD,
|
||||
id: ACTION_CATEGORIZE_FIELD,
|
||||
getDisplayName: () => 'test',
|
||||
isCompatible: async () => true,
|
||||
execute: async (context: CategorizeFieldContext) => {
|
||||
mockExecuteAction(context);
|
||||
},
|
||||
getHref: async () => '/app/test',
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(uiActions, 'getTriggerCompatibleActions')
|
||||
.mockResolvedValue([categorizeAction as ActionInternal<object>]);
|
||||
jest.spyOn(uiActions, 'getTrigger').mockReturnValue({
|
||||
id: ACTION_CATEGORIZE_FIELD,
|
||||
exec: mockExecuteAction,
|
||||
} as unknown as TriggerContract<object>);
|
||||
|
||||
describe('UnifiedFieldList <FieldCategorizeButton />', () => {
|
||||
it('should render correctly', async () => {
|
||||
const fieldName = 'extension';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
const button = await getFieldCategorizeButton({
|
||||
field,
|
||||
dataView,
|
||||
originatingApp: ORIGINATING_APP,
|
||||
uiActions,
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = await mountWithIntl(button!);
|
||||
});
|
||||
|
||||
await wrapper!.update();
|
||||
|
||||
expect(uiActions.getTriggerCompatibleActions).toHaveBeenCalledWith(CATEGORIZE_FIELD_TRIGGER, {
|
||||
dataView,
|
||||
field,
|
||||
});
|
||||
|
||||
expect(wrapper!.text()).toBe('Run pattern analysis');
|
||||
wrapper!.find(`button[data-test-subj="fieldCategorize-${fieldName}"]`).simulate('click');
|
||||
|
||||
expect(mockExecuteAction).toHaveBeenCalledWith({
|
||||
dataView,
|
||||
field,
|
||||
originatingApp: ORIGINATING_APP,
|
||||
});
|
||||
|
||||
expect(wrapper!.find(EuiButton).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render for non text field', async () => {
|
||||
const fieldName = 'phpmemory';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
|
||||
const button = await getFieldCategorizeButton({
|
||||
field,
|
||||
dataView,
|
||||
originatingApp: ORIGINATING_APP,
|
||||
uiActions,
|
||||
});
|
||||
|
||||
expect(button).toBe(null);
|
||||
});
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonProps } from '@elastic/eui';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { FieldCategorizeButtonInner } from './field_categorize_button_inner';
|
||||
import { triggerCategorizeActions, canCategorize } from './categorize_trigger_utils';
|
||||
|
||||
export interface FieldCategorizeButtonProps {
|
||||
field: DataViewField;
|
||||
dataView: DataView;
|
||||
originatingApp: string; // plugin id
|
||||
uiActions: UiActionsStart;
|
||||
contextualFields?: string[]; // names of fields which were also selected (like columns in Discover grid)
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
buttonProps?: Partial<EuiButtonProps>;
|
||||
closePopover?: () => void;
|
||||
}
|
||||
|
||||
export const FieldCategorizeButton: React.FC<FieldCategorizeButtonProps> = React.memo(
|
||||
({ field, dataView, trackUiMetric, originatingApp, uiActions, buttonProps, closePopover }) => {
|
||||
const handleVisualizeLinkClick = async (
|
||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
|
||||
) => {
|
||||
// regular link click. let the uiActions code handle the navigation and show popup if needed
|
||||
event.preventDefault();
|
||||
const triggerVisualization = (updatedDataView: DataView) => {
|
||||
trackUiMetric?.(METRIC_TYPE.CLICK, 'categorize_link_click');
|
||||
triggerCategorizeActions(uiActions, field, originatingApp, updatedDataView);
|
||||
};
|
||||
triggerVisualization(dataView);
|
||||
if (closePopover) {
|
||||
closePopover();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldCategorizeButtonInner
|
||||
fieldName={field.name}
|
||||
handleVisualizeLinkClick={handleVisualizeLinkClick}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export async function getFieldCategorizeButton(props: FieldCategorizeButtonProps) {
|
||||
const showButton = await canCategorize(props.uiActions, props.field, props.dataView);
|
||||
return showButton ? <FieldCategorizeButton {...props} /> : null;
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiButtonProps } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
interface FieldVisualizeButtonInnerProps {
|
||||
fieldName: string;
|
||||
handleVisualizeLinkClick: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
buttonProps?: Partial<EuiButtonProps>;
|
||||
}
|
||||
|
||||
export const FieldCategorizeButtonInner: React.FC<FieldVisualizeButtonInnerProps> = ({
|
||||
fieldName,
|
||||
handleVisualizeLinkClick,
|
||||
buttonProps,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<EuiButton
|
||||
fullWidth
|
||||
size="s"
|
||||
data-test-subj={`fieldCategorize-${fieldName}`}
|
||||
{...(buttonProps || {})}
|
||||
onClick={handleVisualizeLinkClick}
|
||||
iconSide="left"
|
||||
iconType="machineLearningApp"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="unifiedFieldList.fieldCategorizeButton.label"
|
||||
defaultMessage="Run pattern analysis"
|
||||
/>
|
||||
</EuiButton>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -7,16 +7,14 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiPopoverFooter, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiPopoverFooter } from '@elastic/eui';
|
||||
import { type FieldVisualizeButtonProps, getFieldVisualizeButton } from '../field_visualize_button';
|
||||
import { FieldCategorizeButtonProps, getFieldCategorizeButton } from '../field_categorize_button';
|
||||
import { ErrorBoundary } from '../error_boundary';
|
||||
|
||||
export type FieldPopoverFooterProps = FieldVisualizeButtonProps | FieldCategorizeButtonProps;
|
||||
export type FieldPopoverFooterProps = FieldVisualizeButtonProps;
|
||||
|
||||
const FieldPopoverFooterComponent: React.FC<FieldPopoverFooterProps> = (props) => {
|
||||
const [visualizeButton, setVisualizeButton] = useState<JSX.Element | null>(null);
|
||||
const [categorizeButton, setCategorizeButton] = useState<JSX.Element | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getFieldVisualizeButton(props)
|
||||
|
@ -25,21 +23,9 @@ const FieldPopoverFooterComponent: React.FC<FieldPopoverFooterProps> = (props) =
|
|||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
});
|
||||
getFieldCategorizeButton(props)
|
||||
.then(setCategorizeButton)
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
});
|
||||
}, [props]);
|
||||
|
||||
return visualizeButton || categorizeButton ? (
|
||||
<EuiPopoverFooter>
|
||||
{visualizeButton}
|
||||
{visualizeButton && categorizeButton ? <EuiSpacer size="s" /> : null}
|
||||
{categorizeButton}
|
||||
</EuiPopoverFooter>
|
||||
) : null;
|
||||
return visualizeButton ? <EuiPopoverFooter>{visualizeButton}</EuiPopoverFooter> : null;
|
||||
};
|
||||
|
||||
export const FieldPopoverFooter: React.FC<FieldPopoverFooterProps> = (props) => {
|
||||
|
|
|
@ -309,7 +309,6 @@ function UnifiedFieldListItemComponent({
|
|||
contextualFields={workspaceSelectedFieldNames}
|
||||
originatingApp={stateService.creationOptions.originatingApp}
|
||||
uiActions={services.uiActions}
|
||||
closePopover={() => closePopover()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
"@kbn/ebt-tools",
|
||||
"@kbn/shared-ux-button-toolbar",
|
||||
"@kbn/field-utils",
|
||||
"@kbn/ml-ui-actions",
|
||||
"@kbn/visualization-utils",
|
||||
"@kbn/esql-utils",
|
||||
"@kbn/search-types"
|
||||
|
|
|
@ -138,7 +138,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"risk-engine-configuration": "aea0c371a462e6d07c3ceb3aff11891b47feb09d",
|
||||
"rules-settings": "892a2918ebaeba809a612b8d97cec0b07c800b5f",
|
||||
"sample-data-telemetry": "37441b12f5b0159c2d6d5138a494c9f440e950b5",
|
||||
"search": "7598e4a701ddcaa5e3f44f22e797618a48595e6f",
|
||||
"search": "4579401660a4089d5122b2fc8624825cb97b0480",
|
||||
"search-session": "b2fcd840e12a45039ada50b1355faeafa39876d1",
|
||||
"search-telemetry": "b568601618744720b5662946d3103e3fb75fe8ee",
|
||||
"security-rule": "07abb4d7e707d91675ec0495c73816394c7b521f",
|
||||
|
|
|
@ -15,6 +15,7 @@ export const ROWS_PER_PAGE_OPTIONS = [10, 25, 50, DEFAULT_ROWS_PER_PAGE, 250, 50
|
|||
export enum VIEW_MODE {
|
||||
DOCUMENT_LEVEL = 'documents',
|
||||
AGGREGATED_LEVEL = 'aggregated',
|
||||
PATTERN_LEVEL = 'patterns',
|
||||
}
|
||||
|
||||
export const getDefaultRowsPerPage = (uiSettings: IUiSettingsClient): number => {
|
||||
|
|
|
@ -39,7 +39,8 @@
|
|||
"lens",
|
||||
"noDataPage",
|
||||
"globalSearch",
|
||||
"observabilityAIAssistant"
|
||||
"observabilityAIAssistant",
|
||||
"aiops"
|
||||
],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "unifiedSearch", "savedObjects"],
|
||||
"extraPublicDirs": ["common"]
|
||||
|
|
|
@ -156,6 +156,10 @@ export function createDiscoverServicesMock(): DiscoverServices {
|
|||
dataVisualizer: {
|
||||
FieldStatisticsTable: jest.fn(() => createElement('div')),
|
||||
},
|
||||
aiops: {
|
||||
getPatternAnalysisAvailable: jest.fn().mockResolvedValue(jest.fn().mockResolvedValue(true)),
|
||||
PatternAnalysisComponent: jest.fn(() => createElement('div')),
|
||||
},
|
||||
docLinks: docLinksServiceMock.createStartContract(),
|
||||
capabilities: {
|
||||
visualize: {
|
||||
|
|
|
@ -87,6 +87,9 @@ export function DiscoverLayout({ stateContainer }: DiscoverLayoutProps) {
|
|||
]);
|
||||
const isEsqlMode = useIsEsqlMode();
|
||||
const viewMode: VIEW_MODE = useAppStateSelector((state) => {
|
||||
if (state.viewMode === VIEW_MODE.DOCUMENT_LEVEL || state.viewMode === VIEW_MODE.PATTERN_LEVEL) {
|
||||
return state.viewMode;
|
||||
}
|
||||
if (uiSettings.get(SHOW_FIELD_STATISTICS) !== true || isEsqlMode)
|
||||
return VIEW_MODE.DOCUMENT_LEVEL;
|
||||
return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL;
|
||||
|
|
|
@ -30,6 +30,7 @@ import { DocumentViewModeToggle } from '../../../../components/view_mode_toggle'
|
|||
import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
import { DiscoverDocuments } from './discover_documents';
|
||||
import { FieldStatisticsTab } from '../field_stats_table';
|
||||
import { PatternAnalysisTab } from '../pattern_analysis';
|
||||
import { DiscoverMainProvider } from '../../state_management/discover_state_provider';
|
||||
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
|
||||
import { PanelsToggle } from '../../../../components/panels_toggle';
|
||||
|
@ -189,13 +190,22 @@ describe('Discover main content component', () => {
|
|||
it('should show DiscoverDocuments when VIEW_MODE is DOCUMENT_LEVEL', async () => {
|
||||
const component = await mountComponent();
|
||||
expect(component.find(DiscoverDocuments).exists()).toBe(true);
|
||||
expect(component.find(PatternAnalysisTab).exists()).toBe(false);
|
||||
expect(component.find(FieldStatisticsTab).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show FieldStatisticsTableMemoized when VIEW_MODE is not DOCUMENT_LEVEL', async () => {
|
||||
it('should show FieldStatisticsTab when VIEW_MODE is AGGREGATED_LEVEL', async () => {
|
||||
const component = await mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL });
|
||||
expect(component.find(DiscoverDocuments).exists()).toBe(false);
|
||||
expect(component.find(PatternAnalysisTab).exists()).toBe(false);
|
||||
expect(component.find(FieldStatisticsTab).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should show PatternAnalysisTab when VIEW_MODE is PATTERN_LEVEL', async () => {
|
||||
const component = await mountComponent({ viewMode: VIEW_MODE.PATTERN_LEVEL });
|
||||
expect(component.find(DiscoverDocuments).exists()).toBe(false);
|
||||
expect(component.find(PatternAnalysisTab).exists()).toBe(true);
|
||||
expect(component.find(FieldStatisticsTab).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,8 @@ import { DiscoverDocuments } from './discover_documents';
|
|||
import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants';
|
||||
import { useAppStateSelector } from '../../state_management/discover_app_state_container';
|
||||
import type { PanelsToggleProps } from '../../../../components/panels_toggle';
|
||||
import { PatternAnalysisTab } from '../pattern_analysis/pattern_analysis_tab';
|
||||
import { PATTERN_ANALYSIS_VIEW_CLICK } from '../pattern_analysis/constants';
|
||||
import { useIsEsqlMode } from '../../hooks/use_is_esql_mode';
|
||||
|
||||
const DROP_PROPS = {
|
||||
|
@ -60,9 +62,8 @@ export const DiscoverMainContent = ({
|
|||
panelsToggle,
|
||||
isChartAvailable,
|
||||
}: DiscoverMainContentProps) => {
|
||||
const { trackUiMetric, dataVisualizer: dataVisualizerService } = useDiscoverServices();
|
||||
const { trackUiMetric } = useDiscoverServices();
|
||||
const isEsqlMode = useIsEsqlMode();
|
||||
const shouldShowViewModeToggle = dataVisualizerService !== undefined;
|
||||
|
||||
const setDiscoverViewMode = useCallback(
|
||||
(mode: VIEW_MODE) => {
|
||||
|
@ -71,6 +72,8 @@ export const DiscoverMainContent = ({
|
|||
if (trackUiMetric) {
|
||||
if (mode === VIEW_MODE.AGGREGATED_LEVEL) {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, FIELD_STATISTICS_VIEW_CLICK);
|
||||
} else if (mode === VIEW_MODE.PATTERN_LEVEL) {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, PATTERN_ANALYSIS_VIEW_CLICK);
|
||||
} else {
|
||||
trackUiMetric(METRIC_TYPE.CLICK, DOCUMENTS_VIEW_CLICK);
|
||||
}
|
||||
|
@ -81,31 +84,36 @@ export const DiscoverMainContent = ({
|
|||
|
||||
const isDropAllowed = Boolean(onDropFieldToTable);
|
||||
|
||||
const viewModeToggle = useMemo(() => {
|
||||
return shouldShowViewModeToggle ? (
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
isEsqlMode={isEsqlMode}
|
||||
stateContainer={stateContainer}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
prepend={
|
||||
React.isValidElement(panelsToggle)
|
||||
? React.cloneElement(panelsToggle, { renderedFor: 'tabs', isChartAvailable })
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<React.Fragment />
|
||||
);
|
||||
}, [
|
||||
shouldShowViewModeToggle,
|
||||
viewMode,
|
||||
isEsqlMode,
|
||||
stateContainer,
|
||||
setDiscoverViewMode,
|
||||
panelsToggle,
|
||||
isChartAvailable,
|
||||
]);
|
||||
const renderViewModeToggle = useCallback(
|
||||
(patternCount?: number) => {
|
||||
return (
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
isEsqlMode={isEsqlMode}
|
||||
stateContainer={stateContainer}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
patternCount={patternCount}
|
||||
dataView={dataView}
|
||||
prepend={
|
||||
React.isValidElement(panelsToggle)
|
||||
? React.cloneElement(panelsToggle, { renderedFor: 'tabs', isChartAvailable })
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
viewMode,
|
||||
isEsqlMode,
|
||||
stateContainer,
|
||||
setDiscoverViewMode,
|
||||
dataView,
|
||||
panelsToggle,
|
||||
isChartAvailable,
|
||||
]
|
||||
);
|
||||
|
||||
const viewModeToggle = useMemo(() => renderViewModeToggle(), [renderViewModeToggle]);
|
||||
|
||||
const showChart = useAppStateSelector((state) => !state.hideChart);
|
||||
|
||||
|
@ -133,7 +141,8 @@ export const DiscoverMainContent = ({
|
|||
stateContainer={stateContainer}
|
||||
onFieldEdited={!isEsqlMode ? onFieldEdited : undefined}
|
||||
/>
|
||||
) : (
|
||||
) : null}
|
||||
{viewMode === VIEW_MODE.AGGREGATED_LEVEL ? (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>{viewModeToggle}</EuiFlexItem>
|
||||
<FieldStatisticsTab
|
||||
|
@ -144,7 +153,16 @@ export const DiscoverMainContent = ({
|
|||
trackUiMetric={trackUiMetric}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
{viewMode === VIEW_MODE.PATTERN_LEVEL ? (
|
||||
<PatternAnalysisTab
|
||||
dataView={dataView}
|
||||
stateContainer={stateContainer}
|
||||
switchToDocumentView={() => setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL)}
|
||||
trackUiMetric={trackUiMetric}
|
||||
renderViewModeToggle={renderViewModeToggle}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</DropOverlayWrapper>
|
||||
</Droppable>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/** Telemetry related to field statistics table **/
|
||||
export const PATTERN_ANALYSIS_LOADED = 'pattern_analysis_loaded';
|
||||
export const PATTERN_ANALYSIS_VIEW_CLICK = 'pattern_analysis_view_click';
|
|
@ -6,10 +6,5 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export {
|
||||
type FieldCategorizeButtonProps,
|
||||
FieldCategorizeButton,
|
||||
getFieldCategorizeButton,
|
||||
} from './field_categorize_button';
|
||||
|
||||
export { triggerCategorizeActions, canCategorize } from './categorize_trigger_utils';
|
||||
export { PatternAnalysisTable } from './pattern_analysis_table';
|
||||
export { PatternAnalysisTab } from './pattern_analysis_tab';
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
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}
|
||||
trackUiMetric={props.trackUiMetric}
|
||||
renderViewModeToggle={props.renderViewModeToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
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 { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import type { DiscoverStateContainer } from '../../state_management/discover_state';
|
||||
import { PATTERN_ANALYSIS_LOADED } from './constants';
|
||||
|
||||
export type PatternAnalysisTableProps = EmbeddablePatternAnalysisInput & {
|
||||
stateContainer?: DiscoverStateContainer;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
renderViewModeToggle: (patternCount?: number) => React.ReactElement;
|
||||
};
|
||||
|
||||
export const PatternAnalysisTable = (props: PatternAnalysisTableProps) => {
|
||||
const [lastReloadRequestTime, setLastReloadRequestTime] = useState<number | undefined>(undefined);
|
||||
|
||||
const services = useDiscoverServices();
|
||||
const aiopsService = services.aiops;
|
||||
const { trackUiMetric, stateContainer } = props;
|
||||
|
||||
useEffect(() => {
|
||||
const refetch = stateContainer?.dataState.refetch$.subscribe(() => {
|
||||
setLastReloadRequestTime(Date.now());
|
||||
});
|
||||
|
||||
return () => {
|
||||
refetch?.unsubscribe();
|
||||
};
|
||||
}, [stateContainer]);
|
||||
|
||||
useEffect(() => {
|
||||
// Track should only be called once when component is loaded
|
||||
if (aiopsService) {
|
||||
trackUiMetric?.(METRIC_TYPE.LOADED, PATTERN_ANALYSIS_LOADED);
|
||||
}
|
||||
}, [aiopsService, trackUiMetric]);
|
||||
|
||||
const patternAnalysisComponentProps: LogCategorizationEmbeddableProps = useMemo(
|
||||
() => ({
|
||||
input: Object.assign(
|
||||
{},
|
||||
pick(props, ['dataView', 'savedSearch', 'query', 'filters', 'switchToDocumentView']),
|
||||
{ lastReloadRequestTime }
|
||||
),
|
||||
renderViewModeToggle: props.renderViewModeToggle,
|
||||
}),
|
||||
[lastReloadRequestTime, props]
|
||||
);
|
||||
|
||||
if (!aiopsService) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<aiopsService.PatternAnalysisComponent
|
||||
props={patternAnalysisComponentProps}
|
||||
deps={services}
|
||||
embeddingOrigin="discover"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -114,6 +114,18 @@ describe('useEsqlMode', () => {
|
|||
viewMode: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('should change viewMode to undefined (default) if it was PATTERN_LEVEL', async () => {
|
||||
const { replaceUrlState } = renderHookWithContext(false, {
|
||||
viewMode: VIEW_MODE.PATTERN_LEVEL,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(replaceUrlState).toHaveBeenCalledTimes(1));
|
||||
expect(replaceUrlState).toHaveBeenCalledWith({
|
||||
viewMode: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('changing an ES|QL query with different result columns should change state when loading and finished', async () => {
|
||||
const { replaceUrlState, stateContainer } = renderHookWithContext(false);
|
||||
const documents$ = stateContainer.dataState.data$.documents$;
|
||||
|
|
|
@ -98,14 +98,23 @@ describe('getStateDefaults', () => {
|
|||
});
|
||||
expect(actualForUndefinedViewMode.viewMode).toBeUndefined();
|
||||
|
||||
const actualForEsqlWithInvalidViewMode = getStateDefaults({
|
||||
const actualForEsqlWithInvalidAggLevelViewMode = getStateDefaults({
|
||||
services: discoverServiceMock,
|
||||
savedSearch: {
|
||||
...savedSearchMockWithESQL,
|
||||
viewMode: VIEW_MODE.AGGREGATED_LEVEL,
|
||||
},
|
||||
});
|
||||
expect(actualForEsqlWithInvalidViewMode.viewMode).toBe(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
expect(actualForEsqlWithInvalidAggLevelViewMode.viewMode).toBe(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
|
||||
const actualForEsqlWithInvalidPatternLevelViewMode = getStateDefaults({
|
||||
services: discoverServiceMock,
|
||||
savedSearch: {
|
||||
...savedSearchMockWithESQL,
|
||||
viewMode: VIEW_MODE.PATTERN_LEVEL,
|
||||
},
|
||||
});
|
||||
expect(actualForEsqlWithInvalidPatternLevelViewMode.viewMode).toBe(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
|
||||
const actualForEsqlWithValidViewMode = getStateDefaults({
|
||||
services: discoverServiceMock,
|
||||
|
@ -117,15 +126,29 @@ describe('getStateDefaults', () => {
|
|||
expect(actualForEsqlWithValidViewMode.viewMode).toBe(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
expect(actualForEsqlWithValidViewMode.dataSource).toEqual(createEsqlDataSource());
|
||||
|
||||
const actualForWithValidViewMode = getStateDefaults({
|
||||
const actualForWithValidAggLevelViewMode = getStateDefaults({
|
||||
services: discoverServiceMock,
|
||||
savedSearch: {
|
||||
...savedSearchMock,
|
||||
viewMode: VIEW_MODE.AGGREGATED_LEVEL,
|
||||
},
|
||||
});
|
||||
expect(actualForWithValidViewMode.viewMode).toBe(VIEW_MODE.AGGREGATED_LEVEL);
|
||||
expect(actualForWithValidViewMode.dataSource).toEqual(
|
||||
expect(actualForWithValidAggLevelViewMode.viewMode).toBe(VIEW_MODE.AGGREGATED_LEVEL);
|
||||
expect(actualForWithValidAggLevelViewMode.dataSource).toEqual(
|
||||
createDataViewDataSource({
|
||||
dataViewId: savedSearchMock.searchSource.getField('index')?.id!,
|
||||
})
|
||||
);
|
||||
|
||||
const actualForWithValidPatternLevelViewMode = getStateDefaults({
|
||||
services: discoverServiceMock,
|
||||
savedSearch: {
|
||||
...savedSearchMock,
|
||||
viewMode: VIEW_MODE.PATTERN_LEVEL,
|
||||
},
|
||||
});
|
||||
expect(actualForWithValidPatternLevelViewMode.viewMode).toBe(VIEW_MODE.PATTERN_LEVEL);
|
||||
expect(actualForWithValidPatternLevelViewMode.dataSource).toEqual(
|
||||
createDataViewDataSource({
|
||||
dataViewId: savedSearchMock.searchSource.getField('index')?.id!,
|
||||
})
|
||||
|
|
|
@ -31,6 +31,13 @@ describe('getValidViewMode', () => {
|
|||
isEsqlMode: false,
|
||||
})
|
||||
).toBe(VIEW_MODE.AGGREGATED_LEVEL);
|
||||
|
||||
expect(
|
||||
getValidViewMode({
|
||||
viewMode: VIEW_MODE.PATTERN_LEVEL,
|
||||
isEsqlMode: false,
|
||||
})
|
||||
).toBe(VIEW_MODE.PATTERN_LEVEL);
|
||||
});
|
||||
|
||||
test('should work correctly for ES|QL mode', () => {
|
||||
|
@ -54,5 +61,12 @@ describe('getValidViewMode', () => {
|
|||
isEsqlMode: true,
|
||||
})
|
||||
).toBe(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
|
||||
expect(
|
||||
getValidViewMode({
|
||||
viewMode: VIEW_MODE.PATTERN_LEVEL,
|
||||
isEsqlMode: true,
|
||||
})
|
||||
).toBe(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,8 +20,11 @@ export const getValidViewMode = ({
|
|||
viewMode?: VIEW_MODE;
|
||||
isEsqlMode: boolean;
|
||||
}): VIEW_MODE | undefined => {
|
||||
if (viewMode === VIEW_MODE.AGGREGATED_LEVEL && isEsqlMode) {
|
||||
// only this mode is supported for ES|QL languages
|
||||
if (
|
||||
(viewMode === VIEW_MODE.PATTERN_LEVEL || viewMode === VIEW_MODE.AGGREGATED_LEVEL) &&
|
||||
isEsqlMode
|
||||
) {
|
||||
// only this mode is supported for text-based languages
|
||||
return VIEW_MODE.DOCUMENT_LEVEL;
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ import type { ContentClient } from '@kbn/content-management-plugin/public';
|
|||
import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import { memoize, noop } from 'lodash';
|
||||
import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public';
|
||||
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
|
||||
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
|
||||
import { DiscoverStartPlugins } from './plugin';
|
||||
import { DiscoverContextAppLocator } from './application/context/services/locator';
|
||||
|
@ -77,6 +78,7 @@ export interface UrlTracker {
|
|||
}
|
||||
|
||||
export interface DiscoverServices {
|
||||
aiops?: AiopsPluginStart;
|
||||
application: ApplicationStart;
|
||||
addBasePath: (path: string) => string;
|
||||
analytics: AnalyticsServiceStart;
|
||||
|
@ -157,6 +159,7 @@ export const buildServices = memoize(
|
|||
const storage = new Storage(localStorage);
|
||||
|
||||
return {
|
||||
aiops: plugins.aiops,
|
||||
application: core.application,
|
||||
addBasePath: core.http.basePath.prepend,
|
||||
analytics: core.analytics,
|
||||
|
|
|
@ -12,84 +12,153 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import React from 'react';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { DocumentViewModeToggle } from './view_mode_toggle';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { getDiscoverStateMock } from '../../__mocks__/discover_state.mock';
|
||||
import { DataTotalHits$ } from '../../application/main/state_management/discover_data_state_container';
|
||||
import { FetchStatus } from '../../application/types';
|
||||
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
describe('Document view mode toggle component', () => {
|
||||
const mountComponent = ({
|
||||
const mountComponent = async ({
|
||||
showFieldStatistics = true,
|
||||
viewMode = VIEW_MODE.DOCUMENT_LEVEL,
|
||||
isEsqlMode = false,
|
||||
setDiscoverViewMode = jest.fn(),
|
||||
useDataViewWithTextFields = true,
|
||||
} = {}) => {
|
||||
const services = {
|
||||
...discoverServiceMock,
|
||||
uiSettings: {
|
||||
get: () => showFieldStatistics,
|
||||
},
|
||||
aiops: {
|
||||
getPatternAnalysisAvailable: jest
|
||||
.fn()
|
||||
.mockResolvedValue(jest.fn().mockResolvedValue(useDataViewWithTextFields)),
|
||||
},
|
||||
};
|
||||
|
||||
const dataViewWithTextFields = {
|
||||
fields: [
|
||||
{
|
||||
name: 'field1',
|
||||
esTypes: [ES_FIELD_TYPES.TEXT],
|
||||
},
|
||||
],
|
||||
} as unknown as DataView;
|
||||
|
||||
const dataViewWithoutTextFields = {
|
||||
fields: [
|
||||
{
|
||||
name: 'field1',
|
||||
esTypes: [ES_FIELD_TYPES.FLOAT],
|
||||
},
|
||||
],
|
||||
} as unknown as DataView;
|
||||
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
stateContainer.dataState.data$.totalHits$ = new BehaviorSubject({
|
||||
fetchStatus: FetchStatus.COMPLETE,
|
||||
result: 10,
|
||||
}) as DataTotalHits$;
|
||||
|
||||
return mountWithIntl(
|
||||
const component = mountWithIntl(
|
||||
<KibanaContextProvider services={services}>
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
isEsqlMode={isEsqlMode}
|
||||
stateContainer={stateContainer}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
dataView={useDataViewWithTextFields ? dataViewWithTextFields : dataViewWithoutTextFields}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
component.update();
|
||||
});
|
||||
component!.update();
|
||||
return component!;
|
||||
};
|
||||
|
||||
it('should render if SHOW_FIELD_STATISTICS is true', () => {
|
||||
const component = mountComponent();
|
||||
it('should render if SHOW_FIELD_STATISTICS is true', async () => {
|
||||
const component = await mountComponent();
|
||||
expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(true);
|
||||
expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true);
|
||||
|
||||
expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(true);
|
||||
expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(true);
|
||||
expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render if SHOW_FIELD_STATISTICS is false', () => {
|
||||
const component = mountComponent({ showFieldStatistics: false });
|
||||
it('should not render if SHOW_FIELD_STATISTICS is false', async () => {
|
||||
const component = await mountComponent({ showFieldStatistics: false });
|
||||
expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(true);
|
||||
expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true);
|
||||
|
||||
expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(true);
|
||||
expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(true);
|
||||
expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not render if ES|QL', async () => {
|
||||
const component = await mountComponent({ isEsqlMode: true });
|
||||
expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(false);
|
||||
expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true);
|
||||
|
||||
expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(false);
|
||||
expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(false);
|
||||
expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not render if ES|QL', () => {
|
||||
const component = mountComponent({ isEsqlMode: true });
|
||||
expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(false);
|
||||
expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', () => {
|
||||
it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', async () => {
|
||||
const setDiscoverViewMode = jest.fn();
|
||||
const component = mountComponent({ setDiscoverViewMode });
|
||||
const component = await mountComponent({ setDiscoverViewMode });
|
||||
component.find('[data-test-subj="dscViewModeDocumentButton"]').at(0).simulate('click');
|
||||
expect(setDiscoverViewMode).toHaveBeenCalledWith(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
});
|
||||
|
||||
it('should set the view mode to VIEW_MODE.AGGREGATED_LEVEL when dscViewModeFieldStatsButton is clicked', () => {
|
||||
it('should set the view mode to VIEW_MODE.PATTERN_LEVEL when dscViewModePatternAnalysisButton is clicked', async () => {
|
||||
const setDiscoverViewMode = jest.fn();
|
||||
const component = mountComponent({ setDiscoverViewMode });
|
||||
const component = await mountComponent({ setDiscoverViewMode });
|
||||
component.find('[data-test-subj="dscViewModePatternAnalysisButton"]').at(0).simulate('click');
|
||||
expect(setDiscoverViewMode).toHaveBeenCalledWith(VIEW_MODE.PATTERN_LEVEL);
|
||||
});
|
||||
|
||||
it('should set the view mode to VIEW_MODE.AGGREGATED_LEVEL when dscViewModeFieldStatsButton is clicked', async () => {
|
||||
const setDiscoverViewMode = jest.fn();
|
||||
const component = await mountComponent({ setDiscoverViewMode });
|
||||
component.find('[data-test-subj="dscViewModeFieldStatsButton"]').at(0).simulate('click');
|
||||
expect(setDiscoverViewMode).toHaveBeenCalledWith(VIEW_MODE.AGGREGATED_LEVEL);
|
||||
});
|
||||
|
||||
it('should select the Documents tab if viewMode is VIEW_MODE.DOCUMENT_LEVEL', () => {
|
||||
const component = mountComponent();
|
||||
it('should select the Documents tab if viewMode is VIEW_MODE.DOCUMENT_LEVEL', async () => {
|
||||
const component = await mountComponent();
|
||||
expect(component.find(EuiTab).at(0).prop('isSelected')).toBe(true);
|
||||
});
|
||||
|
||||
it('should select the Field statistics tab if viewMode is VIEW_MODE.AGGREGATED_LEVEL', () => {
|
||||
const component = mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL });
|
||||
it('should select the Pattern Analysis tab if viewMode is VIEW_MODE.PATTERN_LEVEL', async () => {
|
||||
const component = await mountComponent({ viewMode: VIEW_MODE.PATTERN_LEVEL });
|
||||
expect(component.find(EuiTab).at(1).prop('isSelected')).toBe(true);
|
||||
});
|
||||
|
||||
it('should select the Field statistics tab if viewMode is VIEW_MODE.AGGREGATED_LEVEL', async () => {
|
||||
const component = await mountComponent({ viewMode: VIEW_MODE.AGGREGATED_LEVEL });
|
||||
expect(component.find(EuiTab).at(2).prop('isSelected')).toBe(true);
|
||||
});
|
||||
|
||||
it('should switch to document and hide pattern tab when there are no text fields', async () => {
|
||||
const setDiscoverViewMode = jest.fn();
|
||||
const component = await mountComponent({
|
||||
viewMode: VIEW_MODE.PATTERN_LEVEL,
|
||||
useDataViewWithTextFields: false,
|
||||
setDiscoverViewMode,
|
||||
});
|
||||
expect(setDiscoverViewMode).toHaveBeenCalledWith(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
expect(component.find(EuiTab).length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,14 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useMemo, ReactElement } from 'react';
|
||||
import React, { useMemo, useEffect, useState, type ReactElement, useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { css } from '@emotion/react';
|
||||
import { isLegacyTableEnabled, SHOW_FIELD_STATISTICS } from '@kbn/discover-utils';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import useMountedState from 'react-use/lib/useMountedState';
|
||||
import { VIEW_MODE } from '../../../common/constants';
|
||||
import { useDiscoverServices } from '../../hooks/use_discover_services';
|
||||
import { DiscoverStateContainer } from '../../application/main/state_management/discover_state';
|
||||
import type { DiscoverStateContainer } from '../../application/main/state_management/discover_state';
|
||||
import { HitsCounter, HitsCounterMode } from '../hits_counter';
|
||||
|
||||
export const DocumentViewModeToggle = ({
|
||||
|
@ -22,20 +24,70 @@ export const DocumentViewModeToggle = ({
|
|||
prepend,
|
||||
stateContainer,
|
||||
setDiscoverViewMode,
|
||||
patternCount,
|
||||
dataView,
|
||||
}: {
|
||||
viewMode: VIEW_MODE;
|
||||
isEsqlMode: boolean;
|
||||
prepend?: ReactElement;
|
||||
stateContainer: DiscoverStateContainer;
|
||||
setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
|
||||
patternCount?: number;
|
||||
dataView: DataView;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { uiSettings, dataVisualizer: dataVisualizerService } = useDiscoverServices();
|
||||
const {
|
||||
uiSettings,
|
||||
dataVisualizer: dataVisualizerService,
|
||||
aiops: aiopsService,
|
||||
} = useDiscoverServices();
|
||||
const isLegacy = useMemo(
|
||||
() => isLegacyTableEnabled({ uiSettings, isEsqlMode }),
|
||||
[uiSettings, isEsqlMode]
|
||||
);
|
||||
const includesNormalTabsStyle = viewMode === VIEW_MODE.AGGREGATED_LEVEL || isLegacy;
|
||||
const [showPatternAnalysisTab, setShowPatternAnalysisTab] = useState<boolean | null>(null);
|
||||
const showFieldStatisticsTab = useMemo(
|
||||
() => uiSettings.get(SHOW_FIELD_STATISTICS) && dataVisualizerService !== undefined,
|
||||
[dataVisualizerService, uiSettings]
|
||||
);
|
||||
const isMounted = useMountedState();
|
||||
|
||||
const setShowPatternAnalysisTabWrapper = useCallback(
|
||||
(value: boolean) => {
|
||||
if (isMounted()) {
|
||||
setShowPatternAnalysisTab(value);
|
||||
}
|
||||
},
|
||||
[isMounted]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function checkForPatternAnalysis() {
|
||||
if (!aiopsService) {
|
||||
setShowPatternAnalysisTab(false);
|
||||
return;
|
||||
}
|
||||
aiopsService
|
||||
.getPatternAnalysisAvailable()
|
||||
.then((patternAnalysisAvailable) => {
|
||||
patternAnalysisAvailable(dataView)
|
||||
.then(setShowPatternAnalysisTabWrapper)
|
||||
.catch(() => setShowPatternAnalysisTabWrapper(false));
|
||||
})
|
||||
.catch(() => setShowPatternAnalysisTabWrapper(false));
|
||||
},
|
||||
[aiopsService, dataView, setShowPatternAnalysisTabWrapper]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPatternAnalysisTab === false && viewMode === VIEW_MODE.PATTERN_LEVEL) {
|
||||
// switch to document view if no text fields are available
|
||||
setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL);
|
||||
}
|
||||
}, [showPatternAnalysisTab, viewMode, setDiscoverViewMode]);
|
||||
|
||||
const includesNormalTabsStyle =
|
||||
viewMode === VIEW_MODE.AGGREGATED_LEVEL || viewMode === VIEW_MODE.PATTERN_LEVEL || isLegacy;
|
||||
|
||||
const containerPadding = includesNormalTabsStyle ? euiTheme.size.s : 0;
|
||||
const containerCss = css`
|
||||
|
@ -48,9 +100,6 @@ export const DocumentViewModeToggle = ({
|
|||
}
|
||||
`;
|
||||
|
||||
const showViewModeToggle =
|
||||
(uiSettings.get(SHOW_FIELD_STATISTICS) && dataVisualizerService !== undefined) ?? false;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
|
@ -72,7 +121,7 @@ export const DocumentViewModeToggle = ({
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
{isEsqlMode || !showViewModeToggle ? (
|
||||
{isEsqlMode || (showFieldStatisticsTab === false && showPatternAnalysisTab === false) ? (
|
||||
<HitsCounter mode={HitsCounterMode.standalone} stateContainer={stateContainer} />
|
||||
) : (
|
||||
<EuiTabs size="m" css={tabsCss} data-test-subj="dscViewModeToggle" bottomBorder={false}>
|
||||
|
@ -84,16 +133,33 @@ export const DocumentViewModeToggle = ({
|
|||
<FormattedMessage id="discover.viewModes.document.label" defaultMessage="Documents" />
|
||||
<HitsCounter mode={HitsCounterMode.appended} stateContainer={stateContainer} />
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.AGGREGATED_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)}
|
||||
data-test-subj="dscViewModeFieldStatsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.viewModes.fieldStatistics.label"
|
||||
defaultMessage="Field statistics"
|
||||
/>
|
||||
</EuiTab>
|
||||
|
||||
{showPatternAnalysisTab ? (
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.PATTERN_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.PATTERN_LEVEL)}
|
||||
data-test-subj="dscViewModePatternAnalysisButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.viewModes.patternAnalysis.label"
|
||||
defaultMessage="Patterns {patternCount}"
|
||||
values={{ patternCount: patternCount !== undefined ? ` (${patternCount})` : '' }}
|
||||
/>
|
||||
</EuiTab>
|
||||
) : null}
|
||||
|
||||
{showFieldStatisticsTab ? (
|
||||
<EuiTab
|
||||
isSelected={viewMode === VIEW_MODE.AGGREGATED_LEVEL}
|
||||
onClick={() => setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)}
|
||||
data-test-subj="dscViewModeFieldStatsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.viewModes.fieldStatistics.label"
|
||||
defaultMessage="Field statistics"
|
||||
/>
|
||||
</EuiTab>
|
||||
) : null}
|
||||
</EuiTabs>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -51,6 +51,7 @@ import type {
|
|||
ObservabilityAIAssistantPublicSetup,
|
||||
ObservabilityAIAssistantPublicStart,
|
||||
} from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import type { AiopsPluginStart } from '@kbn/aiops-plugin/public';
|
||||
import type { DataVisualizerPluginStart } from '@kbn/data-visualizer-plugin/public';
|
||||
import { PLUGIN_ID } from '../common';
|
||||
import { registerFeature } from './register_feature';
|
||||
|
@ -179,6 +180,7 @@ export interface DiscoverSetupPlugins {
|
|||
* @internal
|
||||
*/
|
||||
export interface DiscoverStartPlugins {
|
||||
aiops?: AiopsPluginStart;
|
||||
dataViews: DataViewsServicePublic;
|
||||
dataViewEditor: DataViewEditorStart;
|
||||
dataVisualizer?: DataVisualizerPluginStart;
|
||||
|
|
|
@ -84,12 +84,14 @@
|
|||
"@kbn/shared-ux-markdown",
|
||||
"@kbn/data-view-utils",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/aiops-log-pattern-analysis",
|
||||
"@kbn/field-types",
|
||||
"@kbn/elastic-agent-utils",
|
||||
"@kbn/custom-icons",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/aiops-plugin",
|
||||
"@kbn/data-visualizer-plugin",
|
||||
"@kbn/search-types",
|
||||
"@kbn/custom-icons",
|
||||
"@kbn/observability-ai-assistant-plugin"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
|
|
|
@ -42,7 +42,11 @@ const savedSearchAttributesSchema = schema.object(
|
|||
searchSourceJSON: schema.string(),
|
||||
}),
|
||||
viewMode: schema.maybe(
|
||||
schema.oneOf([schema.literal('documents'), schema.literal('aggregated')])
|
||||
schema.oneOf([
|
||||
schema.literal('documents'),
|
||||
schema.literal('patterns'),
|
||||
schema.literal('aggregated'),
|
||||
])
|
||||
),
|
||||
hideAggregatedPreview: schema.maybe(schema.boolean()),
|
||||
rowHeight: schema.maybe(schema.number()),
|
||||
|
|
|
@ -19,6 +19,7 @@ export type {
|
|||
export enum VIEW_MODE {
|
||||
DOCUMENT_LEVEL = 'documents',
|
||||
AGGREGATED_LEVEL = 'aggregated',
|
||||
PATTERN_LEVEL = 'patterns',
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
|
@ -119,3 +119,13 @@ export const SCHEMA_SEARCH_MODEL_VERSION_3 = SCHEMA_SEARCH_MODEL_VERSION_2.exten
|
|||
])
|
||||
),
|
||||
});
|
||||
|
||||
export const SCHEMA_SEARCH_MODEL_VERSION_4 = SCHEMA_SEARCH_MODEL_VERSION_3.extends({
|
||||
viewMode: schema.maybe(
|
||||
schema.oneOf([
|
||||
schema.literal(VIEW_MODE.DOCUMENT_LEVEL),
|
||||
schema.literal(VIEW_MODE.PATTERN_LEVEL),
|
||||
schema.literal(VIEW_MODE.AGGREGATED_LEVEL),
|
||||
])
|
||||
),
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
SCHEMA_SEARCH_MODEL_VERSION_1,
|
||||
SCHEMA_SEARCH_MODEL_VERSION_2,
|
||||
SCHEMA_SEARCH_MODEL_VERSION_3,
|
||||
SCHEMA_SEARCH_MODEL_VERSION_4,
|
||||
} from './schema';
|
||||
|
||||
export function getSavedSearchObjectType(
|
||||
|
@ -62,6 +63,13 @@ export function getSavedSearchObjectType(
|
|||
create: SCHEMA_SEARCH_MODEL_VERSION_3,
|
||||
},
|
||||
},
|
||||
4: {
|
||||
changes: [],
|
||||
schemas: {
|
||||
forwardCompatibility: SCHEMA_SEARCH_MODEL_VERSION_4.extends({}, { unknowns: 'ignore' }),
|
||||
create: SCHEMA_SEARCH_MODEL_VERSION_4,
|
||||
},
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
|
|
|
@ -88,6 +88,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await testSubjects.missingOrFail('discoverErrorCalloutTitle');
|
||||
});
|
||||
|
||||
it('should not show Patterns tab (basic license)', async () => {
|
||||
await testSubjects.missingOrFail('dscViewModePatternAnalysisButton');
|
||||
await retry.try(async () => {
|
||||
const documentTab = await testSubjects.find('dscViewModeDocumentButton');
|
||||
expect(await documentTab.getAttribute('aria-selected')).to.be('true');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show Field Statistics tab', async () => {
|
||||
await testSubjects.click('dscViewModeFieldStatsButton');
|
||||
|
||||
|
|
|
@ -32,7 +32,8 @@ export function createCategoryRequest(
|
|||
wrap: ReturnType<typeof createRandomSamplerWrapper>['wrap'],
|
||||
intervalMs?: number,
|
||||
additionalFilter?: CategorizationAdditionalFilter,
|
||||
useStandardTokenizer: boolean = true
|
||||
useStandardTokenizer: boolean = true,
|
||||
includeSparkline: boolean = true
|
||||
) {
|
||||
const query = createCategorizeQuery(queryIn, timeField, timeRange);
|
||||
const aggs = {
|
||||
|
@ -50,7 +51,7 @@ export function createCategoryRequest(
|
|||
_source: field,
|
||||
},
|
||||
},
|
||||
...(intervalMs
|
||||
...(intervalMs && includeSparkline
|
||||
? {
|
||||
sparkline: {
|
||||
date_histogram: {
|
||||
|
@ -76,6 +77,16 @@ export function createCategoryRequest(
|
|||
_source: field,
|
||||
},
|
||||
},
|
||||
...(intervalMs
|
||||
? {
|
||||
sparkline: {
|
||||
date_histogram: {
|
||||
field: timeField,
|
||||
fixed_interval: `${intervalMs}ms`,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(additionalFilter.field
|
||||
? {
|
||||
sub_field: {
|
||||
|
|
20
x-pack/packages/ml/aiops_log_pattern_analysis/embeddable.ts
Normal file
20
x-pack/packages/ml/aiops_log_pattern_analysis/embeddable.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { Query, AggregateQuery, Filter } from '@kbn/es-query';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
||||
export interface EmbeddablePatternAnalysisInput<T = Query | AggregateQuery> {
|
||||
dataView: DataView;
|
||||
savedSearch?: SavedSearch | null;
|
||||
query?: T;
|
||||
filters?: Filter[];
|
||||
embeddingOrigin?: string;
|
||||
switchToDocumentView?: () => void;
|
||||
lastReloadRequestTime?: number;
|
||||
}
|
|
@ -11,7 +11,7 @@ import type { estypes } from '@elastic/elasticsearch';
|
|||
|
||||
import type { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
|
||||
|
||||
import type { Category, CategoriesAgg, CatResponse } from './types';
|
||||
import type { Category, CategoriesAgg, CatResponse, Sparkline } from './types';
|
||||
|
||||
export function processCategoryResults(
|
||||
result: CatResponse,
|
||||
|
@ -29,24 +29,17 @@ export function processCategoryResults(
|
|||
) as CategoriesAgg;
|
||||
|
||||
const categories: Category[] = buckets.map((b) => {
|
||||
const sparkline =
|
||||
b.sparkline === undefined
|
||||
? {}
|
||||
: b.sparkline.buckets.reduce<Record<number, number>>((acc2, cur2) => {
|
||||
acc2[cur2.key] = cur2.doc_count;
|
||||
return acc2;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
key: b.key,
|
||||
count: b.doc_count,
|
||||
examples: b.examples.hits.hits.map((h) => get(h._source, field)),
|
||||
sparkline,
|
||||
sparkline: getSparkline(b.sparkline),
|
||||
subTimeRangeCount: b.sub_time_range?.buckets[0].doc_count ?? undefined,
|
||||
subFieldCount: b.sub_time_range?.buckets[0].sub_field?.doc_count ?? undefined,
|
||||
subFieldExamples:
|
||||
b.sub_time_range?.buckets[0].examples.hits.hits.map((h) => get(h._source, field)) ??
|
||||
undefined,
|
||||
subFieldSparkline: getSparkline(b.sub_time_range?.buckets[0].sparkline),
|
||||
regex: b.regex,
|
||||
};
|
||||
});
|
||||
|
@ -59,3 +52,12 @@ export function processCategoryResults(
|
|||
hasExamples,
|
||||
};
|
||||
}
|
||||
|
||||
function getSparkline(sparkline?: Sparkline) {
|
||||
return sparkline === undefined
|
||||
? {}
|
||||
: sparkline.buckets.reduce<Record<number, number>>((acc, cur) => {
|
||||
acc[cur.key] = cur.doc_count;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
|
|
@ -20,5 +20,8 @@
|
|||
"@kbn/config-schema",
|
||||
"@kbn/i18n",
|
||||
"@kbn/ml-runtime-field-utils",
|
||||
"@kbn/es-query",
|
||||
"@kbn/saved-search-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface Category {
|
|||
subTimeRangeCount?: number;
|
||||
subFieldCount?: number;
|
||||
subFieldExamples?: string[];
|
||||
subFieldSparkline?: Record<number, number>;
|
||||
examples: string[];
|
||||
sparkline?: Record<number, number>;
|
||||
regex: string;
|
||||
|
@ -22,6 +23,10 @@ interface CategoryExamples {
|
|||
hits: { hits: Array<{ _source: { message: string } }> };
|
||||
}
|
||||
|
||||
export interface Sparkline {
|
||||
buckets: Array<{ key_as_string: string; key: number; doc_count: number }>;
|
||||
}
|
||||
|
||||
export interface CategoriesAgg {
|
||||
categories: {
|
||||
buckets: Array<{
|
||||
|
@ -29,9 +34,7 @@ export interface CategoriesAgg {
|
|||
doc_count: number;
|
||||
examples: CategoryExamples;
|
||||
regex: string;
|
||||
sparkline: {
|
||||
buckets: Array<{ key_as_string: string; key: number; doc_count: number }>;
|
||||
};
|
||||
sparkline: Sparkline;
|
||||
sub_time_range?: {
|
||||
buckets: Array<{
|
||||
key: number;
|
||||
|
@ -44,6 +47,7 @@ export interface CategoriesAgg {
|
|||
doc_count: number;
|
||||
};
|
||||
examples: CategoryExamples;
|
||||
sparkline: Sparkline;
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { getTimeFieldRange } from './src/services/time_field_range';
|
||||
export {
|
||||
DatePickerContextProvider,
|
||||
type DatePickerDependencies,
|
||||
|
|
|
@ -40,6 +40,8 @@ interface GetTimeFieldRangeOptions {
|
|||
* API path ('/internal/file_upload/time_field_range')
|
||||
*/
|
||||
path: string;
|
||||
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,12 +50,13 @@ interface GetTimeFieldRangeOptions {
|
|||
* @returns GetTimeFieldRangeResponse
|
||||
*/
|
||||
export async function getTimeFieldRange(options: GetTimeFieldRangeOptions) {
|
||||
const { http, path, ...body } = options;
|
||||
const { http, path, signal, ...body } = options;
|
||||
|
||||
return await http.fetch<GetTimeFieldRangeResponse>({
|
||||
path,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
version: '1',
|
||||
...(signal ? { signal } : {}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,134 +7,73 @@
|
|||
|
||||
import type { FC } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import type { EuiBasicTableColumn, EuiTableSelectionType } from '@elastic/eui';
|
||||
import {
|
||||
useEuiBackgroundColor,
|
||||
EuiInMemoryTable,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { useTableState } from '@kbn/ml-in-memory-table';
|
||||
import type { CategorizationAdditionalFilter } from '@kbn/aiops-log-pattern-analysis/create_category_request';
|
||||
import { type QueryMode, QUERY_MODE } from '@kbn/aiops-log-pattern-analysis/get_category_query';
|
||||
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';
|
||||
import type { LogCategorizationAppState } from '../../../application/url_state/log_pattern_analysis';
|
||||
|
||||
import { MiniHistogram } from '../../mini_histogram';
|
||||
|
||||
import { useDiscoverLinks, createFilter } from '../use_discover_links';
|
||||
import type { EventRate } from '../use_categorize_request';
|
||||
|
||||
import { getLabels } from './labels';
|
||||
import { TableHeader } from './table_header';
|
||||
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;
|
||||
dataViewId: string;
|
||||
selectedField: DataViewField | string | undefined;
|
||||
timefilter: TimefilterContract;
|
||||
aiopsListState: LogCategorizationAppState;
|
||||
pinnedCategory: Category | null;
|
||||
setPinnedCategory: (category: Category | null) => void;
|
||||
selectedCategory: Category | null;
|
||||
setSelectedCategory: (category: Category | null) => void;
|
||||
onAddFilter?: (values: Filter, alias?: string) => void;
|
||||
onClose?: () => void;
|
||||
highlightedCategory: Category | null;
|
||||
setHighlightedCategory: (category: Category | null) => void;
|
||||
setSelectedCategories: (category: Category[]) => void;
|
||||
openInDiscover: OpenInDiscover;
|
||||
tableState: UseTableState<Category>;
|
||||
enableRowActions?: boolean;
|
||||
additionalFilter?: CategorizationAdditionalFilter;
|
||||
navigateToDiscover?: boolean;
|
||||
displayExamples?: boolean;
|
||||
}
|
||||
|
||||
export const CategoryTable: FC<Props> = ({
|
||||
categories,
|
||||
eventRate,
|
||||
dataViewId,
|
||||
selectedField,
|
||||
timefilter,
|
||||
aiopsListState,
|
||||
pinnedCategory,
|
||||
setPinnedCategory,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
onAddFilter,
|
||||
onClose = () => {},
|
||||
highlightedCategory,
|
||||
setHighlightedCategory,
|
||||
setSelectedCategories,
|
||||
openInDiscover,
|
||||
tableState,
|
||||
enableRowActions = true,
|
||||
additionalFilter,
|
||||
navigateToDiscover = true,
|
||||
displayExamples = true,
|
||||
}) => {
|
||||
const euiTheme = useEuiTheme();
|
||||
const primaryBackgroundColor = useEuiBackgroundColor('primary');
|
||||
const { openInDiscoverWithFilter } = useDiscoverLinks();
|
||||
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
|
||||
const { onTableChange, pagination, sorting } = useTableState<Category>(categories ?? [], 'key');
|
||||
const { onTableChange, pagination, sorting } = tableState;
|
||||
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, JSX.Element>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const labels = useMemo(() => {
|
||||
const isFlyout = onAddFilter !== undefined && onClose !== undefined;
|
||||
return getLabels(isFlyout && navigateToDiscover === false);
|
||||
}, [navigateToDiscover, onAddFilter, onClose]);
|
||||
|
||||
const showSparkline = useMemo(() => {
|
||||
return categories.some((category) => category.sparkline !== undefined);
|
||||
}, [categories]);
|
||||
|
||||
const openInDiscover = (mode: QueryMode, category?: Category) => {
|
||||
if (
|
||||
onAddFilter !== undefined &&
|
||||
selectedField !== undefined &&
|
||||
typeof selectedField !== 'string' &&
|
||||
navigateToDiscover === false
|
||||
) {
|
||||
onAddFilter(
|
||||
createFilter('', selectedField.name, selectedCategories, mode, category),
|
||||
`Patterns - ${selectedField.name}`
|
||||
);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const timefilterActiveBounds =
|
||||
additionalFilter !== undefined
|
||||
? {
|
||||
min: moment(additionalFilter.from),
|
||||
max: moment(additionalFilter.to),
|
||||
}
|
||||
: timefilter.getActiveBounds();
|
||||
|
||||
if (timefilterActiveBounds === undefined || selectedField === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
openInDiscoverWithFilter(
|
||||
dataViewId,
|
||||
typeof selectedField === 'string' ? selectedField : selectedField.name,
|
||||
selectedCategories,
|
||||
aiopsListState,
|
||||
timefilterActiveBounds,
|
||||
mode,
|
||||
category,
|
||||
additionalFilter?.field
|
||||
);
|
||||
};
|
||||
const { labels: openInDiscoverLabels, openFunction: openInDiscoverFunction } = openInDiscover;
|
||||
|
||||
const toggleDetails = useCallback(
|
||||
(category: Category) => {
|
||||
|
@ -197,20 +136,20 @@ export const CategoryTable: FC<Props> = ({
|
|||
width: '65px',
|
||||
actions: [
|
||||
{
|
||||
name: labels.singleSelect.in,
|
||||
description: labels.singleSelect.in,
|
||||
name: openInDiscoverLabels.singleSelect.in,
|
||||
description: openInDiscoverLabels.singleSelect.in,
|
||||
icon: 'plusInCircle',
|
||||
type: 'icon',
|
||||
'data-test-subj': 'aiopsLogPatternsActionFilterInButton',
|
||||
onClick: (category) => openInDiscover(QUERY_MODE.INCLUDE, category),
|
||||
onClick: (category) => openInDiscoverFunction(QUERY_MODE.INCLUDE, category),
|
||||
},
|
||||
{
|
||||
name: labels.singleSelect.out,
|
||||
description: labels.singleSelect.out,
|
||||
name: openInDiscoverLabels.singleSelect.out,
|
||||
description: openInDiscoverLabels.singleSelect.out,
|
||||
icon: 'minusInCircle',
|
||||
type: 'icon',
|
||||
'data-test-subj': 'aiopsLogPatternsActionFilterOutButton',
|
||||
onClick: (category) => openInDiscover(QUERY_MODE.EXCLUDE, category),
|
||||
onClick: (category) => openInDiscoverFunction(QUERY_MODE.EXCLUDE, category),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -291,7 +230,7 @@ export const CategoryTable: FC<Props> = ({
|
|||
};
|
||||
}
|
||||
|
||||
if (selectedCategory && selectedCategory.key === category.key) {
|
||||
if (highlightedCategory && highlightedCategory.key === category.key) {
|
||||
return {
|
||||
backgroundColor: euiTheme.euiColorLightestShade,
|
||||
};
|
||||
|
@ -302,49 +241,49 @@ export const CategoryTable: FC<Props> = ({
|
|||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableHeader
|
||||
categoriesCount={categories.length}
|
||||
selectedCategoriesCount={selectedCategories.length}
|
||||
labels={labels}
|
||||
openInDiscover={(queryMode: QueryMode) => openInDiscover(queryMode)}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
const tableStyle = css({
|
||||
thead: {
|
||||
position: 'sticky',
|
||||
insetBlockStart: 0,
|
||||
zIndex: 1,
|
||||
backgroundColor: euiTheme.euiColorEmptyShade,
|
||||
boxShadow: `inset 0 0px 0, inset 0 -1px 0 ${euiTheme.euiBorderColor}`,
|
||||
},
|
||||
});
|
||||
|
||||
<EuiInMemoryTable<Category>
|
||||
compressed
|
||||
items={categories}
|
||||
columns={columns}
|
||||
selection={selectionValue}
|
||||
itemId="key"
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
data-test-subj="aiopsLogPatternsTable"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
rowProps={(category) => {
|
||||
return enableRowActions
|
||||
? {
|
||||
onClick: () => {
|
||||
if (category.key === pinnedCategory?.key) {
|
||||
setPinnedCategory(null);
|
||||
} else {
|
||||
setPinnedCategory(category);
|
||||
}
|
||||
},
|
||||
onMouseEnter: () => {
|
||||
setSelectedCategory(category);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setSelectedCategory(null);
|
||||
},
|
||||
style: getRowStyle(category),
|
||||
}
|
||||
: undefined;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,76 +5,103 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { type QueryMode, QUERY_MODE } from '@kbn/aiops-log-pattern-analysis/get_category_query';
|
||||
import { QUERY_MODE } from '@kbn/aiops-log-pattern-analysis/get_category_query';
|
||||
import { useEuiTheme } from '../../../hooks/use_eui_theme';
|
||||
import type { getLabels } from './labels';
|
||||
import type { OpenInDiscover } from './use_open_in_discover';
|
||||
|
||||
interface Props {
|
||||
categoriesCount: number;
|
||||
selectedCategoriesCount: number;
|
||||
labels: ReturnType<typeof getLabels>;
|
||||
openInDiscover: (mode: QueryMode) => void;
|
||||
openInDiscover: OpenInDiscover;
|
||||
}
|
||||
|
||||
export const TableHeader: FC<Props> = ({
|
||||
categoriesCount,
|
||||
selectedCategoriesCount,
|
||||
labels,
|
||||
openInDiscover,
|
||||
}) => {
|
||||
const euiTheme = useEuiTheme();
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center" css={{ minHeight: euiTheme.euiSizeXL }}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" data-test-subj="aiopsLogPatternsFoundCount">
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.counts"
|
||||
defaultMessage="{count} {count, plural, one {pattern} other {patterns}} found"
|
||||
values={{ count: categoriesCount }}
|
||||
/>
|
||||
{selectedCategoriesCount > 0 ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.selectedCounts"
|
||||
defaultMessage=" | {count} selected"
|
||||
values={{ count: selectedCategoriesCount }}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</EuiText>
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center" css={{ minHeight: euiTheme.euiSizeXL }}>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s" data-test-subj="aiopsLogPatternsFoundCount">
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.counts"
|
||||
defaultMessage="{count} {count, plural, one {pattern} other {patterns}} found"
|
||||
values={{ count: categoriesCount }}
|
||||
/>
|
||||
{selectedCategoriesCount > 0 ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.selectedCounts"
|
||||
defaultMessage=" | {count} selected"
|
||||
values={{ count: selectedCategoriesCount }}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{selectedCategoriesCount > 0 ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<OpenInDiscoverButtons openInDiscover={openInDiscover} />
|
||||
</EuiFlexItem>
|
||||
{selectedCategoriesCount > 0 ? (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="aiopsLogPatternAnalysisOpenInDiscoverIncludeButton"
|
||||
size="s"
|
||||
onClick={() => openInDiscover(QUERY_MODE.INCLUDE)}
|
||||
iconType="plusInCircle"
|
||||
iconSide="left"
|
||||
>
|
||||
{labels.multiSelect.in}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="aiopsLogPatternAnalysisOpenInDiscoverExcludeButton"
|
||||
size="s"
|
||||
onClick={() => openInDiscover(QUERY_MODE.EXCLUDE)}
|
||||
iconType="minusInCircle"
|
||||
iconSide="left"
|
||||
>
|
||||
{labels.multiSelect.out}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const OpenInDiscoverButtons: FC<{ openInDiscover: OpenInDiscover; showText?: boolean }> = ({
|
||||
openInDiscover,
|
||||
showText = true,
|
||||
}) => {
|
||||
const { labels, openFunction } = openInDiscover;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TooltipWrapper text={labels.multiSelect.in} showText={showText}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="aiopsLogPatternAnalysisOpenInDiscoverIncludeButton"
|
||||
size="s"
|
||||
onClick={() => openFunction(QUERY_MODE.INCLUDE)}
|
||||
iconType="plusInCircle"
|
||||
iconSide="left"
|
||||
>
|
||||
{labels.multiSelect.in}
|
||||
</EuiButtonEmpty>
|
||||
</TooltipWrapper>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TooltipWrapper text={labels.multiSelect.out} showText={showText}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="aiopsLogPatternAnalysisOpenInDiscoverExcludeButton"
|
||||
size="s"
|
||||
onClick={() => openFunction(QUERY_MODE.EXCLUDE)}
|
||||
iconType="minusInCircle"
|
||||
iconSide="left"
|
||||
>
|
||||
{labels.multiSelect.out}
|
||||
</EuiButtonEmpty>
|
||||
</TooltipWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const TooltipWrapper: FC<PropsWithChildren<{ text: string; showText: boolean }>> = ({
|
||||
text,
|
||||
showText,
|
||||
children,
|
||||
}) => {
|
||||
return showText ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<EuiToolTip content={text}>
|
||||
<>{children}</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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, useMemo } from 'react';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import { type QueryMode } 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 { useDiscoverLinks, createFilter } from '../use_discover_links';
|
||||
import type { LogCategorizationAppState } from '../../../application/url_state/log_pattern_analysis';
|
||||
import { getLabels } from './labels';
|
||||
|
||||
export interface OpenInDiscover {
|
||||
openFunction: (mode: QueryMode, category?: Category) => void;
|
||||
labels: ReturnType<typeof getLabels>;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function useOpenInDiscover(
|
||||
dataViewId: string,
|
||||
selectedField: DataViewField | string | undefined,
|
||||
selectedCategories: Category[],
|
||||
aiopsListState: LogCategorizationAppState,
|
||||
timefilter: TimefilterContract,
|
||||
navigateToDiscover?: boolean,
|
||||
onAddFilter?: (values: Filter, alias?: string) => void,
|
||||
additionalFilter?: CategorizationAdditionalFilter,
|
||||
onClose: () => void = () => {}
|
||||
): OpenInDiscover {
|
||||
const { openInDiscoverWithFilter } = useDiscoverLinks();
|
||||
|
||||
const openFunction = useCallback(
|
||||
(mode: QueryMode, category?: Category) => {
|
||||
if (
|
||||
onAddFilter !== undefined &&
|
||||
selectedField !== undefined &&
|
||||
typeof selectedField !== 'string' &&
|
||||
navigateToDiscover === false
|
||||
) {
|
||||
onAddFilter(
|
||||
createFilter('', selectedField.name, selectedCategories, mode, category),
|
||||
`Patterns - ${selectedField.name}`
|
||||
);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const timefilterActiveBounds =
|
||||
additionalFilter !== undefined
|
||||
? {
|
||||
min: moment(additionalFilter.from),
|
||||
max: moment(additionalFilter.to),
|
||||
}
|
||||
: timefilter.getActiveBounds();
|
||||
|
||||
if (timefilterActiveBounds === undefined || selectedField === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
openInDiscoverWithFilter(
|
||||
dataViewId,
|
||||
typeof selectedField === 'string' ? selectedField : selectedField.name,
|
||||
selectedCategories,
|
||||
aiopsListState,
|
||||
timefilterActiveBounds,
|
||||
mode,
|
||||
category,
|
||||
additionalFilter?.field
|
||||
);
|
||||
},
|
||||
[
|
||||
onAddFilter,
|
||||
selectedField,
|
||||
navigateToDiscover,
|
||||
additionalFilter,
|
||||
timefilter,
|
||||
openInDiscoverWithFilter,
|
||||
dataViewId,
|
||||
selectedCategories,
|
||||
aiopsListState,
|
||||
onClose,
|
||||
]
|
||||
);
|
||||
|
||||
const labels = useMemo(() => {
|
||||
const isFlyout = onAddFilter !== undefined && onClose !== undefined;
|
||||
return getLabels(isFlyout && navigateToDiscover === false);
|
||||
}, [navigateToDiscover, onAddFilter, onClose]);
|
||||
|
||||
return { openFunction, labels, count: selectedCategories.length };
|
||||
}
|
|
@ -9,7 +9,7 @@ import type { FC } from 'react';
|
|||
import React from 'react';
|
||||
|
||||
import moment from 'moment';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
|
@ -18,6 +18,7 @@ import {
|
|||
} from '@kbn/ml-ui-actions';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
|
||||
|
||||
interface Props {
|
||||
|
@ -26,6 +27,7 @@ interface Props {
|
|||
query: QueryDslQueryContainer;
|
||||
earliest: number | undefined;
|
||||
latest: number | undefined;
|
||||
iconOnly?: boolean;
|
||||
}
|
||||
|
||||
export const CreateCategorizationJobButton: FC<Props> = ({
|
||||
|
@ -34,6 +36,7 @@ export const CreateCategorizationJobButton: FC<Props> = ({
|
|||
query,
|
||||
earliest,
|
||||
latest,
|
||||
iconOnly = false,
|
||||
}) => {
|
||||
const {
|
||||
uiActions,
|
||||
|
@ -58,20 +61,40 @@ export const CreateCategorizationJobButton: FC<Props> = ({
|
|||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="aiopsLogCategorizationFlyoutAdJobButton"
|
||||
onClick={createADJob}
|
||||
flush="left"
|
||||
iconSide="left"
|
||||
iconType={'machineLearningApp'}
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.aiops.categorizeFlyout.findAnomalies.tooltip', {
|
||||
defaultMessage: 'Create anomaly detection job to find anomalies in patterns',
|
||||
})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.categorizeFlyout.findAnomalies"
|
||||
defaultMessage="Find anomalies in patterns"
|
||||
<EuiButtonIcon
|
||||
data-test-subj="aiopsEmbeddableMenuOptionsButton"
|
||||
size="s"
|
||||
iconType="machineLearningApp"
|
||||
onClick={createADJob}
|
||||
// @ts-ignore - subdued does work
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.aiops.categorizeFlyout.findAnomalies.tooltip', {
|
||||
defaultMessage: 'Create anomaly detection job to find anomalies in patterns',
|
||||
})}
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="aiopsLogCategorizationFlyoutAdJobButton"
|
||||
onClick={createADJob}
|
||||
flush="left"
|
||||
iconSide="left"
|
||||
iconType={'machineLearningApp'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.categorizeFlyout.findAnomalies"
|
||||
defaultMessage="Find anomalies in patterns"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -95,12 +95,15 @@ export const useCreateFormattedExample = () => {
|
|||
const elements: JSX.Element[] = [];
|
||||
let pos = 0;
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
const elementKey = `${key}-token-${i}`;
|
||||
elements.push(
|
||||
<span css={wildcardStyle}>{tempExample.substring(pos, positions[i].start)}</span>
|
||||
<span css={wildcardStyle} key={elementKey}>
|
||||
{tempExample.substring(pos, positions[i].start)}
|
||||
</span>
|
||||
);
|
||||
|
||||
elements.push(
|
||||
<span css={tokenStyle}>
|
||||
<span css={tokenStyle} key={`${elementKey}-2`}>
|
||||
{tempExample.substring(positions[i].start, positions[i].end)}
|
||||
</span>
|
||||
);
|
||||
|
@ -108,7 +111,7 @@ export const useCreateFormattedExample = () => {
|
|||
}
|
||||
|
||||
elements.push(
|
||||
<span css={wildcardStyle}>
|
||||
<span css={wildcardStyle} key={key}>
|
||||
{tempExample.substring(positions[positions.length - 1].end)}
|
||||
</span>
|
||||
);
|
||||
|
@ -131,10 +134,10 @@ export const FormattedPatternExamples: FC<Props> = ({ category, count }) => {
|
|||
.fill(0)
|
||||
.map((_, i) => createFormattedExample(key, examples[i]));
|
||||
return formattedExamples.map((example, i) => (
|
||||
<>
|
||||
<React.Fragment key={`example-${i}`}>
|
||||
<code>{example}</code>
|
||||
{i < formattedExamples.length - 1 ? <EuiHorizontalRule margin="s" /> : null}
|
||||
</>
|
||||
</React.Fragment>
|
||||
));
|
||||
}, [category, count, createFormattedExample]);
|
||||
|
||||
|
@ -150,10 +153,19 @@ export const FormattedRegex: FC<Props> = ({ category }) => {
|
|||
const elements: JSX.Element[] = [];
|
||||
for (let i = 0; i < regexTokens.length; i++) {
|
||||
const token = regexTokens[i];
|
||||
const key = `regex-${i}`;
|
||||
if (token.match(/\.\*\?|\.\+\?/)) {
|
||||
elements.push(<span css={wildcardStyle}>{token}</span>);
|
||||
elements.push(
|
||||
<span css={wildcardStyle} key={key}>
|
||||
{token}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
elements.push(<span css={tokenStyle}>{token}</span>);
|
||||
elements.push(
|
||||
<span css={tokenStyle} key={key}>
|
||||
{token}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
|
|
|
@ -10,93 +10,124 @@ import React from 'react';
|
|||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
|
||||
interface Props {
|
||||
eventRateLength: number;
|
||||
fieldSelected: boolean;
|
||||
fields?: DataViewField[];
|
||||
categoriesLength: number | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const InformationText: FC<Props> = ({
|
||||
eventRateLength,
|
||||
fieldSelected,
|
||||
fields,
|
||||
categoriesLength,
|
||||
loading,
|
||||
}) => {
|
||||
if (loading === true) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{eventRateLength === 0 ? (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noDocsTitle"
|
||||
defaultMessage="No documents found"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noDocsBody"
|
||||
defaultMessage="Ensure the selected time range contains documents."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoWindowParametersEmptyPrompt"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{eventRateLength > 0 && categoriesLength === null ? (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.emptyPromptTitle"
|
||||
defaultMessage="Select a text field and click run pattern analysis to start analysis"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.emptyPromptBody"
|
||||
defaultMessage="Log pattern analysis groups messages into common patterns."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoWindowParametersEmptyPrompt"
|
||||
/>
|
||||
) : null}
|
||||
if (fields?.length === 0) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noTextFieldsTitle"
|
||||
defaultMessage="No text fields found"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noTextFieldsBody"
|
||||
defaultMessage="Pattern analysis can only be run on text fields."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoTextFieldsEmptyPrompt"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
{eventRateLength > 0 && categoriesLength !== null && categoriesLength === 0 ? (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noCategoriesTitle"
|
||||
defaultMessage="No patterns found"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noCategoriesBody"
|
||||
defaultMessage="Ensure the selected field is populated in the selected time range."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoWindowParametersEmptyPrompt"
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
if (eventRateLength === 0) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noDocsTitle"
|
||||
defaultMessage="No documents found"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noDocsBody"
|
||||
defaultMessage="Ensure the selected time range contains documents."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoDocsEmptyPrompt"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventRateLength > 0 && categoriesLength === null) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.emptyPromptTitle"
|
||||
defaultMessage="Select a text field and click run pattern analysis to start analysis"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.emptyPromptBody"
|
||||
defaultMessage="Log pattern analysis groups messages into common patterns."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoWindowParametersEmptyPrompt"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventRateLength > 0 && categoriesLength !== null && categoriesLength === 0) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noCategoriesTitle"
|
||||
defaultMessage="No patterns found"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.noCategoriesBody"
|
||||
defaultMessage="Ensure the selected field is populated in the selected time range."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoCategoriesEmptyPrompt"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -19,26 +19,26 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const LoadingCategorization: FC<Props> = ({ onClose }) => (
|
||||
export const LoadingCategorization: FC<Props> = ({ onCancel }) => (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false} css={{ textAlign: 'center' }}>
|
||||
<EuiFlexGrid columns={1}>
|
||||
<EuiFlexItem>
|
||||
<EuiLoadingElastic size="xxl" />
|
||||
<EuiLoadingElastic size="xl" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h2>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.categorizeFlyout.loading.title"
|
||||
defaultMessage="Loading pattern analysis"
|
||||
/>
|
||||
</h2>
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
|
@ -46,7 +46,7 @@ export const LoadingCategorization: FC<Props> = ({ onClose }) => (
|
|||
<EuiFlexItem grow={false} css={{ textAlign: 'center' }}>
|
||||
<EuiButton
|
||||
data-test-subj="aiopsLoadingCategorizationCancelButton"
|
||||
onClick={() => onClose()}
|
||||
onClick={() => onCancel()}
|
||||
>
|
||||
Cancel
|
||||
</EuiButton>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export function getPatternAnalysisAvailable(licensing: LicensingPluginStart) {
|
||||
const lic = firstValueFrom(licensing.license$);
|
||||
return async (dataView: DataView) => {
|
||||
const isPlatinum = (await lic).hasAtLeast('platinum');
|
||||
return (
|
||||
isPlatinum &&
|
||||
dataView.isTimeBased() &&
|
||||
dataView.fields.some((f) => f.esTypes?.includes(ES_FIELD_TYPES.TEXT))
|
||||
);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/common';
|
||||
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { OpenInDiscover } from '../category_table/use_open_in_discover';
|
||||
import { EmbeddableMenu } from './embeddable_menu';
|
||||
import type { RandomSampler } from '../sampling_menu';
|
||||
import type { MinimumTimeRangeOption } from './minimum_time_range';
|
||||
import { SelectedPatterns } from './selected_patterns';
|
||||
import { CreateCategorizationJobButton } from '../create_categorization_job';
|
||||
import { SelectedField } from './field_selector';
|
||||
|
||||
interface Props {
|
||||
renderViewModeToggle: (patternCount?: number) => React.ReactElement;
|
||||
randomSampler: RandomSampler;
|
||||
openInDiscover: OpenInDiscover;
|
||||
selectedCategories: Category[];
|
||||
loadCategories: () => void;
|
||||
fields: DataViewField[];
|
||||
setSelectedField: React.Dispatch<React.SetStateAction<DataViewField | null>>;
|
||||
selectedField: DataViewField | null;
|
||||
minimumTimeRangeOption: MinimumTimeRangeOption;
|
||||
setMinimumTimeRangeOption: (w: MinimumTimeRangeOption) => void;
|
||||
dataview: DataView;
|
||||
earliest: number | undefined;
|
||||
latest: number | undefined;
|
||||
query: QueryDslQueryContainer;
|
||||
data: {
|
||||
categories: Category[];
|
||||
displayExamples: boolean;
|
||||
totalCategories: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const DiscoverTabs: FC<Props> = ({
|
||||
renderViewModeToggle,
|
||||
randomSampler,
|
||||
openInDiscover,
|
||||
selectedCategories,
|
||||
loadCategories,
|
||||
fields,
|
||||
setSelectedField,
|
||||
selectedField,
|
||||
minimumTimeRangeOption,
|
||||
setMinimumTimeRangeOption,
|
||||
data,
|
||||
dataview,
|
||||
earliest,
|
||||
latest,
|
||||
query,
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<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}
|
||||
<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">
|
||||
<CreateCategorizationJobButton
|
||||
dataView={dataview}
|
||||
field={selectedField}
|
||||
query={query}
|
||||
earliest={earliest}
|
||||
latest={latest}
|
||||
iconOnly={true}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 { EuiPopoverTitle, EuiSuperSelect, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
import type { FC } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { RandomSampler } from '../sampling_menu';
|
||||
import { SamplingPanel } from '../sampling_menu/sampling_panel';
|
||||
import type { MinimumTimeRangeOption } from './minimum_time_range';
|
||||
import { MINIMUM_TIME_RANGE } from './minimum_time_range';
|
||||
|
||||
interface Props {
|
||||
randomSampler: RandomSampler;
|
||||
minimumTimeRangeOption: MinimumTimeRangeOption;
|
||||
setMinimumTimeRangeOption: (w: MinimumTimeRangeOption) => void;
|
||||
categoryCount: number | undefined;
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
const minimumTimeRangeOptions = Object.keys(MINIMUM_TIME_RANGE).map((value) => ({
|
||||
inputDisplay: value,
|
||||
value: value as MinimumTimeRangeOption,
|
||||
}));
|
||||
|
||||
export const EmbeddableMenu: FC<Props> = ({
|
||||
randomSampler,
|
||||
minimumTimeRangeOption,
|
||||
setMinimumTimeRangeOption,
|
||||
categoryCount,
|
||||
reload,
|
||||
}) => {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const togglePopover = () => setShowMenu(!showMenu);
|
||||
|
||||
const button = (
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.aiops.logCategorization.embeddableMenu.tooltip', {
|
||||
defaultMessage: 'Options',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="aiopsEmbeddableMenuOptionsButton"
|
||||
size="s"
|
||||
iconType="controlsHorizontal"
|
||||
onClick={() => togglePopover()}
|
||||
// @ts-ignore - subdued does work
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.aiops.logCategorization.embeddableMenu.aria', {
|
||||
defaultMessage: 'Pattern analysis options',
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id={'embeddableMenu'}
|
||||
button={button}
|
||||
isOpen={showMenu}
|
||||
closePopover={() => togglePopover()}
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="downRight"
|
||||
>
|
||||
<EuiPanel color="transparent" paddingSize="s" css={{ maxWidth: '400px' }}>
|
||||
<EuiTitle size="xxxs">
|
||||
<EuiPopoverTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.embeddableMenu.patternAnalysisSettingsTitle"
|
||||
defaultMessage=" Pattern analysis settings"
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
</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>
|
||||
|
||||
<EuiHorizontalRule margin="m" />
|
||||
|
||||
<SamplingPanel randomSampler={randomSampler} reload={reload} calloutPosition="bottom" />
|
||||
</EuiPanel>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiDataGridToolbarControl,
|
||||
EuiPopover,
|
||||
EuiFormRow,
|
||||
EuiSuperSelect,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiToken,
|
||||
} from '@elastic/eui';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface Props {
|
||||
fields: DataViewField[];
|
||||
selectedField: DataViewField | null;
|
||||
setSelectedField: (field: DataViewField) => void;
|
||||
}
|
||||
|
||||
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"
|
||||
onClick={() => togglePopover()}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiToken iconType="tokenString" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{selectedField?.name}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDataGridToolbarControl>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
closePopover={() => setShowPopover(false)}
|
||||
isOpen={showPopover}
|
||||
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>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 { LogCategorizationEmbeddable } from './log_categorization_for_embeddable';
|
||||
export { LogCategorizationWrapper } from './log_categorization_wrapper';
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
* 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, useRef, 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 {
|
||||
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 { useOpenInDiscover } from '../category_table/use_open_in_discover';
|
||||
import { DiscoverTabs } from './discover_tabs';
|
||||
|
||||
export interface LogCategorizationEmbeddableProps {
|
||||
input: Readonly<EmbeddablePatternAnalysisInput>;
|
||||
renderViewModeToggle: (patternCount?: number) => React.ReactElement;
|
||||
}
|
||||
|
||||
const BAR_TARGET = 20;
|
||||
|
||||
export const LogCategorizationEmbeddable: 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 mounted = useRef(false);
|
||||
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);
|
||||
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;
|
||||
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() {
|
||||
mounted.current = true;
|
||||
return () => {
|
||||
mounted.current = false;
|
||||
cancelRequest();
|
||||
};
|
||||
},
|
||||
[cancelRequest, mounted]
|
||||
);
|
||||
|
||||
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) => {
|
||||
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]
|
||||
);
|
||||
|
||||
const openInDiscover = useOpenInDiscover(
|
||||
dataView.id!,
|
||||
selectedField ?? undefined,
|
||||
selectedCategories,
|
||||
stateFromUrl,
|
||||
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 ||
|
||||
timeField === undefined ||
|
||||
earliest === undefined ||
|
||||
latest === undefined ||
|
||||
minimumTimeRangeOption === undefined ||
|
||||
mounted.current !== true
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelRequest();
|
||||
|
||||
setLoading(true);
|
||||
setData(null);
|
||||
setFieldValidationResult(null);
|
||||
|
||||
const additionalFilter: CategorizationAdditionalFilter = {
|
||||
from: earliest,
|
||||
to: latest,
|
||||
};
|
||||
|
||||
try {
|
||||
const timeRange = await getMinimumTimeRange(
|
||||
index,
|
||||
timeField,
|
||||
additionalFilter,
|
||||
minimumTimeRangeOption,
|
||||
searchQuery
|
||||
);
|
||||
|
||||
if (mounted.current !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [validationResult, categorizationResult] = await Promise.all([
|
||||
runValidateFieldRequest(index, selectedField.name, timeField, timeRange, searchQuery, {
|
||||
[AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN]: embeddingOrigin,
|
||||
}),
|
||||
runCategorizeRequest(
|
||||
index,
|
||||
selectedField.name,
|
||||
timeField,
|
||||
{ to: timeRange.to, from: timeRange.from },
|
||||
searchQuery,
|
||||
intervalMs,
|
||||
timeRange.useSubAgg ? additionalFilter : undefined
|
||||
),
|
||||
]);
|
||||
|
||||
if (mounted.current !== true) {
|
||||
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') {
|
||||
// ignore error
|
||||
} else {
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.aiops.logCategorization.errorLoadingCategories', {
|
||||
defaultMessage: 'Error loading categories',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted.current === true) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
dataView,
|
||||
loading,
|
||||
selectedField,
|
||||
earliest,
|
||||
latest,
|
||||
minimumTimeRangeOption,
|
||||
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();
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[input.lastReloadRequestTime]
|
||||
);
|
||||
const style = css({
|
||||
overflowY: 'auto',
|
||||
'.kbnDocTableWrapper': {
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
});
|
||||
|
||||
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}
|
||||
pinnedCategory={pinnedCategory}
|
||||
setPinnedCategory={setPinnedCategory}
|
||||
highlightedCategory={highlightedCategory}
|
||||
setHighlightedCategory={setHighlightedCategory}
|
||||
enableRowActions={false}
|
||||
displayExamples={data.displayExamples}
|
||||
setSelectedCategories={setSelectedCategories}
|
||||
openInDiscover={openInDiscover}
|
||||
tableState={tableState}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default LogCategorizationEmbeddable;
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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, { Suspense } from 'react';
|
||||
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { IUiSettingsClient } from '@kbn/core/public';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
|
||||
import { StorageContextProvider } from '@kbn/ml-local-storage';
|
||||
|
||||
import { pick } from 'lodash';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
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';
|
||||
|
||||
export interface EmbeddableLogCategorizationDeps {
|
||||
theme: ThemeServiceStart;
|
||||
data: DataPublicPluginStart;
|
||||
uiSettings: IUiSettingsClient;
|
||||
http: CoreStart['http'];
|
||||
notifications: CoreStart['notifications'];
|
||||
i18n: CoreStart['i18n'];
|
||||
lens: LensPublicStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
application: CoreStart['application'];
|
||||
charts: ChartsPluginStart;
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
export interface LogCategorizationEmbeddableWrapperProps {
|
||||
deps: EmbeddableLogCategorizationDeps;
|
||||
props: LogCategorizationEmbeddableProps;
|
||||
embeddingOrigin?: string;
|
||||
}
|
||||
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
|
||||
export const LogCategorizationWrapper: FC<LogCategorizationEmbeddableWrapperProps> = ({
|
||||
deps,
|
||||
props,
|
||||
embeddingOrigin,
|
||||
}) => {
|
||||
const I18nContext = deps.i18n.Context;
|
||||
const aiopsAppContextValue = {
|
||||
embeddingOrigin,
|
||||
...deps,
|
||||
} as unknown as AiopsAppDependencies;
|
||||
|
||||
const datePickerDeps = {
|
||||
...pick(deps, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']),
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nContext>
|
||||
<AiopsAppContext.Provider value={aiopsAppContextValue}>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<Suspense fallback={null}>
|
||||
<LogCategorizationEmbeddable
|
||||
input={props.input}
|
||||
renderViewModeToggle={props.renderViewModeToggle}
|
||||
/>
|
||||
</Suspense>
|
||||
</StorageContextProvider>
|
||||
</DatePickerContextProvider>
|
||||
</AiopsAppContext.Provider>
|
||||
</I18nContext>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default LogCategorizationWrapper;
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { 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 }>;
|
||||
|
||||
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' },
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiDataGridToolbarControl,
|
||||
EuiPopover,
|
||||
EuiContextMenuPanel,
|
||||
EuiContextMenuItem,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
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 [showMenu, setShowMenu] = useState(false);
|
||||
const togglePopover = () => setShowMenu(!showMenu);
|
||||
|
||||
const button = (
|
||||
<EuiDataGridToolbarControl
|
||||
data-test-subj="aiopsEmbeddableMenuOptionsButton"
|
||||
iconType="documents"
|
||||
onClick={() => togglePopover()}
|
||||
badgeContent={openInDiscover.count}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.selectedResultsButtonLabel"
|
||||
defaultMessage="Selected"
|
||||
description="Selected results"
|
||||
/>
|
||||
</EuiDataGridToolbarControl>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
closePopover={() => setShowMenu(false)}
|
||||
isOpen={showMenu}
|
||||
panelPaddingSize="none"
|
||||
button={button}
|
||||
className="unifiedDataTableToolbarControlButton"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
items={[
|
||||
<EuiContextMenuItem
|
||||
key="in"
|
||||
icon="plusInCircle"
|
||||
onClick={() => openFunction(QUERY_MODE.INCLUDE)}
|
||||
>
|
||||
{labels.multiSelect.in}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="out"
|
||||
icon="minusInCircle"
|
||||
onClick={() => openFunction(QUERY_MODE.EXCLUDE)}
|
||||
>
|
||||
{labels.multiSelect.out}
|
||||
</EuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { useRef, useCallback } from 'react';
|
||||
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { HttpFetchOptions } from '@kbn/core/public';
|
||||
import { getTimeFieldRange } from '@kbn/ml-date-picker';
|
||||
import moment from 'moment';
|
||||
import { useStorage } from '@kbn/ml-local-storage';
|
||||
import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context';
|
||||
import type { MinimumTimeRangeOption } from './minimum_time_range';
|
||||
import { MINIMUM_TIME_RANGE } from './minimum_time_range';
|
||||
import type { AiOpsKey, AiOpsStorageMapped } from '../../../types/storage';
|
||||
import { AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE } from '../../../types/storage';
|
||||
|
||||
export function useMinimumTimeRange() {
|
||||
const { http } = useAiopsAppContext();
|
||||
const abortController = useRef(new AbortController());
|
||||
|
||||
const getMinimumTimeRange = useCallback(
|
||||
async (
|
||||
index: string,
|
||||
timeField: string,
|
||||
timeRange: { from: number; to: number },
|
||||
minimumTimeRangeOption: MinimumTimeRangeOption,
|
||||
queryIn: QueryDslQueryContainer,
|
||||
headers?: HttpFetchOptions['headers']
|
||||
) => {
|
||||
const minimumTimeRange = MINIMUM_TIME_RANGE[minimumTimeRangeOption];
|
||||
const minimumTimeRangeMs = moment
|
||||
.duration(minimumTimeRange.factor, minimumTimeRange.unit)
|
||||
.asMilliseconds();
|
||||
const currentMinimumTimeRange = timeRange.to - timeRange.from;
|
||||
|
||||
// the time range is already wide enough
|
||||
if (currentMinimumTimeRange > minimumTimeRangeMs) {
|
||||
return { ...timeRange, useSubAgg: false };
|
||||
}
|
||||
|
||||
const resp = await getTimeFieldRange({
|
||||
http,
|
||||
index,
|
||||
timeFieldName: timeField,
|
||||
query: queryIn,
|
||||
path: '/internal/file_upload/time_field_range',
|
||||
signal: abortController.current.signal,
|
||||
});
|
||||
|
||||
// the index isn't big enough to get a wider time range
|
||||
const indexTimeRangeMs = resp.end.epoch - resp.start.epoch;
|
||||
if (indexTimeRangeMs < minimumTimeRangeMs) {
|
||||
return {
|
||||
from: resp.start.epoch,
|
||||
to: resp.end.epoch,
|
||||
useSubAgg: true,
|
||||
};
|
||||
}
|
||||
|
||||
const remainder = minimumTimeRangeMs - currentMinimumTimeRange;
|
||||
const newFrom = Math.max(timeRange.from - remainder, resp.start.epoch);
|
||||
const newTo = Math.min(newFrom + minimumTimeRangeMs, resp.end.epoch);
|
||||
|
||||
return {
|
||||
from: newFrom,
|
||||
to: newTo,
|
||||
useSubAgg: true,
|
||||
};
|
||||
},
|
||||
[http]
|
||||
);
|
||||
|
||||
const [minimumTimeRangeOption, setMinimumTimeRangeOption] = useStorage<
|
||||
AiOpsKey,
|
||||
AiOpsStorageMapped<typeof AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE>
|
||||
>(AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE, '1 week');
|
||||
|
||||
const cancelRequest = useCallback(() => {
|
||||
abortController.current.abort();
|
||||
abortController.current = new AbortController();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
getMinimumTimeRange,
|
||||
cancelRequest,
|
||||
minimumTimeRangeOption,
|
||||
setMinimumTimeRangeOption,
|
||||
};
|
||||
}
|
|
@ -19,20 +19,20 @@ import {
|
|||
EuiSpacer,
|
||||
EuiToolTip,
|
||||
EuiIcon,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
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 { AIOPS_TELEMETRY_ID } from '@kbn/aiops-common/constants';
|
||||
import type { CategorizationAdditionalFilter } from '@kbn/aiops-log-pattern-analysis/create_category_request';
|
||||
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
|
||||
|
||||
import { useTableState } from '@kbn/ml-in-memory-table/hooks/use_table_state';
|
||||
import {
|
||||
type LogCategorizationPageUrlState,
|
||||
getDefaultLogCategorizationAppState,
|
||||
|
@ -51,6 +51,8 @@ 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';
|
||||
|
||||
enum SELECTED_TAB {
|
||||
BUCKET,
|
||||
|
@ -80,7 +82,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
|
|||
const {
|
||||
notifications: { toasts },
|
||||
data: {
|
||||
query: { getState, filterManager },
|
||||
query: { getState },
|
||||
},
|
||||
uiSettings,
|
||||
} = useAiopsAppContext();
|
||||
|
@ -102,7 +104,8 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
|
|||
searchQuery: createMergedEsQuery(query, filters, dataView, uiSettings),
|
||||
})
|
||||
);
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null);
|
||||
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>([]);
|
||||
|
@ -117,6 +120,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
|
|||
);
|
||||
const [showTabs, setShowTabs] = useState<boolean>(false);
|
||||
const [selectedTab, setSelectedTab] = useState<SELECTED_TAB>(SELECTED_TAB.FULL_TIME_RANGE);
|
||||
const tableState = useTableState<Category>([], 'key');
|
||||
|
||||
const cancelRequest = useCallback(() => {
|
||||
cancelValidationRequest();
|
||||
|
@ -150,6 +154,17 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
|
|||
BAR_TARGET
|
||||
);
|
||||
|
||||
const openInDiscover = useOpenInDiscover(
|
||||
dataView.id!,
|
||||
selectedField,
|
||||
selectedCategories,
|
||||
stateFromUrl,
|
||||
timefilter,
|
||||
true,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
const loadCategories = useCallback(async () => {
|
||||
const { getIndexPattern, timeFieldName: timeField } = dataView;
|
||||
const index = getIndexPattern();
|
||||
|
@ -243,18 +258,6 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
|
|||
toasts,
|
||||
]);
|
||||
|
||||
const onAddFilter = useCallback(
|
||||
(values: Filter, alias?: string) => {
|
||||
const filter = buildEmptyFilter(false, dataView.id);
|
||||
if (alias) {
|
||||
filter.meta.alias = alias;
|
||||
}
|
||||
filter.query = values.query;
|
||||
filterManager.addFilters([filter]);
|
||||
},
|
||||
[dataView, filterManager]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (documentStats.documentCountStats?.buckets) {
|
||||
randomSampler.setDocCount(documentStats.totalCount);
|
||||
|
@ -312,12 +315,11 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
|
|||
/>
|
||||
) : null}
|
||||
<FieldValidationCallout validationResults={fieldValidationResult} />
|
||||
{loading === true ? <LoadingCategorization onClose={onClose} /> : null}
|
||||
{loading === true ? <LoadingCategorization onCancel={onClose} /> : null}
|
||||
<InformationText
|
||||
loading={loading}
|
||||
categoriesLength={data?.categories?.length ?? null}
|
||||
eventRateLength={eventRate.length}
|
||||
fieldSelected={selectedField !== null}
|
||||
/>
|
||||
{loading === false && data !== null && data.categories.length > 0 ? (
|
||||
<>
|
||||
|
@ -388,31 +390,31 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
|
|||
</>
|
||||
) : null}
|
||||
|
||||
<TableHeader
|
||||
categoriesCount={data.categories.length}
|
||||
selectedCategoriesCount={selectedCategories.length}
|
||||
openInDiscover={openInDiscover}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
<CategoryTable
|
||||
categories={
|
||||
selectedTab === SELECTED_TAB.BUCKET && data.categoriesInBucket !== null
|
||||
? data.categoriesInBucket
|
||||
: data.categories
|
||||
}
|
||||
aiopsListState={stateFromUrl}
|
||||
dataViewId={dataView.id!}
|
||||
eventRate={eventRate}
|
||||
selectedField={selectedField}
|
||||
pinnedCategory={pinnedCategory}
|
||||
setPinnedCategory={setPinnedCategory}
|
||||
selectedCategory={selectedCategory}
|
||||
setSelectedCategory={setSelectedCategory}
|
||||
timefilter={timefilter}
|
||||
onAddFilter={onAddFilter}
|
||||
onClose={onClose}
|
||||
highlightedCategory={highlightedCategory}
|
||||
setHighlightedCategory={setHighlightedCategory}
|
||||
enableRowActions={false}
|
||||
additionalFilter={
|
||||
selectedTab === SELECTED_TAB.BUCKET && additionalFilter !== undefined
|
||||
? additionalFilter
|
||||
: undefined
|
||||
}
|
||||
navigateToDiscover={additionalFilter !== undefined}
|
||||
displayExamples={data.displayExamples}
|
||||
setSelectedCategories={setSelectedCategories}
|
||||
openInDiscover={openInDiscover}
|
||||
tableState={tableState}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
|
@ -27,10 +28,10 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { usePageUrlState, useUrlState } from '@kbn/ml-url-state';
|
||||
import type { FieldValidationResults } from '@kbn/ml-category-validator';
|
||||
import type { SearchQueryLanguage } from '@kbn/ml-query-utils';
|
||||
import { stringHash } from '@kbn/ml-string-hash';
|
||||
import { AIOPS_TELEMETRY_ID } from '@kbn/aiops-common/constants';
|
||||
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
|
||||
|
||||
import { useTableState } from '@kbn/ml-in-memory-table/hooks/use_table_state';
|
||||
import { useDataSource } from '../../hooks/use_data_source';
|
||||
import { useData } from '../../hooks/use_data';
|
||||
import { useSearch } from '../../hooks/use_search';
|
||||
|
@ -51,7 +52,9 @@ import { InformationText } from './information_text';
|
|||
import { SamplingMenu } from './sampling_menu';
|
||||
import { useValidateFieldRequest } from './use_validate_category_field';
|
||||
import { FieldValidationCallout } from './category_validation_callout';
|
||||
import type { DocumentStats } from '../../hooks/use_document_count_stats';
|
||||
import { createDocumentStatsHash } from './utils';
|
||||
import { TableHeader } from './category_table/table_header';
|
||||
import { useOpenInDiscover } from './category_table/use_open_in_discover';
|
||||
|
||||
const BAR_TARGET = 20;
|
||||
const DEFAULT_SELECTED_FIELD = 'message';
|
||||
|
@ -80,7 +83,8 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
|
|||
);
|
||||
const [globalState, setGlobalState] = useUrlState('_g');
|
||||
const [selectedField, setSelectedField] = useState<string | undefined>();
|
||||
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null);
|
||||
const [highlightedCategory, setHighlightedCategory] = useState<Category | null>(null);
|
||||
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
|
||||
const [selectedSavedSearch, setSelectedSavedSearch] = useState(savedSearch);
|
||||
const [previousDocumentStatsHash, setPreviousDocumentStatsHash] = useState<number>(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
@ -94,6 +98,7 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
|
|||
const [fieldValidationResult, setFieldValidationResult] = useState<FieldValidationResults | null>(
|
||||
null
|
||||
);
|
||||
const tableState = useTableState<Category>([], 'key');
|
||||
|
||||
const cancelRequest = useCallback(() => {
|
||||
cancelValidationRequest();
|
||||
|
@ -154,6 +159,17 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
|
|||
BAR_TARGET
|
||||
);
|
||||
|
||||
const openInDiscover = useOpenInDiscover(
|
||||
dataView.id!,
|
||||
selectedField,
|
||||
selectedCategories,
|
||||
stateFromUrl,
|
||||
timefilter,
|
||||
true,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (globalState?.time !== undefined) {
|
||||
timefilter.setTime({
|
||||
|
@ -253,8 +269,6 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
|
|||
docCount,
|
||||
}))
|
||||
);
|
||||
setData(null);
|
||||
setFieldValidationResult(null);
|
||||
setTotalCount(documentStats.totalCount);
|
||||
if (fieldValidationResult !== null) {
|
||||
loadCategories();
|
||||
|
@ -371,7 +385,7 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
|
|||
<DocumentCountChart
|
||||
eventRate={eventRate}
|
||||
pinnedCategory={pinnedCategory}
|
||||
selectedCategory={selectedCategory}
|
||||
selectedCategory={highlightedCategory}
|
||||
totalCount={totalCount}
|
||||
documentCountStats={documentStats.documentCountStats}
|
||||
/>
|
||||
|
@ -387,36 +401,33 @@ export const LogCategorizationPage: FC<LogCategorizationPageProps> = ({ embeddin
|
|||
loading={loading}
|
||||
categoriesLength={data?.categories?.length ?? null}
|
||||
eventRateLength={eventRate.length}
|
||||
fieldSelected={selectedField !== null}
|
||||
/>
|
||||
|
||||
{selectedField !== undefined && data !== null && data.categories.length > 0 ? (
|
||||
<CategoryTable
|
||||
categories={data.categories}
|
||||
aiopsListState={stateFromUrl}
|
||||
dataViewId={dataView.id!}
|
||||
eventRate={eventRate}
|
||||
selectedField={selectedField}
|
||||
pinnedCategory={pinnedCategory}
|
||||
setPinnedCategory={setPinnedCategory}
|
||||
selectedCategory={selectedCategory}
|
||||
setSelectedCategory={setSelectedCategory}
|
||||
timefilter={timefilter}
|
||||
displayExamples={data.displayExamples}
|
||||
/>
|
||||
<>
|
||||
<TableHeader
|
||||
categoriesCount={data.categories.length}
|
||||
selectedCategoriesCount={selectedCategories.length}
|
||||
openInDiscover={openInDiscover}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
<CategoryTable
|
||||
categories={data.categories}
|
||||
eventRate={eventRate}
|
||||
pinnedCategory={pinnedCategory}
|
||||
setPinnedCategory={setPinnedCategory}
|
||||
highlightedCategory={highlightedCategory}
|
||||
setHighlightedCategory={setHighlightedCategory}
|
||||
displayExamples={data.displayExamples}
|
||||
setSelectedCategories={setSelectedCategories}
|
||||
openInDiscover={openInDiscover}
|
||||
tableState={tableState}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a hash from the document stats to determine if the document stats have changed.
|
||||
*/
|
||||
function createDocumentStatsHash(documentStats: DocumentStats) {
|
||||
const lastTimeStampMs = documentStats.documentCountStats?.lastDocTimeStampMs;
|
||||
const totalCount = documentStats.documentCountStats?.totalCount;
|
||||
const times = Object.keys(documentStats.documentCountStats?.buckets ?? {});
|
||||
const firstBucketTimeStamp = times.length ? times[0] : undefined;
|
||||
const lastBucketTimeStamp = times.length ? times[times.length - 1] : undefined;
|
||||
return stringHash(`${lastTimeStampMs}${totalCount}${firstBucketTimeStamp}${lastBucketTimeStamp}`);
|
||||
}
|
||||
|
|
|
@ -28,27 +28,33 @@ export type RandomSamplerProbability = number | null;
|
|||
|
||||
export const RANDOM_SAMPLER_SELECT_OPTIONS: Array<{
|
||||
value: RandomSamplerOption;
|
||||
text: string;
|
||||
inputDisplay: string;
|
||||
'data-test-subj': string;
|
||||
}> = [
|
||||
{
|
||||
'data-test-subj': 'aiopsRandomSamplerOptionOnAutomatic',
|
||||
value: RANDOM_SAMPLER_OPTION.ON_AUTOMATIC,
|
||||
text: i18n.translate('xpack.aiops.logCategorization.randomSamplerPreference.onAutomaticLabel', {
|
||||
defaultMessage: 'On - automatic',
|
||||
}),
|
||||
inputDisplay: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerPreference.onAutomaticLabel',
|
||||
{
|
||||
defaultMessage: 'On - automatic',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'aiopsRandomSamplerOptionOnManual',
|
||||
value: RANDOM_SAMPLER_OPTION.ON_MANUAL,
|
||||
text: i18n.translate('xpack.aiops.logCategorization.randomSamplerPreference.onManualLabel', {
|
||||
defaultMessage: 'On - manual',
|
||||
}),
|
||||
inputDisplay: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerPreference.onManualLabel',
|
||||
{
|
||||
defaultMessage: 'On - manual',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
'data-test-subj': 'aiopsRandomSamplerOptionOff',
|
||||
value: RANDOM_SAMPLER_OPTION.OFF,
|
||||
text: i18n.translate('xpack.aiops.logCategorization.randomSamplerPreference.offLabel', {
|
||||
inputDisplay: i18n.translate('xpack.aiops.logCategorization.randomSamplerPreference.offLabel', {
|
||||
defaultMessage: 'Off',
|
||||
}),
|
||||
},
|
||||
|
@ -126,3 +132,58 @@ export class RandomSampler {
|
|||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
export const randomSamplerText = (randomSamplerPreference: RandomSamplerOption) => {
|
||||
switch (randomSamplerPreference) {
|
||||
case RANDOM_SAMPLER_OPTION.OFF:
|
||||
return {
|
||||
calloutInfoMessage: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.offCallout.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'Random sampling can be turned on to increase analysis speed. Accuracy will slightly decrease.',
|
||||
}
|
||||
),
|
||||
buttonText: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.offCallout.button',
|
||||
{
|
||||
defaultMessage: 'No sampling',
|
||||
}
|
||||
),
|
||||
};
|
||||
case RANDOM_SAMPLER_OPTION.ON_AUTOMATIC:
|
||||
return {
|
||||
calloutInfoMessage: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.onAutomaticCallout.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'The pattern analysis will use random sampler aggregations. The probability is automatically set to balance accuracy and speed.',
|
||||
}
|
||||
),
|
||||
buttonText: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.onAutomaticCallout.button',
|
||||
{
|
||||
defaultMessage: 'Auto sampling',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
case RANDOM_SAMPLER_OPTION.ON_MANUAL:
|
||||
default:
|
||||
return {
|
||||
calloutInfoMessage: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.onManualCallout.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'The pattern analysis will use random sampler aggregations. A lower percentage probability increases performance, but some accuracy is lost.',
|
||||
}
|
||||
),
|
||||
buttonText: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.onManualCallout.button',
|
||||
{
|
||||
defaultMessage: 'Manual sampling',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,24 +6,14 @@
|
|||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiCallOut,
|
||||
EuiSelect,
|
||||
EuiFormRow,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { useMemo } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiPopover, EuiButtonEmpty, EuiPanel } from '@elastic/eui';
|
||||
|
||||
import { RandomSamplerRangeSlider } from './random_sampler_range_slider';
|
||||
import type { RandomSampler, RandomSamplerOption } from './random_sampler';
|
||||
import { RANDOM_SAMPLER_OPTION, RANDOM_SAMPLER_SELECT_OPTIONS } from './random_sampler';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import type { RandomSampler } from './random_sampler';
|
||||
import { randomSamplerText } from './random_sampler';
|
||||
import { SamplingPanel } from './sampling_panel';
|
||||
|
||||
interface Props {
|
||||
randomSampler: RandomSampler;
|
||||
|
@ -33,82 +23,12 @@ interface Props {
|
|||
export const SamplingMenu: FC<Props> = ({ randomSampler, reload }) => {
|
||||
const [showSamplingOptionsPopover, setShowSamplingOptionsPopover] = useState(false);
|
||||
|
||||
const samplingProbability = useObservable(
|
||||
randomSampler.getProbability$(),
|
||||
randomSampler.getProbability()
|
||||
);
|
||||
const setSamplingProbability = useCallback(
|
||||
(probability: number | null) => {
|
||||
randomSampler.setProbability(probability);
|
||||
reload();
|
||||
},
|
||||
[reload, randomSampler]
|
||||
);
|
||||
|
||||
const randomSamplerPreference = useObservable(randomSampler.getMode$(), randomSampler.getMode());
|
||||
const setRandomSamplerPreference = useCallback(
|
||||
(mode: RandomSamplerOption) => {
|
||||
randomSampler.setMode(mode);
|
||||
reload();
|
||||
},
|
||||
[randomSampler, reload]
|
||||
const { buttonText } = useMemo(
|
||||
() => randomSamplerText(randomSamplerPreference),
|
||||
[randomSamplerPreference]
|
||||
);
|
||||
|
||||
const { calloutInfoMessage, buttonText } = useMemo(() => {
|
||||
switch (randomSamplerPreference) {
|
||||
case RANDOM_SAMPLER_OPTION.OFF:
|
||||
return {
|
||||
calloutInfoMessage: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.offCallout.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'Random sampling can be turned on to increase the speed of analysis, although some accuracy will be lost.',
|
||||
}
|
||||
),
|
||||
buttonText: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.offCallout.button',
|
||||
{
|
||||
defaultMessage: 'No sampling',
|
||||
}
|
||||
),
|
||||
};
|
||||
case RANDOM_SAMPLER_OPTION.ON_AUTOMATIC:
|
||||
return {
|
||||
calloutInfoMessage: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.onAutomaticCallout.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'The pattern analysis will use random sampler aggregations. The probability is automatically set to balance accuracy and speed.',
|
||||
}
|
||||
),
|
||||
buttonText: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.onAutomaticCallout.button',
|
||||
{
|
||||
defaultMessage: 'Auto sampling',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
case RANDOM_SAMPLER_OPTION.ON_MANUAL:
|
||||
default:
|
||||
return {
|
||||
calloutInfoMessage: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.onManualCallout.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'The pattern analysis will use random sampler aggregations. A lower percentage probability increases performance, but some accuracy is lost.',
|
||||
}
|
||||
),
|
||||
buttonText: i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.onManualCallout.button',
|
||||
{
|
||||
defaultMessage: 'Manual sampling',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
}, [randomSamplerPreference]);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
data-test-subj="aiopsRandomSamplerOptionsPopover"
|
||||
|
@ -129,55 +49,8 @@ export const SamplingMenu: FC<Props> = ({ randomSampler, reload }) => {
|
|||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiPanel style={{ maxWidth: 400 }}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiCallOut size="s" color={'primary'} title={calloutInfoMessage} />
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFormRow
|
||||
data-test-subj="aiopsRandomSamplerOptionsFormRow"
|
||||
label={i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.randomSamplerRowLabel',
|
||||
{
|
||||
defaultMessage: 'Random sampling',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiSelect
|
||||
data-test-subj="aiopsRandomSamplerOptionsSelect"
|
||||
options={RANDOM_SAMPLER_SELECT_OPTIONS}
|
||||
value={randomSamplerPreference}
|
||||
onChange={(e) => setRandomSamplerPreference(e.target.value as RandomSamplerOption)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_MANUAL ? (
|
||||
<RandomSamplerRangeSlider
|
||||
samplingProbability={samplingProbability}
|
||||
setSamplingProbability={setSamplingProbability}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_AUTOMATIC ? (
|
||||
<ProbabilityUsedMessage samplingProbability={samplingProbability} />
|
||||
) : null}
|
||||
<SamplingPanel randomSampler={randomSampler} reload={reload} />
|
||||
</EuiPanel>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
const ProbabilityUsedMessage: FC<{ samplingProbability: number | null }> = ({
|
||||
samplingProbability,
|
||||
}) => {
|
||||
return samplingProbability !== null ? (
|
||||
<div data-test-subj="aiopsRandomSamplerProbabilityUsedMsg">
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.randomSamplerSettingsPopUp.probabilityLabel"
|
||||
defaultMessage="Probability used: {samplingProbability}%"
|
||||
values={{ samplingProbability: samplingProbability * 100 }}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import { EuiSpacer, EuiCallOut, EuiFormRow, EuiSuperSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import { RandomSamplerRangeSlider } from './random_sampler_range_slider';
|
||||
import type { RandomSampler, RandomSamplerOption } from './random_sampler';
|
||||
import { randomSamplerText } from './random_sampler';
|
||||
import { RANDOM_SAMPLER_OPTION, RANDOM_SAMPLER_SELECT_OPTIONS } from './random_sampler';
|
||||
|
||||
interface Props {
|
||||
randomSampler: RandomSampler;
|
||||
calloutPosition?: 'top' | 'bottom';
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
export const SamplingPanel: FC<Props> = ({ randomSampler, reload, calloutPosition = 'top' }) => {
|
||||
const samplingProbability = useObservable(
|
||||
randomSampler.getProbability$(),
|
||||
randomSampler.getProbability()
|
||||
);
|
||||
const setSamplingProbability = useCallback(
|
||||
(probability: number | null) => {
|
||||
randomSampler.setProbability(probability);
|
||||
reload();
|
||||
},
|
||||
[reload, randomSampler]
|
||||
);
|
||||
|
||||
const randomSamplerPreference = useObservable(randomSampler.getMode$(), randomSampler.getMode());
|
||||
const setRandomSamplerPreference = useCallback(
|
||||
(mode: RandomSamplerOption) => {
|
||||
randomSampler.setMode(mode);
|
||||
reload();
|
||||
},
|
||||
[randomSampler, reload]
|
||||
);
|
||||
|
||||
const { calloutInfoMessage } = useMemo(
|
||||
() => randomSamplerText(randomSamplerPreference),
|
||||
[randomSamplerPreference]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{calloutPosition === 'top' ? (
|
||||
<CalloutInfoMessage
|
||||
calloutInfoMessage={calloutInfoMessage}
|
||||
calloutPosition={calloutPosition}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<EuiFormRow
|
||||
data-test-subj="aiopsRandomSamplerOptionsFormRow"
|
||||
label={i18n.translate(
|
||||
'xpack.aiops.logCategorization.randomSamplerSettingsPopUp.randomSamplerRowLabel',
|
||||
{
|
||||
defaultMessage: 'Random sampling',
|
||||
}
|
||||
)}
|
||||
helpText={
|
||||
randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_AUTOMATIC ? (
|
||||
<ProbabilityUsedMessage samplingProbability={samplingProbability} />
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="aiopsRandomSamplerOptionsSelect"
|
||||
options={RANDOM_SAMPLER_SELECT_OPTIONS}
|
||||
valueOfSelected={randomSamplerPreference}
|
||||
onChange={setRandomSamplerPreference}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{randomSamplerPreference === RANDOM_SAMPLER_OPTION.ON_MANUAL ? (
|
||||
<RandomSamplerRangeSlider
|
||||
samplingProbability={samplingProbability}
|
||||
setSamplingProbability={setSamplingProbability}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{calloutPosition === 'bottom' ? (
|
||||
<CalloutInfoMessage
|
||||
calloutInfoMessage={calloutInfoMessage}
|
||||
calloutPosition={calloutPosition}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ProbabilityUsedMessage: FC<{ samplingProbability: number | null }> = ({
|
||||
samplingProbability,
|
||||
}) => {
|
||||
return samplingProbability !== null ? (
|
||||
<div data-test-subj="aiopsRandomSamplerProbabilityUsedMsg">
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.logCategorization.randomSamplerSettingsPopUp.probabilityLabel"
|
||||
defaultMessage="Probability used: {samplingProbability}%"
|
||||
values={{ samplingProbability: Number((samplingProbability * 100).toPrecision(3)) }}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const CalloutInfoMessage: FC<{
|
||||
calloutInfoMessage: string;
|
||||
calloutPosition: 'top' | 'bottom';
|
||||
}> = ({ calloutInfoMessage, calloutPosition }) => (
|
||||
<>
|
||||
{calloutPosition === 'bottom' ? <EuiSpacer size="s" /> : null}
|
||||
<EuiCallOut size="s" color={'primary'} title={calloutInfoMessage} />
|
||||
{calloutPosition === 'top' ? <EuiSpacer size="s" /> : null}
|
||||
</>
|
||||
);
|
|
@ -85,7 +85,9 @@ export function useCategorizeRequest() {
|
|||
query,
|
||||
wrap,
|
||||
intervalMs,
|
||||
additionalFilter
|
||||
additionalFilter,
|
||||
true,
|
||||
additionalFilter === undefined // don't include the outer sparkline if there is an additional filter
|
||||
),
|
||||
{ abortSignal: abortController.current.signal }
|
||||
)
|
||||
|
|
|
@ -51,6 +51,7 @@ export function useValidateFieldRequest() {
|
|||
}),
|
||||
headers,
|
||||
version: '1',
|
||||
signal: abortController.current.signal,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { stringHash } from '@kbn/ml-string-hash';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { ES_FIELD_TYPES } from '@kbn/field-types';
|
||||
import type { DocumentStats } from '../../hooks/use_document_count_stats';
|
||||
|
||||
/**
|
||||
* Creates a hash from the document stats to determine if the document stats have changed.
|
||||
*/
|
||||
export function createDocumentStatsHash(documentStats: DocumentStats) {
|
||||
const lastTimeStampMs = documentStats.documentCountStats?.lastDocTimeStampMs;
|
||||
const totalCount = documentStats.documentCountStats?.totalCount;
|
||||
const times = Object.keys(documentStats.documentCountStats?.buckets ?? {});
|
||||
const firstBucketTimeStamp = times.length ? times[0] : undefined;
|
||||
const lastBucketTimeStamp = times.length ? times[times.length - 1] : undefined;
|
||||
|
||||
return stringHash(`${lastTimeStampMs}${totalCount}${firstBucketTimeStamp}${lastBucketTimeStamp}`);
|
||||
}
|
||||
|
||||
export function createAdditionalConfigHash(additionalStrings: string[] = []) {
|
||||
return stringHash(`${additionalStrings.join('')}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the message field from a DataView object.
|
||||
* If the message field is not found, it falls back to error.message or event.original or the first text field in the DataView.
|
||||
*
|
||||
* @param dataView - The DataView object containing the fields.
|
||||
* @returns An object containing the message field and all the fields in the DataView.
|
||||
*/
|
||||
export function getMessageField(dataView: DataView): {
|
||||
messageField: DataViewField | null;
|
||||
dataViewFields: DataViewField[];
|
||||
} {
|
||||
const dataViewFields = dataView.fields.filter((f) => f.esTypes?.includes(ES_FIELD_TYPES.TEXT));
|
||||
|
||||
let messageField: DataViewField | null | undefined = dataViewFields.find(
|
||||
(f) => f.name === 'message'
|
||||
);
|
||||
if (messageField === undefined) {
|
||||
messageField = dataViewFields.find((f) => f.name === 'error.message');
|
||||
}
|
||||
if (messageField === undefined) {
|
||||
messageField = dataViewFields.find((f) => f.name === 'event.original');
|
||||
}
|
||||
if (messageField === undefined) {
|
||||
if (dataViewFields.length > 0) {
|
||||
messageField = dataViewFields[0];
|
||||
} else {
|
||||
messageField = null;
|
||||
}
|
||||
}
|
||||
return { messageField, dataViewFields };
|
||||
}
|
|
@ -152,6 +152,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
|
|||
currentSelectedSignificantItem,
|
||||
currentSelectedGroup,
|
||||
undefined,
|
||||
true,
|
||||
timeRange
|
||||
);
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ export const useData = (
|
|||
selectedSignificantItem?: SignificantItem,
|
||||
selectedGroup: GroupTableItem | null = null,
|
||||
barTarget: number = DEFAULT_BAR_TARGET,
|
||||
changePointsByDefault = true,
|
||||
timeRange?: { min: Moment; max: Moment }
|
||||
) => {
|
||||
const { executionContext, uiSettings } = useAiopsAppContext();
|
||||
|
@ -103,7 +104,8 @@ export const useData = (
|
|||
const documentStats = useDocumentCountStats(
|
||||
overallStatsRequest,
|
||||
selectedSignificantItemStatsRequest,
|
||||
lastRefresh
|
||||
lastRefresh,
|
||||
changePointsByDefault
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -111,12 +113,15 @@ export const useData = (
|
|||
timefilter.getAutoRefreshFetch$(),
|
||||
timefilter.getTimeUpdate$(),
|
||||
mlTimefilterRefresh$
|
||||
).subscribe(() => {
|
||||
).subscribe((done) => {
|
||||
if (onUpdate) {
|
||||
onUpdate({
|
||||
time: timefilter.getTime(),
|
||||
refreshInterval: timefilter.getRefreshInterval(),
|
||||
});
|
||||
if (typeof done === 'function') {
|
||||
done();
|
||||
}
|
||||
}
|
||||
setLastRefresh(Date.now());
|
||||
});
|
||||
|
|
|
@ -56,7 +56,8 @@ function displayError(toastNotifications: ToastsStart, index: string, err: any)
|
|||
export function useDocumentCountStats<TParams extends DocumentStatsSearchStrategyParams>(
|
||||
searchParams: TParams | undefined,
|
||||
searchParamsCompare: TParams | undefined,
|
||||
lastRefresh: number
|
||||
lastRefresh: number,
|
||||
changePointsByDefault = true
|
||||
): DocumentStats {
|
||||
const {
|
||||
data,
|
||||
|
@ -96,7 +97,7 @@ export function useDocumentCountStats<TParams extends DocumentStatsSearchStrateg
|
|||
const totalHitsResp = await lastValueFrom(
|
||||
data.search.search(
|
||||
{
|
||||
params: getDocumentCountStatsRequest(totalHitsParams, undefined, true),
|
||||
params: getDocumentCountStatsRequest(totalHitsParams, undefined, changePointsByDefault),
|
||||
},
|
||||
{ abortSignal: abortCtrl.current.signal }
|
||||
)
|
||||
|
@ -116,7 +117,7 @@ export function useDocumentCountStats<TParams extends DocumentStatsSearchStrateg
|
|||
{ ...searchParams, trackTotalHits: false },
|
||||
randomSamplerWrapper,
|
||||
false,
|
||||
searchParamsCompare === undefined
|
||||
searchParamsCompare === undefined && changePointsByDefault
|
||||
),
|
||||
},
|
||||
{ abortSignal: abortCtrl.current.signal }
|
||||
|
|
|
@ -13,6 +13,8 @@ export function plugin() {
|
|||
return new AiopsPlugin();
|
||||
}
|
||||
|
||||
export type { AiopsPluginStart, AiopsPluginSetup } from './types';
|
||||
|
||||
export type { AiopsAppDependencies } from './hooks/use_aiops_app_context';
|
||||
export type { LogRateAnalysisAppStateProps } from './components/log_rate_analysis';
|
||||
export type { LogRateAnalysisContentWrapperProps } from './components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content_wrapper';
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import type { CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { type CoreSetup } from '@kbn/core/public';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
|
||||
import { getChangePointDetectionComponent } from './shared_components';
|
||||
import type {
|
||||
AiopsPluginSetup,
|
||||
|
@ -59,6 +61,18 @@ export class AiopsPlugin
|
|||
public start(core: CoreStart, plugins: AiopsPluginStartDeps): AiopsPluginStart {
|
||||
return {
|
||||
ChangePointDetectionComponent: getChangePointDetectionComponent(core, plugins),
|
||||
getPatternAnalysisAvailable: async () => {
|
||||
const { getPatternAnalysisAvailable } = await import(
|
||||
'./components/log_categorization/log_categorization_enabled'
|
||||
);
|
||||
return getPatternAnalysisAvailable(plugins.licensing);
|
||||
},
|
||||
PatternAnalysisComponent: dynamic(
|
||||
async () =>
|
||||
import(
|
||||
'./components/log_categorization/log_categorization_for_embeddable/log_categorization_wrapper'
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
7
x-pack/plugins/aiops/public/types.ts → x-pack/plugins/aiops/public/types/index.ts
Executable file → Normal file
7
x-pack/plugins/aiops/public/types.ts → x-pack/plugins/aiops/public/types/index.ts
Executable file → Normal file
|
@ -18,7 +18,10 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
|
|||
import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { CasesPublicSetup } from '@kbn/cases-plugin/public';
|
||||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import type { ChangePointDetectionSharedComponent } from './shared_components';
|
||||
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';
|
||||
|
||||
export interface AiopsPluginSetupDeps {
|
||||
embeddable: EmbeddableSetup;
|
||||
|
@ -45,5 +48,7 @@ export interface AiopsPluginStartDeps {
|
|||
|
||||
export type AiopsPluginSetup = void;
|
||||
export interface AiopsPluginStart {
|
||||
getPatternAnalysisAvailable: () => Promise<(dataView: DataView) => Promise<boolean>>;
|
||||
PatternAnalysisComponent: React.ComponentType<LogCategorizationEmbeddableWrapperProps>;
|
||||
ChangePointDetectionComponent: ChangePointDetectionSharedComponent;
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import { type FrozenTierPreference } from '@kbn/ml-date-picker';
|
||||
import type { MinimumTimeRangeOption } from '../components/log_categorization/log_categorization_for_embeddable/minimum_time_range';
|
||||
|
||||
import {
|
||||
type RandomSamplerOption,
|
||||
type RandomSamplerProbability,
|
||||
|
@ -15,11 +17,14 @@ export const AIOPS_FROZEN_TIER_PREFERENCE = 'aiops.frozenDataTierPreference';
|
|||
export const AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE = 'aiops.randomSamplingModePreference';
|
||||
export const AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE =
|
||||
'aiops.randomSamplingProbabilityPreference';
|
||||
export const AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE =
|
||||
'aiops.patternAnalysisMinimumTimeRangePreference';
|
||||
|
||||
export type AiOps = Partial<{
|
||||
[AIOPS_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
|
||||
[AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE]: RandomSamplerOption;
|
||||
[AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE]: number;
|
||||
[AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE]: MinimumTimeRangeOption;
|
||||
}> | null;
|
||||
|
||||
export type AiOpsKey = keyof Exclude<AiOps, null>;
|
||||
|
@ -30,10 +35,13 @@ export type AiOpsStorageMapped<T extends AiOpsKey> = T extends typeof AIOPS_FROZ
|
|||
? RandomSamplerOption
|
||||
: T extends typeof AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE
|
||||
? RandomSamplerProbability
|
||||
: T extends typeof AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE
|
||||
? MinimumTimeRangeOption
|
||||
: null;
|
||||
|
||||
export const AIOPS_STORAGE_KEYS = [
|
||||
AIOPS_FROZEN_TIER_PREFERENCE,
|
||||
AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE,
|
||||
AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE,
|
||||
AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE,
|
||||
] as const;
|
||||
|
|
|
@ -11,14 +11,28 @@
|
|||
"types/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/aiops-components",
|
||||
"@kbn/aiops-change-point-detection",
|
||||
"@kbn/aiops-common",
|
||||
"@kbn/aiops-components",
|
||||
"@kbn/aiops-log-pattern-analysis",
|
||||
"@kbn/aiops-log-rate-analysis",
|
||||
"@kbn/aiops-test-utils",
|
||||
"@kbn/analytics",
|
||||
"@kbn/cases-plugin",
|
||||
"@kbn/charts-plugin",
|
||||
"@kbn/content-management-utils",
|
||||
"@kbn/core-execution-context-browser",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/core-theme-browser",
|
||||
"@kbn/core-ui-settings-browser-mocks",
|
||||
"@kbn/core",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/data-service",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/datemath",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/field-formats-plugin",
|
||||
"@kbn/field-types",
|
||||
|
@ -44,38 +58,25 @@
|
|||
"@kbn/ml-response-stream",
|
||||
"@kbn/ml-route-utils",
|
||||
"@kbn/ml-string-hash",
|
||||
"@kbn/ml-time-buckets",
|
||||
"@kbn/ml-ui-actions",
|
||||
"@kbn/ml-url-state",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/presentation-util-plugin",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/rison",
|
||||
"@kbn/saved-search-plugin",
|
||||
"@kbn/search-types",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/ui-actions-plugin",
|
||||
"@kbn/unified-field-list",
|
||||
"@kbn/unified-search-plugin",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/presentation-util-plugin",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/cases-plugin",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/analytics",
|
||||
"@kbn/ml-ui-actions",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/core-ui-settings-browser-mocks",
|
||||
"@kbn/ml-time-buckets",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/aiops-test-utils",
|
||||
"@kbn/aiops-log-rate-analysis",
|
||||
"@kbn/aiops-log-pattern-analysis",
|
||||
"@kbn/aiops-change-point-detection",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/data-service",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/search-types",
|
||||
"@kbn/content-management-utils",
|
||||
"@kbn/utility-types",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -6792,7 +6792,6 @@
|
|||
"unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel": "Exclure le {field} : \"{value}\"",
|
||||
"unifiedFieldList.fieldStats.filterValueButtonAriaLabel": "Filtrer sur le {field} : \"{value}\"",
|
||||
"unifiedFieldList.fieldStats.noFieldDataInSampleDescription": "Aucune donnée de champ pour {sampledDocumentsFormatted} {sampledDocuments, plural, one {exemple d'enregistrement} other {exemples d'enregistrement}}.",
|
||||
"unifiedFieldList.fieldCategorizeButton.label": "Exécuter l'analyse du modèle",
|
||||
"unifiedFieldList.fieldItemButton.mappingConflictDescription": "Ce champ est défini avec plusieurs types (chaîne, entier, etc.) dans les différents index qui correspondent à ce modèle. Vous pouvez toujours utiliser ce champ conflictuel, mais il sera indisponible pour les fonctions qui nécessitent que Kibana en connaisse le type. Pour corriger ce problème, vous devrez réindexer vos données.",
|
||||
"unifiedFieldList.fieldItemButton.mappingConflictTitle": "Conflit de mapping",
|
||||
"unifiedFieldList.fieldList.noFieldsCallout.noDataLabel": "Aucun champ.",
|
||||
|
|
|
@ -6781,7 +6781,6 @@
|
|||
"unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"",
|
||||
"unifiedFieldList.fieldStats.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"",
|
||||
"unifiedFieldList.fieldStats.noFieldDataInSampleDescription": "{sampledDocumentsFormatted}サンプル{sampledDocuments, plural, other {レコード}}のフィールドデータがありません。",
|
||||
"unifiedFieldList.fieldCategorizeButton.label": "パターン分析を実行",
|
||||
"unifiedFieldList.fieldItemButton.mappingConflictDescription": "このフィールドは、このパターンと一致するインデックス全体に対して複数の型(文字列、整数など)として定義されています。この競合フィールドを使用することはできますが、Kibana で型を認識する必要がある関数では使用できません。この問題を修正するにはデータのレンダリングが必要です。",
|
||||
"unifiedFieldList.fieldItemButton.mappingConflictTitle": "マッピングの矛盾",
|
||||
"unifiedFieldList.fieldList.noFieldsCallout.noDataLabel": "フィールドがありません。",
|
||||
|
|
|
@ -6795,7 +6795,6 @@
|
|||
"unifiedFieldList.fieldStats.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”",
|
||||
"unifiedFieldList.fieldStats.filterValueButtonAriaLabel": "筛留 {field}:“{value}”",
|
||||
"unifiedFieldList.fieldStats.noFieldDataInSampleDescription": "{sampledDocumentsFormatted} 个样例{sampledDocuments, plural, other {记录}}无字段数据。",
|
||||
"unifiedFieldList.fieldCategorizeButton.label": "运行模式分析",
|
||||
"unifiedFieldList.fieldItemButton.mappingConflictDescription": "此字段在匹配此模式的各个索引中已定义为若干类型(字符串、整数等)。您可能仍可以使用此冲突字段,但它无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。",
|
||||
"unifiedFieldList.fieldItemButton.mappingConflictTitle": "映射冲突",
|
||||
"unifiedFieldList.fieldList.noFieldsCallout.noDataLabel": "无字段。",
|
||||
|
|
|
@ -14,7 +14,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const retry = getService('retry');
|
||||
const ml = getService('ml');
|
||||
const PageObjects = getPageObjects(['common', 'timePicker', 'discover']);
|
||||
const selectedField = '@message';
|
||||
const totalDocCount = 14005;
|
||||
|
||||
async function retrySwitchTab(tabIndex: number, seconds: number) {
|
||||
|
@ -55,15 +54,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.discover.selectIndexPattern('logstash-*');
|
||||
await aiops.logPatternAnalysisPage.assertDiscoverDocCount(totalDocCount);
|
||||
|
||||
await aiops.logPatternAnalysisPage.clickDiscoverField(selectedField);
|
||||
await aiops.logPatternAnalysisPage.clickDiscoverMenuAnalyzeButton(selectedField);
|
||||
await aiops.logPatternAnalysisPage.clickPatternsTab();
|
||||
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisTabContentsExists();
|
||||
|
||||
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutExists();
|
||||
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutTitle(selectedField);
|
||||
await aiops.logPatternAnalysisPage.setRandomSamplingOptionDiscover(
|
||||
'aiopsRandomSamplerOptionOff'
|
||||
);
|
||||
|
||||
await aiops.logPatternAnalysisPage.setRandomSamplingOption('aiopsRandomSamplerOptionOff');
|
||||
|
||||
await aiops.logPatternAnalysisPage.assertTotalCategoriesFound(3);
|
||||
await aiops.logPatternAnalysisPage.assertTotalCategoriesFoundDiscover(3);
|
||||
await aiops.logPatternAnalysisPage.assertCategoryTableRows(3);
|
||||
|
||||
// get category count from the first row
|
||||
|
@ -87,15 +85,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
);
|
||||
await aiops.logPatternAnalysisPage.assertDiscoverDocCount(totalDocCount);
|
||||
|
||||
await aiops.logPatternAnalysisPage.clickDiscoverField(selectedField);
|
||||
await aiops.logPatternAnalysisPage.clickDiscoverMenuAnalyzeButton(selectedField);
|
||||
await aiops.logPatternAnalysisPage.clickPatternsTab();
|
||||
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisTabContentsExists();
|
||||
|
||||
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutExists();
|
||||
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutTitle(selectedField);
|
||||
await aiops.logPatternAnalysisPage.setRandomSamplingOptionDiscover(
|
||||
'aiopsRandomSamplerOptionOff'
|
||||
);
|
||||
|
||||
await aiops.logPatternAnalysisPage.setRandomSamplingOption('aiopsRandomSamplerOptionOff');
|
||||
|
||||
await aiops.logPatternAnalysisPage.assertTotalCategoriesFound(3);
|
||||
await aiops.logPatternAnalysisPage.assertTotalCategoriesFoundDiscover(3);
|
||||
await aiops.logPatternAnalysisPage.assertCategoryTableRows(3);
|
||||
|
||||
// get category count from the first row
|
||||
|
|
|
@ -35,6 +35,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./index_data_visualizer_random_sampler'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_filters'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_grid_in_discover'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_grid_in_discover_trial'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_grid_in_dashboard'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_actions_panel'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_data_view_management'));
|
||||
|
|
|
@ -28,24 +28,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const startTime = 'Jan 1, 2016 @ 00:00:00.000';
|
||||
const endTime = 'Nov 1, 2020 @ 00:00:00.000';
|
||||
|
||||
function runTestsWhenDisabled(testData: TestData) {
|
||||
it('should not show view mode toggle or Field stats table', async function () {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
if (testData.isSavedSearch) {
|
||||
await retry.tryForTime(2 * 1000, async () => {
|
||||
await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
|
||||
});
|
||||
} else {
|
||||
await dataViews.switchToAndValidate(testData.sourceIndexOrSavedSearch);
|
||||
}
|
||||
|
||||
await PageObjects.timePicker.setAbsoluteRange(startTime, endTime);
|
||||
|
||||
await PageObjects.discover.assertViewModeToggleNotExists();
|
||||
await PageObjects.discover.assertFieldStatsTableNotExists();
|
||||
});
|
||||
}
|
||||
|
||||
function runTests(testData: TestData) {
|
||||
describe(`with ${testData.suiteTitle}`, function () {
|
||||
it(`displays the 'Field statistics' table content correctly`, async function () {
|
||||
|
@ -128,14 +110,5 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
runTests(farequoteLuceneFiltersSearchTestData);
|
||||
runTests(sampleLogTestData);
|
||||
});
|
||||
|
||||
describe('when disabled', function () {
|
||||
before(async function () {
|
||||
// Ensure that the setting is set to default state which is false
|
||||
await ml.testResources.setAdvancedSettingProperty(SHOW_FIELD_STATISTICS, false);
|
||||
});
|
||||
|
||||
runTestsWhenDisabled(farequoteDataViewTestData);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { TestData } from './types';
|
||||
|
||||
const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics';
|
||||
import { farequoteDataViewTestData } from './index_test_data';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']);
|
||||
const ml = getService('ml');
|
||||
const retry = getService('retry');
|
||||
const dataViews = getService('dataViews');
|
||||
|
||||
const startTime = 'Jan 1, 2016 @ 00:00:00.000';
|
||||
const endTime = 'Nov 1, 2020 @ 00:00:00.000';
|
||||
|
||||
function runTestsWhenDisabled(testData: TestData) {
|
||||
it('should not show view mode toggle or Field stats table', async function () {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
if (testData.isSavedSearch) {
|
||||
await retry.tryForTime(2 * 1000, async () => {
|
||||
await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
|
||||
});
|
||||
} else {
|
||||
await dataViews.switchToAndValidate(testData.sourceIndexOrSavedSearch);
|
||||
}
|
||||
|
||||
await PageObjects.timePicker.setAbsoluteRange(startTime, endTime);
|
||||
|
||||
await PageObjects.discover.assertViewModeToggleNotExists();
|
||||
await PageObjects.discover.assertFieldStatsTableNotExists();
|
||||
});
|
||||
}
|
||||
|
||||
describe('field statistics in Discover (basic license)', function () {
|
||||
before(async function () {
|
||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
|
||||
await ml.testResources.createDataViewIfNeeded('ft_farequote', '@timestamp');
|
||||
await ml.testResources.createDataViewIfNeeded('ft_module_sample_logs', '@timestamp');
|
||||
await ml.testResources.createSavedSearchFarequoteKueryIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteFilterAndLuceneIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteFilterAndKueryIfNeeded();
|
||||
|
||||
await ml.securityUI.loginAsMlPowerUser();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await ml.testResources.clearAdvancedSettingProperty(SHOW_FIELD_STATISTICS);
|
||||
await ml.testResources.deleteSavedSearches();
|
||||
await ml.testResources.deleteDataViewByTitle('ft_farequote');
|
||||
});
|
||||
|
||||
describe('when disabled', function () {
|
||||
before(async function () {
|
||||
// Ensure that the setting is set to default state which is false
|
||||
await ml.testResources.setAdvancedSettingProperty(SHOW_FIELD_STATISTICS, false);
|
||||
});
|
||||
|
||||
runTestsWhenDisabled(farequoteDataViewTestData);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { TestData } from './types';
|
||||
|
||||
const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics';
|
||||
import { farequoteDataViewTestData } from './index_test_data';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']);
|
||||
const ml = getService('ml');
|
||||
const retry = getService('retry');
|
||||
const dataViews = getService('dataViews');
|
||||
|
||||
const startTime = 'Jan 1, 2016 @ 00:00:00.000';
|
||||
const endTime = 'Nov 1, 2020 @ 00:00:00.000';
|
||||
|
||||
function runTestsWhenDisabled(testData: TestData) {
|
||||
it('should show view mode toggle but not Field stats tab', async function () {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
if (testData.isSavedSearch) {
|
||||
await retry.tryForTime(2 * 1000, async () => {
|
||||
await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
|
||||
});
|
||||
} else {
|
||||
await dataViews.switchToAndValidate(testData.sourceIndexOrSavedSearch);
|
||||
}
|
||||
|
||||
await PageObjects.timePicker.setAbsoluteRange(startTime, endTime);
|
||||
|
||||
await PageObjects.discover.assertViewModeToggleExists();
|
||||
await PageObjects.discover.assertFieldStatsTableNotExists();
|
||||
});
|
||||
}
|
||||
|
||||
describe('field statistics in Discover (trial license)', function () {
|
||||
before(async function () {
|
||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
|
||||
await ml.testResources.createDataViewIfNeeded('ft_farequote', '@timestamp');
|
||||
await ml.testResources.createDataViewIfNeeded('ft_module_sample_logs', '@timestamp');
|
||||
await ml.testResources.createSavedSearchFarequoteKueryIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteFilterAndLuceneIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteFilterAndKueryIfNeeded();
|
||||
|
||||
await ml.securityUI.loginAsMlPowerUser();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await ml.testResources.clearAdvancedSettingProperty(SHOW_FIELD_STATISTICS);
|
||||
await ml.testResources.deleteSavedSearches();
|
||||
await ml.testResources.deleteDataViewByTitle('ft_farequote');
|
||||
});
|
||||
|
||||
describe('when disabled', function () {
|
||||
before(async function () {
|
||||
// Ensure that the setting is set to default state which is false
|
||||
await ml.testResources.setAdvancedSettingProperty(SHOW_FIELD_STATISTICS, false);
|
||||
});
|
||||
|
||||
runTestsWhenDisabled(farequoteDataViewTestData);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -76,6 +76,17 @@ export function LogPatternAnalysisPageProvider({ getService, getPageObject }: Ft
|
|||
});
|
||||
},
|
||||
|
||||
async assertTotalCategoriesFoundDiscover(expectedMinimumCategoryCount: number) {
|
||||
await retry.tryForTime(5000, async () => {
|
||||
const actualText = await testSubjects.getVisibleText('dscViewModePatternAnalysisButton');
|
||||
const actualCount = Number(actualText.match(/Patterns \((.+)\)/)![1]);
|
||||
expect(actualCount + 1).to.greaterThan(
|
||||
expectedMinimumCategoryCount,
|
||||
`Expected patterns found count to be >= '${expectedMinimumCategoryCount}' (got '${actualCount}')`
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
async assertCategoryTableRows(expectedMinimumCategoryCount: number) {
|
||||
await retry.tryForTime(5000, async () => {
|
||||
const tableListContainer = await testSubjects.find('aiopsLogPatternsTable');
|
||||
|
@ -170,12 +181,22 @@ export function LogPatternAnalysisPageProvider({ getService, getPageObject }: Ft
|
|||
});
|
||||
},
|
||||
|
||||
async clickPatternsTab() {
|
||||
await testSubjects.click('dscViewModePatternAnalysisButton');
|
||||
},
|
||||
|
||||
async assertLogPatternAnalysisFlyoutExists() {
|
||||
await retry.tryForTime(30 * 1000, async () => {
|
||||
await testSubjects.existOrFail('mlJobSelectorFlyoutBody');
|
||||
});
|
||||
},
|
||||
|
||||
async assertLogPatternAnalysisTabContentsExists() {
|
||||
await retry.tryForTime(30 * 1000, async () => {
|
||||
await testSubjects.existOrFail('aiopsLogPatternsTable');
|
||||
});
|
||||
},
|
||||
|
||||
async assertLogPatternAnalysisFlyoutDoesNotExist() {
|
||||
await retry.tryForTime(30 * 1000, async () => {
|
||||
await testSubjects.missingOrFail('mlJobSelectorFlyoutBody');
|
||||
|
@ -210,5 +231,23 @@ export function LogPatternAnalysisPageProvider({ getService, getPageObject }: Ft
|
|||
await testSubjects.missingOrFail('aiopsRandomSamplerOptionsFormRow', { timeout: 1000 });
|
||||
});
|
||||
},
|
||||
|
||||
async setRandomSamplingOptionDiscover(option: RandomSamplerOption) {
|
||||
await retry.tryForTime(20000, async () => {
|
||||
await testSubjects.existOrFail('aiopsEmbeddableMenuOptionsButton');
|
||||
await testSubjects.clickWhenNotDisabled('aiopsEmbeddableMenuOptionsButton');
|
||||
|
||||
await testSubjects.clickWhenNotDisabled('aiopsRandomSamplerOptionsSelect');
|
||||
|
||||
await testSubjects.existOrFail('aiopsRandomSamplerOptionOff', { timeout: 1000 });
|
||||
await testSubjects.existOrFail('aiopsRandomSamplerOptionOnManual', { timeout: 1000 });
|
||||
await testSubjects.existOrFail('aiopsRandomSamplerOptionOnAutomatic', { timeout: 1000 });
|
||||
|
||||
await testSubjects.click(option);
|
||||
|
||||
await testSubjects.clickWhenNotDisabled('aiopsEmbeddableMenuOptionsButton');
|
||||
await testSubjects.missingOrFail('aiopsRandomSamplerOptionsFormRow', { timeout: 1000 });
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -41,6 +41,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
'../../../../../functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover'
|
||||
)
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve(
|
||||
'../../../../../functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover_basic'
|
||||
)
|
||||
);
|
||||
loadTestFile(require.resolve('./index_data_visualizer_actions_panel'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue