Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] AIOps: Link from Explain Log Rate Spikes to Log Pattern Analysis #155121

Merged
merged 14 commits into from
Apr 24, 2023
Merged
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions x-pack/plugins/aiops/public/application/utils/url_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';

import type { Filter, Query } from '@kbn/es-query';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';

import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from './search_utils';

const defaultSearchQuery = {
match_all: {},
};

export interface AiOpsPageUrlState {
pageKey: 'AIOPS_INDEX_VIEWER';
pageUrlState: AiOpsIndexBasedAppState;
}

export interface AiOpsIndexBasedAppState {
searchString?: Query['query'];
searchQuery?: estypes.QueryDslQueryContainer;
searchQueryLanguage: SearchQueryLanguage;
filters?: Filter[];
}

export type AiOpsFullIndexBasedAppState = Required<AiOpsIndexBasedAppState>;

export const getDefaultAiOpsListState = (
overrides?: Partial<AiOpsIndexBasedAppState>
): AiOpsFullIndexBasedAppState => ({
searchString: '',
searchQuery: defaultSearchQuery,
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
filters: [],
...overrides,
});

export const isFullAiOpsListState = (arg: unknown): arg is AiOpsFullIndexBasedAppState => {
return isPopulatedObject(arg, Object.keys(getDefaultAiOpsListState()));
};
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@ import { pick } from 'lodash';

import { EuiCallOut } from '@elastic/eui';

import type { Filter, Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
@@ -21,7 +20,6 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';

import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../application/utils/search_utils';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import { DataSourceContext } from '../../hooks/use_data_source';
@@ -42,34 +40,6 @@ export interface ExplainLogRateSpikesAppStateProps {
appDependencies: AiopsAppDependencies;
}

const defaultSearchQuery = {
match_all: {},
};

export interface AiOpsPageUrlState {
pageKey: 'AIOPS_INDEX_VIEWER';
pageUrlState: AiOpsIndexBasedAppState;
}

export interface AiOpsIndexBasedAppState {
searchString?: Query['query'];
searchQuery?: Query['query'];
searchQueryLanguage: SearchQueryLanguage;
filters?: Filter[];
}

export const getDefaultAiOpsListState = (
overrides?: Partial<AiOpsIndexBasedAppState>
): Required<AiOpsIndexBasedAppState> => ({
searchString: '',
searchQuery: defaultSearchQuery,
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
filters: [],
...overrides,
});

export const restorableDefaults = getDefaultAiOpsListState();

export const ExplainLogRateSpikesAppState: FC<ExplainLogRateSpikesAppStateProps> = ({
dataView,
savedSearch,
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@

import React, { useCallback, useEffect, useState, FC } from 'react';

import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
EuiEmptyPrompt,
EuiFlexGroup,
@@ -28,14 +29,17 @@ import { useDataSource } from '../../hooks/use_data_source';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { SearchQueryLanguage } from '../../application/utils/search_utils';
import { useData } from '../../hooks/use_data';
import {
getDefaultAiOpsListState,
type AiOpsPageUrlState,
} from '../../application/utils/url_state';

import { DocumentCountContent } from '../document_count_content/document_count_content';
import { SearchPanel } from '../search_panel';
import type { GroupTableItem } from '../spike_analysis_table/types';
import { useSpikeAnalysisTableRowContext } from '../spike_analysis_table/spike_analysis_table_row_provider';
import { PageHeader } from '../page_header';

import { restorableDefaults, type AiOpsPageUrlState } from './explain_log_rate_spikes_app_state';
import { ExplainLogRateSpikesAnalysis } from './explain_log_rate_spikes_analysis';

function getDocumentCountStatsSplitLabel(
@@ -66,7 +70,7 @@ export const ExplainLogRateSpikesPage: FC = () => {

const [aiopsListState, setAiopsListState] = usePageUrlState<AiOpsPageUrlState>(
'AIOPS_INDEX_VIEWER',
restorableDefaults
getDefaultAiOpsListState()
);
const [globalState, setGlobalState] = useUrlState('_g');

@@ -80,7 +84,7 @@ export const ExplainLogRateSpikesPage: FC = () => {

const setSearchParams = useCallback(
(searchParams: {
searchQuery: Query['query'];
searchQuery: estypes.QueryDslQueryContainer;
searchString: Query['query'];
queryLanguage: SearchQueryLanguage;
filters: Filter[];
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ import {
import { useDiscoverLinks } from '../use_discover_links';
import { MiniHistogram } from '../../mini_histogram';
import { useEuiTheme } from '../../../hooks/use_eui_theme';
import type { AiOpsIndexBasedAppState } from '../../explain_log_rate_spikes/explain_log_rate_spikes_app_state';
import type { AiOpsFullIndexBasedAppState } from '../../../application/utils/url_state';
import type { EventRate, Category, SparkLinesPerCategory } from '../use_categorize_request';
import { useTableState } from './use_table_state';

@@ -42,7 +42,7 @@ interface Props {
dataViewId: string;
selectedField: string | undefined;
timefilter: TimefilterContract;
aiopsListState: Required<AiOpsIndexBasedAppState>;
aiopsListState: AiOpsFullIndexBasedAppState;
pinnedCategory: Category | null;
setPinnedCategory: (category: Category | null) => void;
selectedCategory: Category | null;
Original file line number Diff line number Diff line change
@@ -4,8 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { FC, useState, useEffect, useCallback, useMemo } from 'react';

import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
EuiButton,
EuiSpacer,
@@ -21,14 +23,18 @@ import {
import { Filter, Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useUrlState } from '@kbn/ml-url-state';
import { usePageUrlState, useUrlState } from '@kbn/ml-url-state';

import { useDataSource } from '../../hooks/use_data_source';
import { useData } from '../../hooks/use_data';
import type { SearchQueryLanguage } from '../../application/utils/search_utils';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import {
getDefaultAiOpsListState,
isFullAiOpsListState,
type AiOpsPageUrlState,
} from '../../application/utils/url_state';

import { restorableDefaults } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state';
import { SearchPanel } from '../search_panel';
import { PageHeader } from '../page_header';

@@ -47,7 +53,10 @@ export const LogCategorizationPage: FC = () => {
const { dataView, savedSearch } = useDataSource();

const { runCategorizeRequest, cancelRequest } = useCategorizeRequest();
const [aiopsListState, setAiopsListState] = useState(restorableDefaults);
const [aiopsListState, setAiopsListState] = usePageUrlState<AiOpsPageUrlState>(
'AIOPS_INDEX_VIEWER',
getDefaultAiOpsListState()
);
const [globalState, setGlobalState] = useUrlState('_g');
const [selectedField, setSelectedField] = useState<string | undefined>();
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null);
@@ -76,7 +85,7 @@ export const LogCategorizationPage: FC = () => {

const setSearchParams = useCallback(
(searchParams: {
searchQuery: Query['query'];
searchQuery: estypes.QueryDslQueryContainer;
searchString: Query['query'];
queryLanguage: SearchQueryLanguage;
filters: Filter[];
@@ -288,7 +297,10 @@ export const LogCategorizationPage: FC = () => {
fieldSelected={selectedField !== null}
/>

{selectedField !== undefined && categories !== null && categories.length > 0 ? (
{selectedField !== undefined &&
categories !== null &&
categories.length > 0 &&
isFullAiOpsListState(aiopsListState) ? (
<CategoryTable
categories={categories}
aiopsListState={aiopsListState}
Original file line number Diff line number Diff line change
@@ -10,10 +10,10 @@ import moment from 'moment';

import type { TimeRangeBounds } from '@kbn/data-plugin/common';
import { i18n } from '@kbn/i18n';
import type { AiOpsIndexBasedAppState } from '../../application/utils/url_state';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import type { Category } from './use_categorize_request';
import type { QueryMode } from './category_table';
import type { AiOpsIndexBasedAppState } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state';

export function useDiscoverLinks() {
const {
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ import { useSpikeAnalysisTableRowContext } from './spike_analysis_table_row_prov
import { FieldStatsPopover } from '../field_stats_popover';
import { useCopyToClipboardAction } from './use_copy_to_clipboard_action';
import { useViewInDiscoverAction } from './use_view_in_discover_action';
import { useViewInLogPatternAnalysisAction } from './use_view_in_log_pattern_analysis_action';

const NARROW_COLUMN_WIDTH = '120px';
const ACTIONS_COLUMN_WIDTH = '60px';
@@ -95,6 +96,7 @@ export const SpikeAnalysisTable: FC<SpikeAnalysisTableProps> = ({

const copyToClipBoardAction = useCopyToClipboardAction();
const viewInDiscoverAction = useViewInDiscoverAction(dataViewId);
const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId);

const columns: Array<EuiBasicTableColumn<SignificantTerm>> = [
{
@@ -238,7 +240,7 @@ export const SpikeAnalysisTable: FC<SpikeAnalysisTableProps> = ({
name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', {
defaultMessage: 'Actions',
}),
actions: [viewInDiscoverAction, copyToClipBoardAction],
actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction],
width: ACTIONS_COLUMN_WIDTH,
valign: 'middle',
},
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ import { useSpikeAnalysisTableRowContext } from './spike_analysis_table_row_prov
import type { GroupTableItem } from './types';
import { useCopyToClipboardAction } from './use_copy_to_clipboard_action';
import { useViewInDiscoverAction } from './use_view_in_discover_action';
import { useViewInLogPatternAnalysisAction } from './use_view_in_log_pattern_analysis_action';

const NARROW_COLUMN_WIDTH = '120px';
const EXPAND_COLUMN_WIDTH = '40px';
@@ -121,6 +122,7 @@ export const SpikeAnalysisGroupsTable: FC<SpikeAnalysisTableProps> = ({

const copyToClipBoardAction = useCopyToClipboardAction();
const viewInDiscoverAction = useViewInDiscoverAction(dataViewId);
const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId);

const columns: Array<EuiBasicTableColumn<GroupTableItem>> = [
{
@@ -355,7 +357,7 @@ export const SpikeAnalysisGroupsTable: FC<SpikeAnalysisTableProps> = ({
name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', {
defaultMessage: 'Actions',
}),
actions: [viewInDiscoverAction, copyToClipBoardAction],
actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction],
width: ACTIONS_COLUMN_WIDTH,
valign: 'top',
},
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { type FC } from 'react';

import { EuiLink, EuiIcon, EuiText, EuiToolTip, type IconType } from '@elastic/eui';

interface TableActionButtonProps {
iconType: IconType;
dataTestSubjPostfix: string;
isDisabled: boolean;
label: string;
message?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, maybe tooltipText or something similar would be a more descriptive name for this prop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in ab28bf2.

onClick: () => void;
}

export const TableActionButton: FC<TableActionButtonProps> = ({
iconType,
dataTestSubjPostfix,
isDisabled,
label,
message,
onClick,
}) => {
const buttonContent = (
<>
<EuiIcon type={iconType} css={{ marginRight: '8px' }} />
{label}
</>
);

const unwrappedButton = !isDisabled ? (
<EuiLink
data-test-subj={`aiopsTableActionButton${dataTestSubjPostfix} enabled`}
onClick={onClick}
color={'text'}
aria-label={message}
>
{buttonContent}
</EuiLink>
) : (
<EuiText
data-test-subj={`aiopsTableActionButton${dataTestSubjPostfix} disabled`}
size="s"
color={'subdued'}
aria-label={message}
css={{ fontWeight: 500 }}
>
{buttonContent}
</EuiText>
);

if (message) {
return <EuiToolTip content={message}>{unwrappedButton}</EuiToolTip>;
}

return unwrappedButton;
};
Original file line number Diff line number Diff line change
@@ -30,11 +30,19 @@ describe('useCopyToClipboardAction', () => {
it('renders the action for a single significant term', async () => {
execCommandMock.mockImplementationOnce(() => true);
const { result } = renderHook(() => useCopyToClipboardAction());
const { getByLabelText } = render((result.current as Action).render(significantTerms[0]));
const { findByText, getByTestId } = render(
(result.current as Action).render(significantTerms[0])
);

const button = getByLabelText('Copy field/value pair as KQL syntax to clipboard');
const button = getByTestId('aiopsTableActionButtonCopyToClipboard enabled');

expect(button).toBeInTheDocument();
userEvent.hover(button);

// The tooltip from EUI takes 250ms to appear, so we must
// use a `find*` query to asynchronously poll for it.
expect(
await findByText('Copy field/value pair as KQL syntax to clipboard')
).toBeInTheDocument();

await act(async () => {
await userEvent.click(button);
@@ -50,12 +58,16 @@ describe('useCopyToClipboardAction', () => {
it('renders the action for a group of items', async () => {
execCommandMock.mockImplementationOnce(() => true);
const groupTableItems = getGroupTableItems(finalSignificantTermGroups);
const { result } = renderHook(() => useCopyToClipboardAction());
const { getByLabelText } = render((result.current as Action).render(groupTableItems[0]));
const { result } = renderHook(useCopyToClipboardAction);
const { findByText, getByText } = render((result.current as Action).render(groupTableItems[0]));

const button = getByText('Copy to clipboard');

const button = getByLabelText('Copy group items as KQL syntax to clipboard');
userEvent.hover(button);

expect(button).toBeInTheDocument();
// The tooltip from EUI takes 250ms to appear, so we must
// use a `find*` query to asynchronously poll for it.
expect(await findByText('Copy group items as KQL syntax to clipboard')).toBeInTheDocument();

await act(async () => {
await userEvent.click(button);
Loading