Skip to content

Commit

Permalink
[ML] AIOps: Log Rate Analysis embeddable (elastic#197943)
Browse files Browse the repository at this point in the history
## Summary

Follow up to elastic#192167 (rebase/push gone wrong)

<img width="1920" alt="image"
src="https://github.com/user-attachments/assets/0ee12b65-0bff-4a02-805d-adab1be2a52a">

- [x] Let's users create a Log Rate Analysis panel using the "Add Panel"
button when editing dashboards.
- [x] Retains functionality of links in results table to Discover and
Pattern Analysis.
[41b4337](elastic@41b4337)
- [x] Create `Logs AIOps` section in Add Panel menu.
- [x] Brushes not working with multiple panels fixed in
[75ca4ca](elastic@75ca4ca).
The reason was the `DualBrush` component used hard coded html ids.
- [x] Panel now updates when data view is changed in options flyout,
fixed in
[2b58567](elastic@2b58567).
- [x] When the user selects a data view without time field, we now show
the same warning as used for pattern analysis and the apply button gets
disabled, fixed in
[a01975d](elastic@a01975d).
- [x] Pass on and use global search/filters to embeddable.
[2c24dbd](elastic@2c24dbd)
- [x] Moving labels
[26cd1a5](elastic@26cd1a5)
- [x] No results after time range update
[632b711](elastic@632b711)

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

(cherry picked from commit 6b77e05)
  • Loading branch information
walterra committed Nov 5, 2024
1 parent 6254773 commit 2bea9bc
Show file tree
Hide file tree
Showing 75 changed files with 2,202 additions and 661 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
*/

import type { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector, useStore } from 'react-redux';
import type { AppDispatch, AppStore, RootState } from './store';
import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppStore: () => AppStore = useStore;
1 change: 1 addition & 0 deletions x-pack/.i18nrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"paths": {
"xpack.actions": "plugins/actions",
"xpack.aiops": [
"packages/ml/aiops_common",
"packages/ml/aiops_components",
"packages/ml/aiops_log_pattern_analysis",
"packages/ml/aiops_log_rate_analysis",
Expand Down
13 changes: 13 additions & 0 deletions x-pack/packages/ml/aiops_common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* 2.0.
*/

import { i18n } from '@kbn/i18n';

/**
* AIOPS_PLUGIN_ID is used as a unique identifier for the aiops plugin
*/
Expand All @@ -28,3 +30,14 @@ export const AIOPS_EMBEDDABLE_ORIGIN = {
DISCOVER: 'discover',
ML_AIOPS_LABS: 'ml_aiops_labs',
} as const;

export const AIOPS_EMBEDDABLE_GROUPING = [
{
id: 'logs-aiops',
getDisplayName: () =>
i18n.translate('xpack.aiops.embedabble.groupingDisplayName', {
defaultMessage: 'Logs AIOps',
}),
getIconType: () => 'machineLearningApp',
},
];
1 change: 1 addition & 0 deletions x-pack/packages/ml/aiops_common/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
],
"kbn_references": [
"@kbn/ml-is-populated-object",
"@kbn/i18n",
],
"exclude": [
"target/**/*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,12 @@ export const DocumentCountChart: FC<DocumentCountChartProps> = (props) => {
<>
{isBrushVisible && (
<div className="aiopsHistogramBrushes" data-test-subj={'aiopsHistogramBrushes'}>
<div css={{ height: BADGE_HEIGHT }}>
{/**
* We need position:relative on this parent container of the BrushBadges,
* because of the absolute positioning of the BrushBadges. Without it, the
* BrushBadges would not be positioned correctly when used in embedded panels.
*/}
<div css={{ height: BADGE_HEIGHT, position: 'relative' }}>
<BrushBadge
label={
baselineBrush.label ??
Expand Down
15 changes: 11 additions & 4 deletions x-pack/packages/ml/aiops_components/src/dual_brush/dual_brush.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/

import { isEqual } from 'lodash';
import React, { useEffect, useRef, type FC } from 'react';
import React, { useEffect, useMemo, useRef, type FC } from 'react';

import { htmlIdGenerator } from '@elastic/eui';

import * as d3Brush from 'd3-brush';
import * as d3Scale from 'd3-scale';
Expand Down Expand Up @@ -100,6 +102,10 @@ export const DualBrush: FC<DualBrushProps> = (props) => {
const d3BrushContainer = useRef(null);
const brushes = useRef<DualBrush[]>([]);

// id to prefix html ids for the brushes since this component can be used
// multiple times within dashboard and embedded charts.
const htmlId = useMemo(() => htmlIdGenerator()(), []);

// We need to pass props to refs here because the d3-brush code doesn't consider
// native React prop changes. The brush code does its own check whether these props changed then.
// The initialized brushes might otherwise act on stale data.
Expand Down Expand Up @@ -135,10 +141,10 @@ export const DualBrush: FC<DualBrushProps> = (props) => {
const xMax = x(maxRef.current) ?? 0;
const minExtentPx = Math.round((xMax - xMin) / 100);

const baselineBrush = d3.select('#aiops-brush-baseline');
const baselineBrush = d3.select(`#aiops-brush-baseline-${htmlId}`);
const baselineSelection = d3.brushSelection(baselineBrush.node() as SVGGElement);

const deviationBrush = d3.select('#aiops-brush-deviation');
const deviationBrush = d3.select(`#aiops-brush-deviation-${htmlId}`);
const deviationSelection = d3.brushSelection(deviationBrush.node() as SVGGElement);

if (!isBrushXSelection(deviationSelection) || !isBrushXSelection(baselineSelection)) {
Expand Down Expand Up @@ -260,7 +266,7 @@ export const DualBrush: FC<DualBrushProps> = (props) => {
.insert('g', '.brush')
.attr('class', 'brush')
.attr('id', (b: DualBrush) => {
return 'aiops-brush-' + b.id;
return `aiops-brush-${b.id}-${htmlId}`;
})
.attr('data-test-subj', (b: DualBrush) => {
// Uppercase the first character of the `id` so we get aiopsBrushBaseline/aiopsBrushDeviation.
Expand Down Expand Up @@ -339,6 +345,7 @@ export const DualBrush: FC<DualBrushProps> = (props) => {
drawBrushes();
}
}, [
htmlId,
min,
max,
width,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export const ProgressControls: FC<PropsWithChildren<ProgressControlProps>> = (pr
const { euiTheme } = useEuiTheme();

const runningProgressBarStyles = useAnimatedProgressBarBackground(euiTheme.colors.success);
const analysisCompleteStyle = { display: 'none' };

return (
<EuiFlexGroup alignItems="center" gutterSize="s">
Expand Down Expand Up @@ -144,32 +143,30 @@ export const ProgressControls: FC<PropsWithChildren<ProgressControlProps>> = (pr
</EuiFlexItem>
</EuiFlexGroup>
) : null}
<EuiFlexGroup
direction="column"
gutterSize="none"
css={progress === 1 ? analysisCompleteStyle : undefined}
>
<EuiFlexItem data-test-subj="aiopProgressTitle">
<EuiText size="xs" color="subdued">
<FormattedMessage
data-test-subj="aiopsProgressTitleMessage"
id="xpack.aiops.progressTitle"
defaultMessage="Progress: {progress}% — {progressMessage}"
values={{ progress: progressOutput, progressMessage }}
{progress !== 1 ? (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem data-test-subj="aiopProgressTitle">
<EuiText size="xs" color="subdued">
<FormattedMessage
data-test-subj="aiopsProgressTitleMessage"
id="xpack.aiops.progressTitle"
defaultMessage="Progress: {progress}% — {progressMessage}"
values={{ progress: progressOutput, progressMessage }}
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem css={isRunning ? runningProgressBarStyles : undefined}>
<EuiProgress
aria-label={i18n.translate('xpack.aiops.progressAriaLabel', {
defaultMessage: 'Progress',
})}
value={progressOutput}
max={100}
size="m"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem css={isRunning ? runningProgressBarStyles : undefined}>
<EuiProgress
aria-label={i18n.translate('xpack.aiops.progressAriaLabel', {
defaultMessage: 'Progress',
})}
value={progressOutput}
max={100}
size="m"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
) : null}
</EuiFlexItem>
{children}
</EuiFlexGroup>
Expand Down
6 changes: 6 additions & 0 deletions x-pack/packages/ml/aiops_log_rate_analysis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ export const RANDOM_SAMPLER_SEED = 3867412;

/** Highlighting color for charts */
export const LOG_RATE_ANALYSIS_HIGHLIGHT_COLOR = 'orange';

/** */
export const EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE = 'aiopsLogRateAnalysisEmbeddable' as const;

/** */
export const LOG_RATE_ANALYSIS_DATA_VIEW_REF_NAME = 'aiopsLogRateAnalysisEmbeddableDataViewId';
5 changes: 2 additions & 3 deletions x-pack/packages/ml/aiops_log_rate_analysis/state/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
*/

import type { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector, useStore } from 'react-redux';
import type { AppDispatch, AppStore, RootState } from './store';
import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, 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;
6 changes: 4 additions & 2 deletions x-pack/packages/ml/aiops_log_rate_analysis/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
setAnalysisType,
setAutoRunAnalysis,
setDocumentCountChartData,
setGroupResults,
setInitialAnalysisStart,
setIsBrushCleared,
setStickyHistogram,
Expand All @@ -23,9 +24,10 @@ export {
setPinnedSignificantItem,
setSelectedGroup,
setSelectedSignificantItem,
} from './log_rate_analysis_table_row_slice';
setSkippedColumns,
} from './log_rate_analysis_table_slice';
export { LogRateAnalysisReduxProvider } from './store';
export { useAppDispatch, useAppSelector, useAppStore } from './hooks';
export { useAppDispatch, useAppSelector } from './hooks';
export { useCurrentSelectedGroup } from './use_current_selected_group';
export { useCurrentSelectedSignificantItem } from './use_current_selected_significant_item';
export type { GroupTableItem, GroupTableItemGroup, TableItemAction } from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import { httpServiceMock } from '@kbn/core/public/mocks';

import type { FetchFieldCandidatesResponse } from '../queries/fetch_field_candidates';

import { fetchFieldCandidates } from './log_rate_analysis_field_candidates_slice';
import { fetchFieldCandidates, getDefaultState } from './log_rate_analysis_field_candidates_slice';

const mockHttp = httpServiceMock.createStartContract();

describe('fetchFieldCandidates', () => {
it('dispatches field candidates', async () => {
const mockDispatch = jest.fn();
const mockGetState = jest.fn();
const mockGetState = jest.fn().mockReturnValue({
logRateAnalysisFieldCandidates: getDefaultState(),
});

const mockResponse: FetchFieldCandidatesResponse = {
isECS: false,
Expand Down Expand Up @@ -60,7 +62,12 @@ describe('fetchFieldCandidates', () => {
payload: {
fieldSelectionMessage:
'2 out of 5 fields were preselected for the analysis. Use the "Fields" dropdown to adjust the selection.',
fieldFilterSkippedItems: [
initialFieldFilterSkippedItems: [
'another-keyword-field',
'another-text-field',
'yet-another-text-field',
],
currentFieldFilterSkippedItems: [
'another-keyword-field',
'another-text-field',
'yet-another-text-field',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,14 @@ export const fetchFieldCandidates = createAsyncThunk(
...selectedKeywordFieldCandidates,
...selectedTextFieldCandidates,
];
const fieldFilterSkippedItems = fieldFilterUniqueItems.filter(
const initialFieldFilterSkippedItems = fieldFilterUniqueItems.filter(
(d) => !fieldFilterUniqueSelectedItems.includes(d)
);

const currentFieldFilterSkippedItems = (
thunkApi.getState() as { logRateAnalysisFieldCandidates: FieldCandidatesState }
).logRateAnalysisFieldCandidates.currentFieldFilterSkippedItems;

thunkApi.dispatch(
setAllFieldCandidates({
fieldSelectionMessage: getFieldSelectionMessage(
Expand All @@ -102,7 +106,13 @@ export const fetchFieldCandidates = createAsyncThunk(
fieldFilterUniqueSelectedItems.length
),
fieldFilterUniqueItems,
fieldFilterSkippedItems,
initialFieldFilterSkippedItems,
// If the currentFieldFilterSkippedItems is null, we're on the first load,
// only then we set the current skipped fields to the initial skipped fields.
currentFieldFilterSkippedItems:
currentFieldFilterSkippedItems === null
? initialFieldFilterSkippedItems
: currentFieldFilterSkippedItems,
keywordFieldCandidates,
textFieldCandidates,
selectedKeywordFieldCandidates,
Expand All @@ -116,18 +126,20 @@ export interface FieldCandidatesState {
isLoading: boolean;
fieldSelectionMessage?: string;
fieldFilterUniqueItems: string[];
fieldFilterSkippedItems: string[];
initialFieldFilterSkippedItems: string[];
currentFieldFilterSkippedItems: string[] | null;
keywordFieldCandidates: string[];
textFieldCandidates: string[];
selectedKeywordFieldCandidates: string[];
selectedTextFieldCandidates: string[];
}

function getDefaultState(): FieldCandidatesState {
export function getDefaultState(): FieldCandidatesState {
return {
isLoading: false,
fieldFilterUniqueItems: [],
fieldFilterSkippedItems: [],
initialFieldFilterSkippedItems: [],
currentFieldFilterSkippedItems: null,
keywordFieldCandidates: [],
textFieldCandidates: [],
selectedKeywordFieldCandidates: [],
Expand All @@ -145,6 +157,12 @@ export const logRateAnalysisFieldCandidatesSlice = createSlice({
) => {
return { ...state, ...action.payload };
},
setCurrentFieldFilterSkippedItems: (
state: FieldCandidatesState,
action: PayloadAction<string[]>
) => {
return { ...state, currentFieldFilterSkippedItems: action.payload };
},
},
extraReducers: (builder) => {
builder.addCase(fetchFieldCandidates.pending, (state) => {
Expand All @@ -157,4 +175,5 @@ export const logRateAnalysisFieldCandidatesSlice = createSlice({
});

// Action creators are generated for each case reducer function
export const { setAllFieldCandidates } = logRateAnalysisFieldCandidatesSlice.actions;
export const { setAllFieldCandidates, setCurrentFieldFilterSkippedItems } =
logRateAnalysisFieldCandidatesSlice.actions;
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface LogRateAnalysisState {
autoRunAnalysis: boolean;
initialAnalysisStart: InitialAnalysisStart;
isBrushCleared: boolean;
groupResults: boolean;
stickyHistogram: boolean;
chartWindowParameters?: WindowParameters;
earliest?: number;
Expand All @@ -48,6 +49,7 @@ function getDefaultState(): LogRateAnalysisState {
autoRunAnalysis: true,
initialAnalysisStart: undefined,
isBrushCleared: true,
groupResults: false,
documentStats: {
sampleProbability: 1,
totalCount: 0,
Expand Down Expand Up @@ -98,6 +100,9 @@ export const logRateAnalysisSlice = createSlice({
state.intervalMs = action.payload.intervalMs;
state.documentStats = action.payload.documentStats;
},
setGroupResults: (state: LogRateAnalysisState, action: PayloadAction<boolean>) => {
state.groupResults = action.payload;
},
setInitialAnalysisStart: (
state: LogRateAnalysisState,
action: PayloadAction<InitialAnalysisStart>
Expand Down Expand Up @@ -127,6 +132,7 @@ export const {
setAnalysisType,
setAutoRunAnalysis,
setDocumentCountChartData,
setGroupResults,
setInitialAnalysisStart,
setIsBrushCleared,
setStickyHistogram,
Expand Down
Loading

0 comments on commit 2bea9bc

Please sign in to comment.