mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[AIOps] Explain Log Rate Spikes: create shareable component containing only analysis (#158629)
## Summary
This PR exposes the `ExplainLogRateSpikesContent` shared component so
that it can be used independently of the search bar/datepicker
- The component accepts various external settings including a timerange
and query to run the analysis against.
- The `ExplainLogRateSpikesPage` component now uses the
`ExplainLogRateSpikesContent` component
- The `useData` hook has been simplified - the set up for the search
query has been extracted into a separate hook
<img width="1245" alt="image"
src="30dec4b2
-3162-4a39-b598-0dec70993fa7">
This is the first step for the Observability Alert Details Page
Integration.
Style edits:
The component now uses EUI's ResizeableContainer to allow the main
histogram to be sticky.
Also adds some style updates from
https://github.com/elastic/kibana/issues/156605
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
parent
0ff50e14cd
commit
03d4fe7515
19 changed files with 616 additions and 310 deletions
|
@ -12,6 +12,7 @@ import {
|
|||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiIconTip,
|
||||
EuiProgress,
|
||||
EuiText,
|
||||
|
@ -45,11 +46,69 @@ export const ProgressControls: FC<ProgressControlProps> = ({
|
|||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const runningProgressBarStyles = useAnimatedProgressBarBackground(euiTheme.colors.success);
|
||||
const analysisCompleteStyle = { display: 'none' };
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{!isRunning && (
|
||||
<EuiButton
|
||||
data-test-subj={`aiopsRerunAnalysisButton${shouldRerunAnalysis ? ' shouldRerun' : ''}`}
|
||||
size="s"
|
||||
onClick={onRefresh}
|
||||
color={shouldRerunAnalysis ? 'warning' : 'primary'}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.rerunAnalysisButtonTitle"
|
||||
defaultMessage="Run analysis"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{shouldRerunAnalysis && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiIconTip
|
||||
aria-label="Warning"
|
||||
type="warning"
|
||||
color="warning"
|
||||
content={i18n.translate('xpack.aiops.rerunAnalysisTooltipContent', {
|
||||
defaultMessage:
|
||||
'Analysis data may be out of date due to selection update. Rerun analysis.',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiButton>
|
||||
)}
|
||||
{isRunning && (
|
||||
<EuiButton data-test-subj="aiopsCancelAnalysisButton" size="s" onClick={onCancel}>
|
||||
<FormattedMessage id="xpack.aiops.cancelAnalysisButtonTitle" defaultMessage="Cancel" />
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{progress === 1 ? (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="checkInCircleFilled" color={euiTheme.colors.success} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} data-test-subj="aiopsAnalysisComplete">
|
||||
<small>
|
||||
{i18n.translate('xpack.aiops.analysisCompleteLabel', {
|
||||
defaultMessage: 'Analysis complete',
|
||||
})}
|
||||
</small>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
css={progress === 1 ? analysisCompleteStyle : undefined}
|
||||
>
|
||||
<EuiFlexItem data-test-subj="aiopProgressTitle">
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
|
@ -72,46 +131,6 @@ export const ProgressControls: FC<ProgressControlProps> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{!isRunning && (
|
||||
<EuiButton
|
||||
data-test-subj={`aiopsRerunAnalysisButton${shouldRerunAnalysis ? ' shouldRerun' : ''}`}
|
||||
size="s"
|
||||
onClick={onRefresh}
|
||||
color={shouldRerunAnalysis ? 'warning' : 'primary'}
|
||||
fill
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.rerunAnalysisButtonTitle"
|
||||
defaultMessage="Rerun analysis"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{shouldRerunAnalysis && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiIconTip
|
||||
aria-label="Warning"
|
||||
type="warning"
|
||||
color="warning"
|
||||
content={i18n.translate('xpack.aiops.rerunAnalysisTooltipContent', {
|
||||
defaultMessage:
|
||||
'Analysis data may be out of date due to selection update. Rerun analysis.',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiButton>
|
||||
)}
|
||||
{isRunning && (
|
||||
<EuiButton data-test-subj="aiopsCancelAnalysisButton" size="s" onClick={onCancel} fill>
|
||||
<FormattedMessage id="xpack.aiops.cancelAnalysisButtonTitle" defaultMessage="Cancel" />
|
||||
</EuiButton>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{children}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -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<DocumentCountChartProps> = ({
|
|||
interval,
|
||||
chartPointsSplitLabel,
|
||||
isBrushCleared,
|
||||
autoAnalysisStart,
|
||||
}) => {
|
||||
const { data, uiSettings, fieldFormats, charts } = useAiopsAppContext();
|
||||
|
||||
|
@ -201,39 +204,6 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = ({
|
|||
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<DocumentCountChartProps> = ({
|
|||
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<DocumentCountChartProps> = ({
|
|||
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}
|
||||
|
|
|
@ -33,6 +33,7 @@ export interface DocumentCountContentProps {
|
|||
totalCount: number;
|
||||
sampleProbability: number;
|
||||
windowParameters?: WindowParameters;
|
||||
incomingInitialAnalysisStart?: number;
|
||||
}
|
||||
|
||||
export const DocumentCountContent: FC<DocumentCountContentProps> = ({
|
||||
|
@ -44,8 +45,12 @@ export const DocumentCountContent: FC<DocumentCountContentProps> = ({
|
|||
totalCount,
|
||||
sampleProbability,
|
||||
windowParameters,
|
||||
incomingInitialAnalysisStart,
|
||||
}) => {
|
||||
const [isBrushCleared, setIsBrushCleared] = useState(true);
|
||||
const [initialAnalysisStart, setInitialAnalysisStart] = useState<number | undefined>(
|
||||
incomingInitialAnalysisStart
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsBrushCleared(windowParameters === undefined);
|
||||
|
@ -95,6 +100,7 @@ export const DocumentCountContent: FC<DocumentCountContentProps> = ({
|
|||
|
||||
function clearSelection() {
|
||||
setIsBrushCleared(true);
|
||||
setInitialAnalysisStart(undefined);
|
||||
clearSelectionHandler();
|
||||
}
|
||||
|
||||
|
@ -126,6 +132,7 @@ export const DocumentCountContent: FC<DocumentCountContentProps> = ({
|
|||
interval={documentCountStats.interval}
|
||||
chartPointsSplitLabel={documentCountStatsSplitLabel}
|
||||
isBrushCleared={isBrushCleared}
|
||||
autoAnalysisStart={initialAnalysisStart}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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<ExplainLogRateSpikesAnalysisProps>
|
|||
ApiExplainLogRateSpikes['body']['overrides'] | undefined
|
||||
>(undefined);
|
||||
const [shouldStart, setShouldStart] = useState(false);
|
||||
const [toggleIdSelected, setToggleIdSelected] = useState(resultsGroupedOffId);
|
||||
|
||||
const onGroupResultsToggle = (e: { target: { checked: React.SetStateAction<boolean> } }) => {
|
||||
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<ExplainLogRateSpikesAnalysisProps>
|
|||
// 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<ExplainLogRateSpikesAnalysisProps>
|
|||
// 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 (
|
||||
<div data-test-subj="aiopsExplainLogRateSpikesAnalysis">
|
||||
<ProgressControls
|
||||
|
@ -233,17 +264,24 @@ export const ExplainLogRateSpikesAnalysis: FC<ExplainLogRateSpikesAnalysisProps>
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow display="columnCompressedSwitch">
|
||||
<EuiSwitch
|
||||
data-test-subj={`aiopsExplainLogRateSpikesGroupSwitch${
|
||||
groupResults ? ' checked' : ''
|
||||
}`}
|
||||
disabled={disabledGroupResultsSwitch}
|
||||
showLabel={true}
|
||||
label={groupResultsMessage}
|
||||
checked={groupResults}
|
||||
onChange={onGroupResultsToggle}
|
||||
compressed
|
||||
/>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">{groupResultsMessage}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonGroup
|
||||
data-test-subj={`aiopsExplainLogRateSpikesGroupSwitch${
|
||||
groupResults ? ' checked' : ''
|
||||
}`}
|
||||
buttonSize="s"
|
||||
isDisabled={disabledGroupResultsSwitch}
|
||||
legend="Smart grouping"
|
||||
options={toggleButtons}
|
||||
idSelected={toggleIdSelected}
|
||||
onChange={onGroupResultsToggle}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -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<unknown>) => 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<ExplainLogRateSpikesContentProps> = ({
|
||||
dataView,
|
||||
setGlobalState,
|
||||
initialAnalysisStart,
|
||||
timeRange,
|
||||
esSearchQuery = DEFAULT_SEARCH_QUERY,
|
||||
}) => {
|
||||
const [windowParameters, setWindowParameters] = useState<WindowParameters | undefined>();
|
||||
|
||||
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 (
|
||||
<EuiResizableContainer direction="vertical">
|
||||
{(EuiResizablePanel, EuiResizableButton) => (
|
||||
<>
|
||||
<EuiResizablePanel
|
||||
mode={'collapsible'}
|
||||
initialSize={40}
|
||||
minSize={'20%'}
|
||||
tabIndex={0}
|
||||
paddingSize="s"
|
||||
>
|
||||
{documentCountStats !== undefined && (
|
||||
<DocumentCountContent
|
||||
brushSelectionUpdateHandler={setWindowParameters}
|
||||
clearSelectionHandler={clearSelection}
|
||||
documentCountStats={documentCountStats}
|
||||
documentCountStatsSplit={documentCountStatsCompare}
|
||||
documentCountStatsSplitLabel={getDocumentCountStatsSplitLabel(
|
||||
currentSelectedSignificantTerm,
|
||||
currentSelectedGroup
|
||||
)}
|
||||
totalCount={totalCount}
|
||||
sampleProbability={sampleProbability}
|
||||
windowParameters={windowParameters}
|
||||
incomingInitialAnalysisStart={initialAnalysisStart}
|
||||
/>
|
||||
)}
|
||||
<EuiHorizontalRule />
|
||||
</EuiResizablePanel>
|
||||
{/* <EuiResizableButton /> */}
|
||||
<EuiResizablePanel
|
||||
paddingSize="s"
|
||||
mode={'main'}
|
||||
initialSize={60}
|
||||
minSize={'40%'}
|
||||
tabIndex={0}
|
||||
>
|
||||
{earliest !== undefined && latest !== undefined && windowParameters !== undefined && (
|
||||
<ExplainLogRateSpikesAnalysis
|
||||
dataView={dataView}
|
||||
earliest={earliest}
|
||||
latest={latest}
|
||||
windowParameters={windowParameters}
|
||||
searchQuery={esSearchQuery}
|
||||
sampleProbability={sampleProbability}
|
||||
/>
|
||||
)}
|
||||
{windowParameters === undefined && (
|
||||
<EuiEmptyPrompt
|
||||
color="subdued"
|
||||
hasShadow={false}
|
||||
hasBorder={false}
|
||||
css={{ minWidth: '100%' }}
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.explainLogRateSpikesPage.emptyPromptTitle"
|
||||
defaultMessage="Click a spike in the histogram chart to start the analysis."
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.explainLogRateSpikesPage.emptyPromptBody"
|
||||
defaultMessage="The explain log rate spikes feature identifies statistically significant field/value combinations that contribute to a log rate spike."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoWindowParametersEmptyPrompt"
|
||||
/>
|
||||
)}
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</EuiResizableContainer>
|
||||
);
|
||||
};
|
|
@ -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<ExplainLogRateSpikesContentWrapperProps> = ({
|
||||
dataView,
|
||||
appDependencies,
|
||||
setGlobalState,
|
||||
initialAnalysisStart,
|
||||
timeRange,
|
||||
esSearchQuery,
|
||||
}) => {
|
||||
if (!dataView) return null;
|
||||
|
||||
if (!dataView.isTimeBased()) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.aiops.index.dataViewNotBasedOnTimeSeriesNotificationTitle', {
|
||||
defaultMessage: 'The data view "{dataViewTitle}" is not based on a time series.',
|
||||
values: { dataViewTitle: dataView.getName() },
|
||||
})}
|
||||
color="danger"
|
||||
iconType="warning"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('xpack.aiops.index.dataViewNotBasedOnTimeSeriesNotificationDescription', {
|
||||
defaultMessage: 'Log rate spike analysis only runs over time-based indices.',
|
||||
})}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
const datePickerDeps = {
|
||||
...pick(appDependencies, ['data', 'http', 'notifications', 'theme', 'uiSettings']),
|
||||
toMountPoint,
|
||||
wrapWithTheme,
|
||||
uiSettingsKeys: UI_SETTINGS,
|
||||
};
|
||||
|
||||
return (
|
||||
<AiopsAppContext.Provider value={appDependencies}>
|
||||
<UrlStateProvider>
|
||||
<DataSourceContext.Provider value={{ dataView, savedSearch: null }}>
|
||||
<SpikeAnalysisTableRowStateProvider>
|
||||
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
|
||||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<ExplainLogRateSpikesContent
|
||||
dataView={dataView}
|
||||
setGlobalState={setGlobalState}
|
||||
initialAnalysisStart={initialAnalysisStart}
|
||||
timeRange={timeRange}
|
||||
esSearchQuery={esSearchQuery}
|
||||
/>
|
||||
</DatePickerContextProvider>
|
||||
</StorageContextProvider>
|
||||
</SpikeAnalysisTableRowStateProvider>
|
||||
</DataSourceContext.Provider>
|
||||
</UrlStateProvider>
|
||||
</AiopsAppContext.Provider>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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 { EuiFlexGroup, EuiFlexItem, EuiPageBody, EuiPageSection, 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 { 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<AiOpsPageUrlState>(
|
||||
'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<WindowParameters | undefined>();
|
||||
|
||||
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 (
|
||||
<EuiPageBody data-test-subj="aiopsExplainLogRateSpikesPage" paddingSize="none" panelled={false}>
|
||||
<PageHeader />
|
||||
|
@ -191,61 +143,11 @@ export const ExplainLogRateSpikesPage: FC = () => {
|
|||
setSearchParams={setSearchParams}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{documentCountStats !== undefined && (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel paddingSize="m">
|
||||
<DocumentCountContent
|
||||
brushSelectionUpdateHandler={setWindowParameters}
|
||||
clearSelectionHandler={clearSelection}
|
||||
documentCountStats={documentCountStats}
|
||||
documentCountStatsSplit={documentCountStatsCompare}
|
||||
documentCountStatsSplitLabel={getDocumentCountStatsSplitLabel(
|
||||
currentSelectedSignificantTerm,
|
||||
currentSelectedGroup
|
||||
)}
|
||||
totalCount={totalCount}
|
||||
sampleProbability={sampleProbability}
|
||||
windowParameters={windowParameters}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiPanel paddingSize="m">
|
||||
{earliest !== undefined && latest !== undefined && windowParameters !== undefined && (
|
||||
<ExplainLogRateSpikesAnalysis
|
||||
dataView={dataView}
|
||||
earliest={earliest}
|
||||
latest={latest}
|
||||
windowParameters={windowParameters}
|
||||
searchQuery={searchQuery}
|
||||
sampleProbability={sampleProbability}
|
||||
/>
|
||||
)}
|
||||
{windowParameters === undefined && (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.explainLogRateSpikesPage.emptyPromptTitle"
|
||||
defaultMessage="Click a spike in the histogram chart to start the analysis."
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.explainLogRateSpikesPage.emptyPromptBody"
|
||||
defaultMessage="The explain log rate spikes feature identifies statistically significant field/value combinations that contribute to a log rate spike."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
data-test-subj="aiopsNoWindowParametersEmptyPrompt"
|
||||
/>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<ExplainLogRateSpikesContent
|
||||
dataView={dataView}
|
||||
setGlobalState={setGlobalState}
|
||||
esSearchQuery={searchQuery}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageSection>
|
||||
</EuiPageBody>
|
||||
|
|
|
@ -102,6 +102,7 @@ export const FieldFilterPopover: FC<FieldFilterPopoverProps> = ({
|
|||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
iconSize="s"
|
||||
color="text"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.aiops.explainLogRateSpikesPage.fieldFilterButtonLabel"
|
||||
|
|
|
@ -22,6 +22,7 @@ import { buildEmptyFilter, Filter } from '@kbn/es-query';
|
|||
|
||||
import { usePageUrlState } from '@kbn/ml-url-state';
|
||||
import { useData } from '../../hooks/use_data';
|
||||
import { useSearch } from '../../hooks/use_search';
|
||||
import { useCategorizeRequest } from './use_categorize_request';
|
||||
import type { EventRate, Category, SparkLinesPerCategory } from './use_categorize_request';
|
||||
import { CategoryTable } from './category_table';
|
||||
|
@ -91,27 +92,22 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
|
|||
[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,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
BAR_TARGET,
|
||||
true
|
||||
);
|
||||
|
||||
const { documentStats, timefilter, earliest, latest, intervalMs, forceRefresh } = useData(
|
||||
dataView,
|
||||
'log_categorization',
|
||||
searchQuery,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
BAR_TARGET
|
||||
);
|
||||
|
||||
const loadCategories = useCallback(async () => {
|
||||
const { title: index, timeFieldName: timeField } = dataView;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -240,7 +240,11 @@ export const SpikeAnalysisTable: FC<SpikeAnalysisTableProps> = ({
|
|||
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',
|
||||
},
|
||||
|
|
|
@ -350,10 +350,14 @@ export const SpikeAnalysisGroupsTable: FC<SpikeAnalysisTableProps> = ({
|
|||
},
|
||||
{
|
||||
'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',
|
||||
},
|
||||
|
|
|
@ -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<unknown>) => 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()),
|
||||
};
|
||||
};
|
||||
|
|
53
x-pack/plugins/aiops/public/hooks/use_search.ts
Normal file
53
x-pack/plugins/aiops/public/hooks/use_search.ts
Normal file
|
@ -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,
|
||||
};
|
||||
}
|
||||
};
|
|
@ -20,6 +20,7 @@ export type { ChangePointDetectionAppStateProps } from './components/change_poin
|
|||
|
||||
export {
|
||||
ExplainLogRateSpikes,
|
||||
ExplainLogRateSpikesContent,
|
||||
LogCategorization,
|
||||
ChangePointDetection,
|
||||
} from './shared_lazy_components';
|
||||
|
|
|
@ -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 }) => (
|
||||
<EuiErrorBoundary>
|
||||
<Suspense fallback={<EuiSkeletonText lines={3} />}>{children}</Suspense>
|
||||
|
@ -32,6 +37,16 @@ export const ExplainLogRateSpikes: FC<ExplainLogRateSpikesAppStateProps> = (prop
|
|||
</LazyWrapper>
|
||||
);
|
||||
|
||||
/**
|
||||
* Lazy-wrapped ExplainLogRateSpikesContentWrapperReact component
|
||||
* @param {ExplainLogRateSpikesContentWrapperProps} props - properties specifying the data on which to run the analysis.
|
||||
*/
|
||||
export const ExplainLogRateSpikesContent: FC<ExplainLogRateSpikesContentWrapperProps> = (props) => (
|
||||
<LazyWrapper>
|
||||
<ExplainLogRateSpikesContentWrapperLazy {...props} />
|
||||
</LazyWrapper>
|
||||
);
|
||||
|
||||
const LogCategorizationAppStateLazy = React.lazy(() => import('./components/log_categorization'));
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue