From 2bea9bc430e96fc4b28c99198d1a30021d64a7a5 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 5 Nov 2024 15:48:45 +0100 Subject: [PATCH] [ML] AIOps: Log Rate Analysis embeddable (#197943) ## Summary Follow up to #192167 (rebase/push gone wrong) image - [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](https://github.com/elastic/kibana/pull/192167/commits/41b4337f9ac15103c6b151c94cfb6b7f944aeb89) - [x] Create `Logs AIOps` section in Add Panel menu. - [x] Brushes not working with multiple panels fixed in [75ca4ca](https://github.com/elastic/kibana/pull/192167/commits/75ca4cac37abda94ae7fb9d5132283e1def0b85e). 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](https://github.com/elastic/kibana/pull/192167/commits/2b5856777151d3ccc5e882c895704008e575aaff). - [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](https://github.com/elastic/kibana/pull/192167/commits/a01975dba7b5218ea0f094ebc5cb74cd8670131a). - [x] Pass on and use global search/filters to embeddable. [2c24dbd](https://github.com/elastic/kibana/pull/192167/commits/2c24dbd116ef4ae19ff58796b2c5fc4f61a86234) - [x] Moving labels [26cd1a5](https://github.com/elastic/kibana/pull/192167/commits/26cd1a53dfbbea34094cd840eb885a78f29d8de6) - [x] No results after time range update [632b711](https://github.com/elastic/kibana/pull/192167/commits/632b711ca1de77d113192649f56c5b18ae453f86) ### 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 6b77e05586524892dc42c34d0b5b2a561cebc0ae) --- .../app/pages/page_redux_stream/hooks.ts | 5 +- x-pack/.i18nrc.json | 1 + x-pack/packages/ml/aiops_common/constants.ts | 13 + x-pack/packages/ml/aiops_common/tsconfig.json | 1 + .../document_count_chart.tsx | 7 +- .../src/dual_brush/dual_brush.tsx | 15 +- .../progress_controls/progress_controls.tsx | 49 ++-- .../ml/aiops_log_rate_analysis/constants.ts | 6 + .../ml/aiops_log_rate_analysis/state/hooks.ts | 5 +- .../ml/aiops_log_rate_analysis/state/index.ts | 6 +- ...te_analysis_field_candidates_slice.test.ts | 13 +- ...og_rate_analysis_field_candidates_slice.ts | 31 ++- .../state/log_rate_analysis_slice.ts | 6 + .../log_rate_analysis_table_row_slice.ts | 72 ------ .../log_rate_analysis_table_slice.test.ts | 130 ++++++++++ .../state/log_rate_analysis_table_slice.ts | 172 +++++++++++++ .../aiops_log_rate_analysis/state/store.tsx | 21 +- .../state/use_current_selected_group.ts | 4 +- .../use_current_selected_significant_item.ts | 5 +- .../field_stats_flyout_provider.tsx | 24 +- .../field_stats_info_button.tsx | 16 +- .../ml/field_stats_flyout/tsconfig.json | 3 +- .../use_field_stats_flyout_context.ts | 25 +- .../use_field_stats_trigger.tsx | 2 + .../change_point_detection/fields_config.tsx | 3 +- .../document_count_content.tsx | 17 +- .../log_rate_analysis_content.tsx | 18 +- .../log_rate_analysis_for_embeddable.tsx | 65 +++++ .../log_rate_analysis_options.tsx | 190 ++++++++++++++ .../log_rate_analysis_results.tsx | 237 +++++------------- .../log_rate_analysis_results_table/index.ts | 1 - .../log_rate_analysis_results_table.tsx | 10 +- ...log_rate_analysis_results_table_groups.tsx | 4 +- .../use_columns.tsx | 74 ++---- .../use_view_in_discover_action.tsx | 4 +- ...se_view_in_log_pattern_analysis_action.tsx | 2 +- .../public/components/time_field_warning.tsx | 32 +++ .../change_point_chart_initializer.tsx | 76 +++++- .../embeddable_change_point_chart_factory.tsx | 41 +-- .../embeddables/change_point_chart/types.ts | 8 - .../plugins/aiops/public/embeddables/index.ts | 5 + .../embeddable_log_rate_analysis_factory.tsx | 211 ++++++++++++++++ .../embeddables/log_rate_analysis/index.ts | 8 + ...ize_log_rate_analysis_analysis_controls.ts | 43 ++++ ...g_rate_analysis_embeddable_initializer.tsx | 230 +++++++++++++++++ ...resolve_log_rate_analysis_config_input.tsx | 94 +++++++ .../embeddables/log_rate_analysis/types.ts | 39 +++ .../embeddable_pattern_analysis_factory.tsx | 45 +--- ...=> pattern_analysis_component_wrapper.tsx} | 0 .../pattern_analysis_initializer.tsx | 32 +-- .../embeddables/pattern_analysis/types.ts | 6 - .../public/hooks/use_aiops_app_context.ts | 27 +- .../aiops/public/hooks/use_data_source.tsx | 11 +- .../change_point_detection.tsx | 56 ++--- .../aiops/public/shared_components/index.tsx | 22 ++ .../log_rate_analysis_embeddable_wrapper.tsx | 179 +++++++++++++ .../shared_components/pattern_analysis.tsx | 54 ++-- x-pack/plugins/aiops/public/types/storage.ts | 5 - .../ui_actions/create_change_point_chart.tsx | 2 + .../create_log_rate_analysis_actions.tsx | 91 +++++++ .../create_pattern_analysis_action.tsx | 16 +- .../plugins/aiops/public/ui_actions/index.ts | 4 + .../log_rate_analysis_action_context.ts | 24 ++ x-pack/plugins/aiops/tsconfig.json | 3 +- .../configuration_step_form.tsx | 1 + .../new_job/pages/new_job/wizard_steps.tsx | 1 + .../components/wizard/wizard.tsx | 3 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - x-pack/test/functional/apps/aiops/index.ts | 1 + .../log_rate_analysis_dashboard_embeddable.ts | 104 ++++++++ .../services/aiops/dashboard_embeddables.ts | 110 ++++++++ .../test/functional/services/aiops/index.ts | 3 + .../services/aiops/log_rate_analysis_page.ts | 16 ++ 75 files changed, 2202 insertions(+), 661 deletions(-) delete mode 100644 x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_row_slice.ts create mode 100644 x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_slice.test.ts create mode 100644 x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_slice.ts create mode 100644 x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_for_embeddable.tsx create mode 100644 x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_options.tsx create mode 100644 x-pack/plugins/aiops/public/components/time_field_warning.tsx create mode 100644 x-pack/plugins/aiops/public/embeddables/log_rate_analysis/embeddable_log_rate_analysis_factory.tsx create mode 100644 x-pack/plugins/aiops/public/embeddables/log_rate_analysis/index.ts create mode 100644 x-pack/plugins/aiops/public/embeddables/log_rate_analysis/initialize_log_rate_analysis_analysis_controls.ts create mode 100644 x-pack/plugins/aiops/public/embeddables/log_rate_analysis/log_rate_analysis_embeddable_initializer.tsx create mode 100644 x-pack/plugins/aiops/public/embeddables/log_rate_analysis/resolve_log_rate_analysis_config_input.tsx create mode 100644 x-pack/plugins/aiops/public/embeddables/log_rate_analysis/types.ts rename x-pack/plugins/aiops/public/embeddables/pattern_analysis/{pattern_analysys_component_wrapper.tsx => pattern_analysis_component_wrapper.tsx} (100%) create mode 100644 x-pack/plugins/aiops/public/shared_components/log_rate_analysis_embeddable_wrapper.tsx create mode 100644 x-pack/plugins/aiops/public/ui_actions/create_log_rate_analysis_actions.tsx create mode 100644 x-pack/plugins/aiops/public/ui_actions/log_rate_analysis_action_context.ts create mode 100644 x-pack/test/functional/apps/aiops/log_rate_analysis_dashboard_embeddable.ts create mode 100644 x-pack/test/functional/services/aiops/dashboard_embeddables.ts diff --git a/examples/response_stream/public/containers/app/pages/page_redux_stream/hooks.ts b/examples/response_stream/public/containers/app/pages/page_redux_stream/hooks.ts index f1c8c671611a8..735e70916593f 100644 --- a/examples/response_stream/public/containers/app/pages/page_redux_stream/hooks.ts +++ b/examples/response_stream/public/containers/app/pages/page_redux_stream/hooks.ts @@ -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 = useSelector; -export const useAppStore: () => AppStore = useStore; diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 7afbc9dc704c4..e1e8478aa0517 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -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", diff --git a/x-pack/packages/ml/aiops_common/constants.ts b/x-pack/packages/ml/aiops_common/constants.ts index 39a0fdc5842c8..1a75e929c147a 100644 --- a/x-pack/packages/ml/aiops_common/constants.ts +++ b/x-pack/packages/ml/aiops_common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; + /** * AIOPS_PLUGIN_ID is used as a unique identifier for the aiops plugin */ @@ -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', + }, +]; diff --git a/x-pack/packages/ml/aiops_common/tsconfig.json b/x-pack/packages/ml/aiops_common/tsconfig.json index 806b5b07e847e..ffd8c074a421d 100644 --- a/x-pack/packages/ml/aiops_common/tsconfig.json +++ b/x-pack/packages/ml/aiops_common/tsconfig.json @@ -15,6 +15,7 @@ ], "kbn_references": [ "@kbn/ml-is-populated-object", + "@kbn/i18n", ], "exclude": [ "target/**/*", diff --git a/x-pack/packages/ml/aiops_components/src/document_count_chart/document_count_chart.tsx b/x-pack/packages/ml/aiops_components/src/document_count_chart/document_count_chart.tsx index 39291762b8fca..d9f68fe7ef890 100644 --- a/x-pack/packages/ml/aiops_components/src/document_count_chart/document_count_chart.tsx +++ b/x-pack/packages/ml/aiops_components/src/document_count_chart/document_count_chart.tsx @@ -426,7 +426,12 @@ export const DocumentCountChart: FC = (props) => { <> {isBrushVisible && (
-
+ {/** + * 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. + */} +
= (props) => { const d3BrushContainer = useRef(null); const brushes = useRef([]); + // 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. @@ -135,10 +141,10 @@ export const DualBrush: FC = (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)) { @@ -260,7 +266,7 @@ export const DualBrush: FC = (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. @@ -339,6 +345,7 @@ export const DualBrush: FC = (props) => { drawBrushes(); } }, [ + htmlId, min, max, width, diff --git a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx index 098f7038f82c8..173f33e08f0b4 100644 --- a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx +++ b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx @@ -70,7 +70,6 @@ export const ProgressControls: FC> = (pr const { euiTheme } = useEuiTheme(); const runningProgressBarStyles = useAnimatedProgressBarBackground(euiTheme.colors.success); - const analysisCompleteStyle = { display: 'none' }; return ( @@ -144,32 +143,30 @@ export const ProgressControls: FC> = (pr ) : null} - - - - + + + + + + + - - - - - - + + + ) : null} {children} diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/constants.ts b/x-pack/packages/ml/aiops_log_rate_analysis/constants.ts index 054bb876a4f7a..a9812a7507441 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/constants.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/constants.ts @@ -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'; diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/hooks.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/hooks.ts index 4652d604c5d61..d02a3bea22bf3 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/hooks.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/hooks.ts @@ -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 = useSelector; -export const useAppStore: () => AppStore = useStore; diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/index.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/index.ts index 785bb02c24f31..7f7710ec23f3b 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/index.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/index.ts @@ -11,6 +11,7 @@ export { setAnalysisType, setAutoRunAnalysis, setDocumentCountChartData, + setGroupResults, setInitialAnalysisStart, setIsBrushCleared, setStickyHistogram, @@ -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'; diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_field_candidates_slice.test.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_field_candidates_slice.test.ts index 4f829b0e0bf5a..5b4946dc2eb2b 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_field_candidates_slice.test.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_field_candidates_slice.test.ts @@ -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, @@ -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', diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_field_candidates_slice.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_field_candidates_slice.ts index aa5cb969e5401..07b1cd6fee402 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_field_candidates_slice.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_field_candidates_slice.ts @@ -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( @@ -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, @@ -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: [], @@ -145,6 +157,12 @@ export const logRateAnalysisFieldCandidatesSlice = createSlice({ ) => { return { ...state, ...action.payload }; }, + setCurrentFieldFilterSkippedItems: ( + state: FieldCandidatesState, + action: PayloadAction + ) => { + return { ...state, currentFieldFilterSkippedItems: action.payload }; + }, }, extraReducers: (builder) => { builder.addCase(fetchFieldCandidates.pending, (state) => { @@ -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; diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_slice.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_slice.ts index 251f0d3263800..8399e896900c6 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_slice.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_slice.ts @@ -34,6 +34,7 @@ export interface LogRateAnalysisState { autoRunAnalysis: boolean; initialAnalysisStart: InitialAnalysisStart; isBrushCleared: boolean; + groupResults: boolean; stickyHistogram: boolean; chartWindowParameters?: WindowParameters; earliest?: number; @@ -48,6 +49,7 @@ function getDefaultState(): LogRateAnalysisState { autoRunAnalysis: true, initialAnalysisStart: undefined, isBrushCleared: true, + groupResults: false, documentStats: { sampleProbability: 1, totalCount: 0, @@ -98,6 +100,9 @@ export const logRateAnalysisSlice = createSlice({ state.intervalMs = action.payload.intervalMs; state.documentStats = action.payload.documentStats; }, + setGroupResults: (state: LogRateAnalysisState, action: PayloadAction) => { + state.groupResults = action.payload; + }, setInitialAnalysisStart: ( state: LogRateAnalysisState, action: PayloadAction @@ -127,6 +132,7 @@ export const { setAnalysisType, setAutoRunAnalysis, setDocumentCountChartData, + setGroupResults, setInitialAnalysisStart, setIsBrushCleared, setStickyHistogram, diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_row_slice.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_row_slice.ts deleted file mode 100644 index 3da98e4cc80ff..0000000000000 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_row_slice.ts +++ /dev/null @@ -1,72 +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 { 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) => { - state.pinnedGroup = action.payload; - }, - setPinnedSignificantItem: ( - state: LogRateAnalysisTableRowState, - action: PayloadAction - ) => { - state.pinnedSignificantItem = action.payload; - }, - setSelectedGroup: (state: LogRateAnalysisTableRowState, action: PayloadAction) => { - state.selectedGroup = action.payload; - }, - setSelectedSignificantItem: ( - state: LogRateAnalysisTableRowState, - action: PayloadAction - ) => { - state.selectedSignificantItem = action.payload; - }, - }, -}); - -// Action creators are generated for each case reducer function -export const { - clearAllRowState, - setPinnedGroup, - setPinnedSignificantItem, - setSelectedGroup, - setSelectedSignificantItem, -} = logRateAnalysisTableRowSlice.actions; diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_slice.test.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_slice.test.ts new file mode 100644 index 0000000000000..498ada00654f0 --- /dev/null +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_slice.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { configureStore } from '@reduxjs/toolkit'; +import { + logRateAnalysisTableSlice, + localStorageListenerMiddleware, + setSkippedColumns, + getPreloadedState, + AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS, + type LogRateAnalysisResultsTableColumnName, +} from './log_rate_analysis_table_slice'; + +describe('getPreloadedState', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('should return default state when localStorage is empty', () => { + const state = getPreloadedState(); + expect(state).toEqual({ + skippedColumns: ['p-value', 'Baseline rate', 'Deviation rate'], + pinnedGroup: null, + pinnedSignificantItem: null, + selectedGroup: null, + selectedSignificantItem: null, + }); + }); + + it('should return state with skippedColumns from localStorage', () => { + const skippedColumns = ['Log rate', 'Doc count']; + localStorage.setItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS, JSON.stringify(skippedColumns)); + + const state = getPreloadedState(); + expect(state.skippedColumns).toEqual(skippedColumns); + }); + + it('should return default state when localStorage contains invalid JSON', () => { + localStorage.setItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS, 'invalid-json'); + + const state = getPreloadedState(); + expect(state).toEqual({ + skippedColumns: ['p-value', 'Baseline rate', 'Deviation rate'], + pinnedGroup: null, + pinnedSignificantItem: null, + selectedGroup: null, + selectedSignificantItem: null, + }); + }); + + it('should return default state when localStorage does not contain skippedColumns', () => { + localStorage.setItem('someOtherKey', JSON.stringify(['someValue'])); + + const state = getPreloadedState(); + expect(state).toEqual({ + skippedColumns: ['p-value', 'Baseline rate', 'Deviation rate'], + pinnedGroup: null, + pinnedSignificantItem: null, + selectedGroup: null, + selectedSignificantItem: null, + }); + }); +}); + +type Store = ReturnType; + +describe('localStorageListenerMiddleware', () => { + let store: Store; + + beforeEach(() => { + localStorage.clear(); + store = configureStore({ + reducer: { + logRateAnalysisTable: logRateAnalysisTableSlice.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().prepend(localStorageListenerMiddleware.middleware), + }) as Store; + }); + + it('should save skippedColumns to localStorage when setSkippedColumns is dispatched', () => { + const skippedColumns: LogRateAnalysisResultsTableColumnName[] = ['Log rate', 'Doc count']; + store.dispatch(setSkippedColumns(skippedColumns)); + + const storedSkippedColumns = localStorage.getItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS); + expect(storedSkippedColumns).toEqual(JSON.stringify(skippedColumns)); + }); + + it('should handle invalid JSON in localStorage gracefully', () => { + localStorage.setItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS, 'invalid-json'); + const skippedColumns: LogRateAnalysisResultsTableColumnName[] = ['Log rate', 'Doc count']; + store.dispatch(setSkippedColumns(skippedColumns)); + + const storedSkippedColumns = localStorage.getItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS); + expect(storedSkippedColumns).toEqual(JSON.stringify(skippedColumns)); + }); + + it('should not overwrite other localStorage keys', () => { + const otherKey = 'someOtherKey'; + const otherValue = ['someValue']; + localStorage.setItem(otherKey, JSON.stringify(otherValue)); + + const skippedColumns: LogRateAnalysisResultsTableColumnName[] = ['Log rate', 'Doc count']; + store.dispatch(setSkippedColumns(skippedColumns)); + + const storedOtherValue = localStorage.getItem(otherKey); + expect(storedOtherValue).toEqual(JSON.stringify(otherValue)); + }); + + it('should update localStorage when skippedColumns are updated multiple times', () => { + const initialSkippedColumns: LogRateAnalysisResultsTableColumnName[] = ['Log rate']; + store.dispatch(setSkippedColumns(initialSkippedColumns)); + + let storedSkippedColumns = localStorage.getItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS); + expect(storedSkippedColumns).toEqual(JSON.stringify(initialSkippedColumns)); + + const updatedSkippedColumns: LogRateAnalysisResultsTableColumnName[] = [ + 'Log rate', + 'Doc count', + ]; + store.dispatch(setSkippedColumns(updatedSkippedColumns)); + + storedSkippedColumns = localStorage.getItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS); + expect(storedSkippedColumns).toEqual(JSON.stringify(updatedSkippedColumns)); + }); +}); diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_slice.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_slice.ts new file mode 100644 index 0000000000000..1d9c83dea98a6 --- /dev/null +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/log_rate_analysis_table_slice.ts @@ -0,0 +1,172 @@ +/* + * 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, createListenerMiddleware } from '@reduxjs/toolkit'; + +import { i18n } from '@kbn/i18n'; +import type { SignificantItem } from '@kbn/ml-agg-utils'; + +import type { GroupTableItem } from './types'; + +export const AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS = 'aiops.logRateAnalysisResultColumns'; + +export const commonColumns = { + ['Log rate']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.logRateColumnTitle', { + defaultMessage: 'Log rate', + }), + ['Doc count']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.docCountColumnTitle', { + defaultMessage: 'Doc count', + }), + ['p-value']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.pValueColumnTitle', { + defaultMessage: 'p-value', + }), + ['Impact']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.impactColumnTitle', { + defaultMessage: 'Impact', + }), + ['Baseline rate']: i18n.translate( + 'xpack.aiops.logRateAnalysis.resultsTable.baselineRateColumnTitle', + { + defaultMessage: 'Baseline rate', + } + ), + ['Deviation rate']: i18n.translate( + 'xpack.aiops.logRateAnalysis.resultsTable.deviationRateColumnTitle', + { + defaultMessage: 'Deviation rate', + } + ), + ['Log rate change']: i18n.translate( + 'xpack.aiops.logRateAnalysis.resultsTable.logRateChangeColumnTitle', + { + defaultMessage: 'Log rate change', + } + ), + ['Actions']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.actionsColumnTitle', { + defaultMessage: 'Actions', + }), +}; + +export const significantItemColumns = { + ['Field name']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.fieldNameColumnTitle', { + defaultMessage: 'Field name', + }), + ['Field value']: i18n.translate( + 'xpack.aiops.logRateAnalysis.resultsTable.fieldValueColumnTitle', + { + defaultMessage: 'Field value', + } + ), + ...commonColumns, +} as const; + +export type LogRateAnalysisResultsTableColumnName = keyof typeof significantItemColumns | 'unique'; + +type SignificantItemOrNull = SignificantItem | null; +type GroupOrNull = GroupTableItem | null; + +export interface LogRateAnalysisTableState { + skippedColumns: LogRateAnalysisResultsTableColumnName[]; + pinnedGroup: GroupOrNull; + pinnedSignificantItem: SignificantItemOrNull; + selectedGroup: GroupOrNull; + selectedSignificantItem: SignificantItemOrNull; +} + +function getDefaultState(): LogRateAnalysisTableState { + return { + skippedColumns: ['p-value', 'Baseline rate', 'Deviation rate'], + pinnedGroup: null, + pinnedSignificantItem: null, + selectedGroup: null, + selectedSignificantItem: null, + }; +} + +export function getPreloadedState(): LogRateAnalysisTableState { + const defaultState = getDefaultState(); + + const localStorageSkippedColumns = localStorage.getItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS); + + if (localStorageSkippedColumns === null) { + return defaultState; + } + + try { + defaultState.skippedColumns = JSON.parse(localStorageSkippedColumns); + } catch (err) { + // eslint-disable-next-line no-console + console.warn('Failed to parse skipped columns from local storage:', err); + } + + return defaultState; +} + +export const logRateAnalysisTableSlice = createSlice({ + name: 'logRateAnalysisTable', + initialState: getDefaultState(), + reducers: { + clearAllRowState: (state: LogRateAnalysisTableState) => { + state.pinnedGroup = null; + state.pinnedSignificantItem = null; + state.selectedGroup = null; + state.selectedSignificantItem = null; + }, + setPinnedGroup: (state: LogRateAnalysisTableState, action: PayloadAction) => { + state.pinnedGroup = action.payload; + }, + setPinnedSignificantItem: ( + state: LogRateAnalysisTableState, + action: PayloadAction + ) => { + state.pinnedSignificantItem = action.payload; + }, + setSelectedGroup: (state: LogRateAnalysisTableState, action: PayloadAction) => { + state.selectedGroup = action.payload; + }, + setSelectedSignificantItem: ( + state: LogRateAnalysisTableState, + action: PayloadAction + ) => { + state.selectedSignificantItem = action.payload; + }, + setSkippedColumns: ( + state: LogRateAnalysisTableState, + action: PayloadAction + ) => { + state.skippedColumns = action.payload; + }, + }, +}); + +// Action creators are generated for each case reducer function +export const { + clearAllRowState, + setPinnedGroup, + setPinnedSignificantItem, + setSelectedGroup, + setSelectedSignificantItem, + setSkippedColumns, +} = logRateAnalysisTableSlice.actions; + +// Create listener middleware +export const localStorageListenerMiddleware = createListenerMiddleware(); + +// Add a listener to save skippedColumns to localStorage whenever it changes +localStorageListenerMiddleware.startListening({ + actionCreator: setSkippedColumns, + effect: (action, listenerApi) => { + const state = listenerApi.getState() as { logRateAnalysisTable: LogRateAnalysisTableState }; + try { + const serializedState = JSON.stringify(state.logRateAnalysisTable.skippedColumns); + localStorage.setItem(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS, serializedState); + } catch (err) { + // eslint-disable-next-line no-console + console.warn('Failed to save state to localStorage:', err); + } + }, +}); diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/store.tsx b/x-pack/packages/ml/aiops_log_rate_analysis/state/store.tsx index 1589b27348d89..9fd8e8240dde3 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/store.tsx +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/store.tsx @@ -15,12 +15,19 @@ 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 { + logRateAnalysisTableSlice, + getPreloadedState, + localStorageListenerMiddleware, +} from './log_rate_analysis_table_slice'; import { logRateAnalysisFieldCandidatesSlice } from './log_rate_analysis_field_candidates_slice'; import type { InitialAnalysisStart } from './log_rate_analysis_slice'; const getReduxStore = () => configureStore({ + preloadedState: { + logRateAnalysisTable: getPreloadedState(), + }, reducer: { // General page state logRateAnalysis: logRateAnalysisSlice.reducer, @@ -28,11 +35,13 @@ const getReduxStore = () => logRateAnalysisFieldCandidates: logRateAnalysisFieldCandidatesSlice.reducer, // Analysis results logRateAnalysisResults: logRateAnalysisResultsSlice.reducer, - // Handles running the analysis - logRateAnalysisStream: streamSlice.reducer, - // Handles hovering and pinning table rows - logRateAnalysisTableRow: logRateAnalysisTableRowSlice.reducer, + // Handles running the analysis, needs to be "stream" for the async thunk to work properly. + stream: streamSlice.reducer, + // Handles hovering and pinning table rows and column selection + logRateAnalysisTable: logRateAnalysisTableSlice.reducer, }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().prepend(localStorageListenerMiddleware.middleware), }); interface LogRateAnalysisReduxProviderProps { @@ -54,6 +63,6 @@ export const LogRateAnalysisReduxProvider: FC< }; // Infer the `RootState` and `AppDispatch` types from the store itself -export type AppStore = ReturnType; +type AppStore = ReturnType; export type RootState = ReturnType; export type AppDispatch = AppStore['dispatch']; diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/use_current_selected_group.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/use_current_selected_group.ts index 9653691d3efd4..a19bd3e18a735 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/use_current_selected_group.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/use_current_selected_group.ts @@ -10,8 +10,8 @@ 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 selectSelectedGroup = (s: RootState) => s.logRateAnalysisTable.selectedGroup; +const selectPinnedGroup = (s: RootState) => s.logRateAnalysisTable.pinnedGroup; const selectCurrentSelectedGroup = createSelector( selectSelectedGroup, selectPinnedGroup, diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/state/use_current_selected_significant_item.ts b/x-pack/packages/ml/aiops_log_rate_analysis/state/use_current_selected_significant_item.ts index d189d16fc2fa0..f7327d3033df0 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/state/use_current_selected_significant_item.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/state/use_current_selected_significant_item.ts @@ -11,9 +11,8 @@ import type { RootState } from './store'; import { useAppSelector } from './hooks'; const selectSelectedSignificantItem = (s: RootState) => - s.logRateAnalysisTableRow.selectedSignificantItem; -const selectPinnedSignificantItem = (s: RootState) => - s.logRateAnalysisTableRow.pinnedSignificantItem; + s.logRateAnalysisTable.selectedSignificantItem; +const selectPinnedSignificantItem = (s: RootState) => s.logRateAnalysisTable.pinnedSignificantItem; const selectCurrentSelectedSignificantItem = createSelector( selectSelectedSignificantItem, selectPinnedSignificantItem, diff --git a/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx b/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx index 678dec7d36f42..4e7a501140b01 100644 --- a/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx +++ b/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx @@ -7,9 +7,8 @@ import type { PropsWithChildren, FC } from 'react'; import React, { useCallback, useState } from 'react'; -import type { CoreStart } from '@kbn/core/public'; +import type { ThemeServiceStart } from '@kbn/core-theme-browser'; import type { DataView } from '@kbn/data-plugin/common'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; import type { FieldStatsProps } from '@kbn/unified-field-list/src/components/field_stats'; @@ -18,32 +17,18 @@ import { getProcessedFields } from '@kbn/ml-data-grid'; import { stringHash } from '@kbn/ml-string-hash'; import { lastValueFrom } from 'rxjs'; import { useRef } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; import { getMergedSampleDocsForPopulatedFieldsQuery } from './populated_fields/get_merged_populated_fields_query'; import { FieldStatsFlyout } from './field_stats_flyout'; import { MLFieldStatsFlyoutContext } from './use_field_stats_flyout_context'; import { PopulatedFieldsCacheManager } from './populated_fields/populated_fields_cache_manager'; -type Services = CoreStart & { - data: DataPublicPluginStart; -}; - -function useDataSearch() { - const { data } = useKibana().services; - - if (!data) { - throw new Error('Kibana data service not available.'); - } - - return data.search; -} - /** * Props for the FieldStatsFlyoutProvider component. * * @typedef {Object} FieldStatsFlyoutProviderProps * @property dataView - The data view object. * @property fieldStatsServices - Services required for field statistics. + * @property theme - The EUI theme service. * @property [timeRangeMs] - Optional time range in milliseconds. * @property [dslQuery] - Optional DSL query for filtering field statistics. * @property [disablePopulatedFields] - Optional flag to disable populated fields. @@ -51,6 +36,7 @@ function useDataSearch() { export type FieldStatsFlyoutProviderProps = PropsWithChildren<{ dataView: DataView; fieldStatsServices: FieldStatsServices; + theme: ThemeServiceStart; timeRangeMs?: TimeRangeMs; dslQuery?: FieldStatsProps['dslQuery']; disablePopulatedFields?: boolean; @@ -79,12 +65,13 @@ export const FieldStatsFlyoutProvider: FC = (prop const { dataView, fieldStatsServices, + theme, timeRangeMs, dslQuery, disablePopulatedFields = false, children, } = props; - const search = useDataSearch(); + const { search } = fieldStatsServices.data; const [isFieldStatsFlyoutVisible, setFieldStatsIsFlyoutVisible] = useState(false); const [fieldName, setFieldName] = useState(); const [fieldValue, setFieldValue] = useState(); @@ -187,6 +174,7 @@ export const FieldStatsFlyoutProvider: FC = (prop fieldValue, timeRangeMs, populatedFields, + theme, }} > = (props) => { const { field, label, onButtonClick, disabled, isEmpty, hideTrigger } = props; - const themeVars = useThemeVars(); + const theme = useFieldStatsFlyoutThemeVars(); + const themeVars = useCurrentEuiThemeVars(theme); + const emptyFieldMessage = isEmpty ? ' ' + i18n.translate('xpack.ml.newJob.wizard.fieldContextPopover.emptyFieldInSampleDocsMsg', { diff --git a/x-pack/packages/ml/field_stats_flyout/tsconfig.json b/x-pack/packages/ml/field_stats_flyout/tsconfig.json index 0010d79432e34..df70aa27788b8 100644 --- a/x-pack/packages/ml/field_stats_flyout/tsconfig.json +++ b/x-pack/packages/ml/field_stats_flyout/tsconfig.json @@ -23,9 +23,7 @@ "@kbn/i18n", "@kbn/react-field", "@kbn/ml-anomaly-utils", - "@kbn/kibana-react-plugin", "@kbn/ml-kibana-theme", - "@kbn/core", "@kbn/ml-data-grid", "@kbn/ml-string-hash", "@kbn/ml-is-populated-object", @@ -33,5 +31,6 @@ "@kbn/ml-is-defined", "@kbn/field-types", "@kbn/ui-theme", + "@kbn/core-theme-browser", ] } diff --git a/x-pack/packages/ml/field_stats_flyout/use_field_stats_flyout_context.ts b/x-pack/packages/ml/field_stats_flyout/use_field_stats_flyout_context.ts index ec6c28873011c..121426352e6e4 100644 --- a/x-pack/packages/ml/field_stats_flyout/use_field_stats_flyout_context.ts +++ b/x-pack/packages/ml/field_stats_flyout/use_field_stats_flyout_context.ts @@ -7,6 +7,7 @@ import { createContext, useContext } from 'react'; import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; +import type { ThemeServiceStart } from '@kbn/core-theme-browser'; /** * Represents the properties for the MLJobWizardFieldStatsFlyout component. @@ -21,6 +22,7 @@ interface MLJobWizardFieldStatsFlyoutProps { fieldValue?: string | number; timeRangeMs?: TimeRangeMs; populatedFields?: Set; + theme?: ThemeServiceStart; } /** @@ -34,6 +36,7 @@ export const MLFieldStatsFlyoutContext = createContext {}, timeRangeMs: undefined, populatedFields: undefined, + theme: undefined, }); /** @@ -41,5 +44,25 @@ export const MLFieldStatsFlyoutContext = createContext() { populatedFields, }; } + +export type UseFieldStatsTrigger = typeof useFieldStatsTrigger; diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx index b1c0e3d89f35a..f967fffd45647 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/fields_config.tsx @@ -638,7 +638,7 @@ export const FieldsControls: FC> = ({ }) => { const { splitFieldsOptions, combinedQuery } = useChangePointDetectionContext(); const { dataView } = useDataSource(); - const { data, uiSettings, fieldFormats, charts, fieldStats } = useAiopsAppContext(); + const { data, uiSettings, fieldFormats, charts, fieldStats, theme } = useAiopsAppContext(); const timefilter = useTimefilter(); // required in order to trigger state updates useTimeRangeUpdates(); @@ -677,6 +677,7 @@ export const FieldsControls: FC> = ({ } : undefined } + theme={theme} > diff --git a/x-pack/plugins/aiops/public/components/document_count_content/document_count_content/document_count_content.tsx b/x-pack/plugins/aiops/public/components/document_count_content/document_count_content/document_count_content.tsx index 23caf21c39ee3..4dbf021e3b10b 100644 --- a/x-pack/plugins/aiops/public/components/document_count_content/document_count_content/document_count_content.tsx +++ b/x-pack/plugins/aiops/public/components/document_count_content/document_count_content/document_count_content.tsx @@ -15,6 +15,7 @@ import type { import { useAppSelector } from '@kbn/aiops-log-rate-analysis/state'; import { DocumentCountChartRedux } from '@kbn/aiops-components'; +import { AIOPS_EMBEDDABLE_ORIGIN } from '@kbn/aiops-common/constants'; import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context'; @@ -37,17 +38,29 @@ export const DocumentCountContent: FC = ({ barHighlightColorOverride, ...docCountChartProps }) => { - const { data, uiSettings, fieldFormats, charts } = useAiopsAppContext(); + const { data, uiSettings, fieldFormats, charts, embeddingOrigin } = useAiopsAppContext(); const { documentStats } = useAppSelector((s) => s.logRateAnalysis); const { sampleProbability, totalCount, documentCountStats } = documentStats; if (documentCountStats === undefined) { - return totalCount !== undefined ? ( + return totalCount !== undefined && embeddingOrigin !== AIOPS_EMBEDDABLE_ORIGIN.DASHBOARD ? ( ) : null; } + if (embeddingOrigin === AIOPS_EMBEDDABLE_ORIGIN.DASHBOARD) { + return ( + + ); + } + return ( diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx index 7bf43037f45c0..2821b59353b52 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx @@ -7,7 +7,7 @@ import { isEqual } from 'lodash'; import React, { useCallback, useEffect, useMemo, useRef, type FC } from 'react'; -import { EuiButton, EuiEmptyPrompt, EuiHorizontalRule, EuiPanel } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { BarStyleAccessor } from '@elastic/charts/dist/chart_types/xy_chart/utils/specs'; @@ -28,13 +28,16 @@ import { setInitialAnalysisStart, useAppDispatch, useAppSelector, + setGroupResults, } from '@kbn/aiops-log-rate-analysis/state'; +import { AIOPS_EMBEDDABLE_ORIGIN } from '@kbn/aiops-common/constants'; import { DocumentCountContent } from '../../document_count_content/document_count_content'; import { LogRateAnalysisResults, type LogRateAnalysisResultsData, } from '../log_rate_analysis_results'; +import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context'; export const DEFAULT_SEARCH_QUERY: estypes.QueryDslQueryContainer = { match_all: {} }; const DEFAULT_SEARCH_BAR_QUERY: estypes.QueryDslQueryContainer = { @@ -69,9 +72,11 @@ export const LogRateAnalysisContent: FC = ({ onAnalysisCompleted, onWindowParametersChange, }) => { + const { embeddingOrigin } = useAiopsAppContext(); + const dispatch = useAppDispatch(); - const isRunning = useAppSelector((s) => s.logRateAnalysisStream.isRunning); + const isRunning = useAppSelector((s) => s.stream.isRunning); const significantItems = useAppSelector((s) => s.logRateAnalysisResults.significantItems); const significantItemsGroups = useAppSelector( (s) => s.logRateAnalysisResults.significantItemsGroups @@ -116,6 +121,7 @@ export const LogRateAnalysisContent: FC = ({ const { documentCountStats } = documentStats; function clearSelectionHandler() { + dispatch(setGroupResults(false)); dispatch(clearSelection()); dispatch(clearAllRowState()); } @@ -200,7 +206,11 @@ export const LogRateAnalysisContent: FC = ({ const changePointType = documentCountStats?.changePoint?.type; return ( - + {showDocumentCountContent && ( = ({ barStyleAccessor={barStyleAccessor} /> )} - + {showLogRateAnalysisResults && ( = ({ + timeRange, +}) => { + const { uiSettings } = useAiopsAppContext(); + const { dataView } = useDataSource(); + const { filters, query } = useFilterQueryUpdates(); + const appState = getDefaultLogRateAnalysisAppState({ + searchQuery: buildEsQuery( + dataView, + query ?? [], + filters ?? [], + uiSettings ? getEsQueryConfig(uiSettings) : undefined + ), + filters, + }); + const { searchQuery } = useSearch({ dataView, savedSearch: null }, appState, true); + + const timeRangeParsed = useMemo(() => { + if (timeRange) { + const min = datemath.parse(timeRange.from); + const max = datemath.parse(timeRange.to); + if (min && max) { + return { min, max }; + } + } + }, [timeRange]); + + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_options.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_options.tsx new file mode 100644 index 0000000000000..0aba8e2d763d4 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_options.tsx @@ -0,0 +1,190 @@ +/* + * 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 { FC } from 'react'; +import React from 'react'; + +import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { + clearAllRowState, + setGroupResults, + useAppDispatch, + useAppSelector, +} from '@kbn/aiops-log-rate-analysis/state'; +import { + commonColumns, + significantItemColumns, + setSkippedColumns, + type LogRateAnalysisResultsTableColumnName, +} from '@kbn/aiops-log-rate-analysis/state/log_rate_analysis_table_slice'; +import { setCurrentFieldFilterSkippedItems } from '@kbn/aiops-log-rate-analysis/state/log_rate_analysis_field_candidates_slice'; + +import { ItemFilterPopover as FieldFilterPopover } from './item_filter_popover'; +import { ItemFilterPopover as ColumnFilterPopover } from './item_filter_popover'; + +const groupResultsMessage = i18n.translate( + 'xpack.aiops.logRateAnalysis.resultsTable.groupedSwitchLabel.groupResults', + { + defaultMessage: 'Smart grouping', + } +); +const fieldFilterHelpText = i18n.translate('xpack.aiops.logRateAnalysis.page.fieldFilterHelpText', { + defaultMessage: + 'Deselect non-relevant fields to remove them from the analysis and click the Apply button to rerun the analysis. Use the search bar to filter the list, then select/deselect multiple fields with the actions below.', +}); +const columnsFilterHelpText = i18n.translate( + 'xpack.aiops.logRateAnalysis.page.columnsFilterHelpText', + { + defaultMessage: 'Configure visible columns.', + } +); +const disabledFieldFilterApplyButtonTooltipContent = i18n.translate( + 'xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected', + { + defaultMessage: 'Grouping requires at least 2 fields to be selected.', + } +); +const disabledColumnFilterApplyButtonTooltipContent = i18n.translate( + 'xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected', + { + defaultMessage: 'At least one column must be selected.', + } +); +const columnSearchAriaLabel = i18n.translate('xpack.aiops.analysis.columnSelectorAriaLabel', { + defaultMessage: 'Filter columns', +}); +const columnsButton = i18n.translate('xpack.aiops.logRateAnalysis.page.columnsFilterButtonLabel', { + defaultMessage: 'Columns', +}); +const fieldsButton = i18n.translate('xpack.aiops.analysis.fieldsButtonLabel', { + defaultMessage: 'Fields', +}); +const groupResultsOffMessage = i18n.translate( + 'xpack.aiops.logRateAnalysis.resultsTable.groupedSwitchLabel.groupResultsOff', + { + defaultMessage: 'Off', + } +); +const groupResultsOnMessage = i18n.translate( + 'xpack.aiops.logRateAnalysis.resultsTable.groupedSwitchLabel.groupResultsOn', + { + defaultMessage: 'On', + } +); +const resultsGroupedOffId = 'aiopsLogRateAnalysisGroupingOff'; +const resultsGroupedOnId = 'aiopsLogRateAnalysisGroupingOn'; + +export interface LogRateAnalysisOptionsProps { + foundGroups: boolean; + growFirstItem?: boolean; +} + +export const LogRateAnalysisOptions: FC = ({ + foundGroups, + growFirstItem = false, +}) => { + const dispatch = useAppDispatch(); + + const { groupResults } = useAppSelector((s) => s.logRateAnalysis); + const { isRunning } = useAppSelector((s) => s.stream); + const fieldCandidates = useAppSelector((s) => s.logRateAnalysisFieldCandidates); + const { skippedColumns } = useAppSelector((s) => s.logRateAnalysisTable); + const { fieldFilterUniqueItems, initialFieldFilterSkippedItems } = fieldCandidates; + const fieldFilterButtonDisabled = + isRunning || fieldCandidates.isLoading || fieldFilterUniqueItems.length === 0; + const toggleIdSelected = groupResults ? resultsGroupedOnId : resultsGroupedOffId; + + const onGroupResultsToggle = (optionId: string) => { + dispatch(setGroupResults(optionId === resultsGroupedOnId)); + // When toggling the group switch, clear all row selections + dispatch(clearAllRowState()); + }; + + const onVisibleColumnsChange = (columns: LogRateAnalysisResultsTableColumnName[]) => { + dispatch(setSkippedColumns(columns)); + }; + + const onFieldsFilterChange = (skippedFieldsUpdate: string[]) => { + dispatch(setCurrentFieldFilterSkippedItems(skippedFieldsUpdate)); + }; + + // 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. + const disabledGroupResultsSwitch = !foundGroups && !groupResults; + + const toggleButtons = [ + { + id: resultsGroupedOffId, + label: groupResultsOffMessage, + 'data-test-subj': 'aiopsLogRateAnalysisGroupSwitchOff', + }, + { + id: resultsGroupedOnId, + label: groupResultsOnMessage, + 'data-test-subj': 'aiopsLogRateAnalysisGroupSwitchOn', + }, + ]; + + return ( + <> + + + + {groupResultsMessage} + + + + + + + + + + + void} + /> + + + ); +}; diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx index ad8e7740b505d..1eb4f8fd0af0d 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_results.tsx @@ -11,12 +11,13 @@ import { isEqual } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { + EuiButtonIcon, EuiButton, - EuiButtonGroup, EuiCallOut, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, + EuiToolTip, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -26,6 +27,7 @@ import { ProgressControls } from '@kbn/aiops-components'; import { cancelStream, startStream } from '@kbn/ml-response-stream/client'; import { clearAllRowState, + setGroupResults, useAppDispatch, useAppSelector, } from '@kbn/aiops-log-rate-analysis/state'; @@ -37,8 +39,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { SignificantItem, SignificantItemGroup } from '@kbn/ml-agg-utils'; -import { useStorage } from '@kbn/ml-local-storage'; -import { AIOPS_ANALYSIS_RUN_ORIGIN } from '@kbn/aiops-common/constants'; +import { AIOPS_ANALYSIS_RUN_ORIGIN, AIOPS_EMBEDDABLE_ORIGIN } from '@kbn/aiops-common/constants'; import type { AiopsLogRateAnalysisSchema } from '@kbn/aiops-log-rate-analysis/api/schema'; import type { AiopsLogRateAnalysisSchemaSignificantItem } from '@kbn/aiops-log-rate-analysis/api/schema_v3'; import { @@ -50,15 +51,6 @@ import { fetchFieldCandidates } from '@kbn/aiops-log-rate-analysis/state/log_rat import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { useDataSource } from '../../hooks/use_data_source'; -import { - commonColumns, - significantItemColumns, -} from '../log_rate_analysis_results_table/use_columns'; -import { - AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS, - type AiOpsKey, - type AiOpsStorageMapped, -} from '../../types/storage'; import { getGroupTableItems, @@ -66,68 +58,15 @@ import { LogRateAnalysisResultsGroupsTable, } from '../log_rate_analysis_results_table'; -import { ItemFilterPopover as FieldFilterPopover } from './item_filter_popover'; -import { ItemFilterPopover as ColumnFilterPopover } from './item_filter_popover'; import { LogRateAnalysisInfoPopover } from './log_rate_analysis_info_popover'; -import type { ColumnNames } from '../log_rate_analysis_results_table'; +import { LogRateAnalysisOptions } from './log_rate_analysis_options'; -const groupResultsMessage = i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTable.groupedSwitchLabel.groupResults', - { - defaultMessage: 'Smart grouping', - } -); const groupResultsHelpMessage = i18n.translate( 'xpack.aiops.logRateAnalysis.resultsTable.groupedSwitchLabel.groupResultsHelpMessage', { defaultMessage: 'Items which are unique to a group are marked by an asterisk (*).', } ); -const groupResultsOffMessage = i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTable.groupedSwitchLabel.groupResultsOff', - { - defaultMessage: 'Off', - } -); -const groupResultsOnMessage = i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTable.groupedSwitchLabel.groupResultsOn', - { - defaultMessage: 'On', - } -); -const resultsGroupedOffId = 'aiopsLogRateAnalysisGroupingOff'; -const resultsGroupedOnId = 'aiopsLogRateAnalysisGroupingOn'; -const fieldFilterHelpText = i18n.translate('xpack.aiops.logRateAnalysis.page.fieldFilterHelpText', { - defaultMessage: - 'Deselect non-relevant fields to remove them from the analysis and click the Apply button to rerun the analysis. Use the search bar to filter the list, then select/deselect multiple fields with the actions below.', -}); -const columnsFilterHelpText = i18n.translate( - 'xpack.aiops.logRateAnalysis.page.columnsFilterHelpText', - { - defaultMessage: 'Configure visible columns.', - } -); -const disabledFieldFilterApplyButtonTooltipContent = i18n.translate( - 'xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected', - { - defaultMessage: 'Grouping requires at least 2 fields to be selected.', - } -); -const disabledColumnFilterApplyButtonTooltipContent = i18n.translate( - 'xpack.aiops.analysis.columnSelectorNotEnoughColumnsSelected', - { - defaultMessage: 'At least one column must be selected.', - } -); -const columnSearchAriaLabel = i18n.translate('xpack.aiops.analysis.columnSelectorAriaLabel', { - defaultMessage: 'Filter columns', -}); -const columnsButton = i18n.translate('xpack.aiops.logRateAnalysis.page.columnsFilterButtonLabel', { - defaultMessage: 'Columns', -}); -const fieldsButton = i18n.translate('xpack.aiops.analysis.fieldsButtonLabel', { - defaultMessage: 'Fields', -}); /** * Interface for log rate analysis results data. @@ -173,10 +112,12 @@ export const LogRateAnalysisResults: FC = ({ documentStats: { sampleProbability }, stickyHistogram, isBrushCleared, + groupResults, } = useAppSelector((s) => s.logRateAnalysis); - const { isRunning, errors: streamErrors } = useAppSelector((s) => s.logRateAnalysisStream); + const { isRunning, errors: streamErrors } = useAppSelector((s) => s.stream); const data = useAppSelector((s) => s.logRateAnalysisResults); const fieldCandidates = useAppSelector((s) => s.logRateAnalysisFieldCandidates); + const { skippedColumns } = useAppSelector((s) => s.logRateAnalysisTable); const { currentAnalysisWindowParameters } = data; // Store the performance metric's start time using a ref @@ -185,61 +126,37 @@ export const LogRateAnalysisResults: FC = ({ const abortCtrl = useRef(new AbortController()); const previousSearchQuery = useRef(searchQuery); - const [groupResults, setGroupResults] = useState(false); const [overrides, setOverrides] = useState( undefined ); const [shouldStart, setShouldStart] = useState(false); - const [toggleIdSelected, setToggleIdSelected] = useState(resultsGroupedOffId); - const [skippedColumns, setSkippedColumns] = useStorage< - AiOpsKey, - AiOpsStorageMapped - >(AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS, ['p-value', 'Baseline rate', 'Deviation rate']); - // null is used as the uninitialized state to identify the first load. - const [skippedFields, setSkippedFields] = useState(null); - - const onGroupResultsToggle = (optionId: string) => { - setToggleIdSelected(optionId); - setGroupResults(optionId === resultsGroupedOnId); - - // When toggling the group switch, clear all row selections - dispatch(clearAllRowState()); + const [embeddableOptionsVisible, setEmbeddableOptionsVisible] = useState(false); + + const onEmbeddableOptionsClickHandler = () => { + setEmbeddableOptionsVisible((s) => !s); }; - const { - fieldFilterUniqueItems, - fieldFilterSkippedItems, - keywordFieldCandidates, - textFieldCandidates, - } = fieldCandidates; - const fieldFilterButtonDisabled = - isRunning || fieldCandidates.isLoading || fieldFilterUniqueItems.length === 0; - - // Set skipped fields only on first load, otherwise we'd overwrite the user's selection. + const { currentFieldFilterSkippedItems, keywordFieldCandidates, textFieldCandidates } = + fieldCandidates; + useEffect(() => { - if (skippedFields === null && fieldFilterSkippedItems.length > 0) - setSkippedFields(fieldFilterSkippedItems); - }, [fieldFilterSkippedItems, skippedFields]); + if (currentFieldFilterSkippedItems === null) return; - const onFieldsFilterChange = (skippedFieldsUpdate: string[]) => { dispatch(resetResults()); - setSkippedFields(skippedFieldsUpdate); setOverrides({ loaded: 0, remainingKeywordFieldCandidates: keywordFieldCandidates.filter( - (d) => !skippedFieldsUpdate.includes(d) + (d) => !currentFieldFilterSkippedItems.includes(d) ), remainingTextFieldCandidates: textFieldCandidates.filter( - (d) => !skippedFieldsUpdate.includes(d) + (d) => !currentFieldFilterSkippedItems.includes(d) ), regroupOnly: false, }); startHandler(true, false); - }; - - const onVisibleColumnsChange = (columns: ColumnNames[]) => { - setSkippedColumns(columns); - }; + // custom check to trigger on currentFieldFilterSkippedItems change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentFieldFilterSkippedItems]); function cancelHandler() { abortCtrl.current.abort(); @@ -299,18 +216,20 @@ export const LogRateAnalysisResults: FC = ({ dispatch(resetResults()); setOverrides({ remainingKeywordFieldCandidates: keywordFieldCandidates.filter( - (d) => skippedFields === null || !skippedFields.includes(d) + (d) => + currentFieldFilterSkippedItems === null || !currentFieldFilterSkippedItems.includes(d) ), remainingTextFieldCandidates: textFieldCandidates.filter( - (d) => skippedFields === null || !skippedFields.includes(d) + (d) => + currentFieldFilterSkippedItems === null || !currentFieldFilterSkippedItems.includes(d) ), }); } // Reset grouping to false and clear all row selections when restarting the analysis. if (resetGroupButton) { - setGroupResults(false); - setToggleIdSelected(resultsGroupedOffId); + dispatch(setGroupResults(false)); + // When toggling the group switch, clear all row selections dispatch(clearAllRowState()); } @@ -372,12 +291,13 @@ export const LogRateAnalysisResults: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [shouldStart]); + // On mount, fetch field candidates first. Once they are populated, + // the actual analysis will be triggered. useEffect(() => { if (startParams) { dispatch(fetchFieldCandidates(startParams)); dispatch(setCurrentAnalysisType(analysisType)); dispatch(setCurrentAnalysisWindowParameters(chartWindowParameters)); - dispatch(startStream(startParams)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -413,23 +333,6 @@ export const LogRateAnalysisResults: FC = ({ }, 0); const foundGroups = groupTableItems.length > 0 && groupItemCount > 0; - // 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. - const disabledGroupResultsSwitch = !foundGroups && !groupResults; - - const toggleButtons = [ - { - id: resultsGroupedOffId, - label: groupResultsOffMessage, - 'data-test-subj': 'aiopsLogRateAnalysisGroupSwitchOff', - }, - { - id: resultsGroupedOnId, - label: groupResultsOnMessage, - 'data-test-subj': 'aiopsLogRateAnalysisGroupSwitchOn', - }, - ]; - return (
= ({ shouldRerunAnalysis={shouldRerunAnalysis || searchQueryUpdated} analysisInfo={} > - - + <> + {embeddingOrigin !== AIOPS_EMBEDDABLE_ORIGIN.DASHBOARD && ( + + )} + {embeddingOrigin === AIOPS_EMBEDDABLE_ORIGIN.DASHBOARD && ( - {groupResultsMessage} + + + - - - - - - - - - - void} - /> - + )} + + {embeddingOrigin === AIOPS_EMBEDDABLE_ORIGIN.DASHBOARD && embeddableOptionsVisible && ( + <> + + + + + + )} + {errors.length > 0 ? ( <> diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/index.ts b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/index.ts index 6813e71704918..c5112723e2784 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/index.ts +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/index.ts @@ -8,4 +8,3 @@ export { getGroupTableItems } from './get_group_table_items'; export { LogRateAnalysisResultsTable } from './log_rate_analysis_results_table'; export { LogRateAnalysisResultsGroupsTable } from './log_rate_analysis_results_table_groups'; -export type { ColumnNames } from './use_columns'; diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table.tsx index 83be306e93f50..e9072c2929f14 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table.tsx @@ -83,13 +83,11 @@ export const LogRateAnalysisResultsTable: FC = }, [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 pinnedGroup = useAppSelector((s) => s.logRateAnalysisTable.pinnedGroup); + const selectedGroup = useAppSelector((s) => s.logRateAnalysisTable.selectedGroup); + const pinnedSignificantItem = useAppSelector((s) => s.logRateAnalysisTable.pinnedSignificantItem); const selectedSignificantItem = useAppSelector( - (s) => s.logRateAnalysisTableRow.selectedSignificantItem + (s) => s.logRateAnalysisTable.selectedSignificantItem ); const dispatch = useAppDispatch(); diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table_groups.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table_groups.tsx index d69a0fec7200f..6bd0a5e4ce213 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/log_rate_analysis_results_table_groups.tsx @@ -91,8 +91,8 @@ export const LogRateAnalysisResultsGroupsTable: FC s.logRateAnalysisTableRow.pinnedGroup); - const selectedGroup = useAppSelector((s) => s.logRateAnalysisTableRow.selectedGroup); + const pinnedGroup = useAppSelector((s) => s.logRateAnalysisTable.pinnedGroup); + const selectedGroup = useAppSelector((s) => s.logRateAnalysisTable.selectedGroup); const dispatch = useAppDispatch(); const isMounted = useMountedState(); diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_columns.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_columns.tsx index f3b8195767101..c5b7a83e33641 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_columns.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_columns.tsx @@ -21,6 +21,11 @@ import { type SignificantItem, SIGNIFICANT_ITEM_TYPE } from '@kbn/ml-agg-utils'; import { getCategoryQuery } from '@kbn/aiops-log-pattern-analysis/get_category_query'; import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; import { useAppSelector } from '@kbn/aiops-log-rate-analysis/state'; +import { + commonColumns, + significantItemColumns, + type LogRateAnalysisResultsTableColumnName, +} from '@kbn/aiops-log-rate-analysis/state/log_rate_analysis_table_slice'; import { getBaselineAndDeviationRates, getLogRateChange, @@ -40,55 +45,6 @@ const TRUNCATE_TEXT_LINES = 3; const UNIQUE_COLUMN_WIDTH = '40px'; const NOT_AVAILABLE = '--'; -export const commonColumns = { - ['Log rate']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.logRateColumnTitle', { - defaultMessage: 'Log rate', - }), - ['Doc count']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.docCountColumnTitle', { - defaultMessage: 'Doc count', - }), - ['p-value']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.pValueColumnTitle', { - defaultMessage: 'p-value', - }), - ['Impact']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.impactColumnTitle', { - defaultMessage: 'Impact', - }), - ['Baseline rate']: i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTable.baselineRateColumnTitle', - { - defaultMessage: 'Baseline rate', - } - ), - ['Deviation rate']: i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTable.deviationRateColumnTitle', - { - defaultMessage: 'Deviation rate', - } - ), - ['Log rate change']: i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTable.logRateChangeColumnTitle', - { - defaultMessage: 'Log rate change', - } - ), - ['Actions']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.actionsColumnTitle', { - defaultMessage: 'Actions', - }), -}; - -export const significantItemColumns = { - ['Field name']: i18n.translate('xpack.aiops.logRateAnalysis.resultsTable.fieldNameColumnTitle', { - defaultMessage: 'Field name', - }), - ['Field value']: i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTable.fieldValueColumnTitle', - { - defaultMessage: 'Field value', - } - ), - ...commonColumns, -} as const; - export const LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE = { GROUPS: 'groups', SIGNIFICANT_ITEMS: 'significantItems', @@ -96,8 +52,6 @@ export const LOG_RATE_ANALYSIS_RESULTS_TABLE_TYPE = { 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( 'xpack.aiops.logRateAnalysis.resultsTable.logRateColumnTooltip', { @@ -213,7 +167,7 @@ export const useColumns = ( const { earliest, latest } = useAppSelector((s) => s.logRateAnalysis); const timeRangeMs = { from: earliest ?? 0, to: latest ?? 0 }; - const loading = useAppSelector((s) => s.logRateAnalysisStream.isRunning); + const loading = useAppSelector((s) => s.stream.isRunning); const zeroDocsFallback = useAppSelector((s) => s.logRateAnalysisResults.zeroDocsFallback); const { documentStats: { documentCountStats }, @@ -271,7 +225,10 @@ export const useColumns = ( [currentAnalysisType, buckets] ); - const columnsMap: Record> = useMemo( + const columnsMap: Record< + LogRateAnalysisResultsTableColumnName, + EuiBasicTableColumn + > = useMemo( () => ({ ['Field name']: { 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnFieldName', @@ -615,20 +572,21 @@ export const useColumns = ( ); const columns = useMemo(() => { - const columnNamesToReturn: Partial> = isGroupsTable - ? commonColumns - : significantItemColumns; + const columnNamesToReturn: Partial> = + isGroupsTable ? commonColumns : significantItemColumns; const columnsToReturn = []; for (const columnName in columnNamesToReturn) { if ( Object.hasOwn(columnNamesToReturn, columnName) === false || - skippedColumns.includes(columnNamesToReturn[columnName as ColumnNames] as string) || + skippedColumns.includes( + columnNamesToReturn[columnName as LogRateAnalysisResultsTableColumnName] as string + ) || ((columnName === 'p-value' || columnName === 'Impact') && zeroDocsFallback) ) continue; - columnsToReturn.push(columnsMap[columnName as ColumnNames]); + columnsToReturn.push(columnsMap[columnName as LogRateAnalysisResultsTableColumnName]); } if (isExpandedRow === true) { diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_discover_action.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_discover_action.tsx index ec4284d6452e5..765f1435a93ad 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_discover_action.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_discover_action.tsx @@ -28,8 +28,8 @@ export const useViewInDiscoverAction = (dataViewId?: string): TableItemAction => const { application, share, data } = useAiopsAppContext(); const discoverLocator = useMemo( - () => share.url.locators.get('DISCOVER_APP_LOCATOR'), - [share.url.locators] + () => share?.url.locators.get('DISCOVER_APP_LOCATOR'), + [share?.url.locators] ); const discoverUrlError = useMemo(() => { diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_log_pattern_analysis_action.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_log_pattern_analysis_action.tsx index dbac6fbe8c9f3..ec1f6774b6b46 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_log_pattern_analysis_action.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis_results_table/use_view_in_log_pattern_analysis_action.tsx @@ -32,7 +32,7 @@ const viewInLogPatternAnalysisMessage = i18n.translate( export const useViewInLogPatternAnalysisAction = (dataViewId?: string): TableItemAction => { const { application, share, data } = useAiopsAppContext(); - const mlLocator = useMemo(() => share.url.locators.get('ML_APP_LOCATOR'), [share.url.locators]); + const mlLocator = useMemo(() => share?.url.locators.get('ML_APP_LOCATOR'), [share?.url.locators]); const generateLogPatternAnalysisUrl = async ( groupTableItem: GroupTableItem | SignificantItem diff --git a/x-pack/plugins/aiops/public/components/time_field_warning.tsx b/x-pack/plugins/aiops/public/components/time_field_warning.tsx new file mode 100644 index 0000000000000..beb64917af538 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/time_field_warning.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const TimeFieldWarning = () => { + return ( + <> + +

+ {i18n.translate('xpack.aiops.embeddableMenu.timeFieldWarning.title.description', { + defaultMessage: 'The analysis can only be run on data views with a time field.', + })} +

+
+ + + ); +}; diff --git a/x-pack/plugins/aiops/public/embeddables/change_point_chart/change_point_chart_initializer.tsx b/x-pack/plugins/aiops/public/embeddables/change_point_chart/change_point_chart_initializer.tsx index 08cf6ffd7dcb1..e69511fe45f92 100644 --- a/x-pack/plugins/aiops/public/embeddables/change_point_chart/change_point_chart_initializer.tsx +++ b/x-pack/plugins/aiops/public/embeddables/change_point_chart/change_point_chart_initializer.tsx @@ -18,16 +18,23 @@ import { EuiHorizontalRule, EuiTitle, } from '@elastic/eui'; +import { DatePickerContextProvider, type DatePickerDependencies } from '@kbn/ml-date-picker'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; +import type { FieldStatsServices } from '@kbn/unified-field-list/src/components/field_stats'; import { ES_FIELD_TYPES } from '@kbn/field-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { FieldStatsFlyoutProvider } from '@kbn/ml-field-stats-flyout'; +import { useTimefilter } from '@kbn/ml-date-picker'; import { pick } from 'lodash'; import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import { + ChangePointDetectionContextProvider, ChangePointDetectionControlsContextProvider, + useChangePointDetectionContext, useChangePointDetectionControlsContext, } from '../../components/change_point_detection/change_point_detection_context'; import { DEFAULT_AGG_FUNCTION } from '../../components/change_point_detection/constants'; @@ -38,7 +45,8 @@ import { PartitionsSelector } from '../../components/change_point_detection/part import { SplitFieldSelector } from '../../components/change_point_detection/split_field_selector'; import { ViewTypeSelector } from '../../components/change_point_detection/view_type_selector'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; -import { DataSourceContextProvider } from '../../hooks/use_data_source'; +import { useDataSource, DataSourceContextProvider } from '../../hooks/use_data_source'; +import { FilterQueryContextProvider } from '../../hooks/use_filters_query'; import { DEFAULT_SERIES } from './const'; import type { ChangePointEmbeddableRuntimeState } from './types'; @@ -53,11 +61,18 @@ export const ChangePointChartInitializer: FC = ({ onCreate, onCancel, }) => { + const appContextValue = useAiopsAppContext(); const { + data: { dataViews }, unifiedSearch: { ui: { IndexPatternSelect }, }, - } = useAiopsAppContext(); + } = appContextValue; + + const datePickerDeps: DatePickerDependencies = { + ...pick(appContextValue, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']), + uiSettingsKeys: UI_SETTINGS, + }; const [dataViewId, setDataViewId] = useState(initialInput?.dataViewId ?? ''); const [viewType, setViewType] = useState(initialInput?.viewType ?? 'charts'); @@ -135,15 +150,21 @@ export const ChangePointChartInitializer: FC = ({ }} /> - - - - - + + + + + + + + + + + @@ -190,7 +211,13 @@ export const FormControls: FC<{ onChange: (update: FormControlsProps) => void; onValidationChange: (isValid: boolean) => void; }> = ({ formInput, onChange, onValidationChange }) => { + const { charts, data, fieldFormats, theme, uiSettings } = useAiopsAppContext(); + const { dataView } = useDataSource(); + const { combinedQuery } = useChangePointDetectionContext(); const { metricFieldOptions, splitFieldsOptions } = useChangePointDetectionControlsContext(); + const timefilter = useTimefilter(); + const timefilterActiveBounds = timefilter.getActiveBounds(); + const prevMetricFieldOptions = usePrevious(metricFieldOptions); const enableSearch = useMemo(() => { @@ -238,10 +265,33 @@ export const FormControls: FC<{ [formInput, onChange] ); + const fieldStatsServices: FieldStatsServices = useMemo(() => { + return { + uiSettings, + dataViews: data.dataViews, + data, + fieldFormats, + charts, + }; + }, [uiSettings, data, fieldFormats, charts]); + if (!isPopulatedObject(formInput)) return null; return ( - <> + updateCallback({ maxSeriesToPlot: v })} onValidationChange={(result) => onValidationChange(result === null)} /> - + ); }; diff --git a/x-pack/plugins/aiops/public/embeddables/change_point_chart/embeddable_change_point_chart_factory.tsx b/x-pack/plugins/aiops/public/embeddables/change_point_chart/embeddable_change_point_chart_factory.tsx index 7cf39eb1cf4ae..2ce1a46780db1 100644 --- a/x-pack/plugins/aiops/public/embeddables/change_point_chart/embeddable_change_point_chart_factory.tsx +++ b/x-pack/plugins/aiops/public/embeddables/change_point_chart/embeddable_change_point_chart_factory.tsx @@ -11,7 +11,6 @@ import { } from '@kbn/aiops-change-point-detection/constants'; import type { Reference } from '@kbn/content-management-utils'; import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; -import { type DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; import type { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; @@ -38,32 +37,8 @@ import type { ChangePointEmbeddableState, } from './types'; -export interface EmbeddableChangePointChartStartServices { - data: DataPublicPluginStart; -} - export type EmbeddableChangePointChartType = typeof EMBEDDABLE_CHANGE_POINT_CHART_TYPE; -export const getDependencies = async ( - getStartServices: StartServicesAccessor -) => { - const [ - { http, uiSettings, notifications, ...startServices }, - { lens, data, usageCollection, fieldFormats }, - ] = await getStartServices(); - - return { - http, - uiSettings, - data, - notifications, - lens, - usageCollection, - fieldFormats, - ...startServices, - }; -}; - export const getChangePointChartEmbeddableFactory = ( getStartServices: StartServicesAccessor ) => { @@ -88,20 +63,6 @@ export const getChangePointChartEmbeddableFactory = ( buildEmbeddable: async (state, buildApi, uuid, parentApi) => { const [coreStart, pluginStart] = await getStartServices(); - const { http, uiSettings, notifications, ...startServices } = coreStart; - const { lens, data, usageCollection, fieldFormats } = pluginStart; - - const deps = { - http, - uiSettings, - data, - notifications, - lens, - usageCollection, - fieldFormats, - ...startServices, - }; - const { api: timeRangeApi, comparators: timeRangeComparators, @@ -120,7 +81,7 @@ export const getChangePointChartEmbeddableFactory = ( const blockingError = new BehaviorSubject(undefined); const dataViews$ = new BehaviorSubject([ - await deps.data.dataViews.get(state.dataViewId), + await pluginStart.data.dataViews.get(state.dataViewId), ]); const api = buildApi( diff --git a/x-pack/plugins/aiops/public/embeddables/change_point_chart/types.ts b/x-pack/plugins/aiops/public/embeddables/change_point_chart/types.ts index 4a39020a299c9..f86270bdaf19d 100644 --- a/x-pack/plugins/aiops/public/embeddables/change_point_chart/types.ts +++ b/x-pack/plugins/aiops/public/embeddables/change_point_chart/types.ts @@ -15,14 +15,6 @@ import type { SerializedTimeRange, SerializedTitles, } from '@kbn/presentation-publishing'; -import type { FC } from 'react'; -import type { SelectedChangePoint } from '../../components/change_point_detection/change_point_detection_context'; - -export type ViewComponent = FC<{ - changePoints: SelectedChangePoint[]; - interval: string; - onRenderComplete?: () => void; -}>; export interface ChangePointComponentApi { viewType: PublishingSubject; diff --git a/x-pack/plugins/aiops/public/embeddables/index.ts b/x-pack/plugins/aiops/public/embeddables/index.ts index b7d9ad25951fb..dae1f0eb3eeec 100644 --- a/x-pack/plugins/aiops/public/embeddables/index.ts +++ b/x-pack/plugins/aiops/public/embeddables/index.ts @@ -9,6 +9,7 @@ import type { CoreSetup } from '@kbn/core-lifecycle-browser'; import type { EmbeddableSetup } from '@kbn/embeddable-plugin/public'; import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '@kbn/aiops-change-point-detection/constants'; import { EMBEDDABLE_PATTERN_ANALYSIS_TYPE } from '@kbn/aiops-log-pattern-analysis/constants'; +import { EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE } from '@kbn/aiops-log-rate-analysis/constants'; import type { AiopsPluginStart, AiopsPluginStartDeps } from '../types'; export const registerEmbeddables = ( @@ -23,4 +24,8 @@ export const registerEmbeddables = ( const { getPatternAnalysisEmbeddableFactory } = await import('./pattern_analysis'); return getPatternAnalysisEmbeddableFactory(core.getStartServices); }); + embeddable.registerReactEmbeddableFactory(EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE, async () => { + const { getLogRateAnalysisEmbeddableFactory } = await import('./log_rate_analysis'); + return getLogRateAnalysisEmbeddableFactory(core.getStartServices); + }); }; diff --git a/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/embeddable_log_rate_analysis_factory.tsx b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/embeddable_log_rate_analysis_factory.tsx new file mode 100644 index 0000000000000..592ec32cef120 --- /dev/null +++ b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/embeddable_log_rate_analysis_factory.tsx @@ -0,0 +1,211 @@ +/* + * 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 { + EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE, + LOG_RATE_ANALYSIS_DATA_VIEW_REF_NAME, +} from '@kbn/aiops-log-rate-analysis/constants'; +import type { Reference } from '@kbn/content-management-utils'; +import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; +import type { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { + apiHasExecutionContext, + fetch$, + initializeTimeRange, + initializeTitles, + useBatchedPublishingSubjects, +} from '@kbn/presentation-publishing'; + +import fastIsEqual from 'fast-deep-equal'; +import { cloneDeep } from 'lodash'; +import React, { useMemo } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { BehaviorSubject, distinctUntilChanged, map, skipWhile } from 'rxjs'; +import { getLogRateAnalysisEmbeddableWrapperComponent } from '../../shared_components'; +import type { AiopsPluginStart, AiopsPluginStartDeps } from '../../types'; +import { initializeLogRateAnalysisControls } from './initialize_log_rate_analysis_analysis_controls'; +import type { + LogRateAnalysisEmbeddableApi, + LogRateAnalysisEmbeddableRuntimeState, + LogRateAnalysisEmbeddableState, +} from './types'; + +export const getLogRateAnalysisEmbeddableFactory = ( + getStartServices: StartServicesAccessor +) => { + const factory: ReactEmbeddableFactory< + LogRateAnalysisEmbeddableState, + LogRateAnalysisEmbeddableRuntimeState, + LogRateAnalysisEmbeddableApi + > = { + type: EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE, + deserializeState: (state) => { + const serializedState = cloneDeep(state.rawState); + // inject the reference + const dataViewIdRef = state.references?.find( + (ref) => ref.name === LOG_RATE_ANALYSIS_DATA_VIEW_REF_NAME + ); + // if the serializedState already contains a dataViewId, we don't want to overwrite it. (Unsaved state can cause this) + if (dataViewIdRef && serializedState && !serializedState.dataViewId) { + serializedState.dataViewId = dataViewIdRef?.id; + } + return serializedState; + }, + buildEmbeddable: async (state, buildApi, uuid, parentApi) => { + const [coreStart, pluginStart] = await getStartServices(); + + const { + api: timeRangeApi, + comparators: timeRangeComparators, + serialize: serializeTimeRange, + } = initializeTimeRange(state); + + const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); + + const { + logRateAnalysisControlsApi, + serializeLogRateAnalysisChartState, + logRateAnalysisControlsComparators, + } = initializeLogRateAnalysisControls(state); + + const dataLoading = new BehaviorSubject(true); + const blockingError = new BehaviorSubject(undefined); + + const dataViews$ = new BehaviorSubject([ + await pluginStart.data.dataViews.get( + state.dataViewId ?? (await pluginStart.data.dataViews.getDefaultId()) + ), + ]); + + const api = buildApi( + { + ...timeRangeApi, + ...titlesApi, + ...logRateAnalysisControlsApi, + getTypeDisplayName: () => + i18n.translate('xpack.aiops.logRateAnalysis.typeDisplayName', { + defaultMessage: 'log rate analysis', + }), + isEditingEnabled: () => true, + onEdit: async () => { + try { + const { resolveEmbeddableLogRateAnalysisUserInput } = await import( + './resolve_log_rate_analysis_config_input' + ); + + const result = await resolveEmbeddableLogRateAnalysisUserInput( + coreStart, + pluginStart, + parentApi, + uuid, + false, + logRateAnalysisControlsApi, + undefined, + serializeLogRateAnalysisChartState() + ); + + logRateAnalysisControlsApi.updateUserInput(result); + } catch (e) { + return Promise.reject(); + } + }, + dataLoading, + blockingError, + dataViews: dataViews$, + serializeState: () => { + const dataViewId = logRateAnalysisControlsApi.dataViewId.getValue(); + const references: Reference[] = dataViewId + ? [ + { + type: DATA_VIEW_SAVED_OBJECT_TYPE, + name: LOG_RATE_ANALYSIS_DATA_VIEW_REF_NAME, + id: dataViewId, + }, + ] + : []; + return { + rawState: { + timeRange: undefined, + ...serializeTitles(), + ...serializeTimeRange(), + ...serializeLogRateAnalysisChartState(), + }, + references, + }; + }, + }, + { + ...timeRangeComparators, + ...titleComparators, + ...logRateAnalysisControlsComparators, + } + ); + + const LogRateAnalysisEmbeddableWrapper = getLogRateAnalysisEmbeddableWrapperComponent( + coreStart, + pluginStart + ); + + const onLoading = (v: boolean) => dataLoading.next(v); + const onRenderComplete = () => dataLoading.next(false); + const onError = (error: Error) => blockingError.next(error); + + return { + api, + Component: () => { + if (!apiHasExecutionContext(parentApi)) { + throw new Error('Parent API does not have execution context'); + } + + const [dataViewId] = useBatchedPublishingSubjects(api.dataViewId); + + const reload$ = useMemo( + () => + fetch$(api).pipe( + skipWhile((fetchContext) => !fetchContext.isReload), + map((fetchContext) => Date.now()) + ), + [] + ); + + const timeRange$ = useMemo( + () => + fetch$(api).pipe( + map((fetchContext) => fetchContext.timeRange), + distinctUntilChanged(fastIsEqual) + ), + [] + ); + + const lastReloadRequestTime = useObservable(reload$, Date.now()); + const timeRange = useObservable(timeRange$, undefined); + + const embeddingOrigin = apiHasExecutionContext(parentApi) + ? parentApi.executionContext.type + : undefined; + + return ( + + ); + }, + }; + }, + }; + + return factory; +}; diff --git a/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/index.ts b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/index.ts new file mode 100644 index 0000000000000..2203d4c64bc8b --- /dev/null +++ b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { getLogRateAnalysisEmbeddableFactory } from './embeddable_log_rate_analysis_factory'; diff --git a/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/initialize_log_rate_analysis_analysis_controls.ts b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/initialize_log_rate_analysis_analysis_controls.ts new file mode 100644 index 0000000000000..9d8a49b8f0e9c --- /dev/null +++ b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/initialize_log_rate_analysis_analysis_controls.ts @@ -0,0 +1,43 @@ +/* + * 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 { StateComparators } from '@kbn/presentation-publishing'; +import { BehaviorSubject } from 'rxjs'; +import type { LogRateAnalysisComponentApi, LogRateAnalysisEmbeddableState } from './types'; + +type LogRateAnalysisEmbeddableCustomState = Omit< + LogRateAnalysisEmbeddableState, + 'timeRange' | 'title' | 'description' | 'hidePanelTitles' +>; + +export const initializeLogRateAnalysisControls = (rawState: LogRateAnalysisEmbeddableState) => { + const dataViewId = new BehaviorSubject(rawState.dataViewId); + + const updateUserInput = (update: LogRateAnalysisEmbeddableCustomState) => { + dataViewId.next(update.dataViewId); + }; + + const serializeLogRateAnalysisChartState = (): LogRateAnalysisEmbeddableCustomState => { + return { + dataViewId: dataViewId.getValue(), + }; + }; + + const logRateAnalysisControlsComparators: StateComparators = + { + dataViewId: [dataViewId, (arg) => dataViewId.next(arg)], + }; + + return { + logRateAnalysisControlsApi: { + dataViewId, + updateUserInput, + } as unknown as LogRateAnalysisComponentApi, + serializeLogRateAnalysisChartState, + logRateAnalysisControlsComparators, + }; +}; diff --git a/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/log_rate_analysis_embeddable_initializer.tsx b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/log_rate_analysis_embeddable_initializer.tsx new file mode 100644 index 0000000000000..bf53f07677739 --- /dev/null +++ b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/log_rate_analysis_embeddable_initializer.tsx @@ -0,0 +1,230 @@ +/* + * 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 { FC } from 'react'; +import React, { useEffect, useMemo, useState, useCallback } from 'react'; +import { pick } from 'lodash'; +import useMountedState from 'react-use/lib/useMountedState'; + +import { + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiForm, + EuiFormRow, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiSpacer, +} from '@elastic/eui'; + +import type { IndexPatternSelectProps } from '@kbn/unified-search-plugin/public'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; + +import { TimeFieldWarning } from '../../components/time_field_warning'; + +import type { LogRateAnalysisEmbeddableRuntimeState } from './types'; + +export interface LogRateAnalysisEmbeddableInitializerProps { + dataViews: DataViewsPublicPluginStart; + IndexPatternSelect: React.ComponentType; + initialInput?: Partial; + onCreate: (props: LogRateAnalysisEmbeddableRuntimeState) => void; + onCancel: () => void; + onPreview: (update: LogRateAnalysisEmbeddableRuntimeState) => Promise; + isNewPanel: boolean; +} + +export const LogRateAnalysisEmbeddableInitializer: FC< + LogRateAnalysisEmbeddableInitializerProps +> = ({ + dataViews, + IndexPatternSelect, + initialInput, + onCreate, + onCancel, + onPreview, + isNewPanel, +}) => { + const isMounted = useMountedState(); + + const [formInput, setFormInput] = useState( + pick(initialInput ?? {}, ['dataViewId']) as LogRateAnalysisEmbeddableRuntimeState + ); + + // State to track if the selected data view is time based, undefined is used + // to track that the check is in progress. + const [isDataViewTimeBased, setIsDataViewTimeBased] = useState(); + + const isFormValid = useMemo( + () => + isPopulatedObject(formInput, ['dataViewId']) && + formInput.dataViewId !== '' && + isDataViewTimeBased === true, + [formInput, isDataViewTimeBased] + ); + + const updatedProps = useMemo(() => { + return { + ...formInput, + title: isPopulatedObject(formInput) + ? i18n.translate('xpack.aiops.embeddableLogRateAnalysis.attachmentTitle', { + defaultMessage: 'Log rate analysis', + }) + : '', + }; + }, [formInput]); + + useEffect( + function previewChanges() { + if (isFormValid) { + onPreview(updatedProps); + } + }, + [isFormValid, onPreview, updatedProps, isDataViewTimeBased] + ); + + const setDataViewId = useCallback( + (dataViewId: string | undefined) => { + setFormInput({ + ...formInput, + dataViewId: dataViewId ?? '', + }); + setIsDataViewTimeBased(undefined); + }, + [formInput] + ); + + useEffect( + function checkIsDataViewTimeBased() { + setIsDataViewTimeBased(undefined); + + const { dataViewId } = formInput; + + if (!dataViewId) { + return; + } + + dataViews + .get(dataViewId) + .then((dataView) => { + if (!isMounted()) { + return; + } + setIsDataViewTimeBased(dataView.isTimeBased()); + }) + .catch(() => { + setIsDataViewTimeBased(undefined); + }); + }, + [dataViews, formInput, isMounted] + ); + + return ( + <> + + +

+ {isNewPanel + ? i18n.translate('xpack.aiops.embeddableLogRateAnalysis.config.title.new', { + defaultMessage: 'Create log rate analysis', + }) + : i18n.translate('xpack.aiops.embeddableLogRateAnalysis.config.title.edit', { + defaultMessage: 'Edit log rate analysis', + })} +

+
+
+ + + + + <> + { + setDataViewId(newId ?? ''); + }} + data-test-subj="aiopsLogRateAnalysisEmbeddableDataViewSelector" + /> + {isDataViewTimeBased === false && ( + <> + + + + )} + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/resolve_log_rate_analysis_config_input.tsx b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/resolve_log_rate_analysis_config_input.tsx new file mode 100644 index 0000000000000..a066b5bc722f2 --- /dev/null +++ b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/resolve_log_rate_analysis_config_input.tsx @@ -0,0 +1,94 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { tracksOverlays } from '@kbn/presentation-containers'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import React from 'react'; +import type { AiopsPluginStartDeps } from '../../types'; +import { LogRateAnalysisEmbeddableInitializer } from './log_rate_analysis_embeddable_initializer'; +import type { LogRateAnalysisComponentApi, LogRateAnalysisEmbeddableState } from './types'; + +export async function resolveEmbeddableLogRateAnalysisUserInput( + coreStart: CoreStart, + pluginStart: AiopsPluginStartDeps, + parentApi: unknown, + focusedPanelId: string, + isNewPanel: boolean, + logRateAnalysisControlsApi: LogRateAnalysisComponentApi, + deletePanel?: () => void, + initialState?: LogRateAnalysisEmbeddableState +): Promise { + const { overlays } = coreStart; + + const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined; + + let hasChanged = false; + return new Promise(async (resolve, reject) => { + try { + const cancelChanges = () => { + if (isNewPanel && deletePanel) { + deletePanel(); + } else if (hasChanged && logRateAnalysisControlsApi && initialState) { + // Reset to initialState in case user has changed the preview state + logRateAnalysisControlsApi.updateUserInput(initialState); + } + + flyoutSession.close(); + overlayTracker?.clearOverlays(); + }; + + const update = async (nextUpdate: LogRateAnalysisEmbeddableState) => { + resolve(nextUpdate); + flyoutSession.close(); + overlayTracker?.clearOverlays(); + }; + + const preview = async (nextUpdate: LogRateAnalysisEmbeddableState) => { + if (logRateAnalysisControlsApi) { + logRateAnalysisControlsApi.updateUserInput(nextUpdate); + hasChanged = true; + } + }; + + const flyoutSession = overlays.openFlyout( + toMountPoint( + , + coreStart + ), + { + ownFocus: true, + size: 's', + type: 'push', + paddingSize: 'm', + hideCloseButton: true, + 'data-test-subj': 'aiopsLogRateAnalysisEmbeddableInitializer', + 'aria-labelledby': 'logRateAnalysisConfig', + onClose: () => { + reject(); + flyoutSession.close(); + overlayTracker?.clearOverlays(); + }, + } + ); + + if (tracksOverlays(parentApi)) { + parentApi.openOverlay(flyoutSession, { focusedPanelId }); + } + } catch (error) { + reject(error); + } + }); +} diff --git a/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/types.ts b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/types.ts new file mode 100644 index 0000000000000..d2255e6dacb87 --- /dev/null +++ b/x-pack/plugins/aiops/public/embeddables/log_rate_analysis/types.ts @@ -0,0 +1,39 @@ +/* + * 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 { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; +import type { + HasEditCapabilities, + PublishesDataViews, + PublishesTimeRange, + PublishingSubject, + SerializedTimeRange, + SerializedTitles, +} from '@kbn/presentation-publishing'; + +export interface LogRateAnalysisComponentApi { + dataViewId: PublishingSubject; + updateUserInput: (update: LogRateAnalysisEmbeddableState) => void; +} + +export type LogRateAnalysisEmbeddableApi = DefaultEmbeddableApi & + HasEditCapabilities & + PublishesDataViews & + PublishesTimeRange & + LogRateAnalysisComponentApi; + +export interface LogRateAnalysisEmbeddableState extends SerializedTitles, SerializedTimeRange { + dataViewId: string; +} + +export interface LogRateAnalysisEmbeddableInitialState + extends SerializedTitles, + SerializedTimeRange { + dataViewId?: string; +} + +export type LogRateAnalysisEmbeddableRuntimeState = LogRateAnalysisEmbeddableState; diff --git a/x-pack/plugins/aiops/public/embeddables/pattern_analysis/embeddable_pattern_analysis_factory.tsx b/x-pack/plugins/aiops/public/embeddables/pattern_analysis/embeddable_pattern_analysis_factory.tsx index e7b1d6da3be61..d84043ea5f637 100644 --- a/x-pack/plugins/aiops/public/embeddables/pattern_analysis/embeddable_pattern_analysis_factory.tsx +++ b/x-pack/plugins/aiops/public/embeddables/pattern_analysis/embeddable_pattern_analysis_factory.tsx @@ -11,7 +11,6 @@ import { } from '@kbn/aiops-log-pattern-analysis/constants'; import type { Reference } from '@kbn/content-management-utils'; import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; -import { type DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; import type { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; @@ -37,32 +36,6 @@ import type { PatternAnalysisEmbeddableState, } from './types'; -export interface EmbeddablePatternAnalysisStartServices { - data: DataPublicPluginStart; -} - -export type EmbeddablePatternAnalysisType = typeof EMBEDDABLE_PATTERN_ANALYSIS_TYPE; - -export const getDependencies = async ( - getStartServices: StartServicesAccessor -) => { - const [ - { http, uiSettings, notifications, ...startServices }, - { lens, data, usageCollection, fieldFormats }, - ] = await getStartServices(); - - return { - http, - uiSettings, - data, - notifications, - lens, - usageCollection, - fieldFormats, - ...startServices, - }; -}; - export const getPatternAnalysisEmbeddableFactory = ( getStartServices: StartServicesAccessor ) => { @@ -87,20 +60,6 @@ export const getPatternAnalysisEmbeddableFactory = ( buildEmbeddable: async (state, buildApi, uuid, parentApi) => { const [coreStart, pluginStart] = await getStartServices(); - const { http, uiSettings, notifications, ...startServices } = coreStart; - const { lens, data, usageCollection, fieldFormats } = pluginStart; - - const deps = { - http, - uiSettings, - data, - notifications, - lens, - usageCollection, - fieldFormats, - ...startServices, - }; - const { api: timeRangeApi, comparators: timeRangeComparators, @@ -119,8 +78,8 @@ export const getPatternAnalysisEmbeddableFactory = ( const blockingError = new BehaviorSubject(undefined); const dataViews$ = new BehaviorSubject([ - await deps.data.dataViews.get( - state.dataViewId ?? (await deps.data.dataViews.getDefaultId()) + await pluginStart.data.dataViews.get( + state.dataViewId ?? (await pluginStart.data.dataViews.getDefaultId()) ), ]); diff --git a/x-pack/plugins/aiops/public/embeddables/pattern_analysis/pattern_analysys_component_wrapper.tsx b/x-pack/plugins/aiops/public/embeddables/pattern_analysis/pattern_analysis_component_wrapper.tsx similarity index 100% rename from x-pack/plugins/aiops/public/embeddables/pattern_analysis/pattern_analysys_component_wrapper.tsx rename to x-pack/plugins/aiops/public/embeddables/pattern_analysis/pattern_analysis_component_wrapper.tsx diff --git a/x-pack/plugins/aiops/public/embeddables/pattern_analysis/pattern_analysis_initializer.tsx b/x-pack/plugins/aiops/public/embeddables/pattern_analysis/pattern_analysis_initializer.tsx index f44fff343fb50..ef185518638b8 100644 --- a/x-pack/plugins/aiops/public/embeddables/pattern_analysis/pattern_analysis_initializer.tsx +++ b/x-pack/plugins/aiops/public/embeddables/pattern_analysis/pattern_analysis_initializer.tsx @@ -33,6 +33,7 @@ import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { DataSourceContextProvider } from '../../hooks/use_data_source'; import type { PatternAnalysisEmbeddableRuntimeState } from './types'; import { PatternAnalysisSettings } from '../../components/log_categorization/log_categorization_for_embeddable/embeddable_menu'; +import { TimeFieldWarning } from '../../components/time_field_warning'; import { RandomSampler } from '../../components/log_categorization/sampling_menu'; import { DEFAULT_PROBABILITY, @@ -62,6 +63,7 @@ export const PatternAnalysisEmbeddableInitializer: FC { const { + data: { dataViews }, unifiedSearch: { ui: { IndexPatternSelect }, }, @@ -166,7 +168,7 @@ export const PatternAnalysisEmbeddableInitializer: FC - + { ); }; - -const TimeFieldWarning = () => { - return ( - <> - -

- {i18n.translate( - 'xpack.aiops.logCategorization.embeddableMenu.timeFieldWarning.title.description', - { - defaultMessage: 'Pattern analysis can only be run on data views with a time field.', - } - )} -

-
- - - ); -}; diff --git a/x-pack/plugins/aiops/public/embeddables/pattern_analysis/types.ts b/x-pack/plugins/aiops/public/embeddables/pattern_analysis/types.ts index f78934b9075f1..710e18823a2bb 100644 --- a/x-pack/plugins/aiops/public/embeddables/pattern_analysis/types.ts +++ b/x-pack/plugins/aiops/public/embeddables/pattern_analysis/types.ts @@ -14,18 +14,12 @@ import type { SerializedTimeRange, SerializedTitles, } from '@kbn/presentation-publishing'; -import type { FC } from 'react'; import type { MinimumTimeRangeOption } from '../../components/log_categorization/log_categorization_for_embeddable/minimum_time_range'; import type { RandomSamplerOption, RandomSamplerProbability, } from '../../components/log_categorization/sampling_menu/random_sampler'; -export type ViewComponent = FC<{ - interval: string; - onRenderComplete?: () => void; -}>; - export interface PatternAnalysisComponentApi { dataViewId: PublishingSubject; fieldName: PublishingSubject; diff --git a/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts b/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts index 03762a7ba70ba..c240ec90bc1df 100644 --- a/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts +++ b/x-pack/plugins/aiops/public/hooks/use_aiops_app_context.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { createContext, type FC, type PropsWithChildren, useContext } from 'react'; +import { createContext, type FC, useContext } from 'react'; import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; @@ -24,17 +24,12 @@ import type { ThemeServiceStart, } from '@kbn/core/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; -import { type EuiComboBoxProps } from '@elastic/eui/src/components/combo_box/combo_box'; -import { type DataView } from '@kbn/data-views-plugin/common'; -import type { - FieldStatsProps, - FieldStatsServices, -} from '@kbn/unified-field-list/src/components/field_stats'; -import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { CasesPublicStart } from '@kbn/cases-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { FieldStatsFlyoutProviderProps } from '@kbn/ml-field-stats-flyout/field_stats_flyout_provider'; +import type { UseFieldStatsTrigger } from '@kbn/ml-field-stats-flyout/use_field_stats_trigger'; /** * AIOps app context value to be provided via React context. @@ -98,7 +93,7 @@ export interface AiopsAppContextValue { /** * Used to create deep links to other plugins. */ - share: SharePluginStart; + share?: SharePluginStart; /** * Used to create lens embeddables. */ @@ -115,18 +110,8 @@ export interface AiopsAppContextValue { * Deps for unified fields stats. */ fieldStats?: { - useFieldStatsTrigger: () => { - renderOption: EuiComboBoxProps['renderOption']; - closeFlyout: () => void; - }; - FieldStatsFlyoutProvider: FC< - PropsWithChildren<{ - dataView: DataView; - fieldStatsServices: FieldStatsServices; - timeRangeMs?: TimeRangeMs; - dslQuery?: FieldStatsProps['dslQuery']; - }> - >; + useFieldStatsTrigger: UseFieldStatsTrigger; + FieldStatsFlyoutProvider: FC; }; embeddable?: EmbeddableStart; cases?: CasesPublicStart; diff --git a/x-pack/plugins/aiops/public/hooks/use_data_source.tsx b/x-pack/plugins/aiops/public/hooks/use_data_source.tsx index 081e10a34de65..ef574a348b928 100644 --- a/x-pack/plugins/aiops/public/hooks/use_data_source.tsx +++ b/x-pack/plugins/aiops/public/hooks/use_data_source.tsx @@ -8,10 +8,10 @@ import type { FC, PropsWithChildren } from 'react'; import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; import type { DataView } from '@kbn/data-views-plugin/common'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useAiopsAppContext } from './use_aiops_app_context'; export const DataSourceContext = createContext({ get dataView(): never { @@ -30,6 +30,7 @@ export interface DataViewAndSavedSearch { } export interface DataSourceContextProviderProps { + dataViews: DataViewsPublicPluginStart; dataViewId?: string; savedSearchId?: string; /** Output resolves data view objects */ @@ -43,20 +44,14 @@ export interface DataSourceContextProviderProps { * @constructor */ export const DataSourceContextProvider: FC> = ({ + dataViews, dataViewId, - savedSearchId, children, onChange, }) => { const [value, setValue] = useState(); const [error, setError] = useState(); - const { - data: { dataViews }, - // uiSettings, - // savedSearch: savedSearchService, - } = useAiopsAppContext(); - /** * Resolve data view or saved search if exists. */ diff --git a/x-pack/plugins/aiops/public/shared_components/change_point_detection.tsx b/x-pack/plugins/aiops/public/shared_components/change_point_detection.tsx index 9afbd9e1c4c8d..53997219fd639 100644 --- a/x-pack/plugins/aiops/public/shared_components/change_point_detection.tsx +++ b/x-pack/plugins/aiops/public/shared_components/change_point_detection.tsx @@ -11,7 +11,6 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { UI_SETTINGS } from '@kbn/data-service'; import type { TimeRange } from '@kbn/es-query'; import { DatePickerContextProvider } from '@kbn/ml-date-picker'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { pick } from 'lodash'; import React, { useEffect, useMemo, useState, type FC } from 'react'; import type { Observable } from 'rxjs'; @@ -141,33 +140,34 @@ const ChangePointDetectionWrapper: FC = ({ width: 100%; `} > - - - - - - - - - - - - - - - + + + + + + + + + + + + +
); }; diff --git a/x-pack/plugins/aiops/public/shared_components/index.tsx b/x-pack/plugins/aiops/public/shared_components/index.tsx index 1c5b85c4b79b6..b347d3ee24cac 100644 --- a/x-pack/plugins/aiops/public/shared_components/index.tsx +++ b/x-pack/plugins/aiops/public/shared_components/index.tsx @@ -11,6 +11,7 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { AiopsPluginStartDeps } from '../types'; import type { ChangePointDetectionSharedComponent } from './change_point_detection'; import type { PatternAnalysisSharedComponent } from './pattern_analysis'; +import type { LogRateAnalysisEmbeddableWrapper } from './log_rate_analysis_embeddable_wrapper'; const ChangePointDetectionLazy = dynamic(async () => import('./change_point_detection')); @@ -37,3 +38,24 @@ export const getPatternAnalysisComponent = ( }; export type { PatternAnalysisSharedComponent } from './pattern_analysis'; + +const LogRateAnalysisEmbeddableWrapperLazy = dynamic( + async () => import('./log_rate_analysis_embeddable_wrapper') +); + +export const getLogRateAnalysisEmbeddableWrapperComponent = ( + coreStart: CoreStart, + pluginStart: AiopsPluginStartDeps +): LogRateAnalysisEmbeddableWrapper => { + return React.memo((props) => { + return ( + + ); + }); +}; + +export type { LogRateAnalysisEmbeddableWrapper } from './log_rate_analysis_embeddable_wrapper'; diff --git a/x-pack/plugins/aiops/public/shared_components/log_rate_analysis_embeddable_wrapper.tsx b/x-pack/plugins/aiops/public/shared_components/log_rate_analysis_embeddable_wrapper.tsx new file mode 100644 index 0000000000000..9f2a88e73461c --- /dev/null +++ b/x-pack/plugins/aiops/public/shared_components/log_rate_analysis_embeddable_wrapper.tsx @@ -0,0 +1,179 @@ +/* + * 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 { pick } from 'lodash'; +import React, { useEffect, useMemo, useState, type FC } from 'react'; +import usePrevious from 'react-use/lib/usePrevious'; +import type { Observable } from 'rxjs'; +import { BehaviorSubject, combineLatest, distinctUntilChanged, map } from 'rxjs'; +import { createBrowserHistory } from 'history'; + +import { UrlStateProvider } from '@kbn/ml-url-state'; +import { Router } from '@kbn/shared-ux-router'; +import { AIOPS_EMBEDDABLE_ORIGIN } from '@kbn/aiops-common/constants'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { UI_SETTINGS } from '@kbn/data-service'; +import { LogRateAnalysisReduxProvider } from '@kbn/aiops-log-rate-analysis/state'; +import type { TimeRange } from '@kbn/es-query'; +import { DatePickerContextProvider } from '@kbn/ml-date-picker'; +import type { SignificantItem } from '@kbn/ml-agg-utils'; + +import { AiopsAppContext, type AiopsAppContextValue } from '../hooks/use_aiops_app_context'; +import { DataSourceContextProvider } from '../hooks/use_data_source'; +import { ReloadContextProvider } from '../hooks/use_reload'; +import { FilterQueryContextProvider } from '../hooks/use_filters_query'; +import type { AiopsPluginStartDeps } from '../types'; + +import { LogRateAnalysisForEmbeddable } from '../components/log_rate_analysis/log_rate_analysis_for_embeddable'; + +/** + * Only used to initialize internally + */ +export type LogRateAnalysisPropsWithDeps = LogRateAnalysisEmbeddableWrapperProps & { + coreStart: CoreStart; + pluginStart: AiopsPluginStartDeps; +}; + +export type LogRateAnalysisEmbeddableWrapper = FC; + +export interface LogRateAnalysisEmbeddableWrapperProps { + dataViewId: string; + timeRange: TimeRange; + /** + * Component to render if there are no significant items found + */ + emptyState?: React.ReactElement; + /** + * Outputs the most recent significant items + */ + onChange?: (significantItems: SignificantItem[]) => void; + /** + * Last reload request time, can be used for manual reload + */ + lastReloadRequestTime?: number; + /** Origin of the embeddable instance */ + embeddingOrigin?: string; + onLoading: (isLoading: boolean) => void; + onRenderComplete: () => void; + onError: (error: Error) => void; +} + +const LogRateAnalysisEmbeddableWrapperWithDeps: FC = ({ + // Component dependencies + coreStart, + pluginStart, + // Component props + dataViewId, + timeRange, + embeddingOrigin, + lastReloadRequestTime, +}) => { + const deps = useMemo(() => { + const { lens, data, usageCollection, fieldFormats, charts, share, storage, unifiedSearch } = + pluginStart; + + return { + data, + lens, + usageCollection, + fieldFormats, + charts, + share, + storage, + unifiedSearch, + ...coreStart, + }; + }, [coreStart, pluginStart]); + + const datePickerDeps = { + ...pick(deps, ['data', 'http', 'notifications', 'theme', 'uiSettings', 'i18n']), + uiSettingsKeys: UI_SETTINGS, + }; + + const aiopsAppContextValue = useMemo(() => { + return { + embeddingOrigin: embeddingOrigin ?? AIOPS_EMBEDDABLE_ORIGIN.DEFAULT, + ...deps, + }; + }, [deps, embeddingOrigin]); + + const [manualReload$] = useState>( + new BehaviorSubject(lastReloadRequestTime ?? Date.now()) + ); + + useEffect( + function updateManualReloadSubject() { + if (!lastReloadRequestTime) return; + manualReload$.next(lastReloadRequestTime); + }, + [lastReloadRequestTime, manualReload$] + ); + + const resultObservable$ = useMemo>(() => { + return combineLatest([manualReload$]).pipe( + map(([manualReload]) => Math.max(manualReload)), + distinctUntilChanged() + ); + }, [manualReload$]); + + const history = createBrowserHistory(); + + // We use the following pattern to track changes of dataViewId, and if there's + // a change, we unmount and remount the complete inner component. This makes + // sure the component is reinitialized correctly when the options of the + // dashboard panel are used to change the data view. This is a bit of a + // workaround since originally log rate analysis was developed as a standalone + // page with the expectation that the data view is set once and never changes. + const prevDataViewId = usePrevious(dataViewId); + const [_, setRerenderFlag] = useState(false); + useEffect(() => { + if (prevDataViewId && prevDataViewId !== dataViewId) { + setRerenderFlag((prev) => !prev); + } + }, [dataViewId, prevDataViewId]); + const showComponent = prevDataViewId === undefined || prevDataViewId === dataViewId; + + // TODO: Remove data-shared-item as part of https://github.com/elastic/kibana/issues/179376> + return ( +
+ {showComponent && ( + + + + + + + + + + + + + + + + + + )} +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export default LogRateAnalysisEmbeddableWrapperWithDeps; diff --git a/x-pack/plugins/aiops/public/shared_components/pattern_analysis.tsx b/x-pack/plugins/aiops/public/shared_components/pattern_analysis.tsx index 78261cd1f62f0..f601474a5707f 100644 --- a/x-pack/plugins/aiops/public/shared_components/pattern_analysis.tsx +++ b/x-pack/plugins/aiops/public/shared_components/pattern_analysis.tsx @@ -10,7 +10,6 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { UI_SETTINGS } from '@kbn/data-service'; import type { TimeRange } from '@kbn/es-query'; import { DatePickerContextProvider } from '@kbn/ml-date-picker'; -import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { pick } from 'lodash'; import React, { useEffect, useMemo, useState, type FC } from 'react'; import type { Observable } from 'rxjs'; @@ -20,7 +19,7 @@ import type { RandomSamplerOption, RandomSamplerProbability, } from '../components/log_categorization/sampling_menu/random_sampler'; -import { PatternAnalysisEmbeddableWrapper } from '../embeddables/pattern_analysis/pattern_analysys_component_wrapper'; +import { PatternAnalysisEmbeddableWrapper } from '../embeddables/pattern_analysis/pattern_analysis_component_wrapper'; import { AiopsAppContext, type AiopsAppContextValue } from '../hooks/use_aiops_app_context'; import { DataSourceContextProvider } from '../hooks/use_data_source'; import { FilterQueryContextProvider } from '../hooks/use_filters_query'; @@ -139,31 +138,32 @@ const PatternAnalysisWrapper: FC = ({ padding: '10px', }} > - - - - - - - - - - - - - + + + + + + + + + + +
); }; diff --git a/x-pack/plugins/aiops/public/types/storage.ts b/x-pack/plugins/aiops/public/types/storage.ts index a4a29dda2f2c3..ea6fde6b06552 100644 --- a/x-pack/plugins/aiops/public/types/storage.ts +++ b/x-pack/plugins/aiops/public/types/storage.ts @@ -19,14 +19,12 @@ export const AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE = 'aiops.randomSamplingProbabilityPreference'; export const AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE = 'aiops.patternAnalysisMinimumTimeRangePreference'; -export const AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS = 'aiops.logRateAnalysisResultColumns'; export type AiOps = Partial<{ [AIOPS_FROZEN_TIER_PREFERENCE]: FrozenTierPreference; [AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE]: RandomSamplerOption; [AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE]: number; [AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE]: MinimumTimeRangeOption; - [AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS]: string[]; }> | null; export type AiOpsKey = keyof Exclude; @@ -39,8 +37,6 @@ export type AiOpsStorageMapped = T extends typeof AIOPS_FROZ ? RandomSamplerProbability : T extends typeof AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE ? MinimumTimeRangeOption - : T extends typeof AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS - ? string[] : null; export const AIOPS_STORAGE_KEYS = [ @@ -48,5 +44,4 @@ export const AIOPS_STORAGE_KEYS = [ AIOPS_RANDOM_SAMPLING_MODE_PREFERENCE, AIOPS_RANDOM_SAMPLING_PROBABILITY_PREFERENCE, AIOPS_PATTERN_ANALYSIS_MINIMUM_TIME_RANGE_PREFERENCE, - AIOPS_LOG_RATE_ANALYSIS_RESULT_COLUMNS, ] as const; diff --git a/x-pack/plugins/aiops/public/ui_actions/create_change_point_chart.tsx b/x-pack/plugins/aiops/public/ui_actions/create_change_point_chart.tsx index f9078f575818a..4d7ed26e295f5 100644 --- a/x-pack/plugins/aiops/public/ui_actions/create_change_point_chart.tsx +++ b/x-pack/plugins/aiops/public/ui_actions/create_change_point_chart.tsx @@ -12,7 +12,9 @@ import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { EMBEDDABLE_CHANGE_POINT_CHART_TYPE } from '@kbn/aiops-change-point-detection/constants'; import type { CoreStart } from '@kbn/core-lifecycle-browser'; + import type { AiopsPluginStartDeps } from '../types'; + import type { ChangePointChartActionContext } from './change_point_action_context'; const parentApiIsCompatible = async ( diff --git a/x-pack/plugins/aiops/public/ui_actions/create_log_rate_analysis_actions.tsx b/x-pack/plugins/aiops/public/ui_actions/create_log_rate_analysis_actions.tsx new file mode 100644 index 0000000000000..588903e96aa16 --- /dev/null +++ b/x-pack/plugins/aiops/public/ui_actions/create_log_rate_analysis_actions.tsx @@ -0,0 +1,91 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { PresentationContainer } from '@kbn/presentation-containers'; +import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; +import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; +import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE } from '@kbn/aiops-log-rate-analysis/constants'; +import { AIOPS_EMBEDDABLE_GROUPING } from '@kbn/aiops-common/constants'; + +import type { + LogRateAnalysisEmbeddableApi, + LogRateAnalysisEmbeddableInitialState, +} from '../embeddables/log_rate_analysis/types'; +import type { AiopsPluginStartDeps } from '../types'; + +import type { LogRateAnalysisActionContext } from './log_rate_analysis_action_context'; + +const parentApiIsCompatible = async ( + parentApi: unknown +): Promise => { + const { apiIsPresentationContainer } = await import('@kbn/presentation-containers'); + // we cannot have an async type check, so return the casted parentApi rather than a boolean + return apiIsPresentationContainer(parentApi) ? (parentApi as PresentationContainer) : undefined; +}; + +export function createAddLogRateAnalysisEmbeddableAction( + coreStart: CoreStart, + pluginStart: AiopsPluginStartDeps +): UiActionsActionDefinition { + return { + id: 'create-log-rate-analysis-embeddable', + grouping: AIOPS_EMBEDDABLE_GROUPING, + getIconType: () => 'logRateAnalysis', + getDisplayName: () => + i18n.translate('xpack.aiops.embeddableLogRateAnalysisDisplayName', { + defaultMessage: 'Log rate analysis', + }), + async isCompatible(context: EmbeddableApiContext) { + return Boolean(await parentApiIsCompatible(context.embeddable)); + }, + async execute(context) { + const presentationContainerParent = await parentApiIsCompatible(context.embeddable); + if (!presentationContainerParent) throw new IncompatibleActionError(); + + try { + const { resolveEmbeddableLogRateAnalysisUserInput } = await import( + '../embeddables/log_rate_analysis/resolve_log_rate_analysis_config_input' + ); + + const initialState: LogRateAnalysisEmbeddableInitialState = { + dataViewId: undefined, + }; + + const embeddable = await presentationContainerParent.addNewPanel< + object, + LogRateAnalysisEmbeddableApi + >({ + panelType: EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE, + initialState, + }); + + if (!embeddable) { + return; + } + + const deletePanel = () => { + presentationContainerParent.removePanel(embeddable.uuid); + }; + + resolveEmbeddableLogRateAnalysisUserInput( + coreStart, + pluginStart, + context.embeddable, + embeddable.uuid, + true, + embeddable, + deletePanel + ); + } catch (e) { + return Promise.reject(); + } + }, + }; +} diff --git a/x-pack/plugins/aiops/public/ui_actions/create_pattern_analysis_action.tsx b/x-pack/plugins/aiops/public/ui_actions/create_pattern_analysis_action.tsx index 81127559e4e3d..f840e896abac4 100644 --- a/x-pack/plugins/aiops/public/ui_actions/create_pattern_analysis_action.tsx +++ b/x-pack/plugins/aiops/public/ui_actions/create_pattern_analysis_action.tsx @@ -12,13 +12,16 @@ import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import type { CoreStart } from '@kbn/core-lifecycle-browser'; import { EMBEDDABLE_PATTERN_ANALYSIS_TYPE } from '@kbn/aiops-log-pattern-analysis/constants'; +import { AIOPS_EMBEDDABLE_GROUPING } from '@kbn/aiops-common/constants'; + import type { AiopsPluginStartDeps } from '../types'; -import type { PatternAnalysisActionContext } from './pattern_analysis_action_context'; import type { PatternAnalysisEmbeddableApi, PatternAnalysisEmbeddableInitialState, } from '../embeddables/pattern_analysis/types'; +import type { PatternAnalysisActionContext } from './pattern_analysis_action_context'; + const parentApiIsCompatible = async ( parentApi: unknown ): Promise => { @@ -33,16 +36,7 @@ export function createAddPatternAnalysisEmbeddableAction( ): UiActionsActionDefinition { return { id: 'create-pattern-analysis-embeddable', - grouping: [ - { - id: 'ml', - getDisplayName: () => - i18n.translate('xpack.aiops.navMenu.mlAppNameText', { - defaultMessage: 'Machine Learning and Analytics', - }), - getIconType: () => 'logPatternAnalysis', - }, - ], + grouping: AIOPS_EMBEDDABLE_GROUPING, getIconType: () => 'logPatternAnalysis', getDisplayName: () => i18n.translate('xpack.aiops.embeddablePatternAnalysisDisplayName', { diff --git a/x-pack/plugins/aiops/public/ui_actions/index.ts b/x-pack/plugins/aiops/public/ui_actions/index.ts index d14856fd28733..6081541c448e7 100644 --- a/x-pack/plugins/aiops/public/ui_actions/index.ts +++ b/x-pack/plugins/aiops/public/ui_actions/index.ts @@ -18,6 +18,7 @@ import { createOpenChangePointInMlAppAction } from './open_change_point_ml'; import type { AiopsPluginStartDeps } from '../types'; import { createCategorizeFieldAction } from '../components/log_categorization'; import { createAddPatternAnalysisEmbeddableAction } from './create_pattern_analysis_action'; +import { createAddLogRateAnalysisEmbeddableAction } from './create_log_rate_analysis_actions'; export function registerAiopsUiActions( uiActions: UiActionsSetup, @@ -27,6 +28,7 @@ export function registerAiopsUiActions( const openChangePointInMlAppAction = createOpenChangePointInMlAppAction(coreStart, pluginStart); const addChangePointChartAction = createAddChangePointChartAction(coreStart, pluginStart); const addPatternAnalysisAction = createAddPatternAnalysisEmbeddableAction(coreStart, pluginStart); + const addLogRateAnalysisAction = createAddLogRateAnalysisEmbeddableAction(coreStart, pluginStart); uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addPatternAnalysisAction); uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addChangePointChartAction); @@ -39,4 +41,6 @@ export function registerAiopsUiActions( ); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, openChangePointInMlAppAction); + + uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addLogRateAnalysisAction); } diff --git a/x-pack/plugins/aiops/public/ui_actions/log_rate_analysis_action_context.ts b/x-pack/plugins/aiops/public/ui_actions/log_rate_analysis_action_context.ts new file mode 100644 index 0000000000000..d0c1ef715b84c --- /dev/null +++ b/x-pack/plugins/aiops/public/ui_actions/log_rate_analysis_action_context.ts @@ -0,0 +1,24 @@ +/* + * 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 { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { apiIsOfType, type EmbeddableApiContext } from '@kbn/presentation-publishing'; +import { EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE } from '@kbn/aiops-log-rate-analysis/constants'; +import type { LogRateAnalysisEmbeddableApi } from '../embeddables/log_rate_analysis/types'; + +export interface LogRateAnalysisActionContext extends EmbeddableApiContext { + embeddable: LogRateAnalysisEmbeddableApi; +} + +export function isLogRateAnalysisEmbeddableContext( + arg: unknown +): arg is LogRateAnalysisActionContext { + return ( + isPopulatedObject(arg, ['embeddable']) && + apiIsOfType(arg.embeddable, EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE) + ); +} diff --git a/x-pack/plugins/aiops/tsconfig.json b/x-pack/plugins/aiops/tsconfig.json index 188f8b275fbac..e8b4f4f3ed972 100644 --- a/x-pack/plugins/aiops/tsconfig.json +++ b/x-pack/plugins/aiops/tsconfig.json @@ -63,7 +63,6 @@ "@kbn/presentation-containers", "@kbn/presentation-publishing", "@kbn/presentation-util-plugin", - "@kbn/react-kibana-context-render", "@kbn/react-kibana-context-theme", "@kbn/react-kibana-mount", "@kbn/rison", @@ -79,6 +78,8 @@ "@kbn/observability-ai-assistant-plugin", "@kbn/ui-theme", "@kbn/apm-utils", + "@kbn/ml-field-stats-flyout", + "@kbn/shared-ux-router", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 3212eba8b2ddd..7fd678e98f6fd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -576,6 +576,7 @@ export const ConfigurationStepForm: FC = ({ fieldStatsServices={fieldStatsServices} timeRangeMs={indexData.timeRangeMs} dslQuery={jobConfigQuery} + theme={services.theme} > diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx index 6c4600be5d25e..b44c523bc57cf 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx @@ -123,6 +123,7 @@ export const WizardSteps: FC = ({ currentStep, setCurrentStep }) => { fieldStatsServices={fieldStatsServices} timeRangeMs={timeRangeMs} dslQuery={jobCreator.query} + theme={services.theme} > <> diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index ab2865b85eb8a..2c078f93627cc 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -111,7 +111,7 @@ export const CreateTransformWizardContext = createContext<{ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) => { const { showNodeInfo } = useEnabledFeatures(); const appDependencies = useAppDependencies(); - const { uiSettings, data, fieldFormats, charts } = appDependencies; + const { uiSettings, data, fieldFormats, charts, theme } = appDependencies; const { dataView } = searchItems; // The current WIZARD_STEP @@ -247,6 +247,7 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) fieldStatsServices={fieldStatsServices} timeRangeMs={stepDefineState.timeRangeMs} dslQuery={transformConfig.source.query} + theme={theme} > { + await aiops.logRateAnalysisDataGenerator.generateData(testDataSetup.dataGenerator); + + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.securityUI.loginAsMlPowerUser(); + await ml.testResources.createDataViewIfNeeded( + testDataSetup.sourceIndexOrSavedSearch, + '@timestamp' + ); + + await PageObjects.common.setTime({ from, to }); + }); + + after(async () => { + await ml.testResources.deleteDataViewByTitle(testDataSetup.sourceIndexOrSavedSearch); + await aiops.logRateAnalysisDataGenerator.removeGeneratedData(testDataSetup.dataGenerator); + await PageObjects.common.unsetTime(); + }); + + describe(testDataPanel.suiteSuffix, function () { + before(async () => { + await PageObjects.dashboard.navigateToApp(); + }); + + after(async () => { + await ml.testResources.deleteDashboardByTitle(testDataPanel.dashboardTitle); + }); + + it('should open initializer flyout', async () => { + await PageObjects.dashboard.clickNewDashboard(); + await aiops.dashboardEmbeddables.assertDashboardIsEmpty(); + await aiops.dashboardEmbeddables.openEmbeddableInitializer( + EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE + ); + }); + + it('should select data view', async () => { + await aiops.dashboardEmbeddables.assertLogRateAnalysisEmbeddableDataViewSelectorExists(); + await aiops.dashboardEmbeddables.selectLogRateAnalysisEmbeddableDataView( + testDataSetup.sourceIndexOrSavedSearch + ); + }); + + it('should create new log rate analysis panel', async () => { + await aiops.dashboardEmbeddables.clickLogRateAnalysisInitializerConfirmButtonEnabled(); + await PageObjects.timePicker.pauseAutoRefresh(); + await aiops.dashboardEmbeddables.assertDashboardPanelExists(testDataPanel.panelTitle); + await aiops.logRateAnalysisPage.assertAutoRunButtonExists(); + await PageObjects.dashboard.saveDashboard(testDataPanel.dashboardTitle); + }); + + it('should run log rate analysis', async () => { + await aiops.dashboardEmbeddables.assertDashboardPanelExists(testDataPanel.panelTitle); + await aiops.logRateAnalysisPage.clickAutoRunButton(); + // Wait for the analysis to finish + await aiops.logRateAnalysisPage.assertAnalysisComplete( + testDataSetup.analysisType, + testDataSetup.dataGenerator + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/services/aiops/dashboard_embeddables.ts b/x-pack/test/functional/services/aiops/dashboard_embeddables.ts new file mode 100644 index 0000000000000..a24cec24734da --- /dev/null +++ b/x-pack/test/functional/services/aiops/dashboard_embeddables.ts @@ -0,0 +1,110 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function AiopsDashboardEmbeddablesProvider({ getService }: FtrProviderContext) { + const comboBox = getService('comboBox'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const dashboardAddPanel = getService('dashboardAddPanel'); + + return { + async assertLogRateAnalysisEmbeddableInitializerExists() { + await retry.tryForTime(10 * 1000, async () => { + await testSubjects.existOrFail('aiopsLogRateAnalysisEmbeddableInitializer', { + timeout: 1000, + }); + }); + }, + + async assertLogRateAnalysisEmbeddableInitializerNotExists() { + await retry.tryForTime(10 * 1000, async () => { + await testSubjects.missingOrFail('aiopsLogRateAnalysisEmbeddableInitializer', { + timeout: 1000, + }); + }); + }, + + async assertInitializerConfirmButtonEnabled(subj: string) { + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail(subj); + await testSubjects.isEnabled(subj); + }); + }, + + async assertDashboardIsEmpty() { + await retry.tryForTime(60 * 1000, async () => { + await testSubjects.existOrFail('emptyDashboardWidget'); + }); + }, + + async assertDashboardPanelExists(title: string) { + await retry.tryForTime(5000, async () => { + await find.existsByLinkText(title); + }); + }, + + async assertLogsAiopsSectionExists(expectExist = true) { + await retry.tryForTime(60 * 1000, async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.verifyEmbeddableFactoryGroupExists('logs-aiops', expectExist); + }); + }, + + async clickLogRateAnalysisInitializerConfirmButtonEnabled() { + const subj = 'aiopsLogRateAnalysisConfirmButton'; + await retry.tryForTime(60 * 1000, async () => { + await this.assertInitializerConfirmButtonEnabled(subj); + await testSubjects.clickWhenNotDisabledWithoutRetry(subj); + await this.assertLogRateAnalysisEmbeddableInitializerNotExists(); + }); + }, + + async openEmbeddableInitializer(mlEmbeddableType: 'aiopsLogRateAnalysisEmbeddable') { + const name = { + aiopsLogRateAnalysisEmbeddable: 'Log rate analysis', + }; + await retry.tryForTime(60 * 1000, async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await testSubjects.existOrFail('dashboardPanelSelectionFlyout', { timeout: 2000 }); + + await dashboardAddPanel.verifyEmbeddableFactoryGroupExists('logs-aiops'); + + await dashboardAddPanel.clickAddNewPanelFromUIActionLink(name[mlEmbeddableType]); + await testSubjects.existOrFail('aiopsLogRateAnalysisControls', { timeout: 2000 }); + }); + }, + + async assertLogRateAnalysisEmbeddableDataViewSelectorExists() { + await testSubjects.existOrFail( + 'aiopsLogRateAnalysisEmbeddableDataViewSelector > comboBoxInput' + ); + }, + + async assertLogRateAnalysisEmbeddableDataViewSelection(dataViewValue: string) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'aiopsLogRateAnalysisEmbeddableDataViewSelector > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + [dataViewValue], + `Expected data view selection to be '${dataViewValue}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectLogRateAnalysisEmbeddableDataView(dataViewValue: string) { + await comboBox.set( + 'aiopsLogRateAnalysisEmbeddableDataViewSelector > comboBoxInput', + dataViewValue + ); + await this.assertLogRateAnalysisEmbeddableDataViewSelection(dataViewValue); + }, + }; +} diff --git a/x-pack/test/functional/services/aiops/index.ts b/x-pack/test/functional/services/aiops/index.ts index 71de8c397c073..39abf21375f86 100644 --- a/x-pack/test/functional/services/aiops/index.ts +++ b/x-pack/test/functional/services/aiops/index.ts @@ -7,6 +7,7 @@ import type { FtrProviderContext } from '../../ftr_provider_context'; +import { AiopsDashboardEmbeddablesProvider } from './dashboard_embeddables'; import { LogRateAnalysisPageProvider } from './log_rate_analysis_page'; import { LogRateAnalysisResultsTableProvider } from './log_rate_analysis_results_table'; import { LogRateAnalysisResultsGroupsTableProvider } from './log_rate_analysis_results_groups_table'; @@ -16,6 +17,7 @@ import { ChangePointDetectionPageProvider } from './change_point_detection_page' import { MlTableServiceProvider } from '../ml/common_table_service'; export function AiopsProvider(context: FtrProviderContext) { + const dashboardEmbeddables = AiopsDashboardEmbeddablesProvider(context); const logRateAnalysisPage = LogRateAnalysisPageProvider(context); const logRateAnalysisResultsTable = LogRateAnalysisResultsTableProvider(context); const logRateAnalysisResultsGroupsTable = LogRateAnalysisResultsGroupsTableProvider(context); @@ -27,6 +29,7 @@ export function AiopsProvider(context: FtrProviderContext) { const changePointDetectionPage = ChangePointDetectionPageProvider(context, tableService); return { + dashboardEmbeddables, changePointDetectionPage, logRateAnalysisPage, logRateAnalysisResultsTable, diff --git a/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts b/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts index 66c4e64f05efe..0f7b14e3e8be7 100644 --- a/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts +++ b/x-pack/test/functional/services/aiops/log_rate_analysis_page.ts @@ -266,6 +266,22 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr ); }, + async clickAutoRunButton() { + await testSubjects.clickWhenNotDisabledWithoutRetry( + 'aiopsLogRateAnalysisContentRunAnalysisButton' + ); + + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.missingOrFail('aiopsLogRateAnalysisContentRunAnalysisButton'); + }); + }, + + async assertAutoRunButtonExists() { + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('aiopsLogRateAnalysisContentRunAnalysisButton'); + }); + }, + async assertNoAutoRunButtonExists() { await testSubjects.existOrFail('aiopsLogRateAnalysisNoAutoRunContentRunAnalysisButton'); },