Skip to content

Commit

Permalink
[ML] AIOps: Link from Explain Log Rate Spikes to Log Pattern Analysis (
Browse files Browse the repository at this point in the history
…elastic#155121)

Adds table actions to Explain Log Rate Spikes to be able to drill down to Log Pattern Analysis.
  • Loading branch information
walterra authored and nikitaindik committed Apr 25, 2023
1 parent 201f0d3 commit 977ae7b
Show file tree
Hide file tree
Showing 23 changed files with 631 additions and 132 deletions.
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
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -66,7 +70,7 @@ export const ExplainLogRateSpikesPage: FC = () => {

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

Expand All @@ -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[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';

Expand All @@ -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);
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -289,7 +298,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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>> = [
{
Expand Down Expand Up @@ -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',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>> = [
{
Expand Down Expand Up @@ -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',
},
Expand Down
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;
tooltipText?: string;
onClick: () => void;
}

export const TableActionButton: FC<TableActionButtonProps> = ({
iconType,
dataTestSubjPostfix,
isDisabled,
label,
tooltipText,
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={tooltipText}
>
{buttonContent}
</EuiLink>
) : (
<EuiText
data-test-subj={`aiopsTableActionButton${dataTestSubjPostfix} disabled`}
size="s"
color={'subdued'}
aria-label={tooltipText}
css={{ fontWeight: 500 }}
>
{buttonContent}
</EuiText>
);

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

return unwrappedButton;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 977ae7b

Please sign in to comment.