diff --git a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx index 733f61f5b1b166..eaed41d29b0c60 100644 --- a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx +++ b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx @@ -12,6 +12,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiIcon, EuiIconTip, EuiProgress, EuiText, @@ -45,33 +46,10 @@ export const ProgressControls: FC = ({ }) => { const { euiTheme } = useEuiTheme(); const runningProgressBarStyles = useAnimatedProgressBarBackground(euiTheme.colors.success); + const analysisCompleteStyle = { display: 'none' }; return ( - - - - - - - - - - - - {!isRunning && ( = ({ size="s" onClick={onRefresh} color={shouldRerunAnalysis ? 'warning' : 'primary'} - fill > {shouldRerunAnalysis && ( @@ -107,11 +84,53 @@ export const ProgressControls: FC = ({ )} {isRunning && ( - + )} + + {progress === 1 ? ( + + + + + + + {i18n.translate('xpack.aiops.analysisCompleteLabel', { + defaultMessage: 'Analysis complete', + })} + + + + ) : null} + + + + + + + + + + + {children} ); diff --git a/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx b/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx index 4a51957bb934aa..994f9a1f213ef8 100644 --- a/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/aiops/public/components/document_count_content/document_count_chart/document_count_chart.tsx @@ -56,6 +56,8 @@ interface DocumentCountChartProps { interval: number; chartPointsSplitLabel: string; isBrushCleared: boolean; + /* Timestamp for start of initial analysis */ + autoAnalysisStart?: number; } const SPEC_ID = 'document_count'; @@ -101,6 +103,7 @@ export const DocumentCountChart: FC = ({ interval, chartPointsSplitLabel, isBrushCleared, + autoAnalysisStart, }) => { const { data, uiSettings, fieldFormats, charts } = useAiopsAppContext(); @@ -201,39 +204,6 @@ export const DocumentCountChart: FC = ({ timefilterUpdateHandler({ from, to }); }; - const onElementClick: ElementClickListener = ([elementData]) => { - if (brushSelectionUpdateHandler === undefined) { - return; - } - const startRange = (elementData as XYChartElementEvent)[0].x; - - const range = { - from: startRange, - to: startRange + interval, - }; - - if (viewMode === VIEW_MODE.ZOOM) { - timefilterUpdateHandler(range); - } else { - if ( - typeof startRange === 'number' && - originalWindowParameters === undefined && - windowParameters === undefined && - adjustedChartPoints !== undefined - ) { - const wp = getWindowParameters( - startRange + interval / 2, - timeRangeEarliest, - timeRangeLatest + interval - ); - const wpSnap = getSnappedWindowParameters(wp, snapTimestamps); - setOriginalWindowParameters(wpSnap); - setWindowParameters(wpSnap); - brushSelectionUpdateHandler(wpSnap, true); - } - } - }; - const timeZone = getTimezone(uiSettings); const [originalWindowParameters, setOriginalWindowParameters] = useState< @@ -244,6 +214,69 @@ export const DocumentCountChart: FC = ({ WindowParameters | undefined >(); + const triggerAnalysis = useCallback( + (startRange: number) => { + const range = { + from: startRange, + to: startRange + interval, + }; + + if (viewMode === VIEW_MODE.ZOOM) { + timefilterUpdateHandler(range); + } else { + if ( + typeof startRange === 'number' && + originalWindowParameters === undefined && + windowParameters === undefined && + adjustedChartPoints !== undefined + ) { + const wp = getWindowParameters( + startRange + interval / 2, + timeRangeEarliest, + timeRangeLatest + interval + ); + const wpSnap = getSnappedWindowParameters(wp, snapTimestamps); + setOriginalWindowParameters(wpSnap); + setWindowParameters(wpSnap); + if (brushSelectionUpdateHandler !== undefined) { + brushSelectionUpdateHandler(wpSnap, true); + } + } + } + }, + [ + interval, + timeRangeEarliest, + timeRangeLatest, + snapTimestamps, + originalWindowParameters, + setWindowParameters, + brushSelectionUpdateHandler, + adjustedChartPoints, + timefilterUpdateHandler, + viewMode, + windowParameters, + ] + ); + + const onElementClick: ElementClickListener = useCallback( + ([elementData]) => { + if (brushSelectionUpdateHandler === undefined) { + return; + } + const startRange = (elementData as XYChartElementEvent)[0].x; + + triggerAnalysis(startRange); + }, + [triggerAnalysis, brushSelectionUpdateHandler] + ); + + useEffect(() => { + if (autoAnalysisStart !== undefined) { + triggerAnalysis(autoAnalysisStart); + } + }, [triggerAnalysis, autoAnalysisStart]); + useEffect(() => { if (isBrushCleared && originalWindowParameters !== undefined) { setOriginalWindowParameters(undefined); @@ -351,7 +384,6 @@ export const DocumentCountChart: FC = ({ position={Position.Bottom} showOverlappingTicks={true} tickFormat={(value) => xAxisFormatter.convert(value)} - // temporary fix to reduce horizontal chart margin until fixed in Elastic Charts itself labelFormat={useLegacyTimeAxis ? undefined : () => ''} timeAxisLayerCount={useLegacyTimeAxis ? 0 : 2} style={useLegacyTimeAxis ? {} : MULTILAYER_TIME_AXIS_STYLE} diff --git a/x-pack/plugins/aiops/public/components/document_count_content/document_count_content/document_count_content.tsx b/x-pack/plugins/aiops/public/components/document_count_content/document_count_content/document_count_content.tsx index 1e947bc912a6cd..a346accc2a88f9 100644 --- a/x-pack/plugins/aiops/public/components/document_count_content/document_count_content/document_count_content.tsx +++ b/x-pack/plugins/aiops/public/components/document_count_content/document_count_content/document_count_content.tsx @@ -33,6 +33,7 @@ export interface DocumentCountContentProps { totalCount: number; sampleProbability: number; windowParameters?: WindowParameters; + incomingInitialAnalysisStart?: number; } export const DocumentCountContent: FC = ({ @@ -44,8 +45,12 @@ export const DocumentCountContent: FC = ({ totalCount, sampleProbability, windowParameters, + incomingInitialAnalysisStart, }) => { const [isBrushCleared, setIsBrushCleared] = useState(true); + const [initialAnalysisStart, setInitialAnalysisStart] = useState( + incomingInitialAnalysisStart + ); useEffect(() => { setIsBrushCleared(windowParameters === undefined); @@ -95,6 +100,7 @@ export const DocumentCountContent: FC = ({ function clearSelection() { setIsBrushCleared(true); + setInitialAnalysisStart(undefined); clearSelectionHandler(); } @@ -126,6 +132,7 @@ export const DocumentCountContent: FC = ({ interval={documentCountStats.interval} chartPointsSplitLabel={documentCountStatsSplitLabel} isBrushCleared={isBrushCleared} + autoAnalysisStart={initialAnalysisStart} /> )} diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index 7ddef239cf2f45..4287048628b8ca 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -11,12 +11,13 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiButton, + EuiButtonGroup, EuiCallOut, EuiEmptyPrompt, + EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer, - EuiSwitch, EuiText, } from '@elastic/eui'; @@ -44,7 +45,7 @@ import { FieldFilterPopover } from './field_filter_popover'; const groupResultsMessage = i18n.translate( 'xpack.aiops.spikeAnalysisTable.groupedSwitchLabel.groupResults', { - defaultMessage: 'Group results', + defaultMessage: 'Smart grouping', } ); const groupResultsHelpMessage = i18n.translate( @@ -53,6 +54,20 @@ const groupResultsHelpMessage = i18n.translate( defaultMessage: 'Items which are unique to a group are marked by an asterisk (*).', } ); +const groupResultsOffMessage = i18n.translate( + 'xpack.aiops.spikeAnalysisTable.groupedSwitchLabel.groupResultsOff', + { + defaultMessage: 'Off', + } +); +const groupResultsOnMessage = i18n.translate( + 'xpack.aiops.spikeAnalysisTable.groupedSwitchLabel.groupResultsOn', + { + defaultMessage: 'On', + } +); +const resultsGroupedOffId = 'aiopsExplainLogRateSpikesGroupingOff'; +const resultsGroupedOnId = 'aiopsExplainLogRateSpikesGroupingOn'; /** * ExplainLogRateSpikes props require a data view. @@ -95,9 +110,11 @@ export const ExplainLogRateSpikesAnalysis: FC ApiExplainLogRateSpikes['body']['overrides'] | undefined >(undefined); const [shouldStart, setShouldStart] = useState(false); + const [toggleIdSelected, setToggleIdSelected] = useState(resultsGroupedOffId); - const onGroupResultsToggle = (e: { target: { checked: React.SetStateAction } }) => { - setGroupResults(e.target.checked); + const onGroupResultsToggle = (optionId: string) => { + setToggleIdSelected(optionId); + setGroupResults(optionId === resultsGroupedOnId); // When toggling the group switch, clear all row selections clearAllRowState(); @@ -174,6 +191,7 @@ export const ExplainLogRateSpikesAnalysis: FC // Reset grouping to false and clear all row selections when restarting the analysis. if (resetGroupButton) { setGroupResults(false); + setToggleIdSelected(resultsGroupedOffId); clearAllRowState(); } @@ -221,6 +239,19 @@ export const ExplainLogRateSpikesAnalysis: FC // the toggle wasn't enabled already and no fields were selected to be skipped. const disabledGroupResultsSwitch = !foundGroups && !groupResults && groupSkipFields.length === 0; + const toggleButtons = [ + { + id: resultsGroupedOffId, + label: groupResultsOffMessage, + 'data-test-subj': 'aiopsExplainLogRateSpikesGroupSwitchOff', + }, + { + id: resultsGroupedOnId, + label: groupResultsOnMessage, + 'data-test-subj': 'aiopsExplainLogRateSpikesGroupSwitchOn', + }, + ]; + return (
> - + + + {groupResultsMessage} + + + + + diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/explain_log_rate_spikes_content.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/explain_log_rate_spikes_content.tsx new file mode 100644 index 00000000000000..ac936383ee1f19 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/explain_log_rate_spikes_content.tsx @@ -0,0 +1,170 @@ +/* + * 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, { useState, FC } from 'react'; +import { EuiEmptyPrompt, EuiHorizontalRule, EuiResizableContainer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { Dictionary } from '@kbn/ml-url-state'; +import type { WindowParameters } from '@kbn/aiops-utils'; +import type { SignificantTerm } from '@kbn/ml-agg-utils'; + +import type { Moment } from 'moment'; +import { useData } from '../../../hooks/use_data'; +import { DocumentCountContent } from '../../document_count_content/document_count_content'; +import { ExplainLogRateSpikesAnalysis } from '../explain_log_rate_spikes_analysis'; +import type { GroupTableItem } from '../../spike_analysis_table/types'; +import { useSpikeAnalysisTableRowContext } from '../../spike_analysis_table/spike_analysis_table_row_provider'; + +const DEFAULT_SEARCH_QUERY = { match_all: {} }; + +export function getDocumentCountStatsSplitLabel( + significantTerm?: SignificantTerm, + group?: GroupTableItem +) { + if (significantTerm) { + return `${significantTerm?.fieldName}:${significantTerm?.fieldValue}`; + } else if (group) { + return i18n.translate('xpack.aiops.spikeAnalysisPage.documentCountStatsSplitGroupLabel', { + defaultMessage: 'Selected group', + }); + } +} + +export interface ExplainLogRateSpikesContentProps { + /** The data view to analyze. */ + dataView: DataView; + setGlobalState?: (params: Dictionary) => void; + /** Timestamp for the start of the range for initial analysis */ + initialAnalysisStart?: number; + timeRange?: { min: Moment; max: Moment }; + /** Elasticsearch query to pass to analysis endpoint */ + esSearchQuery?: estypes.QueryDslQueryContainer; +} + +export const ExplainLogRateSpikesContent: FC = ({ + dataView, + setGlobalState, + initialAnalysisStart, + timeRange, + esSearchQuery = DEFAULT_SEARCH_QUERY, +}) => { + const [windowParameters, setWindowParameters] = useState(); + + const { + currentSelectedSignificantTerm, + currentSelectedGroup, + setPinnedSignificantTerm, + setPinnedGroup, + setSelectedSignificantTerm, + setSelectedGroup, + } = useSpikeAnalysisTableRowContext(); + + const { documentStats, earliest, latest } = useData( + dataView, + 'explain_log_rage_spikes', + esSearchQuery, + setGlobalState, + currentSelectedSignificantTerm, + currentSelectedGroup, + undefined, + timeRange + ); + + const { sampleProbability, totalCount, documentCountStats, documentCountStatsCompare } = + documentStats; + + function clearSelection() { + setWindowParameters(undefined); + setPinnedSignificantTerm(null); + setPinnedGroup(null); + setSelectedSignificantTerm(null); + setSelectedGroup(null); + } + // Note: Temporarily removed height and disabled sticky histogram until we can fix the scrolling issue in a follow up + return ( + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + {documentCountStats !== undefined && ( + + )} + + + {/* */} + + {earliest !== undefined && latest !== undefined && windowParameters !== undefined && ( + + )} + {windowParameters === undefined && ( + + + + } + titleSize="xs" + body={ +

+ +

+ } + data-test-subj="aiopsNoWindowParametersEmptyPrompt" + /> + )} +
+ + )} +
+ ); +}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/explain_log_rate_spikes_content_wrapper.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/explain_log_rate_spikes_content_wrapper.tsx new file mode 100644 index 00000000000000..35ac82ff62e0b6 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/explain_log_rate_spikes_content_wrapper.tsx @@ -0,0 +1,105 @@ +/* + * 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, { FC } from 'react'; +import { pick } from 'lodash'; +import type { Moment } from 'moment'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { EuiCallOut } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { StorageContextProvider } from '@kbn/ml-local-storage'; +import { UrlStateProvider } from '@kbn/ml-url-state'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +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 { AiopsAppContext, type AiopsAppDependencies } from '../../../hooks/use_aiops_app_context'; +import { DataSourceContext } from '../../../hooks/use_data_source'; +import { AIOPS_STORAGE_KEYS } from '../../../types/storage'; + +import { SpikeAnalysisTableRowStateProvider } from '../../spike_analysis_table/spike_analysis_table_row_provider'; + +import { ExplainLogRateSpikesContent } from './explain_log_rate_spikes_content'; + +const localStorage = new Storage(window.localStorage); + +export interface ExplainLogRateSpikesContentWrapperProps { + /** The data view to analyze. */ + dataView: DataView; + /** App dependencies */ + appDependencies: AiopsAppDependencies; + /** On global timefilter update */ + setGlobalState?: any; + /** Timestamp for start of initial analysis */ + initialAnalysisStart?: number; + timeRange?: { min: Moment; max: Moment }; + /** Elasticsearch query to pass to analysis endpoint */ + esSearchQuery?: estypes.QueryDslQueryContainer; +} + +export const ExplainLogRateSpikesContentWrapper: FC = ({ + dataView, + appDependencies, + setGlobalState, + initialAnalysisStart, + timeRange, + esSearchQuery, +}) => { + if (!dataView) return null; + + if (!dataView.isTimeBased()) { + return ( + +

+ {i18n.translate('xpack.aiops.index.dataViewNotBasedOnTimeSeriesNotificationDescription', { + defaultMessage: 'Log rate spike analysis only runs over time-based indices.', + })} +

+
+ ); + } + + const datePickerDeps = { + ...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']), + toMountPoint, + wrapWithTheme, + uiSettingsKeys: UI_SETTINGS, + }; + + return ( + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/index.ts b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/index.ts new file mode 100644 index 00000000000000..3d530abc985c55 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_content/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { ExplainLogRateSpikesContentWrapper } from './explain_log_rate_spikes_content_wrapper'; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default ExplainLogRateSpikesContentWrapper; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx index 1feb8548a23aab..fcd75a7a3c84a8 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx @@ -8,65 +8,33 @@ import React, { useCallback, useEffect, useState, FC } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiPageBody, - EuiPageSection, - EuiPanel, - EuiSpacer, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import type { WindowParameters } from '@kbn/aiops-utils'; -import type { SignificantTerm } from '@kbn/ml-agg-utils'; +import { EuiFlexGroup, EuiFlexItem, EuiPageBody, EuiPageSection, EuiSpacer } from '@elastic/eui'; + import { Filter, FilterStateStore, Query } from '@kbn/es-query'; -import { FormattedMessage } from '@kbn/i18n-react'; import { useUrlState, usePageUrlState } from '@kbn/ml-url-state'; 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 { useSearch } from '../../hooks/use_search'; 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 { ExplainLogRateSpikesAnalysis } from './explain_log_rate_spikes_analysis'; - -function getDocumentCountStatsSplitLabel( - significantTerm?: SignificantTerm, - group?: GroupTableItem -) { - if (significantTerm) { - return `${significantTerm?.fieldName}:${significantTerm?.fieldValue}`; - } else if (group) { - return i18n.translate('xpack.aiops.spikeAnalysisPage.documentCountStatsSplitGroupLabel', { - defaultMessage: 'Selected group', - }); - } -} +import { ExplainLogRateSpikesContent } from './explain_log_rate_spikes_content/explain_log_rate_spikes_content'; export const ExplainLogRateSpikesPage: FC = () => { const { data: dataService } = useAiopsAppContext(); const { dataView, savedSearch } = useDataSource(); - const { - currentSelectedSignificantTerm, - currentSelectedGroup, - setPinnedSignificantTerm, - setPinnedGroup, - setSelectedSignificantTerm, - setSelectedGroup, - } = useSpikeAnalysisTableRowContext(); + const { currentSelectedSignificantTerm, currentSelectedGroup } = + useSpikeAnalysisTableRowContext(); const [aiopsListState, setAiopsListState] = usePageUrlState( 'AIOPS_INDEX_VIEWER', @@ -106,26 +74,20 @@ export const ExplainLogRateSpikesPage: FC = () => { [selectedSavedSearch, aiopsListState, setAiopsListState] ); - const { - documentStats, - timefilter, - earliest, - latest, - searchQueryLanguage, - searchString, - searchQuery, - } = useData( - { selectedDataView: dataView, selectedSavedSearch }, + const { searchQueryLanguage, searchString, searchQuery } = useSearch( + { dataView, savedSearch }, + aiopsListState + ); + + const { timefilter } = useData( + dataView, 'explain_log_rage_spikes', - aiopsListState, + searchQuery, setGlobalState, currentSelectedSignificantTerm, currentSelectedGroup ); - const { sampleProbability, totalCount, documentCountStats, documentCountStatsCompare } = - documentStats; - useEffect( // TODO: Consolidate this hook/function with with Data visualizer's function clearFiltersOnLeave() { @@ -141,8 +103,6 @@ export const ExplainLogRateSpikesPage: FC = () => { [dataService.query.filterManager] ); - const [windowParameters, setWindowParameters] = useState(); - useEffect(() => { if (globalState?.time !== undefined) { timefilter.setTime({ @@ -168,14 +128,6 @@ export const ExplainLogRateSpikesPage: FC = () => { }); }, [dataService, searchQueryLanguage, searchString]); - function clearSelection() { - setWindowParameters(undefined); - setPinnedSignificantTerm(null); - setPinnedGroup(null); - setSelectedSignificantTerm(null); - setSelectedGroup(null); - } - return ( @@ -191,61 +143,11 @@ export const ExplainLogRateSpikesPage: FC = () => { setSearchParams={setSearchParams} />
- {documentCountStats !== undefined && ( - - - - - - )} - - - {earliest !== undefined && latest !== undefined && windowParameters !== undefined && ( - - )} - {windowParameters === undefined && ( - - - - } - titleSize="xs" - body={ -

- -

- } - data-test-subj="aiopsNoWindowParametersEmptyPrompt" - /> - )} -
-
+ diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx index 38d34983db1a38..80333d4b01b5b7 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -102,6 +102,7 @@ export const FieldFilterPopover: FC = ({ iconType="arrowDown" iconSide="right" iconSize="s" + color="text" > = ({ [cancelRequest, mounted] ); - const { - documentStats, - timefilter, - earliest, - latest, - searchQueryLanguage, - searchString, - searchQuery, - intervalMs, - forceRefresh, - } = useData( - { selectedDataView: dataView, selectedSavedSearch }, - 'log_categorization', + const { searchQueryLanguage, searchString, searchQuery } = useSearch( + { dataView, savedSearch: selectedSavedSearch }, aiopsListState, + true + ); + + const { documentStats, timefilter, earliest, latest, intervalMs, forceRefresh } = useData( + dataView, + 'log_categorization', + searchQuery, undefined, undefined, undefined, - BAR_TARGET, - true + BAR_TARGET ); const loadCategories = useCallback(async () => { diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index 963481181b035c..1dfc080b195a2e 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -27,6 +27,7 @@ import { usePageUrlState, useUrlState } from '@kbn/ml-url-state'; import { useDataSource } from '../../hooks/use_data_source'; import { useData } from '../../hooks/use_data'; +import { useSearch } from '../../hooks/use_search'; import type { SearchQueryLanguage } from '../../application/utils/search_utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { @@ -110,19 +111,15 @@ export const LogCategorizationPage: FC = () => { [selectedSavedSearch, aiopsListState, setAiopsListState] ); - const { - documentStats, - timefilter, - earliest, - latest, - searchQueryLanguage, - searchString, - searchQuery, - intervalMs, - } = useData( - { selectedDataView: dataView, selectedSavedSearch }, + const { searchQueryLanguage, searchString, searchQuery } = useSearch( + { dataView, savedSearch: selectedSavedSearch }, + aiopsListState + ); + + const { documentStats, timefilter, earliest, latest, intervalMs } = useData( + dataView, 'log_categorization', - aiopsListState, + searchQuery, setGlobalState, undefined, undefined, diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx index 2d6d96251d3037..6b7dda6b04e72c 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx @@ -240,7 +240,11 @@ export const SpikeAnalysisTable: FC = ({ name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', { defaultMessage: 'Actions', }), - actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction], + actions: [ + ...(viewInDiscoverAction ? [viewInDiscoverAction] : []), + ...(viewInLogPatternAnalysisAction ? [viewInLogPatternAnalysisAction] : []), + copyToClipBoardAction, + ], width: ACTIONS_COLUMN_WIDTH, valign: 'middle', }, diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index 104906f1a216d3..8f725e44896cc6 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -350,10 +350,14 @@ export const SpikeAnalysisGroupsTable: FC = ({ }, { 'data-test-subj': 'aiOpsSpikeAnalysisTableColumnAction', - name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', { + name: i18n.translate('xpack.aiops.spikeAnalysisGroupsTable.actionsColumnName', { defaultMessage: 'Actions', }), - actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction], + actions: [ + ...(viewInDiscoverAction ? [viewInDiscoverAction] : []), + ...(viewInLogPatternAnalysisAction ? [viewInLogPatternAnalysisAction] : []), + copyToClipBoardAction, + ], width: ACTIONS_COLUMN_WIDTH, valign: 'top', }, diff --git a/x-pack/plugins/aiops/public/hooks/use_data.ts b/x-pack/plugins/aiops/public/hooks/use_data.ts index e1cf21b6e520f4..70d5d2b9170141 100644 --- a/x-pack/plugins/aiops/public/hooks/use_data.ts +++ b/x-pack/plugins/aiops/public/hooks/use_data.ts @@ -7,19 +7,18 @@ import { useEffect, useMemo, useState } from 'react'; import { merge } from 'rxjs'; +import type { Moment } from 'moment'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { SignificantTerm } from '@kbn/ml-agg-utils'; -import type { SavedSearch } from '@kbn/discover-plugin/public'; import type { Dictionary } from '@kbn/ml-url-state'; import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { PLUGIN_ID } from '../../common'; import type { DocumentStatsSearchStrategyParams } from '../get_document_stats'; -import type { AiOpsIndexBasedAppState } from '../application/utils/url_state'; -import { getEsQueryFromSavedSearch } from '../application/utils/search_utils'; import type { GroupTableItem } from '../components/spike_analysis_table/types'; import { useTimeBuckets } from './use_time_buckets'; @@ -30,25 +29,16 @@ import { useDocumentCountStats } from './use_document_count_stats'; const DEFAULT_BAR_TARGET = 75; export const useData = ( - { - selectedDataView, - selectedSavedSearch, - }: { selectedDataView: DataView; selectedSavedSearch: SavedSearch | null }, + selectedDataView: DataView, contextId: string, - aiopsListState: AiOpsIndexBasedAppState, + searchQuery: estypes.QueryDslQueryContainer, onUpdate?: (params: Dictionary) => void, selectedSignificantTerm?: SignificantTerm, - selectedGroup?: GroupTableItem | null, + selectedGroup: GroupTableItem | null = null, barTarget: number = DEFAULT_BAR_TARGET, - readOnly: boolean = false + timeRange?: { min: Moment; max: Moment } ) => { - const { - executionContext, - uiSettings, - data: { - query: { filterManager }, - }, - } = useAiopsAppContext(); + const { executionContext } = useAiopsAppContext(); useExecutionContext(executionContext, { name: PLUGIN_ID, @@ -58,47 +48,6 @@ export const useData = ( const [lastRefresh, setLastRefresh] = useState(0); - /** Prepare required params to pass to search strategy **/ - const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { - const searchData = getEsQueryFromSavedSearch({ - dataView: selectedDataView, - uiSettings, - savedSearch: selectedSavedSearch, - filterManager, - }); - - if (searchData === undefined || aiopsListState.searchString !== '') { - if (aiopsListState.filters && readOnly === false) { - const globalFilters = filterManager?.getGlobalFilters(); - - if (filterManager) filterManager.setFilters(aiopsListState.filters); - if (globalFilters) filterManager?.addFilters(globalFilters); - } - return { - searchQuery: aiopsListState.searchQuery, - searchString: aiopsListState.searchString, - searchQueryLanguage: aiopsListState.searchQueryLanguage, - }; - } else { - return { - searchQuery: searchData.searchQuery, - searchString: searchData.searchString, - searchQueryLanguage: searchData.queryLanguage, - }; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - selectedSavedSearch?.id, - selectedDataView.id, - aiopsListState.searchString, - aiopsListState.searchQueryLanguage, - // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify({ - searchQuery: aiopsListState.searchQuery, - }), - lastRefresh, - ]); - const _timeBuckets = useTimeBuckets(); const timefilter = useTimefilter({ timeRangeSelector: selectedDataView?.timeFieldName !== undefined, @@ -106,7 +55,7 @@ export const useData = ( }); const fieldStatsRequest: DocumentStatsSearchStrategyParams | undefined = useMemo(() => { - const timefilterActiveBounds = timefilter.getActiveBounds(); + const timefilterActiveBounds = timeRange ?? timefilter.getActiveBounds(); if (timefilterActiveBounds !== undefined) { _timeBuckets.setInterval('auto'); _timeBuckets.setBounds(timefilterActiveBounds); @@ -122,7 +71,7 @@ export const useData = ( }; } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [lastRefresh, searchQuery]); + }, [lastRefresh, searchQuery, timeRange]); const overallStatsRequest = useMemo(() => { return fieldStatsRequest @@ -189,9 +138,6 @@ export const useData = ( /** End timestamp filter */ latest: fieldStatsRequest?.latest, intervalMs: fieldStatsRequest?.intervalMs, - searchQueryLanguage, - searchString, - searchQuery, forceRefresh: () => setLastRefresh(Date.now()), }; }; diff --git a/x-pack/plugins/aiops/public/hooks/use_search.ts b/x-pack/plugins/aiops/public/hooks/use_search.ts new file mode 100644 index 00000000000000..bd17e63b5de6d9 --- /dev/null +++ b/x-pack/plugins/aiops/public/hooks/use_search.ts @@ -0,0 +1,53 @@ +/* + * 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 type { SavedSearch } from '@kbn/discover-plugin/public'; + +import { getEsQueryFromSavedSearch } from '../application/utils/search_utils'; +import type { AiOpsIndexBasedAppState } from '../application/utils/url_state'; +import { useAiopsAppContext } from './use_aiops_app_context'; + +export const useSearch = ( + { dataView, savedSearch }: { dataView: DataView; savedSearch: SavedSearch | null }, + aiopsListState: AiOpsIndexBasedAppState, + readOnly: boolean = false +) => { + const { + uiSettings, + data: { + query: { filterManager }, + }, + } = useAiopsAppContext(); + + const searchData = getEsQueryFromSavedSearch({ + dataView, + uiSettings, + savedSearch, + filterManager, + }); + + if (searchData === undefined || (aiopsListState && aiopsListState.searchString !== '')) { + if (aiopsListState?.filters && readOnly === false) { + const globalFilters = filterManager?.getGlobalFilters(); + + if (filterManager) filterManager.setFilters(aiopsListState.filters); + if (globalFilters) filterManager?.addFilters(globalFilters); + } + return { + searchQuery: aiopsListState?.searchQuery, + searchString: aiopsListState?.searchString, + searchQueryLanguage: aiopsListState?.searchQueryLanguage, + }; + } else { + return { + searchQuery: searchData.searchQuery, + searchString: searchData.searchString, + searchQueryLanguage: searchData.queryLanguage, + }; + } +}; diff --git a/x-pack/plugins/aiops/public/index.ts b/x-pack/plugins/aiops/public/index.ts index 04b640c362878d..19076e293f55df 100755 --- a/x-pack/plugins/aiops/public/index.ts +++ b/x-pack/plugins/aiops/public/index.ts @@ -20,6 +20,7 @@ export type { ChangePointDetectionAppStateProps } from './components/change_poin export { ExplainLogRateSpikes, + ExplainLogRateSpikesContent, LogCategorization, ChangePointDetection, } from './shared_lazy_components'; diff --git a/x-pack/plugins/aiops/public/shared_lazy_components.tsx b/x-pack/plugins/aiops/public/shared_lazy_components.tsx index d981455d92848d..6e332b9e05f26a 100644 --- a/x-pack/plugins/aiops/public/shared_lazy_components.tsx +++ b/x-pack/plugins/aiops/public/shared_lazy_components.tsx @@ -9,6 +9,7 @@ import React, { FC, Suspense } from 'react'; import { EuiErrorBoundary, EuiSkeletonText } from '@elastic/eui'; import type { ExplainLogRateSpikesAppStateProps } from './components/explain_log_rate_spikes'; +import type { ExplainLogRateSpikesContentWrapperProps } from './components/explain_log_rate_spikes/explain_log_rate_spikes_content/explain_log_rate_spikes_content_wrapper'; import type { LogCategorizationAppStateProps } from './components/log_categorization'; import type { ChangePointDetectionAppStateProps } from './components/change_point_detection'; @@ -16,6 +17,10 @@ const ExplainLogRateSpikesAppStateLazy = React.lazy( () => import('./components/explain_log_rate_spikes') ); +const ExplainLogRateSpikesContentWrapperLazy = React.lazy( + () => import('./components/explain_log_rate_spikes/explain_log_rate_spikes_content') +); + const LazyWrapper: FC = ({ children }) => ( }>{children} @@ -32,6 +37,16 @@ export const ExplainLogRateSpikes: FC = (prop ); +/** + * Lazy-wrapped ExplainLogRateSpikesContentWrapperReact component + * @param {ExplainLogRateSpikesContentWrapperProps} props - properties specifying the data on which to run the analysis. + */ +export const ExplainLogRateSpikesContent: FC = (props) => ( + + + +); + const LogCategorizationAppStateLazy = React.lazy(() => import('./components/log_categorization')); /** diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index 52985cc799597c..8f2f0ff3299aac 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -150,14 +150,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.clickRerunAnalysisButton(true); } - await aiops.explainLogRateSpikesPage.assertProgressTitle('Progress: 100% — Done.'); + await aiops.explainLogRateSpikesPage.assertAnalysisComplete(); // The group switch should be disabled by default await aiops.explainLogRateSpikesPage.assertSpikeAnalysisGroupSwitchExists(false); if (!isTestDataExpectedWithSampleProbability(testData.expected)) { // Enabled grouping - await aiops.explainLogRateSpikesPage.clickSpikeAnalysisGroupSwitch(false); + await aiops.explainLogRateSpikesPage.clickSpikeAnalysisGroupSwitchOn(); await aiops.explainLogRateSpikesAnalysisGroupsTable.assertSpikeAnalysisTableExists(); diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 99538441be7cb9..b05cdb30b059b6 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -150,15 +150,11 @@ export function ExplainLogRateSpikesPageProvider({ }); }, - async clickSpikeAnalysisGroupSwitch(checked: boolean) { - await testSubjects.clickWhenNotDisabledWithoutRetry( - `aiopsExplainLogRateSpikesGroupSwitch${checked ? ' checked' : ''}` - ); + async clickSpikeAnalysisGroupSwitchOn() { + await testSubjects.clickWhenNotDisabledWithoutRetry('aiopsExplainLogRateSpikesGroupSwitchOn'); await retry.tryForTime(30 * 1000, async () => { - await testSubjects.existOrFail( - `aiopsExplainLogRateSpikesGroupSwitch${!checked ? ' checked' : ''}` - ); + await testSubjects.existOrFail('aiopsExplainLogRateSpikesGroupSwitch checked'); }); }, @@ -246,6 +242,14 @@ export function ExplainLogRateSpikesPageProvider({ }); }, + async assertAnalysisComplete() { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopsAnalysisComplete'); + const currentProgressTitle = await testSubjects.getVisibleText('aiopsAnalysisComplete'); + expect(currentProgressTitle).to.be('Analysis complete'); + }); + }, + async navigateToIndexPatternSelection() { await testSubjects.click('mlMainTab explainLogRateSpikes'); await testSubjects.existOrFail('mlPageSourceSelection');