[ML] AIOps Log Rate Analysis: Replace custom global state management with Redux Toolkit. (#180969)

This commit is contained in:
Walter Rafelsberger 2024-06-13 11:25:22 +02:00 committed by GitHub
parent 4bc122703c
commit 211a11bb72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 1011 additions and 963 deletions

View file

@ -9,15 +9,7 @@ export { DualBrush, DualBrushAnnotation } from './src/dual_brush';
export { ProgressControls } from './src/progress_controls';
export {
DocumentCountChart,
DocumentCountChartWithAutoAnalysisStart,
DocumentCountChartRedux,
type BrushSettings,
type BrushSelectionUpdateHandler,
} from './src/document_count_chart';
export type { DocumentCountChartProps } from './src/document_count_chart';
export {
useLogRateAnalysisStateContext,
LogRateAnalysisStateProvider,
type GroupTableItem,
type GroupTableItemGroup,
type TableItemAction,
} from './src/log_rate_analysis_state_provider';

View file

@ -29,10 +29,10 @@ import {
getSnappedWindowParameters,
getWindowParametersForTrigger,
type DocumentCountStatsChangePoint,
type LogRateAnalysisType,
type LogRateHistogramItem,
type WindowParameters,
} from '@kbn/aiops-log-rate-analysis';
import { type BrushSelectionUpdatePayload } from '@kbn/aiops-log-rate-analysis/state';
import { MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
@ -40,8 +40,6 @@ import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { DualBrush, DualBrushAnnotation } from '../..';
import { useLogRateAnalysisStateContext } from '../log_rate_analysis_state_provider';
import { BrushBadge } from './brush_badge';
declare global {
@ -76,24 +74,19 @@ export interface BrushSettings {
badgeWidth?: number;
}
/**
* Callback function which gets called when the brush selection has changed
*
* @param windowParameters Baseline and deviation time ranges.
* @param force Force update
* @param logRateAnalysisType `spike` or `dip` based on median log rate bucket size
*/
export type BrushSelectionUpdateHandler = (
windowParameters: WindowParameters,
force: boolean,
logRateAnalysisType: LogRateAnalysisType
) => void;
/**
* Callback to set the autoRunAnalysis flag
*/
type SetAutoRunAnalysisFn = (isAutoRun: boolean) => void;
/**
* Brush selection update handler
*/
type BrushSelectionUpdateHandler = (
/** Payload for the brush selection update */
d: BrushSelectionUpdatePayload
) => void;
/**
* Props for document count chart
*/
@ -126,7 +119,7 @@ export interface DocumentCountChartProps {
/** Whether or not brush has been reset */
isBrushCleared: boolean;
/** Callback to set the autoRunAnalysis flag */
setAutoRunAnalysis?: SetAutoRunAnalysisFn;
setAutoRunAnalysisFn?: SetAutoRunAnalysisFn;
/** Timestamp for start of initial analysis */
autoAnalysisStart?: number | WindowParameters;
/** Optional style to override bar chart */
@ -190,7 +183,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
interval,
chartPointsSplitLabel,
isBrushCleared,
setAutoRunAnalysis,
setAutoRunAnalysisFn,
autoAnalysisStart,
barColorOverride,
barStyleAccessor,
@ -315,7 +308,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
windowParameters === undefined &&
adjustedChartPoints !== undefined
) {
if (setAutoRunAnalysis) {
if (setAutoRunAnalysisFn) {
const autoRun =
typeof startRange !== 'number' ||
(typeof startRange === 'number' &&
@ -323,7 +316,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
startRange >= changePoint.startTs &&
startRange <= changePoint.endTs);
setAutoRunAnalysis(autoRun);
setAutoRunAnalysisFn(autoRun);
}
const wp = getWindowParametersForTrigger(
@ -338,11 +331,11 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
setWindowParameters(wpSnap);
if (brushSelectionUpdateHandler !== undefined) {
brushSelectionUpdateHandler(
wpSnap,
true,
getLogRateAnalysisType(adjustedChartPoints, wpSnap)
);
brushSelectionUpdateHandler({
windowParameters: wpSnap,
force: true,
analysisType: getLogRateAnalysisType(adjustedChartPoints, wpSnap),
});
}
}
}
@ -354,7 +347,7 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
timeRangeLatest,
snapTimestamps,
originalWindowParameters,
setAutoRunAnalysis,
setAutoRunAnalysisFn,
setWindowParameters,
brushSelectionUpdateHandler,
adjustedChartPoints,
@ -395,7 +388,11 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
}
setWindowParameters(wp);
setWindowParametersAsPixels(wpPx);
brushSelectionUpdateHandler(wp, false, getLogRateAnalysisType(adjustedChartPoints, wp));
brushSelectionUpdateHandler({
windowParameters: wp,
force: false,
analysisType: getLogRateAnalysisType(adjustedChartPoints, wp),
});
}
const [mlBrushWidth, setMlBrushWidth] = useState<number>();
@ -556,24 +553,3 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
</>
);
};
/**
* Functional component that renders a `DocumentCountChart` with additional properties
* managed by the log rate analysis state. It leverages the `useLogRateAnalysisStateContext`
* to acquire state variables like `initialAnalysisStart` and functions such as
* `setAutoRunAnalysis`. These values are then passed as props to the `DocumentCountChart`.
*
* @param props - The properties passed to the DocumentCountChart component.
* @returns The DocumentCountChart component enhanced with automatic analysis start capabilities.
*/
export const DocumentCountChartWithAutoAnalysisStart: FC<DocumentCountChartProps> = (props) => {
const { initialAnalysisStart, setAutoRunAnalysis } = useLogRateAnalysisStateContext();
return (
<DocumentCountChart
{...props}
autoAnalysisStart={initialAnalysisStart}
setAutoRunAnalysis={setAutoRunAnalysis}
/>
);
};

View file

@ -0,0 +1,126 @@
/*
* 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, { memo, type FC } from 'react';
import { i18n } from '@kbn/i18n';
import type { LogRateHistogramItem } from '@kbn/aiops-log-rate-analysis';
import {
brushSelectionUpdate,
setAutoRunAnalysis,
useAppSelector,
useAppDispatch,
useCurrentSelectedGroup,
useCurrentSelectedSignificantItem,
type GroupTableItem,
} from '@kbn/aiops-log-rate-analysis/state';
import type { SignificantItem } from '@kbn/ml-agg-utils';
import { DocumentCountChart, type DocumentCountChartProps } from './document_count_chart';
function getDocumentCountStatsSplitLabel(
significantItem?: SignificantItem,
group?: GroupTableItem
): string {
if (significantItem) {
return `${significantItem?.fieldName}:${significantItem?.fieldValue}`;
} else if (group) {
return i18n.translate('xpack.aiops.logRateAnalysis.page.documentCountStatsSplitGroupLabel', {
defaultMessage: 'Selected group',
});
} else {
return '';
}
}
type DocumentCountChartReduxProps = Omit<
DocumentCountChartProps,
| 'chartPointsSplitLabel'
| 'autoAnalysisStart'
| 'chartPoints'
| 'chartPointsSplit'
| 'documentStats'
| 'isBrushCleared'
| 'brushSelectionUpdateHandler'
| 'timeRangeEarliest'
| 'timeRangeLatest'
| 'interval'
>;
/**
* Functional component that renders a `DocumentCountChart` with additional properties
* managed by the log rate analysis state. It leverages the `LogRateAnalysisReduxProvider`
* to acquire state variables like `initialAnalysisStart` and functions such as
* `setAutoRunAnalysis`. These values are then passed as props to the `DocumentCountChart`.
* This wrapper component is necessary since the `DocumentCountChart` component is
* also used for log pattern analysis which doesn't use redux.
*
* @param props - The properties passed to the DocumentCountChart component.
* @returns The DocumentCountChart component enhanced with automatic analysis start capabilities.
*/
export const DocumentCountChartRedux: FC<DocumentCountChartReduxProps> = memo((props) => {
const dispatch = useAppDispatch();
const currentSelectedGroup = useCurrentSelectedGroup();
const currentSelectedSignificantItem = useCurrentSelectedSignificantItem();
const { documentStats, initialAnalysisStart, isBrushCleared } = useAppSelector(
(s) => s.logRateAnalysis
);
const { documentCountStats, documentCountStatsCompare } = documentStats;
const bucketTimestamps = Object.keys(documentCountStats?.buckets ?? {}).map((time) => +time);
const splitBucketTimestamps = Object.keys(documentCountStatsCompare?.buckets ?? {}).map(
(time) => +time
);
const timeRangeEarliest = Math.min(...[...bucketTimestamps, ...splitBucketTimestamps]);
const timeRangeLatest = Math.max(...[...bucketTimestamps, ...splitBucketTimestamps]);
if (
documentCountStats === undefined ||
documentCountStats.buckets === undefined ||
documentCountStats.interval === undefined ||
timeRangeEarliest === undefined ||
timeRangeLatest === undefined
) {
return null;
}
const documentCountStatsSplitLabel = getDocumentCountStatsSplitLabel(
currentSelectedSignificantItem,
currentSelectedGroup
);
const chartPoints: LogRateHistogramItem[] = Object.entries(documentCountStats.buckets).map(
([time, value]) => ({
time: +time,
value,
})
);
let chartPointsSplit: LogRateHistogramItem[] | undefined;
if (documentCountStatsCompare?.buckets !== undefined) {
chartPointsSplit = Object.entries(documentCountStatsCompare?.buckets).map(([time, value]) => ({
time: +time,
value,
}));
}
return (
<DocumentCountChart
{...props}
chartPointsSplitLabel={documentCountStatsSplitLabel}
timeRangeEarliest={timeRangeEarliest}
timeRangeLatest={timeRangeLatest}
interval={documentCountStats.interval}
chartPoints={chartPoints}
chartPointsSplit={chartPointsSplit}
autoAnalysisStart={initialAnalysisStart}
brushSelectionUpdateHandler={(d) => dispatch(brushSelectionUpdate(d))}
isBrushCleared={isBrushCleared}
setAutoRunAnalysisFn={(d: boolean) => dispatch(setAutoRunAnalysis(d))}
/>
);
});

View file

@ -5,12 +5,6 @@
* 2.0.
*/
export {
DocumentCountChart,
DocumentCountChartWithAutoAnalysisStart,
} from './document_count_chart';
export type {
BrushSelectionUpdateHandler,
BrushSettings,
DocumentCountChartProps,
} from './document_count_chart';
export { DocumentCountChart } from './document_count_chart';
export { DocumentCountChartRedux } from './document_count_chart_redux';
export type { BrushSettings, DocumentCountChartProps } from './document_count_chart';

View file

@ -1,12 +0,0 @@
/*
* 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.
*/
export {
useLogRateAnalysisStateContext,
LogRateAnalysisStateProvider,
} from './log_rate_analysis_state_provider';
export type { GroupTableItem, GroupTableItemGroup, TableItemAction } from './types';

View file

@ -1,169 +0,0 @@
/*
* 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, {
createContext,
useContext,
useMemo,
useState,
type FC,
type PropsWithChildren,
type Dispatch,
type SetStateAction,
} from 'react';
import type { SignificantItem } from '@kbn/ml-agg-utils';
import type { WindowParameters } from '@kbn/aiops-log-rate-analysis';
import type { GroupTableItem } from './types';
type InitialAnalysisStart = number | WindowParameters | undefined;
type SignificantItemOrNull = SignificantItem | null;
type GroupOrNull = GroupTableItem | null;
interface LogRateAnalysisState {
autoRunAnalysis: boolean;
setAutoRunAnalysis: Dispatch<SetStateAction<boolean>>;
initialAnalysisStart: InitialAnalysisStart;
setInitialAnalysisStart: Dispatch<SetStateAction<InitialAnalysisStart>>;
pinnedSignificantItem: SignificantItemOrNull;
setPinnedSignificantItem: Dispatch<SetStateAction<SignificantItemOrNull>>;
pinnedGroup: GroupOrNull;
setPinnedGroup: Dispatch<SetStateAction<GroupOrNull>>;
selectedSignificantItem: SignificantItemOrNull;
setSelectedSignificantItem: Dispatch<SetStateAction<SignificantItemOrNull>>;
selectedGroup: GroupOrNull;
setSelectedGroup: Dispatch<SetStateAction<GroupOrNull>>;
currentSelectedSignificantItem?: SignificantItem;
currentSelectedGroup?: GroupTableItem;
clearAllRowState: () => void;
}
const LogRateAnalysisStateContext = createContext<LogRateAnalysisState | undefined>(undefined);
/**
* Props for LogRateAnalysisStateProvider.
*/
interface LogRateAnalysisStateProviderProps {
/** The parameters to be used to trigger an analysis. */
initialAnalysisStart?: InitialAnalysisStart;
}
/**
* Context provider component that manages and provides global state for Log Rate Analysis.
* This provider handles several pieces of state important for controlling and displaying
* log rate analysis data, such as the control of automatic analysis runs, and the management
* of both pinned and selected significant items and groups.
*
* The state includes mechanisms for setting initial analysis parameters, toggling analysis,
* and managing the current selection and pinned state of significant items and groups.
*
* @param props - Props object containing initial settings for the analysis,
* including children components to be wrapped by the Provider.
* @returns A context provider wrapping children with access to log rate analysis state.
*/
export const LogRateAnalysisStateProvider: FC<
PropsWithChildren<LogRateAnalysisStateProviderProps>
> = (props) => {
const { children, initialAnalysisStart: incomingInitialAnalysisStart } = props;
const [autoRunAnalysis, setAutoRunAnalysis] = useState(true);
const [initialAnalysisStart, setInitialAnalysisStart] = useState<
number | WindowParameters | undefined
>(incomingInitialAnalysisStart);
// Row state that will be shared with all components
const [pinnedSignificantItem, setPinnedSignificantItem] = useState<SignificantItemOrNull>(null);
const [pinnedGroup, setPinnedGroup] = useState<GroupOrNull>(null);
const [selectedSignificantItem, setSelectedSignificantItem] =
useState<SignificantItemOrNull>(null);
const [selectedGroup, setSelectedGroup] = useState<GroupOrNull>(null);
// If a row is pinned, still overrule with a potentially hovered row.
const currentSelectedSignificantItem = useMemo(() => {
if (selectedSignificantItem) {
return selectedSignificantItem;
} else if (pinnedSignificantItem) {
return pinnedSignificantItem;
}
}, [pinnedSignificantItem, selectedSignificantItem]);
// If a group is pinned, still overrule with a potentially hovered group.
const currentSelectedGroup = useMemo(() => {
if (selectedGroup) {
return selectedGroup;
} else if (pinnedGroup) {
return pinnedGroup;
}
}, [selectedGroup, pinnedGroup]);
const contextValue: LogRateAnalysisState = useMemo(
() => ({
autoRunAnalysis,
setAutoRunAnalysis,
initialAnalysisStart,
setInitialAnalysisStart,
pinnedSignificantItem,
setPinnedSignificantItem,
pinnedGroup,
setPinnedGroup,
selectedSignificantItem,
setSelectedSignificantItem,
selectedGroup,
setSelectedGroup,
currentSelectedSignificantItem,
currentSelectedGroup,
clearAllRowState: () => {
setPinnedSignificantItem(null);
setPinnedGroup(null);
setSelectedSignificantItem(null);
setSelectedGroup(null);
},
}),
[
autoRunAnalysis,
setAutoRunAnalysis,
initialAnalysisStart,
setInitialAnalysisStart,
pinnedSignificantItem,
setPinnedSignificantItem,
pinnedGroup,
setPinnedGroup,
selectedSignificantItem,
setSelectedSignificantItem,
selectedGroup,
setSelectedGroup,
currentSelectedSignificantItem,
currentSelectedGroup,
]
);
return (
// Provider managing the state
<LogRateAnalysisStateContext.Provider value={contextValue}>
{children}
</LogRateAnalysisStateContext.Provider>
);
};
/**
* Custom hook for accessing the state of log rate analysis from the LogRateAnalysisStateContext.
* This hook must be used within a component that is a descendant of the LogRateAnalysisStateContext provider.
*
* @returns The current state of the log rate analysis.
* @throws Throws an error if the hook is used outside of its Provider context.
*/
export const useLogRateAnalysisStateContext = () => {
const logRateAnalysisState = useContext(LogRateAnalysisStateContext);
// If `undefined`, throw an error.
if (logRateAnalysisState === undefined) {
throw new Error('useLogRateAnalysisStateContext was used outside of its Provider');
}
return logRateAnalysisState;
};

View file

@ -30,12 +30,12 @@ import { useAnimatedProgressBarBackground } from './use_animated_progress_bar_ba
* Props for ProgressControlProps
*/
interface ProgressControlProps {
isBrushCleared: boolean;
progress: number;
progressMessage: string;
onRefresh: () => void;
onCancel: () => void;
onReset: () => void;
isBrushCleared: boolean;
isRunning: boolean;
shouldRerunAnalysis: boolean;
runAnalysisDisabled?: boolean;
@ -43,7 +43,7 @@ interface ProgressControlProps {
/**
* ProgressControls React Component
* Component with ability to Run & cancel analysis
* Component with ability to run & cancel analysis
* by default uses `Baseline` and `Deviation` for the badge name
*
* @param props ProgressControls component props
@ -52,12 +52,12 @@ interface ProgressControlProps {
export const ProgressControls: FC<PropsWithChildren<ProgressControlProps>> = (props) => {
const {
children,
isBrushCleared,
progress,
progressMessage,
onRefresh,
onCancel,
onReset,
isBrushCleared,
isRunning,
shouldRerunAnalysis,
runAnalysisDisabled = false,
@ -66,6 +66,7 @@ export const ProgressControls: FC<PropsWithChildren<ProgressControlProps>> = (pr
const progressOutput = Math.round(progress * 100);
const { euiTheme } = useEuiTheme();
const runningProgressBarStyles = useAnimatedProgressBarBackground(euiTheme.colors.success);
const analysisCompleteStyle = { display: 'none' };

View file

@ -1,182 +0,0 @@
/*
* 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 {
SignificantItem,
SignificantItemHistogram,
SignificantItemGroup,
SignificantItemGroupHistogram,
} from '@kbn/ml-agg-utils';
export const API_ACTION_NAME = {
/** @since API v2 */
ADD_SIGNIFICANT_ITEMS: 'add_significant_items',
/** @since API v2 */
ADD_SIGNIFICANT_ITEMS_HISTOGRAM: 'add_significant_items_histogram',
/** @since API v2 */
ADD_SIGNIFICANT_ITEMS_GROUP: 'add_significant_items_group',
/** @since API v2 */
ADD_SIGNIFICANT_ITEMS_GROUP_HISTOGRAM: 'add_significant_items_group_histogram',
ADD_SIGNIFICANT_TERMS_GROUP_HISTOGRAM: 'add_significant_terms_group_histogram',
ADD_ERROR: 'add_error',
PING: 'ping',
RESET_ALL: 'reset_all',
RESET_ERRORS: 'reset_errors',
RESET_GROUPS: 'reset_groups',
SET_ZERO_DOCS_FALLBACK: 'set_zero_docs_fallback',
UPDATE_LOADING_STATE: 'update_loading_state',
} as const;
export type ApiActionName = typeof API_ACTION_NAME[keyof typeof API_ACTION_NAME];
interface ApiActionAddSignificantItems {
type: typeof API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS;
payload: SignificantItem[];
}
export function addSignificantItemsAction(
payload: ApiActionAddSignificantItems['payload']
): ApiActionAddSignificantItems {
return {
type: API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS,
payload,
};
}
interface ApiActionAddSignificantItemsHistogram {
type: typeof API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_HISTOGRAM;
payload: SignificantItemHistogram[];
}
export function addSignificantItemsHistogramAction(
payload: ApiActionAddSignificantItemsHistogram['payload']
): ApiActionAddSignificantItemsHistogram {
return {
type: API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_HISTOGRAM,
payload,
};
}
interface ApiActionAddSignificantItemsGroup {
type: typeof API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_GROUP;
payload: SignificantItemGroup[];
}
export function addSignificantItemsGroupAction(
payload: ApiActionAddSignificantItemsGroup['payload']
): ApiActionAddSignificantItemsGroup {
return {
type: API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_GROUP,
payload,
};
}
interface ApiActionAddSignificantItemsGroupHistogram {
type: typeof API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_GROUP_HISTOGRAM;
payload: SignificantItemGroupHistogram[];
}
export function addSignificantItemsGroupHistogramAction(
payload: ApiActionAddSignificantItemsGroupHistogram['payload']
): ApiActionAddSignificantItemsGroupHistogram {
return {
type: API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_GROUP_HISTOGRAM,
payload,
};
}
interface ApiActionAddError {
type: typeof API_ACTION_NAME.ADD_ERROR;
payload: string;
}
export function addErrorAction(payload: ApiActionAddError['payload']): ApiActionAddError {
return {
type: API_ACTION_NAME.ADD_ERROR,
payload,
};
}
interface ApiActionResetErrors {
type: typeof API_ACTION_NAME.RESET_ERRORS;
}
export function resetErrorsAction() {
return {
type: API_ACTION_NAME.RESET_ERRORS,
};
}
interface ApiActionPing {
type: typeof API_ACTION_NAME.PING;
}
export function pingAction(): ApiActionPing {
return { type: API_ACTION_NAME.PING };
}
interface ApiActionResetAll {
type: typeof API_ACTION_NAME.RESET_ALL;
}
export function resetAllAction(): ApiActionResetAll {
return { type: API_ACTION_NAME.RESET_ALL };
}
interface ApiActionResetGroups {
type: typeof API_ACTION_NAME.RESET_GROUPS;
}
export function resetGroupsAction(): ApiActionResetGroups {
return { type: API_ACTION_NAME.RESET_GROUPS };
}
interface ApiActionUpdateLoadingState {
type: typeof API_ACTION_NAME.UPDATE_LOADING_STATE;
payload: {
ccsWarning: boolean;
loaded: number;
loadingState: string;
remainingFieldCandidates?: string[];
groupsMissing?: boolean;
};
}
export function updateLoadingStateAction(
payload: ApiActionUpdateLoadingState['payload']
): ApiActionUpdateLoadingState {
return {
type: API_ACTION_NAME.UPDATE_LOADING_STATE,
payload,
};
}
interface ApiActionSetZeroDocsFallback {
type: typeof API_ACTION_NAME.SET_ZERO_DOCS_FALLBACK;
payload: boolean;
}
export function setZeroDocsFallback(
payload: ApiActionSetZeroDocsFallback['payload']
): ApiActionSetZeroDocsFallback {
return {
type: API_ACTION_NAME.SET_ZERO_DOCS_FALLBACK,
payload,
};
}
export type AiopsLogRateAnalysisApiAction =
| ApiActionAddSignificantItems
| ApiActionAddSignificantItemsGroup
| ApiActionAddSignificantItemsHistogram
| ApiActionAddSignificantItemsGroupHistogram
| ApiActionAddError
| ApiActionPing
| ApiActionResetAll
| ApiActionResetErrors
| ApiActionResetGroups
| ApiActionUpdateLoadingState
| ApiActionSetZeroDocsFallback;

View file

@ -9,19 +9,20 @@ import { significantTerms } from '@kbn/aiops-test-utils/artificial_logs/signific
import { finalSignificantItemGroups } from '@kbn/aiops-test-utils/artificial_logs/final_significant_item_groups';
import {
addSignificantItemsAction,
addSignificantItemsGroupAction,
resetAllAction,
resetGroupsAction,
updateLoadingStateAction,
} from './actions';
import { initialState, streamReducer } from './stream_reducer';
addSignificantItems,
addSignificantItemsGroup,
resetAll,
resetGroups,
updateLoadingState,
getDefaultState,
streamReducer,
} from './stream_reducer';
describe('streamReducer', () => {
it('updates loading state', () => {
const state = streamReducer(
initialState,
updateLoadingStateAction({ ccsWarning: true, loaded: 50, loadingState: 'Loaded 50%' })
getDefaultState(),
updateLoadingState({ ccsWarning: true, loaded: 50, loadingState: 'Loaded 50%' })
);
expect(state).toEqual({
@ -37,8 +38,8 @@ describe('streamReducer', () => {
it('adds significant item, then resets all state again', () => {
const state1 = streamReducer(
initialState,
addSignificantItemsAction([
getDefaultState(),
addSignificantItems([
{
key: 'the-field-name:the-field-value',
type: 'keyword',
@ -57,26 +58,23 @@ describe('streamReducer', () => {
expect(state1.significantItems).toHaveLength(1);
const state2 = streamReducer(state1, resetAllAction());
const state2 = streamReducer(state1, resetAll());
expect(state2.significantItems).toHaveLength(0);
});
it('adds significant items and groups, then resets groups only', () => {
const state1 = streamReducer(initialState, addSignificantItemsAction(significantTerms));
const state1 = streamReducer(getDefaultState(), addSignificantItems(significantTerms));
expect(state1.significantItems).toHaveLength(4);
expect(state1.significantItemsGroups).toHaveLength(0);
const state2 = streamReducer(
state1,
addSignificantItemsGroupAction(finalSignificantItemGroups)
);
const state2 = streamReducer(state1, addSignificantItemsGroup(finalSignificantItemGroups));
expect(state2.significantItems).toHaveLength(4);
expect(state2.significantItemsGroups).toHaveLength(4);
const state3 = streamReducer(state2, resetGroupsAction());
const state3 = streamReducer(state2, resetGroups());
expect(state3.significantItems).toHaveLength(4);
expect(state3.significantItemsGroups).toHaveLength(0);

View file

@ -5,10 +5,15 @@
* 2.0.
*/
import type { SignificantItem, SignificantItemGroup } from '@kbn/ml-agg-utils';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { AiopsLogRateAnalysisApiAction } from './actions';
import { API_ACTION_NAME } from './actions';
import type {
SignificantItem,
SignificantItemGroup,
SignificantItemHistogram,
SignificantItemGroupHistogram,
} from '@kbn/ml-agg-utils';
export interface StreamState {
ccsWarning: boolean;
@ -22,7 +27,7 @@ export interface StreamState {
zeroDocsFallback: boolean;
}
export const initialState: StreamState = {
export const getDefaultState = (): StreamState => ({
ccsWarning: false,
significantItems: [],
significantItemsGroups: [],
@ -30,50 +35,87 @@ export const initialState: StreamState = {
loaded: 0,
loadingState: '',
zeroDocsFallback: false,
};
});
export function streamReducer(
state: StreamState,
action: AiopsLogRateAnalysisApiAction
): StreamState {
switch (action.type) {
case API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS:
return { ...state, significantItems: [...state.significantItems, ...action.payload] };
case API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_HISTOGRAM:
const significantItems = state.significantItems.map((cp) => {
export const logRateAnalysisResultsSlice = createSlice({
name: 'logRateAnalysisResults',
initialState: getDefaultState(),
reducers: {
addSignificantItems: (state, action: PayloadAction<SignificantItem[]>) => {
state.significantItems.push(...action.payload);
},
addSignificantItemsHistogram: (state, action: PayloadAction<SignificantItemHistogram[]>) => {
state.significantItems = state.significantItems.map((cp) => {
const cpHistogram = action.payload.find(
(h) => h.fieldName === cp.fieldName && h.fieldValue === cp.fieldValue
);
if (cpHistogram) {
cp.histogram = cpHistogram.histogram;
}
return cp;
return {
...cp,
...(cpHistogram ? { histogram: cpHistogram.histogram } : {}),
};
});
return { ...state, significantItems };
case API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_GROUP:
return { ...state, significantItemsGroups: action.payload };
case API_ACTION_NAME.ADD_SIGNIFICANT_ITEMS_GROUP_HISTOGRAM:
const significantItemsGroups = state.significantItemsGroups.map((cpg) => {
},
addSignificantItemsGroup: (state, action: PayloadAction<SignificantItemGroup[]>) => {
state.significantItemsGroups = action.payload;
},
addSignificantItemsGroupHistogram: (
state,
action: PayloadAction<SignificantItemGroupHistogram[]>
) => {
state.significantItemsGroups = state.significantItemsGroups.map((cpg) => {
const cpHistogram = action.payload.find((h) => h.id === cpg.id);
if (cpHistogram) {
cpg.histogram = cpHistogram.histogram;
}
return cpg;
});
return { ...state, significantItemsGroups };
case API_ACTION_NAME.ADD_ERROR:
return { ...state, errors: [...state.errors, action.payload] };
case API_ACTION_NAME.RESET_ERRORS:
return { ...state, errors: [] };
case API_ACTION_NAME.RESET_GROUPS:
return { ...state, significantItemsGroups: [] };
case API_ACTION_NAME.RESET_ALL:
return initialState;
case API_ACTION_NAME.UPDATE_LOADING_STATE:
},
addError: (state, action: PayloadAction<string>) => {
state.errors.push(action.payload);
},
ping: () => {},
resetErrors: (state) => {
state.errors = [];
},
resetGroups: (state) => {
state.significantItemsGroups = [];
},
resetAll: () => getDefaultState(),
updateLoadingState: (
state,
action: PayloadAction<{
ccsWarning: boolean;
loaded: number;
loadingState: string;
remainingFieldCandidates?: string[];
groupsMissing?: boolean;
}>
) => {
return { ...state, ...action.payload };
case API_ACTION_NAME.SET_ZERO_DOCS_FALLBACK:
return { ...state, zeroDocsFallback: action.payload };
default:
return state;
}
}
},
setZeroDocsFallback: (state, action: PayloadAction<boolean>) => {
state.zeroDocsFallback = action.payload;
},
},
});
export const streamReducer = logRateAnalysisResultsSlice.reducer;
export const streamReducerActions = logRateAnalysisResultsSlice.actions;
type StreamReducerActions = typeof streamReducerActions;
export type ApiActionName = keyof StreamReducerActions;
export type AiopsLogRateAnalysisApiAction = ReturnType<StreamReducerActions[ApiActionName]>;
export const {
addError,
addSignificantItems,
addSignificantItemsGroup,
addSignificantItemsGroupHistogram,
addSignificantItemsHistogram,
ping,
resetAll,
resetErrors,
resetGroups,
setZeroDocsFallback,
updateLoadingState,
} = logRateAnalysisResultsSlice.actions;

View file

@ -9,7 +9,7 @@ export { LOG_RATE_ANALYSIS_HIGHLIGHT_COLOR } from './constants';
export { getLogRateAnalysisType } from './get_log_rate_analysis_type';
export { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from './log_rate_analysis_type';
export type { LogRateHistogramItem } from './log_rate_histogram_item';
export type { DocumentCountStatsChangePoint } from './types';
export type { DocumentCountStats, DocumentStats, DocumentCountStatsChangePoint } from './types';
export type { WindowParameters } from './window_parameters';
export { getSnappedTimestamps } from './get_snapped_timestamps';
export { getSnappedWindowParameters } from './get_snapped_window_parameters';

View file

@ -0,0 +1,15 @@
/*
* 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 { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector, useStore } from 'react-redux';
import type { AppDispatch, AppStore, RootState } from './store';
// Improves TypeScript support compared to plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppStore: () => AppStore = useStore;

View file

@ -0,0 +1,31 @@
/*
* 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.
*/
export {
brushSelectionUpdate,
clearSelection,
setAnalysisType,
setAutoRunAnalysis,
setDocumentCountChartData,
setInitialAnalysisStart,
setIsBrushCleared,
setStickyHistogram,
setWindowParameters,
type BrushSelectionUpdatePayload,
} from './log_rate_analysis_slice';
export {
clearAllRowState,
setPinnedGroup,
setPinnedSignificantItem,
setSelectedGroup,
setSelectedSignificantItem,
} from './log_rate_analysis_table_row_slice';
export { LogRateAnalysisReduxProvider } from './store';
export { useAppDispatch, useAppSelector, useAppStore } from './hooks';
export { useCurrentSelectedGroup } from './use_current_selected_group';
export { useCurrentSelectedSignificantItem } from './use_current_selected_significant_item';
export type { GroupTableItem, GroupTableItemGroup, TableItemAction } from './types';

View file

@ -0,0 +1,134 @@
/*
* 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 { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { WindowParameters } from '../window_parameters';
import type { LogRateAnalysisType } from '../log_rate_analysis_type';
import { LOG_RATE_ANALYSIS_TYPE } from '../log_rate_analysis_type';
import type { DocumentStats } from '../types';
export type InitialAnalysisStart = number | WindowParameters | undefined;
/**
* Payload for brushSelectionUpdate action
*/
export interface BrushSelectionUpdatePayload {
/** The window parameters to update the analysis with */
windowParameters: WindowParameters;
/** Flag to force the update */
force: boolean;
analysisType: LogRateAnalysisType;
}
export interface LogRateAnalysisState {
analysisType: LogRateAnalysisType;
autoRunAnalysis: boolean;
initialAnalysisStart: InitialAnalysisStart;
isBrushCleared: boolean;
stickyHistogram: boolean;
windowParameters?: WindowParameters;
earliest?: number;
latest?: number;
intervalMs?: number;
documentStats: DocumentStats;
}
function getDefaultState(): LogRateAnalysisState {
return {
analysisType: LOG_RATE_ANALYSIS_TYPE.SPIKE,
autoRunAnalysis: true,
initialAnalysisStart: undefined,
isBrushCleared: true,
documentStats: {
sampleProbability: 1,
totalCount: 0,
},
// Default to false for now, until page restructure work to enable smooth sticky histogram is done
stickyHistogram: false,
};
}
export const logRateAnalysisSlice = createSlice({
name: 'logRateAnalysis',
initialState: getDefaultState(),
reducers: {
brushSelectionUpdate: (
state: LogRateAnalysisState,
action: PayloadAction<BrushSelectionUpdatePayload>
) => {
if (!state.isBrushCleared || action.payload.force) {
state.windowParameters = action.payload.windowParameters;
}
if (action.payload.force) {
state.isBrushCleared = false;
}
state.analysisType = action.payload.analysisType;
},
clearSelection: (state: LogRateAnalysisState) => {
state.windowParameters = undefined;
state.isBrushCleared = true;
state.initialAnalysisStart = undefined;
},
setAnalysisType: (state: LogRateAnalysisState, action: PayloadAction<LogRateAnalysisType>) => {
state.analysisType = action.payload;
},
setAutoRunAnalysis: (state: LogRateAnalysisState, action: PayloadAction<boolean>) => {
state.autoRunAnalysis = action.payload;
},
setDocumentCountChartData: (
state: LogRateAnalysisState,
action: PayloadAction<{
earliest?: number;
latest?: number;
intervalMs?: number;
documentStats: DocumentStats;
}>
) => {
state.earliest = action.payload.earliest;
state.latest = action.payload.latest;
state.intervalMs = action.payload.intervalMs;
state.documentStats = action.payload.documentStats;
},
setInitialAnalysisStart: (
state: LogRateAnalysisState,
action: PayloadAction<InitialAnalysisStart>
) => {
state.initialAnalysisStart = action.payload;
},
setIsBrushCleared: (state: LogRateAnalysisState, action: PayloadAction<boolean>) => {
state.isBrushCleared = action.payload;
},
setStickyHistogram: (state: LogRateAnalysisState, action: PayloadAction<boolean>) => {
state.stickyHistogram = action.payload;
},
setWindowParameters: (
state: LogRateAnalysisState,
action: PayloadAction<WindowParameters | undefined>
) => {
state.windowParameters = action.payload;
state.isBrushCleared = action.payload === undefined;
},
},
});
// Action creators are generated for each case reducer function
export const {
brushSelectionUpdate,
clearSelection,
setAnalysisType,
setAutoRunAnalysis,
setDocumentCountChartData,
setInitialAnalysisStart,
setIsBrushCleared,
setStickyHistogram,
setWindowParameters,
} = logRateAnalysisSlice.actions;

View file

@ -0,0 +1,72 @@
/*
* 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 { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { SignificantItem } from '@kbn/ml-agg-utils';
import type { GroupTableItem } from './types';
type SignificantItemOrNull = SignificantItem | null;
type GroupOrNull = GroupTableItem | null;
export interface LogRateAnalysisTableRowState {
pinnedGroup: GroupOrNull;
pinnedSignificantItem: SignificantItemOrNull;
selectedGroup: GroupOrNull;
selectedSignificantItem: SignificantItemOrNull;
}
function getDefaultState(): LogRateAnalysisTableRowState {
return {
pinnedGroup: null,
pinnedSignificantItem: null,
selectedGroup: null,
selectedSignificantItem: null,
};
}
export const logRateAnalysisTableRowSlice = createSlice({
name: 'logRateAnalysisTableRow',
initialState: getDefaultState(),
reducers: {
clearAllRowState: (state: LogRateAnalysisTableRowState) => {
state.pinnedGroup = null;
state.pinnedSignificantItem = null;
state.selectedGroup = null;
state.selectedSignificantItem = null;
},
setPinnedGroup: (state: LogRateAnalysisTableRowState, action: PayloadAction<GroupOrNull>) => {
state.pinnedGroup = action.payload;
},
setPinnedSignificantItem: (
state: LogRateAnalysisTableRowState,
action: PayloadAction<SignificantItemOrNull>
) => {
state.pinnedSignificantItem = action.payload;
},
setSelectedGroup: (state: LogRateAnalysisTableRowState, action: PayloadAction<GroupOrNull>) => {
state.selectedGroup = action.payload;
},
setSelectedSignificantItem: (
state: LogRateAnalysisTableRowState,
action: PayloadAction<SignificantItemOrNull>
) => {
state.selectedSignificantItem = action.payload;
},
},
});
// Action creators are generated for each case reducer function
export const {
clearAllRowState,
setPinnedGroup,
setPinnedSignificantItem,
setSelectedGroup,
setSelectedSignificantItem,
} = logRateAnalysisTableRowSlice.actions;

View file

@ -0,0 +1,56 @@
/*
* 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, { useMemo, type FC, type PropsWithChildren } from 'react';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import useMount from 'react-use/lib/useMount';
import { streamSlice } from '@kbn/ml-response-stream/client';
import { logRateAnalysisResultsSlice } from '../api/stream_reducer';
import { logRateAnalysisSlice } from './log_rate_analysis_slice';
import { logRateAnalysisTableRowSlice } from './log_rate_analysis_table_row_slice';
import type { InitialAnalysisStart } from './log_rate_analysis_slice';
const getReduxStore = () =>
configureStore({
reducer: {
// General page state
logRateAnalysis: logRateAnalysisSlice.reducer,
// Analysis results
logRateAnalysisResults: logRateAnalysisResultsSlice.reducer,
// Handles running the analysis
logRateAnalysisStream: streamSlice.reducer,
// Handles hovering and pinning table rows
logRateAnalysisTableRow: logRateAnalysisTableRowSlice.reducer,
},
});
interface LogRateAnalysisReduxProviderProps {
initialAnalysisStart?: InitialAnalysisStart;
}
export const LogRateAnalysisReduxProvider: FC<
PropsWithChildren<LogRateAnalysisReduxProviderProps>
> = ({ children, initialAnalysisStart }) => {
const store = useMemo(getReduxStore, []);
useMount(() => {
if (initialAnalysisStart) {
store.dispatch(logRateAnalysisSlice.actions.setInitialAnalysisStart(initialAnalysisStart));
}
});
return <Provider store={store}>{children}</Provider>;
};
// Infer the `RootState` and `AppDispatch` types from the store itself
export type AppStore = ReturnType<typeof getReduxStore>;
export type RootState = ReturnType<AppStore['getState']>;
export type AppDispatch = AppStore['dispatch'];

View file

@ -0,0 +1,27 @@
/*
* 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 { createSelector } from '@reduxjs/toolkit';
import type { RootState } from './store';
import { useAppSelector } from './hooks';
const selectSelectedGroup = (s: RootState) => s.logRateAnalysisTableRow.selectedGroup;
const selectPinnedGroup = (s: RootState) => s.logRateAnalysisTableRow.pinnedGroup;
const selectCurrentSelectedGroup = createSelector(
selectSelectedGroup,
selectPinnedGroup,
(selectedGroup, pinnedGroup) => {
if (selectedGroup) {
return selectedGroup;
} else if (pinnedGroup) {
return pinnedGroup;
}
}
);
export const useCurrentSelectedGroup = () => useAppSelector(selectCurrentSelectedGroup);

View file

@ -0,0 +1,30 @@
/*
* 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 { createSelector } from '@reduxjs/toolkit';
import type { RootState } from './store';
import { useAppSelector } from './hooks';
const selectSelectedSignificantItem = (s: RootState) =>
s.logRateAnalysisTableRow.selectedSignificantItem;
const selectPinnedSignificantItem = (s: RootState) =>
s.logRateAnalysisTableRow.pinnedSignificantItem;
const selectCurrentSelectedSignificantItem = createSelector(
selectSelectedSignificantItem,
selectPinnedSignificantItem,
(selectedSignificantItem, pinnedSignificantItem) => {
if (selectedSignificantItem) {
return selectedSignificantItem;
} else if (pinnedSignificantItem) {
return pinnedSignificantItem;
}
}
);
export const useCurrentSelectedSignificantItem = () =>
useAppSelector(selectCurrentSelectedSignificantItem);

View file

@ -29,5 +29,6 @@
"@kbn/field-types",
"@kbn/ml-chi2test",
"@kbn/ml-string-hash",
"@kbn/ml-response-stream",
]
}

View file

@ -55,3 +55,37 @@ export interface DocumentCountStatsChangePoint {
/** The type of change point. */
type: string;
}
/**
* Represents the document count statistics for a given time range.
*/
export interface DocumentCountStats {
/** The time interval in milliseconds. */
interval?: number;
/** The document count per time bucket. */
buckets?: { [key: string]: number };
/** The change point in the document count statistics. */
changePoint?: DocumentCountStatsChangePoint;
/** The earliest timestamp in the time range. */
timeRangeEarliest?: number;
/** The latest timestamp in the time range. */
timeRangeLatest?: number;
/** The total document count. */
totalCount: number;
/** The timestamp of the last document in the time range. */
lastDocTimeStampMs?: number;
}
/**
* Represents the overall document stats.
*/
export interface DocumentStats {
/** The probability of sampling. */
sampleProbability: number;
/** The total document count. */
totalCount: number;
/** The document count statistics. */
documentCountStats?: DocumentCountStats;
/** The document count statistics for comparison. */
documentCountStatsCompare?: DocumentCountStats;
}

View file

@ -6,7 +6,7 @@
*/
import type { SignificantItem } from '@kbn/ml-agg-utils';
import type { GroupTableItem } from '@kbn/aiops-components';
import type { GroupTableItem } from '@kbn/aiops-log-rate-analysis/state';
import { buildExtendedBaseFilterCriteria } from './build_extended_base_filter_criteria';

View file

@ -14,7 +14,7 @@ import type { Query } from '@kbn/es-query';
import { type SignificantItem, SIGNIFICANT_ITEM_TYPE } from '@kbn/ml-agg-utils';
import { buildBaseFilterCriteria } from '@kbn/ml-query-utils';
import { getCategoryQuery } from '@kbn/aiops-log-pattern-analysis/get_category_query';
import type { GroupTableItem } from '@kbn/aiops-components';
import type { GroupTableItem } from '@kbn/aiops-log-rate-analysis/state';
/*
* Contains utility functions for building and processing queries.

View file

@ -13,30 +13,18 @@ import type {
RectAnnotationSpec,
} from '@elastic/charts/dist/chart_types/xy_chart/utils/specs';
import type { LogRateHistogramItem, WindowParameters } from '@kbn/aiops-log-rate-analysis';
import {
DocumentCountChartWithAutoAnalysisStart,
type BrushSelectionUpdateHandler,
} from '@kbn/aiops-components';
import { useAppSelector } from '@kbn/aiops-log-rate-analysis/state';
import { DocumentCountChartRedux } from '@kbn/aiops-components';
import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context';
import type { DocumentCountStats } from '../../../get_document_stats';
import { TotalCountHeader } from '../total_count_header';
export interface DocumentCountContentProps {
brushSelectionUpdateHandler: BrushSelectionUpdateHandler;
documentCountStats?: DocumentCountStats;
documentCountStatsSplit?: DocumentCountStats;
documentCountStatsSplitLabel?: string;
isBrushCleared: boolean;
totalCount: number;
sampleProbability: number;
/** Optional color override for the default bar color for charts */
barColorOverride?: string;
/** Optional color override for the highlighted bar color for charts */
barHighlightColorOverride?: string;
windowParameters?: WindowParameters;
baselineLabel?: string;
deviationLabel?: string;
barStyleAccessor?: BarStyleAccessor;
@ -45,77 +33,35 @@ export interface DocumentCountContentProps {
}
export const DocumentCountContent: FC<DocumentCountContentProps> = ({
brushSelectionUpdateHandler,
documentCountStats,
documentCountStatsSplit,
documentCountStatsSplitLabel = '',
isBrushCleared,
totalCount,
sampleProbability,
barColorOverride,
barHighlightColorOverride,
windowParameters,
...docCountChartProps
}) => {
const { data, uiSettings, fieldFormats, charts } = useAiopsAppContext();
const bucketTimestamps = Object.keys(documentCountStats?.buckets ?? {}).map((time) => +time);
const splitBucketTimestamps = Object.keys(documentCountStatsSplit?.buckets ?? {}).map(
(time) => +time
);
const timeRangeEarliest = Math.min(...[...bucketTimestamps, ...splitBucketTimestamps]);
const timeRangeLatest = Math.max(...[...bucketTimestamps, ...splitBucketTimestamps]);
const { documentStats } = useAppSelector((s) => s.logRateAnalysis);
const { sampleProbability, totalCount, documentCountStats } = documentStats;
if (
documentCountStats === undefined ||
documentCountStats.buckets === undefined ||
timeRangeEarliest === undefined ||
timeRangeLatest === undefined
) {
if (documentCountStats === undefined) {
return totalCount !== undefined ? (
<TotalCountHeader totalCount={totalCount} sampleProbability={sampleProbability} />
) : null;
}
const chartPoints: LogRateHistogramItem[] = Object.entries(documentCountStats.buckets).map(
([time, value]) => ({
time: +time,
value,
})
);
let chartPointsSplit: LogRateHistogramItem[] | undefined;
if (documentCountStatsSplit?.buckets !== undefined) {
chartPointsSplit = Object.entries(documentCountStatsSplit?.buckets).map(([time, value]) => ({
time: +time,
value,
}));
}
return (
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem>
<TotalCountHeader totalCount={totalCount} sampleProbability={sampleProbability} />
</EuiFlexItem>
{documentCountStats.interval !== undefined && (
<EuiFlexItem>
<DocumentCountChartWithAutoAnalysisStart
dependencies={{ data, uiSettings, fieldFormats, charts }}
brushSelectionUpdateHandler={brushSelectionUpdateHandler}
chartPoints={chartPoints}
chartPointsSplit={chartPointsSplit}
timeRangeEarliest={timeRangeEarliest}
timeRangeLatest={timeRangeLatest}
interval={documentCountStats.interval}
chartPointsSplitLabel={documentCountStatsSplitLabel}
isBrushCleared={isBrushCleared}
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
changePoint={documentCountStats.changePoint}
{...docCountChartProps}
/>
</EuiFlexItem>
)}
<EuiFlexItem>
<DocumentCountChartRedux
dependencies={{ data, uiSettings, fieldFormats, charts }}
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
changePoint={documentCountStats.changePoint}
{...docCountChartProps}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -11,9 +11,9 @@ import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { DocumentCountChart as DocumentCountChartRoot } from '@kbn/aiops-components';
import type { Category } from '@kbn/aiops-log-pattern-analysis/types';
import type { DocumentCountStats } from '@kbn/aiops-log-rate-analysis/types';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import type { DocumentCountStats } from '../../get_document_stats';
import { TotalCountHeader } from '../document_count_content/total_count_header';

View file

@ -16,8 +16,8 @@ import { UrlStateProvider } from '@kbn/ml-url-state';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { LogRateAnalysisReduxProvider } from '@kbn/aiops-log-rate-analysis/state';
import { LogRateAnalysisStateProvider } from '@kbn/aiops-components';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import { DataSourceContext } from '../../hooks/use_data_source';
@ -38,8 +38,6 @@ export interface LogRateAnalysisAppStateProps {
savedSearch: SavedSearch | null;
/** App dependencies */
appDependencies: AiopsAppDependencies;
/** Option to make main histogram sticky */
stickyHistogram?: boolean;
/** Optional flag to indicate whether kibana is running in serverless */
showFrozenDataTierChoice?: boolean;
}
@ -48,7 +46,6 @@ export const LogRateAnalysisAppState: FC<LogRateAnalysisAppStateProps> = ({
dataView,
savedSearch,
appDependencies,
stickyHistogram,
showFrozenDataTierChoice = true,
}) => {
if (!dataView) return null;
@ -69,13 +66,13 @@ export const LogRateAnalysisAppState: FC<LogRateAnalysisAppStateProps> = ({
<AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider>
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
<LogRateAnalysisStateProvider>
<LogRateAnalysisReduxProvider>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<DatePickerContextProvider {...datePickerDeps}>
<LogRateAnalysisPage stickyHistogram={stickyHistogram} />
<LogRateAnalysisPage />
</DatePickerContextProvider>
</StorageContextProvider>
</LogRateAnalysisStateProvider>
</LogRateAnalysisReduxProvider>
</DataSourceContext.Provider>
</UrlStateProvider>
</AiopsAppContext.Provider>

View file

@ -6,14 +6,13 @@
*/
import { isEqual } from 'lodash';
import React, { useCallback, useEffect, useMemo, useRef, useState, type FC } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, type FC } from 'react';
import { EuiButton, EuiEmptyPrompt, EuiHorizontalRule, EuiPanel } from '@elastic/eui';
import type { Moment } from 'moment';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { BarStyleAccessor } from '@elastic/charts/dist/chart_types/xy_chart/utils/specs';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
getWindowParametersForTrigger,
@ -21,14 +20,16 @@ import {
getSnappedWindowParameters,
LOG_RATE_ANALYSIS_HIGHLIGHT_COLOR,
LOG_RATE_ANALYSIS_TYPE,
type LogRateAnalysisType,
type WindowParameters,
} from '@kbn/aiops-log-rate-analysis';
import type { SignificantItem } from '@kbn/ml-agg-utils';
import { useLogRateAnalysisStateContext, type GroupTableItem } from '@kbn/aiops-components';
import { useData } from '../../../hooks/use_data';
import { useDataSource } from '../../../hooks/use_data_source';
import {
clearAllRowState,
clearSelection,
setAutoRunAnalysis,
setInitialAnalysisStart,
useAppDispatch,
useAppSelector,
} from '@kbn/aiops-log-rate-analysis/state';
import { DocumentCountContent } from '../../document_count_content/document_count_content';
import {
@ -49,26 +50,11 @@ const DEFAULT_SEARCH_BAR_QUERY: estypes.QueryDslQueryContainer = {
},
};
export function getDocumentCountStatsSplitLabel(
significantItem?: SignificantItem,
group?: GroupTableItem
) {
if (significantItem) {
return `${significantItem?.fieldName}:${significantItem?.fieldValue}`;
} else if (group) {
return i18n.translate('xpack.aiops.logRateAnalysis.page.documentCountStatsSplitGroupLabel', {
defaultMessage: 'Selected group',
});
}
}
export interface LogRateAnalysisContentProps {
/** Optional time range override */
timeRange?: { min: Moment; max: Moment };
/** Elasticsearch query to pass to analysis endpoint */
esSearchQuery?: estypes.QueryDslQueryContainer;
/** Option to make the main histogram sticky */
stickyHistogram?: boolean;
/** Optional color override for the default bar color for charts */
barColorOverride?: string;
/** Optional color override for the highlighted bar color for charts */
@ -84,24 +70,22 @@ export interface LogRateAnalysisContentProps {
export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
timeRange,
esSearchQuery = DEFAULT_SEARCH_QUERY,
stickyHistogram,
barColorOverride,
barHighlightColorOverride,
onAnalysisCompleted,
onWindowParametersChange,
embeddingOrigin,
}) => {
const { dataView } = useDataSource();
const dispatch = useAppDispatch();
const [windowParameters, setWindowParameters] = useState<WindowParameters | undefined>();
const [isBrushCleared, setIsBrushCleared] = useState(true);
const [logRateAnalysisType, setLogRateAnalysisType] = useState<LogRateAnalysisType>(
LOG_RATE_ANALYSIS_TYPE.SPIKE
const isRunning = useAppSelector((s) => s.logRateAnalysisStream.isRunning);
const significantItems = useAppSelector((s) => s.logRateAnalysisResults.significantItems);
const significantItemsGroups = useAppSelector(
(s) => s.logRateAnalysisResults.significantItemsGroups
);
useEffect(() => {
setIsBrushCleared(windowParameters === undefined);
}, [windowParameters]);
const loaded = useAppSelector((s) => s.logRateAnalysisResults.loaded);
const analysisType = useAppSelector((s) => s.logRateAnalysis.analysisType);
const windowParameters = useAppSelector((s) => s.logRateAnalysis.windowParameters);
// Window parameters stored in the url state use this components
// `initialAnalysisStart` prop to set the initial params restore from url state.
@ -132,55 +116,15 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
[esSearchQuery]
);
const {
autoRunAnalysis,
currentSelectedSignificantItem,
currentSelectedGroup,
setAutoRunAnalysis,
setInitialAnalysisStart,
setPinnedSignificantItem,
setPinnedGroup,
setSelectedSignificantItem,
setSelectedGroup,
} = useLogRateAnalysisStateContext();
const { documentStats, earliest, latest } = useData(
dataView,
'log_rate_analysis',
searchQuery,
undefined,
currentSelectedSignificantItem,
currentSelectedGroup,
undefined,
true,
timeRange
const { autoRunAnalysis, documentStats, earliest, latest, isBrushCleared } = useAppSelector(
(s) => s.logRateAnalysis
);
const { sampleProbability, totalCount, documentCountStats, documentCountStatsCompare } =
documentStats;
const { documentCountStats } = documentStats;
function brushSelectionUpdate(
windowParametersUpdate: WindowParameters,
force: boolean,
logRateAnalysisTypeUpdate: LogRateAnalysisType
) {
if (!isBrushCleared || force) {
setWindowParameters(windowParametersUpdate);
}
if (force) {
setIsBrushCleared(false);
}
setLogRateAnalysisType(logRateAnalysisTypeUpdate);
}
function clearSelection() {
setWindowParameters(undefined);
setPinnedSignificantItem(null);
setPinnedGroup(null);
setSelectedSignificantItem(null);
setSelectedGroup(null);
setIsBrushCleared(true);
setInitialAnalysisStart(undefined);
function clearSelectionHandler() {
dispatch(clearSelection());
dispatch(clearAllRowState());
}
const barStyle = {
@ -204,8 +148,8 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
: undefined;
const triggerAnalysisForManualSelection = useCallback(() => {
setAutoRunAnalysis(true);
}, [setAutoRunAnalysis]);
dispatch(setAutoRunAnalysis(true));
}, [dispatch]);
const triggerAnalysisForChangePoint = useCallback(() => {
if (documentCountStats) {
@ -224,10 +168,21 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
const wpSnap = getSnappedWindowParameters(wp, snapTimestamps);
triggerAnalysisForManualSelection();
setInitialAnalysisStart(wpSnap);
dispatch(setInitialAnalysisStart(wpSnap));
}
}
}, [documentCountStats, setInitialAnalysisStart, triggerAnalysisForManualSelection]);
}, [documentCountStats, dispatch, triggerAnalysisForManualSelection]);
useEffect(() => {
if (!isRunning && loaded === 1 && onAnalysisCompleted) {
onAnalysisCompleted({
analysisType,
significantItems,
significantItemsGroups,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRunning, loaded]);
const showDocumentCountContent = documentCountStats !== undefined;
@ -255,16 +210,6 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
<EuiPanel hasBorder={false} hasShadow={false}>
{showDocumentCountContent && (
<DocumentCountContent
brushSelectionUpdateHandler={brushSelectionUpdate}
documentCountStats={documentCountStats}
documentCountStatsSplit={documentCountStatsCompare}
documentCountStatsSplitLabel={getDocumentCountStatsSplitLabel(
currentSelectedSignificantItem,
currentSelectedGroup
)}
isBrushCleared={isBrushCleared}
totalCount={totalCount}
sampleProbability={sampleProbability}
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
barStyleAccessor={barStyleAccessor}
@ -273,18 +218,10 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
<EuiHorizontalRule />
{showLogRateAnalysisResults && (
<LogRateAnalysisResults
analysisType={logRateAnalysisType}
earliest={earliest}
isBrushCleared={isBrushCleared}
latest={latest}
stickyHistogram={stickyHistogram}
onReset={clearSelection}
sampleProbability={sampleProbability}
onReset={clearSelectionHandler}
searchQuery={searchQuery}
windowParameters={windowParameters}
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
onAnalysisCompleted={onAnalysisCompleted}
embeddingOrigin={embeddingOrigin}
/>
)}
@ -315,7 +252,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
</EuiButton>{' '}
<EuiButton
data-test-subj="aiopsClearSelectionBadge"
onClick={() => clearSelection()}
onClick={() => clearSelectionHandler()}
color="text"
>
<FormattedMessage
@ -380,7 +317,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
data-test-subj="aiopsChangePointDetectedPrompt"
/>
)}
{showDefaultEmptyPrompt && (
{showDocumentCountContent && showDefaultEmptyPrompt && (
<EuiEmptyPrompt
color="subdued"
hasShadow={false}

View file

@ -18,16 +18,17 @@ 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 { LogRateAnalysisStateProvider } from '@kbn/aiops-components';
import { LogRateAnalysisReduxProvider } from '@kbn/aiops-log-rate-analysis/state';
import { timeSeriesDataViewWarning } from '../../../application/utils/time_series_dataview_check';
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 { LogRateAnalysisContent } from './log_rate_analysis_content';
import type { LogRateAnalysisResultsData } from '../log_rate_analysis_results';
import { LogRateAnalysisContent } from './log_rate_analysis_content';
const localStorage = new Storage(window.localStorage);
/**
@ -36,8 +37,6 @@ const localStorage = new Storage(window.localStorage);
export interface LogRateAnalysisContentWrapperProps {
/** The data view to analyze. */
dataView: DataView;
/** Option to make main histogram sticky */
stickyHistogram?: boolean;
/** App dependencies */
appDependencies: AiopsAppDependencies;
/** Timestamp for start of initial analysis */
@ -67,7 +66,6 @@ export const LogRateAnalysisContentWrapper: FC<LogRateAnalysisContentWrapperProp
initialAnalysisStart,
timeRange,
esSearchQuery,
stickyHistogram,
barColorOverride,
barHighlightColorOverride,
onAnalysisCompleted,
@ -92,13 +90,12 @@ export const LogRateAnalysisContentWrapper: FC<LogRateAnalysisContentWrapperProp
<AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider>
<DataSourceContext.Provider value={{ dataView, savedSearch: null }}>
<LogRateAnalysisStateProvider initialAnalysisStart={initialAnalysisStart}>
<LogRateAnalysisReduxProvider initialAnalysisStart={initialAnalysisStart}>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<DatePickerContextProvider {...datePickerDeps}>
<LogRateAnalysisContent
timeRange={timeRange}
esSearchQuery={esSearchQuery}
stickyHistogram={stickyHistogram}
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
onAnalysisCompleted={onAnalysisCompleted}
@ -106,7 +103,7 @@ export const LogRateAnalysisContentWrapper: FC<LogRateAnalysisContentWrapperProp
/>
</DatePickerContextProvider>
</StorageContextProvider>
</LogRateAnalysisStateProvider>
</LogRateAnalysisReduxProvider>
</DataSourceContext.Provider>
</UrlStateProvider>
</AiopsAppContext.Provider>

View file

@ -18,7 +18,13 @@ import { useUrlState, usePageUrlState } from '@kbn/ml-url-state';
import type { SearchQueryLanguage } from '@kbn/ml-query-utils';
import type { WindowParameters } from '@kbn/aiops-log-rate-analysis';
import { AIOPS_TELEMETRY_ID } from '@kbn/aiops-common/constants';
import { useLogRateAnalysisStateContext } from '@kbn/aiops-components';
import {
useAppDispatch,
useCurrentSelectedSignificantItem,
useCurrentSelectedGroup,
setInitialAnalysisStart,
setDocumentCountChartData,
} from '@kbn/aiops-log-rate-analysis/state';
import { useDataSource } from '../../hooks/use_data_source';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
@ -35,16 +41,14 @@ import { SearchPanel } from '../search_panel';
import { PageHeader } from '../page_header';
import { LogRateAnalysisContent } from './log_rate_analysis_content/log_rate_analysis_content';
interface Props {
stickyHistogram?: boolean;
}
export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
export const LogRateAnalysisPage: FC = () => {
const { data: dataService } = useAiopsAppContext();
const { dataView, savedSearch } = useDataSource();
const { currentSelectedSignificantItem, currentSelectedGroup, setInitialAnalysisStart } =
useLogRateAnalysisStateContext();
const currentSelectedGroup = useCurrentSelectedGroup();
const currentSelectedSignificantItem = useCurrentSelectedSignificantItem();
const dispatch = useAppDispatch();
const [stateFromUrl, setUrlState] = usePageUrlState<LogRateAnalysisPageUrlState>(
'logRateAnalysis',
@ -89,7 +93,7 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
stateFromUrl
);
const { timefilter } = useData(
const { documentStats, timefilter, earliest, latest, intervalMs } = useData(
dataView,
'log_rate_analysis',
searchQuery,
@ -98,6 +102,23 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
currentSelectedGroup
);
// TODO Since `useData` isn't just used within Log Rate Analysis, this is a bit of
// a workaround to pass the result on to the redux store. At least this ensures
// we now use `useData` only once across Log Rate Analysis! Originally `useData`
// was quite general, but over time it got quite some specific features used
// across Log Rate Analysis and Pattern Analysis. We discussed that we should
// split this up into more specific hooks.
useEffect(() => {
dispatch(
setDocumentCountChartData({
earliest,
latest,
intervalMs,
documentStats,
})
);
}, [documentStats, dispatch, earliest, intervalMs, latest]);
useEffect(
// TODO: Consolidate this hook/function with the one in `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_view.tsx`
function clearFiltersOnLeave() {
@ -144,7 +165,7 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
useEffect(
() => {
setInitialAnalysisStart(appStateToWindowParameters(stateFromUrl.wp));
dispatch(setInitialAnalysisStart(appStateToWindowParameters(stateFromUrl.wp)));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
@ -179,7 +200,6 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
embeddingOrigin={AIOPS_TELEMETRY_ID.AIOPS_DEFAULT_SOURCE}
esSearchQuery={searchQuery}
onWindowParametersChange={onWindowParametersHandler}
stickyHistogram={stickyHistogram}
/>
</EuiFlexGroup>
</EuiPageSection>

View file

@ -24,7 +24,12 @@ import {
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
import { ProgressControls } from '@kbn/aiops-components';
import { useFetchStream } from '@kbn/ml-response-stream/client';
import { cancelStream, startStream } from '@kbn/ml-response-stream/client';
import {
clearAllRowState,
useAppDispatch,
useAppSelector,
} from '@kbn/aiops-log-rate-analysis/state';
import {
LOG_RATE_ANALYSIS_TYPE,
type LogRateAnalysisType,
@ -34,10 +39,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { SignificantItem, SignificantItemGroup } from '@kbn/ml-agg-utils';
import { AIOPS_TELEMETRY_ID } from '@kbn/aiops-common/constants';
import { initialState, streamReducer } from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type { AiopsLogRateAnalysisSchema } from '@kbn/aiops-log-rate-analysis/api/schema';
import type { AiopsLogRateAnalysisSchemaSignificantItem } from '@kbn/aiops-log-rate-analysis/api/schema_v2';
import { useLogRateAnalysisStateContext } from '@kbn/aiops-components';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { useDataSource } from '../../hooks/use_data_source';
@ -131,56 +134,45 @@ export interface LogRateAnalysisResultsData {
* LogRateAnalysis props require a data view.
*/
interface LogRateAnalysisResultsProps {
/** The type of analysis, whether it's a spike or dip */
analysisType?: LogRateAnalysisType;
/** Start timestamp filter */
earliest: number;
/** End timestamp filter */
latest: number;
isBrushCleared: boolean;
/** Option to make main histogram sticky */
stickyHistogram?: boolean;
/** Callback for resetting the analysis */
onReset: () => void;
/** Window parameters for the analysis */
windowParameters: WindowParameters;
/** The search query to be applied to the analysis as a filter */
searchQuery: estypes.QueryDslQueryContainer;
/** Sample probability to be applied to random sampler aggregations */
sampleProbability: number;
/** Optional color override for the default bar color for charts */
barColorOverride?: string;
/** Optional color override for the highlighted bar color for charts */
barHighlightColorOverride?: string;
/** Optional callback that exposes data of the completed analysis */
onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void;
/** Identifier to indicate the plugin utilizing the component */
embeddingOrigin: string;
}
export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
analysisType = LOG_RATE_ANALYSIS_TYPE.SPIKE,
earliest,
isBrushCleared,
latest,
stickyHistogram,
onReset,
windowParameters,
searchQuery,
sampleProbability,
barColorOverride,
barHighlightColorOverride,
onAnalysisCompleted,
embeddingOrigin,
}) => {
const { analytics, http } = useAiopsAppContext();
const { dataView } = useDataSource();
const dispatch = useAppDispatch();
const {
analysisType,
earliest,
latest,
windowParameters,
documentStats: { sampleProbability },
stickyHistogram,
isBrushCleared,
} = useAppSelector((s) => s.logRateAnalysis);
const { isRunning, errors: streamErrors } = useAppSelector((s) => s.logRateAnalysisStream);
const data = useAppSelector((s) => s.logRateAnalysisResults);
// Store the performance metric's start time using a ref
// to be able to track it across rerenders.
const analysisStartTime = useRef<number | undefined>(window.performance.now());
const { clearAllRowState } = useLogRateAnalysisStateContext();
const abortCtrl = useRef(new AbortController());
const [currentAnalysisType, setCurrentAnalysisType] = useState<LogRateAnalysisType | undefined>();
const [currentAnalysisWindowParameters, setCurrentAnalysisWindowParameters] = useState<
@ -201,7 +193,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
setGroupResults(optionId === resultsGroupedOnId);
// When toggling the group switch, clear all row selections
clearAllRowState();
dispatch(clearAllRowState());
};
const onFieldsFilterChange = (skippedFields: string[]) => {
@ -221,49 +213,18 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
setSkippedColumns(columns);
};
const {
cancel,
start,
data,
isRunning,
errors: streamErrors,
} = useFetchStream<AiopsLogRateAnalysisSchema<'2'>, typeof streamReducer>(
http,
'/internal/aiops/log_rate_analysis',
'2',
{
start: earliest,
end: latest,
searchQuery: JSON.stringify(searchQuery),
// TODO Handle data view without time fields.
timeFieldName: dataView.timeFieldName ?? '',
index: dataView.getIndexPattern(),
grouping: true,
flushFix: true,
// If analysis type is `spike`, pass on window parameters as is,
// if it's `dip`, swap baseline and deviation.
...(analysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE
? windowParameters
: {
baselineMin: windowParameters.deviationMin,
baselineMax: windowParameters.deviationMax,
deviationMin: windowParameters.baselineMin,
deviationMax: windowParameters.baselineMax,
}),
overrides,
sampleProbability,
},
{ reducer: streamReducer, initialState },
{ [AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN]: embeddingOrigin }
);
const { significantItems, zeroDocsFallback } = data;
const { significantItems } = data;
useEffect(
() => setUniqueFieldNames(uniq(significantItems.map((d) => d.fieldName)).sort()),
[significantItems]
);
function cancelHandler() {
abortCtrl.current.abort();
dispatch(cancelStream());
}
useEffect(() => {
if (!isRunning) {
const { loaded, remainingFieldCandidates, groupsMissing } = data;
@ -282,15 +243,6 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
// Reset all overrides.
setOverrides(undefined);
// If provided call the `onAnalysisCompleted` callback with the analysis results.
if (onAnalysisCompleted) {
onAnalysisCompleted({
analysisType,
significantItems: data.significantItems,
significantItemsGroups: data.significantItemsGroups,
});
}
// Track performance metric
if (analysisStartTime.current !== undefined) {
const analysisDuration = window.performance.now() - analysisStartTime.current;
@ -322,7 +274,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
if (resetGroupButton) {
setGroupResults(false);
setToggleIdSelected(resultsGroupedOffId);
clearAllRowState();
dispatch(clearAllRowState());
}
setCurrentAnalysisType(analysisType);
@ -333,18 +285,67 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
setShouldStart(true);
}
const startParams = useMemo(() => {
if (!windowParameters) {
return undefined;
}
return {
http,
endpoint: '/internal/aiops/log_rate_analysis',
apiVersion: '2',
abortCtrl,
body: {
start: earliest,
end: latest,
searchQuery: JSON.stringify(searchQuery),
// TODO Handle data view without time fields.
timeFieldName: dataView.timeFieldName ?? '',
index: dataView.getIndexPattern(),
grouping: true,
flushFix: true,
// If analysis type is `spike`, pass on window parameters as is,
// if it's `dip`, swap baseline and deviation.
...(analysisType === LOG_RATE_ANALYSIS_TYPE.SPIKE
? windowParameters
: {
baselineMin: windowParameters.deviationMin,
baselineMax: windowParameters.deviationMax,
deviationMin: windowParameters.baselineMin,
deviationMax: windowParameters.baselineMax,
}),
overrides,
sampleProbability,
},
headers: { [AIOPS_TELEMETRY_ID.AIOPS_ANALYSIS_RUN_ORIGIN]: embeddingOrigin },
};
}, [
analysisType,
earliest,
latest,
http,
searchQuery,
dataView,
windowParameters,
sampleProbability,
overrides,
embeddingOrigin,
]);
useEffect(() => {
if (shouldStart) {
start();
if (shouldStart && startParams) {
dispatch(startStream(startParams));
setShouldStart(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldStart]);
useEffect(() => {
setCurrentAnalysisType(analysisType);
setCurrentAnalysisWindowParameters(windowParameters);
start();
if (startParams) {
setCurrentAnalysisType(analysisType);
setCurrentAnalysisWindowParameters(windowParameters);
dispatch(startStream(startParams));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -365,7 +366,6 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
return p + c.groupItemsSortedByUniqueness.length;
}, 0);
const foundGroups = groupTableItems.length > 0 && groupItemCount > 0;
const timeRangeMs = { from: earliest, to: latest };
// Disable the grouping switch toggle only if no groups were found,
// the toggle wasn't enabled already and no fields were selected to be skipped.
@ -392,7 +392,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
progressMessage={data.loadingState ?? ''}
isRunning={isRunning}
onRefresh={() => startHandler(false)}
onCancel={cancel}
onCancel={cancelHandler}
onReset={onReset}
shouldRerunAnalysis={shouldRerunAnalysis}
>
@ -454,10 +454,7 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
{showLogRateAnalysisResultsTable && currentAnalysisType !== undefined && (
<>
<EuiSpacer size="s" />
<LogRateAnalysisTypeCallOut
analysisType={currentAnalysisType}
zeroDocsFallback={zeroDocsFallback}
/>
<LogRateAnalysisTypeCallOut analysisType={currentAnalysisType} />
<EuiSpacer size="xs" />
</>
)}
@ -550,24 +547,17 @@ export const LogRateAnalysisResults: FC<LogRateAnalysisResultsProps> = ({
skippedColumns={skippedColumns}
significantItems={data.significantItems}
groupTableItems={groupTableItems}
loading={isRunning}
timeRangeMs={timeRangeMs}
searchQuery={searchQuery}
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
zeroDocsFallback={zeroDocsFallback}
/>
) : null}
{showLogRateAnalysisResultsTable && !groupResults ? (
<LogRateAnalysisResultsTable
skippedColumns={skippedColumns}
significantItems={data.significantItems}
loading={isRunning}
timeRangeMs={timeRangeMs}
searchQuery={searchQuery}
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
zeroDocsFallback={zeroDocsFallback}
/>
) : null}
</div>

View file

@ -10,17 +10,18 @@ import React, { type FC } from 'react';
import { EuiCallOut, EuiText } from '@elastic/eui';
import { LOG_RATE_ANALYSIS_TYPE, type LogRateAnalysisType } from '@kbn/aiops-log-rate-analysis';
import { useAppSelector } from '@kbn/aiops-log-rate-analysis/state';
import { i18n } from '@kbn/i18n';
interface LogRateAnalysisTypeCallOutProps {
analysisType: LogRateAnalysisType;
zeroDocsFallback: boolean;
}
export const LogRateAnalysisTypeCallOut: FC<LogRateAnalysisTypeCallOutProps> = ({
analysisType,
zeroDocsFallback,
}) => {
const zeroDocsFallback = useAppSelector((s) => s.logRateAnalysisResults.zeroDocsFallback);
let callOutTitle: string;
let callOutText: string;

View file

@ -8,7 +8,7 @@
import { sortBy } from 'lodash';
import type { SignificantItemGroup } from '@kbn/ml-agg-utils';
import type { GroupTableItem, GroupTableItemGroup } from '@kbn/aiops-components';
import type { GroupTableItem, GroupTableItemGroup } from '@kbn/aiops-log-rate-analysis/state';
export function getGroupTableItems(
significantItemsGroups: SignificantItemGroup[]

View file

@ -7,7 +7,7 @@
import { escapeKuery, escapeQuotes } from '@kbn/es-query';
import { isSignificantItem, type SignificantItem } from '@kbn/ml-agg-utils';
import type { GroupTableItem } from '@kbn/aiops-components';
import type { GroupTableItem } from '@kbn/aiops-log-rate-analysis/state';
export const getTableItemAsKQL = (tableItem: GroupTableItem | SignificantItem) => {
if (isSignificantItem(tableItem)) {

View file

@ -12,12 +12,18 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import type { EuiTableSortingType } from '@elastic/eui';
import { useEuiBackgroundColor, EuiBasicTable } from '@elastic/eui';
import { type SignificantItem } from '@kbn/ml-agg-utils';
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
import { useLogRateAnalysisStateContext } from '@kbn/aiops-components';
import type { SignificantItem } from '@kbn/ml-agg-utils';
import {
setPinnedSignificantItem,
setSelectedSignificantItem,
useAppDispatch,
useAppSelector,
} from '@kbn/aiops-log-rate-analysis/state';
import type { GroupTableItemGroup } from '@kbn/aiops-log-rate-analysis/state';
import { useEuiTheme } from '../../hooks/use_eui_theme';
import { useColumns, SIG_ITEMS_TABLE } from './use_columns';
import { useColumns, LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE } from './use_columns';
const PAGINATION_SIZE_OPTIONS = [5, 10, 20, 50];
const DEFAULT_SORT_FIELD = 'pValue';
@ -26,41 +32,59 @@ const DEFAULT_SORT_DIRECTION = 'asc';
const DEFAULT_SORT_DIRECTION_ZERO_DOCS_FALLBACK = 'desc';
interface LogRateAnalysisResultsTableProps {
significantItems: SignificantItem[];
loading: boolean;
isExpandedRow?: boolean;
groupFilter?: GroupTableItemGroup[];
searchQuery: estypes.QueryDslQueryContainer;
timeRangeMs: TimeRangeMs;
/** Optional color override for the default bar color for charts */
barColorOverride?: string;
/** Optional color override for the highlighted bar color for charts */
barHighlightColorOverride?: string;
skippedColumns: string[];
zeroDocsFallback?: boolean;
}
export const LogRateAnalysisResultsTable: FC<LogRateAnalysisResultsTableProps> = ({
significantItems,
loading,
isExpandedRow,
groupFilter,
searchQuery,
timeRangeMs,
barColorOverride,
barHighlightColorOverride,
skippedColumns,
zeroDocsFallback = false,
}) => {
const euiTheme = useEuiTheme();
const primaryBackgroundColor = useEuiBackgroundColor('primary');
const {
pinnedGroup,
pinnedSignificantItem,
selectedGroup,
selectedSignificantItem,
setPinnedSignificantItem,
setSelectedSignificantItem,
} = useLogRateAnalysisStateContext();
const allSignificantItems = useAppSelector((s) => s.logRateAnalysisResults.significantItems);
const significantItems = useMemo(() => {
if (!groupFilter) {
return allSignificantItems;
}
return groupFilter.reduce<SignificantItem[]>((p, groupItem) => {
const st = allSignificantItems.find(
(d) => d.fieldName === groupItem.fieldName && d.fieldValue === groupItem.fieldValue
);
if (st !== undefined) {
p.push({
...st,
unique: (groupItem.duplicate ?? 0) <= 1,
});
}
return p;
}, []);
}, [allSignificantItems, groupFilter]);
const zeroDocsFallback = useAppSelector((s) => s.logRateAnalysisResults.zeroDocsFallback);
const pinnedGroup = useAppSelector((s) => s.logRateAnalysisTableRow.pinnedGroup);
const selectedGroup = useAppSelector((s) => s.logRateAnalysisTableRow.selectedGroup);
const pinnedSignificantItem = useAppSelector(
(s) => s.logRateAnalysisTableRow.pinnedSignificantItem
);
const selectedSignificantItem = useAppSelector(
(s) => s.logRateAnalysisTableRow.selectedSignificantItem
);
const dispatch = useAppDispatch();
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
@ -72,15 +96,12 @@ export const LogRateAnalysisResultsTable: FC<LogRateAnalysisResultsTableProps> =
);
const columns = useColumns(
SIG_ITEMS_TABLE,
LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE.SIGNIFICANT_ITEMS,
skippedColumns,
searchQuery,
timeRangeMs,
loading,
zeroDocsFallback,
barColorOverride,
barHighlightColorOverride,
isExpandedRow
groupFilter !== undefined
);
const onChange = useCallback((tableSettings) => {
@ -151,7 +172,7 @@ export const LogRateAnalysisResultsTable: FC<LogRateAnalysisResultsTableProps> =
selectedGroup === null &&
pinnedGroup === null
) {
setSelectedSignificantItem(pageOfItems[0]);
dispatch(setSelectedSignificantItem(pageOfItems[0]));
}
// If a user switched pages and a pinned row is no longer visible
@ -162,13 +183,12 @@ export const LogRateAnalysisResultsTable: FC<LogRateAnalysisResultsTableProps> =
selectedGroup === null &&
pinnedGroup === null
) {
setPinnedSignificantItem(null);
dispatch(setPinnedSignificantItem(null));
}
}, [
dispatch,
selectedGroup,
selectedSignificantItem,
setSelectedSignificantItem,
setPinnedSignificantItem,
pageOfItems,
pinnedGroup,
pinnedSignificantItem,
@ -178,8 +198,8 @@ export const LogRateAnalysisResultsTable: FC<LogRateAnalysisResultsTableProps> =
// make sure to reset any hovered or pinned rows.
useEffect(
() => () => {
setSelectedSignificantItem(null);
setPinnedSignificantItem(null);
dispatch(setSelectedSignificantItem(null));
dispatch(setPinnedSignificantItem(null));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
@ -236,18 +256,18 @@ export const LogRateAnalysisResultsTable: FC<LogRateAnalysisResultsTableProps> =
significantItem.fieldName === pinnedSignificantItem?.fieldName &&
significantItem.fieldValue === pinnedSignificantItem?.fieldValue
) {
setPinnedSignificantItem(null);
dispatch(setPinnedSignificantItem(null));
} else {
setPinnedSignificantItem(significantItem);
dispatch(setPinnedSignificantItem(significantItem));
}
},
onMouseEnter: () => {
if (pinnedSignificantItem === null) {
setSelectedSignificantItem(significantItem);
dispatch(setSelectedSignificantItem(significantItem));
}
},
onMouseLeave: () => {
setSelectedSignificantItem(null);
dispatch(setSelectedSignificantItem(null));
},
style: getRowStyle(significantItem),
};

View file

@ -27,16 +27,21 @@ import {
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { SignificantItem } from '@kbn/ml-agg-utils';
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
import { type SignificantItem } from '@kbn/ml-agg-utils';
import {
setPinnedGroup,
setSelectedGroup,
useAppDispatch,
useAppSelector,
type GroupTableItem,
} from '@kbn/aiops-log-rate-analysis/state';
import { stringHash } from '@kbn/ml-string-hash';
import { useLogRateAnalysisStateContext, type GroupTableItem } from '@kbn/aiops-components';
import usePrevious from 'react-use/lib/usePrevious';
import useMountedState from 'react-use/lib/useMountedState';
import { LogRateAnalysisResultsTable } from './log_rate_analysis_results_table';
import { GROUPS_TABLE, useColumns } from './use_columns';
import { LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE, useColumns } from './use_columns';
const EXPAND_COLUMN_WIDTH = '40px';
const MAX_GROUP_BADGES = 5;
@ -51,29 +56,25 @@ interface LogRateAnalysisResultsTableProps {
skippedColumns: string[];
significantItems: SignificantItem[];
groupTableItems: GroupTableItem[];
loading: boolean;
searchQuery: estypes.QueryDslQueryContainer;
timeRangeMs: TimeRangeMs;
/** Optional color override for the default bar color for charts */
barColorOverride?: string;
/** Optional color override for the highlighted bar color for charts */
barHighlightColorOverride?: string;
zeroDocsFallback?: boolean;
}
export const LogRateAnalysisResultsGroupsTable: FC<LogRateAnalysisResultsTableProps> = ({
skippedColumns,
significantItems,
groupTableItems,
loading,
timeRangeMs,
searchQuery,
barColorOverride,
barHighlightColorOverride,
zeroDocsFallback = false,
}) => {
const prevSkippedColumns = usePrevious(skippedColumns);
const zeroDocsFallback = useAppSelector((s) => s.logRateAnalysisResults.zeroDocsFallback);
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
const [sortField, setSortField] = useState<'docCount' | 'pValue'>(
@ -90,8 +91,9 @@ export const LogRateAnalysisResultsGroupsTable: FC<LogRateAnalysisResultsTablePr
const visColors = euiPaletteColorBlind();
const primaryBackgroundColor = useEuiBackgroundColor('primary');
const { pinnedGroup, selectedGroup, setPinnedGroup, setSelectedGroup } =
useLogRateAnalysisStateContext();
const pinnedGroup = useAppSelector((s) => s.logRateAnalysisTableRow.pinnedGroup);
const selectedGroup = useAppSelector((s) => s.logRateAnalysisTableRow.selectedGroup);
const dispatch = useAppDispatch();
const isMounted = useMountedState();
const toggleDetails = (item: GroupTableItem) => {
@ -102,26 +104,7 @@ export const LogRateAnalysisResultsGroupsTable: FC<LogRateAnalysisResultsTablePr
itemIdToExpandedRowMapValues[item.id] = (
<LogRateAnalysisResultsTable
skippedColumns={skippedColumns}
significantItems={item.groupItemsSortedByUniqueness.reduce<SignificantItem[]>(
(p, groupItem) => {
const st = significantItems.find(
(d) => d.fieldName === groupItem.fieldName && d.fieldValue === groupItem.fieldValue
);
if (st !== undefined) {
p.push({
...st,
unique: (groupItem.duplicate ?? 0) <= 1,
});
}
return p;
},
[]
)}
loading={loading}
isExpandedRow
timeRangeMs={timeRangeMs}
groupFilter={item.groupItemsSortedByUniqueness}
searchQuery={searchQuery}
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
@ -254,12 +237,9 @@ export const LogRateAnalysisResultsGroupsTable: FC<LogRateAnalysisResultsTablePr
];
const columns = useColumns(
GROUPS_TABLE,
LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE.GROUPS,
skippedColumns,
searchQuery,
timeRangeMs,
loading,
zeroDocsFallback,
barColorOverride,
barHighlightColorOverride
) as Array<EuiBasicTableColumn<GroupTableItem>>;
@ -331,22 +311,22 @@ export const LogRateAnalysisResultsGroupsTable: FC<LogRateAnalysisResultsTablePr
pinnedGroup === null &&
pageOfItems.length > 0
) {
setSelectedGroup(pageOfItems[0]);
dispatch(setSelectedGroup(pageOfItems[0]));
}
// If a user switched pages and a pinned row is no longer visible
// on the current page, set the status of pinned rows back to `null`.
if (pinnedGroup !== null && !pageOfItems.some((item) => isEqual(item, pinnedGroup))) {
setPinnedGroup(null);
dispatch(setPinnedGroup(null));
}
}, [selectedGroup, setSelectedGroup, setPinnedGroup, pageOfItems, pinnedGroup]);
}, [dispatch, selectedGroup, pageOfItems, pinnedGroup]);
// When the analysis results table unmounts,
// make sure to reset any hovered or pinned rows.
useEffect(
() => () => {
setSelectedGroup(null);
setPinnedGroup(null);
dispatch(setSelectedGroup(null));
dispatch(setPinnedGroup(null));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
@ -413,18 +393,18 @@ export const LogRateAnalysisResultsGroupsTable: FC<LogRateAnalysisResultsTablePr
'data-test-subj': `aiopsLogRateAnalysisResultsGroupsTableRow row-${group.id}`,
onClick: () => {
if (group.id === pinnedGroup?.id) {
setPinnedGroup(null);
dispatch(setPinnedGroup(null));
} else {
setPinnedGroup(group);
dispatch(setPinnedGroup(group));
}
},
onMouseEnter: () => {
if (pinnedGroup === null) {
setSelectedGroup(group);
dispatch(setSelectedGroup(group));
}
},
onMouseLeave: () => {
setSelectedGroup(null);
dispatch(setSelectedGroup(null));
},
style: getRowStyle(group),
};

View file

@ -12,8 +12,8 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { type SignificantItem, SIGNIFICANT_ITEM_TYPE } from '@kbn/ml-agg-utils';
import { getCategoryQuery } from '@kbn/aiops-log-pattern-analysis/get_category_query';
import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker';
import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats';
import { useAppSelector } from '@kbn/aiops-log-rate-analysis/state';
import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label';
import { FieldStatsPopover } from '../field_stats_popover';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
@ -61,9 +61,13 @@ export const significantItemColumns = {
...commonColumns,
} as const;
export const GROUPS_TABLE = 'groups';
export const SIG_ITEMS_TABLE = 'significantItems';
type TableType = typeof GROUPS_TABLE | typeof SIG_ITEMS_TABLE;
export const LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE = {
GROUPS: 'groups',
SIGNIFICANT_ITEMS: 'significantItems',
} as const;
export type LogRateAnalysisResultsTableType =
typeof LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE[keyof typeof LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE];
export type ColumnNames = keyof typeof significantItemColumns | 'unique';
const logRateHelpMessage = i18n.translate(
@ -94,12 +98,9 @@ const impactMessage = i18n.translate(
);
export const useColumns = (
tableType: TableType,
tableType: LogRateAnalysisResultsTableType,
skippedColumns: string[],
searchQuery: estypes.QueryDslQueryContainer,
timeRangeMs: TimeRangeMs,
loading: boolean,
zeroDocsFallback: boolean,
barColorOverride?: string,
barHighlightColorOverride?: string,
isExpandedRow: boolean = false
@ -111,7 +112,13 @@ export const useColumns = (
const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataView.id);
const copyToClipBoardAction = useCopyToClipboardAction();
const isGroupsTable = tableType === GROUPS_TABLE;
const { earliest, latest } = useAppSelector((s) => s.logRateAnalysis);
const timeRangeMs = { from: earliest ?? 0, to: latest ?? 0 };
const loading = useAppSelector((s) => s.logRateAnalysisStream.isRunning);
const zeroDocsFallback = useAppSelector((s) => s.logRateAnalysisResults.zeroDocsFallback);
const isGroupsTable = tableType === LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE.GROUPS;
const fieldStatsServices: FieldStatsServices = useMemo(() => {
return {

View file

@ -14,7 +14,7 @@ import type { SignificantItem } from '@kbn/ml-agg-utils';
import { finalSignificantItemGroups } from '@kbn/aiops-test-utils/artificial_logs/final_significant_item_groups';
import { significantTerms } from '@kbn/aiops-test-utils/artificial_logs/significant_terms';
import type { GroupTableItem } from '@kbn/aiops-components';
import type { GroupTableItem } from '@kbn/aiops-log-rate-analysis/state';
import { getGroupTableItems } from './get_group_table_items';
import { useCopyToClipboardAction } from './use_copy_to_clipboard_action';

View file

@ -11,7 +11,7 @@ import { EuiCopy, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isSignificantItem, type SignificantItem } from '@kbn/ml-agg-utils';
import type { GroupTableItem, TableItemAction } from '@kbn/aiops-components';
import type { GroupTableItem, TableItemAction } from '@kbn/aiops-log-rate-analysis/state';
import { TableActionButton } from './table_action_button';
import { getTableItemAsKQL } from './get_table_item_as_kql';

View file

@ -10,7 +10,7 @@ import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import type { SignificantItem } from '@kbn/ml-agg-utils';
import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils';
import type { GroupTableItem, TableItemAction } from '@kbn/aiops-components';
import type { GroupTableItem, TableItemAction } from '@kbn/aiops-log-rate-analysis/state';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';

View file

@ -11,7 +11,7 @@ import type { SerializableRecord } from '@kbn/utility-types';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { isSignificantItem, type SignificantItem, SIGNIFICANT_ITEM_TYPE } from '@kbn/ml-agg-utils';
import type { GroupTableItem, TableItemAction } from '@kbn/aiops-components';
import type { GroupTableItem, TableItemAction } from '@kbn/aiops-log-rate-analysis/state';
import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';

View file

@ -10,28 +10,15 @@ import { get } from 'lodash';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import dateMath from '@kbn/datemath';
import {
getExtendedChangePoint,
type DocumentCountStatsChangePoint,
} from '@kbn/aiops-log-rate-analysis';
import { getExtendedChangePoint, type DocumentCountStats } from '@kbn/aiops-log-rate-analysis';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { SignificantItem } from '@kbn/ml-agg-utils';
import type { Query } from '@kbn/es-query';
import type { RandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
import type { GroupTableItem } from '@kbn/aiops-components';
import type { GroupTableItem } from '@kbn/aiops-log-rate-analysis/state';
import { buildExtendedBaseFilterCriteria } from './application/utils/build_extended_base_filter_criteria';
export interface DocumentCountStats {
interval?: number;
buckets?: { [key: string]: number };
changePoint?: DocumentCountStatsChangePoint;
timeRangeEarliest?: number;
timeRangeLatest?: number;
totalCount: number;
lastDocTimeStampMs?: number;
}
export interface DocumentStatsSearchStrategyParams {
earliest?: number;
latest?: number;

View file

@ -18,7 +18,7 @@ import type { Dictionary } from '@kbn/ml-url-state';
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import { useTimeBuckets } from '@kbn/ml-time-buckets';
import { AIOPS_PLUGIN_ID } from '@kbn/aiops-common/constants';
import type { GroupTableItem } from '@kbn/aiops-components';
import type { GroupTableItem } from '@kbn/aiops-log-rate-analysis/state';
import type { DocumentStatsSearchStrategyParams } from '../get_document_stats';

View file

@ -14,8 +14,9 @@ import { stringHash } from '@kbn/ml-string-hash';
import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
import { extractErrorProperties } from '@kbn/ml-error-utils';
import { RANDOM_SAMPLER_SEED } from '@kbn/aiops-log-rate-analysis/constants';
import type { DocumentCountStats } from '@kbn/aiops-log-rate-analysis/types';
import type { DocumentCountStats, DocumentStatsSearchStrategyParams } from '../get_document_stats';
import type { DocumentStatsSearchStrategyParams } from '../get_document_stats';
import { getDocumentCountStatsRequest, processDocumentCountStats } from '../get_document_stats';
import { useAiopsAppContext } from './use_aiops_app_context';

View file

@ -21,10 +21,10 @@ import {
import { RANDOM_SAMPLER_SEED } from '@kbn/aiops-log-rate-analysis/constants';
import {
addSignificantItemsGroupAction,
addSignificantItemsGroupHistogramAction,
updateLoadingStateAction,
} from '@kbn/aiops-log-rate-analysis/api/actions';
addSignificantItemsGroup,
addSignificantItemsGroupHistogram,
updateLoadingState,
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type { AiopsLogRateAnalysisApiVersion as ApiVersion } from '@kbn/aiops-log-rate-analysis/api/schema';
import { isRequestAbortedError } from '@kbn/aiops-common/is_request_aborted_error';
@ -56,7 +56,7 @@ export const groupingHandlerFactory =
function pushHistogramDataLoadingState() {
responseStream.push(
updateLoadingStateAction({
updateLoadingState({
ccsWarning: false,
loaded: stateHandler.loaded(),
loadingState: i18n.translate(
@ -70,7 +70,7 @@ export const groupingHandlerFactory =
}
responseStream.push(
updateLoadingStateAction({
updateLoadingState({
ccsWarning: false,
loaded: stateHandler.loaded(),
loadingState: i18n.translate('xpack.aiops.logRateAnalysis.loadingState.groupingResults', {
@ -133,7 +133,7 @@ export const groupingHandlerFactory =
const maxItems = Math.max(...significantItemGroups.map((g) => g.group.length));
if (maxItems > 1) {
responseStream.push(addSignificantItemsGroupAction(significantItemGroups));
responseStream.push(addSignificantItemsGroup(significantItemGroups));
}
stateHandler.loaded(PROGRESS_STEP_GROUPING, false);
@ -211,7 +211,7 @@ export const groupingHandlerFactory =
}) ?? [];
responseStream.push(
addSignificantItemsGroupHistogramAction([
addSignificantItemsGroupHistogram([
{
id: cpg.id,
histogram,

View file

@ -18,9 +18,9 @@ import { fetchHistogramsForFields } from '@kbn/ml-agg-utils';
import { RANDOM_SAMPLER_SEED } from '@kbn/aiops-log-rate-analysis/constants';
import {
addSignificantItemsHistogramAction,
updateLoadingStateAction,
} from '@kbn/aiops-log-rate-analysis/api/actions';
addSignificantItemsHistogram,
updateLoadingState,
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type { AiopsLogRateAnalysisApiVersion as ApiVersion } from '@kbn/aiops-log-rate-analysis/api/schema';
import { getCategoryQuery } from '@kbn/aiops-log-pattern-analysis/get_category_query';
@ -50,7 +50,7 @@ export const histogramHandlerFactory =
) => {
function pushHistogramDataLoadingState() {
responseStream.push(
updateLoadingStateAction({
updateLoadingState({
ccsWarning: false,
loaded: stateHandler.loaded(),
loadingState: i18n.translate(
@ -145,7 +145,7 @@ export const histogramHandlerFactory =
stateHandler.loaded((1 / fieldValuePairsCount) * PROGRESS_STEP_HISTOGRAMS, false);
pushHistogramDataLoadingState();
responseStream.push(
addSignificantItemsHistogramAction([
addSignificantItemsHistogram([
{
fieldName,
fieldValue,
@ -238,7 +238,7 @@ export const histogramHandlerFactory =
stateHandler.loaded((1 / fieldValuePairsCount) * PROGRESS_STEP_HISTOGRAMS, false);
pushHistogramDataLoadingState();
responseStream.push(
addSignificantItemsHistogramAction([
addSignificantItemsHistogram([
{
fieldName,
fieldValue,

View file

@ -8,9 +8,9 @@
import { i18n } from '@kbn/i18n';
import {
updateLoadingStateAction,
updateLoadingState,
setZeroDocsFallback,
} from '@kbn/aiops-log-rate-analysis/api/actions';
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type { AiopsLogRateAnalysisApiVersion as ApiVersion } from '@kbn/aiops-log-rate-analysis/api/schema';
import { isRequestAbortedError } from '@kbn/aiops-common/is_request_aborted_error';
@ -43,7 +43,7 @@ export const indexInfoHandlerFactory =
if (!requestBody.overrides?.remainingFieldCandidates) {
logDebugMessage('Fetch index information.');
responseStream.push(
updateLoadingStateAction({
updateLoadingState({
ccsWarning: false,
loaded: stateHandler.loaded(),
loadingState: i18n.translate(
@ -85,7 +85,7 @@ export const indexInfoHandlerFactory =
responseStream.pushPingWithTimeout();
responseStream.push(
updateLoadingStateAction({
updateLoadingState({
ccsWarning: false,
loaded: stateHandler.loaded(),
loadingState: i18n.translate(

View file

@ -6,10 +6,10 @@
*/
import {
resetAllAction,
resetErrorsAction,
resetGroupsAction,
} from '@kbn/aiops-log-rate-analysis/api/actions';
resetAll,
resetErrors,
resetGroups,
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type { AiopsLogRateAnalysisApiVersion as ApiVersion } from '@kbn/aiops-log-rate-analysis/api/schema';
import type { ResponseStreamFetchOptions } from '../response_stream_factory';
@ -24,15 +24,15 @@ export const overridesHandlerFactory =
() => {
if (!requestBody.overrides) {
logDebugMessage('Full Reset.');
responseStream.push(resetAllAction());
responseStream.push(resetAll());
} else {
logDebugMessage('Reset Errors.');
responseStream.push(resetErrorsAction());
responseStream.push(resetErrors());
}
if (requestBody.overrides?.regroupOnly) {
logDebugMessage('Reset Groups.');
responseStream.push(resetGroupsAction());
responseStream.push(resetGroups());
}
if (requestBody.overrides?.loaded) {

View file

@ -10,9 +10,9 @@ import { queue } from 'async';
import { SIGNIFICANT_ITEM_TYPE, type SignificantItem } from '@kbn/ml-agg-utils';
import { i18n } from '@kbn/i18n';
import {
addSignificantItemsAction,
updateLoadingStateAction,
} from '@kbn/aiops-log-rate-analysis/api/actions';
addSignificantItems,
updateLoadingState,
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type {
AiopsLogRateAnalysisSchema,
AiopsLogRateAnalysisApiVersion as ApiVersion,
@ -137,7 +137,7 @@ export const significantItemsHandlerFactory =
});
significantTerms.push(...pValues);
responseStream.push(addSignificantItemsAction(pValues));
responseStream.push(addSignificantItems(pValues));
fieldValuePairsCount += pValues.length;
}
@ -156,7 +156,7 @@ export const significantItemsHandlerFactory =
if (significantCategoriesForField.length > 0) {
significantCategories.push(...significantCategoriesForField);
responseStream.push(addSignificantItemsAction(significantCategoriesForField));
responseStream.push(addSignificantItems(significantCategoriesForField));
fieldValuePairsCount += significantCategoriesForField.length;
}
}
@ -164,7 +164,7 @@ export const significantItemsHandlerFactory =
stateHandler.loaded(loadingStep, false);
responseStream.push(
updateLoadingStateAction({
updateLoadingState({
ccsWarning: false,
loaded: stateHandler.loaded(),
loadingState: i18n.translate(

View file

@ -11,9 +11,9 @@ import { SIGNIFICANT_ITEM_TYPE, type SignificantItem } from '@kbn/ml-agg-utils';
import { i18n } from '@kbn/i18n';
import {
addSignificantItemsAction,
updateLoadingStateAction,
} from '@kbn/aiops-log-rate-analysis/api/actions';
addSignificantItems,
updateLoadingState,
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type {
AiopsLogRateAnalysisSchema,
@ -75,7 +75,7 @@ export const topItemsHandlerFactory =
);
if (topCategories.length > 0) {
responseStream.push(addSignificantItemsAction(topCategories));
responseStream.push(addSignificantItems(topCategories));
}
}
@ -137,11 +137,11 @@ export const topItemsHandlerFactory =
});
topTerms.push(...fetchedTopTerms);
responseStream.push(addSignificantItemsAction(fetchedTopTerms));
responseStream.push(addSignificantItems(fetchedTopTerms));
}
responseStream.push(
updateLoadingStateAction({
updateLoadingState({
ccsWarning: false,
loaded: stateHandler.loaded(),
loadingState: i18n.translate(

View file

@ -10,7 +10,7 @@ import type { ElasticsearchClient } from '@kbn/core/server';
import type { Headers, KibanaRequestEvents } from '@kbn/core-http-server';
import type { Logger } from '@kbn/logging';
import { type AiopsLogRateAnalysisApiAction } from '@kbn/aiops-log-rate-analysis/api/actions';
import { type AiopsLogRateAnalysisApiAction } from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type {
AiopsLogRateAnalysisSchema,

View file

@ -10,9 +10,9 @@ import { i18n } from '@kbn/i18n';
import type { StreamFactoryReturnType } from '@kbn/ml-response-stream/server';
import {
updateLoadingStateAction,
updateLoadingState,
type AiopsLogRateAnalysisApiAction,
} from '@kbn/aiops-log-rate-analysis/api/actions';
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
/**
* Helper function that will push a message to the stream that it's done and
@ -26,7 +26,7 @@ export const streamEndWithUpdatedLoadingStateFactory = (
) => {
return function endWithUpdatedLoadingState() {
push(
updateLoadingStateAction({
updateLoadingState({
ccsWarning: false,
loaded: 1,
loadingState: i18n.translate('xpack.aiops.logRateAnalysis.loadingState.doneMessage', {

View file

@ -8,9 +8,9 @@
import type { StreamFactoryReturnType } from '@kbn/ml-response-stream/server';
import {
addErrorAction,
addError,
type AiopsLogRateAnalysisApiAction,
} from '@kbn/aiops-log-rate-analysis/api/actions';
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type { LogDebugMessage } from './log_debug_message';
@ -25,6 +25,6 @@ export const streamPushErrorFactory = (
) => {
return function pushError(m: string) {
logDebugMessage('Push error.');
push(addErrorAction(m));
push(addError(m));
};
};

View file

@ -8,9 +8,9 @@
import type { StreamFactoryReturnType } from '@kbn/ml-response-stream/server';
import {
pingAction,
ping,
type AiopsLogRateAnalysisApiAction,
} from '@kbn/aiops-log-rate-analysis/api/actions';
} from '@kbn/aiops-log-rate-analysis/api/stream_reducer';
import type { LogDebugMessage } from './log_debug_message';
import type { StateHandler } from './state_handler';
@ -32,7 +32,7 @@ export const streamPushPingWithTimeoutFactory = (
setTimeout(() => {
if (stateHandler.isRunning()) {
logDebugMessage('Ping message.');
push(pingAction());
push(ping());
pushPingWithTimeout();
}
}, PING_FREQUENCY);

View file

@ -33,8 +33,6 @@ export const LogRateAnalysisPage: FC = () => {
</MlPageHeader>
{dataView && (
<LogRateAnalysis
// Default to false for now, until page restructure work to enable smooth sticky histogram is done
stickyHistogram={false}
dataView={dataView}
savedSearch={savedSearch}
showFrozenDataTierChoice={showNodeInfo}

View file

@ -6,15 +6,16 @@
*/
export const getAddSignificationItemsActions = (data: any[]) =>
data.filter((d) => d.type === 'add_significant_items');
data.filter((d) => d.type === 'logRateAnalysisResults/addSignificantItems');
export const getHistogramActions = (data: any[]) =>
data.filter((d) => d.type === 'add_significant_items_histogram');
data.filter((d) => d.type === 'logRateAnalysisResults/addSignificantItemsHistogram');
export const getGroupActions = (data: any[]) =>
data.filter((d) => d.type === 'add_significant_items_group');
data.filter((d) => d.type === 'logRateAnalysisResults/addSignificantItemsGroup');
export const getGroupHistogramActions = (data: any[]) =>
data.filter((d) => d.type === 'add_significant_items_group_histogram');
data.filter((d) => d.type === 'logRateAnalysisResults/addSignificantItemsGroupHistogram');
export const getErrorActions = (data: any[]) => data.filter((d) => d.type === 'add_error');
export const getErrorActions = (data: any[]) =>
data.filter((d) => d.type === 'logRateAnalysisResults/addError');

View file

@ -10,35 +10,35 @@ export const analysisTableTextfieldZerodocsfallback = [
fieldName: 'message',
fieldValue: 'Paul [11/19/2022, 8:00:34 AM] "GET /home.php HTTP/1.1" 200',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
{
fieldName: 'response_code',
fieldValue: '500',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
{
fieldName: 'url',
fieldValue: 'home.php',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
{
fieldName: 'user',
fieldValue: 'Paul',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
{
fieldName: 'version',
fieldValue: 'v1.0.0',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
];

View file

@ -10,28 +10,28 @@ export const analysisTableZerodocsfallback = [
fieldName: 'response_code',
fieldValue: '500',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
{
fieldName: 'url',
fieldValue: 'home.php',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
{
fieldName: 'user',
fieldValue: 'Paul',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
{
fieldName: 'version',
fieldValue: 'v1.0.0',
logRate: 'Chart type:bar chart',
pValue: '1.00',
pValue: '',
impact: '',
},
];