Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] AIOps: Adds Log Rate Analysis embeddable for dashboards #197943

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
030c49a
wip: log rate analysis embeddable boilerplate
walterra Sep 4, 2024
ce6dc17
wip: log rate analysis embeddable boilerplate
walterra Sep 4, 2024
00651f0
ui action
walterra Sep 5, 2024
a66710d
fix register embeddable factory
walterra Sep 5, 2024
83b9265
fix icon
walterra Sep 5, 2024
61893b9
replace here be dragons with actual embedded content
walterra Sep 5, 2024
c4ab732
fix nested embedding with multiple contexts
walterra Sep 26, 2024
17c03f4
tweak progress bar layout
walterra Sep 26, 2024
c8689ea
pass deps to DataSourceContextProvider instead of using context withi…
walterra Sep 26, 2024
0253bce
cleanup settings
walterra Sep 26, 2024
e5324d8
cleanup
walterra Sep 26, 2024
a496056
layout tweaks
walterra Sep 26, 2024
e3b1b64
move log rate analysis results options to their own component
walterra Sep 26, 2024
508fe39
options toggle for embeddable
walterra Sep 27, 2024
ec74af2
linting
walterra Sep 27, 2024
494827f
restructure add panel menu with logs/aiops section
walterra Sep 30, 2024
7c70ccf
tweak embedding constants
walterra Sep 30, 2024
3291862
cleanup embeddingOrigin/context
walterra Sep 30, 2024
e5028a9
fix i18n
walterra Sep 30, 2024
5232171
fix i18n
walterra Sep 30, 2024
8a68122
fix storing skipped columns in local storage via redux toolkit
walterra Oct 7, 2024
fd00b43
add jest tests
walterra Oct 7, 2024
76eb2fb
remove unused useAppStore
walterra Oct 8, 2024
cbcfe51
remove export
walterra Oct 8, 2024
d9367da
move cpd to ml&analytics
walterra Oct 9, 2024
10cedc4
tweak options button
walterra Oct 10, 2024
5898ab6
fix to make html ids unique for each component instance for DualBrush
walterra Oct 10, 2024
da8c698
fix to update panel component when data view changes
walterra Oct 10, 2024
382efbc
adds a check to allow only data views with time fields
walterra Oct 10, 2024
f8e820c
fix url locator
walterra Oct 10, 2024
7435b5f
fix contexts
walterra Oct 11, 2024
6b06e41
Pass on and use global search/filters to embeddable.
walterra Oct 11, 2024
a16180d
Fix DualBrush positioning in embedded panels
walterra Oct 11, 2024
e476f9b
fix current field candidates list
walterra Oct 11, 2024
03c3cdc
linting
walterra Oct 11, 2024
699fe19
fix types
walterra Oct 11, 2024
652c563
fix applying the query
walterra Oct 25, 2024
db0dd47
adds functional test for log rate analysis embeddable
walterra Oct 25, 2024
957caff
Merge branch 'main' into ml-aiops-log-rate-analysis-embeddable-2
walterra Oct 30, 2024
e799c52
fix initial loading
walterra Oct 30, 2024
8c6c990
Merge branch 'main' into ml-aiops-log-rate-analysis-embeddable-2
walterra Nov 4, 2024
7816dc5
use translated string for smart grouping legend
walterra Nov 4, 2024
1e82368
update TimeFieldWarning component texts to be suitable for both log r…
walterra Nov 4, 2024
558463c
Merge branch 'main' into ml-aiops-log-rate-analysis-embeddable-2
walterra Nov 4, 2024
57d354a
Merge branch 'main' into ml-aiops-log-rate-analysis-embeddable-2
walterra Nov 5, 2024
c1fe2f7
aria-label for aiopsLogRateAnalysisOptionsButton
walterra Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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