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
Show file tree
Hide file tree
Changes from all 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
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