From 8d5f16fe26de870bd16ab98ef9c6317807de3530 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 3 Oct 2024 10:40:10 -0600 Subject: [PATCH] [ML][AIOps] Log rate analysis: ensure ability to sort on Log rate change (#193501) ## Summary This PR - updates the `LogRateAnalysisResultsTable` to use `EuiInMemoryTable` to simplify sorting and pagination - adds sorting to `Log rate change` column - persists columns selected for viewing in the result view Related meta issue: https://github.com/elastic/kibana/issues/187684 ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Elastic Machine (cherry picked from commit c18184ae26c4af3f203256b582790f5bf4e0f595) --- x-pack/packages/ml/agg_utils/src/types.ts | 3 + .../get_log_rate_change.ts | 10 +- .../log_rate_analysis_results.tsx | 15 +- .../log_rate_analysis_results_table.tsx | 21 ++- .../use_columns.tsx | 168 ++++++++++-------- x-pack/plugins/aiops/public/types/storage.ts | 5 + 6 files changed, 137 insertions(+), 85 deletions(-) diff --git a/x-pack/packages/ml/agg_utils/src/types.ts b/x-pack/packages/ml/agg_utils/src/types.ts index 843172e323799..4feafd9554126 100644 --- a/x-pack/packages/ml/agg_utils/src/types.ts +++ b/x-pack/packages/ml/agg_utils/src/types.ts @@ -166,6 +166,9 @@ export interface SignificantItem extends FieldValuePair { /** Indicates if the significant item is unique within a group. */ unique?: boolean; + + /** Calculates a numerical value based on bg_count and doc_count for which to sort log rate change */ + logRateChangeSort?: number; } interface SignificantItemHistogramItemBase { diff --git a/x-pack/packages/ml/aiops_log_rate_analysis/get_log_rate_change.ts b/x-pack/packages/ml/aiops_log_rate_analysis/get_log_rate_change.ts index 39e0c4e6f7bdc..426f39749520a 100644 --- a/x-pack/packages/ml/aiops_log_rate_analysis/get_log_rate_change.ts +++ b/x-pack/packages/ml/aiops_log_rate_analysis/get_log_rate_change.ts @@ -38,7 +38,10 @@ export function getLogRateChange( } ); - return { message, factor: roundedFactor }; + return { + message, + factor: roundedFactor, + }; } else { return { message: i18n.translate( @@ -66,7 +69,10 @@ export function getLogRateChange( } ); - return { message, factor: roundedFactor }; + return { + message, + factor: roundedFactor, + }; } else { return { message: i18n.translate( 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 70b37ac2ab5d0..493afc4464974 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 @@ -37,6 +37,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_TELEMETRY_ID } 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'; @@ -53,6 +54,11 @@ 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, @@ -187,11 +193,10 @@ export const LogRateAnalysisResults: FC = ({ ); const [shouldStart, setShouldStart] = useState(false); const [toggleIdSelected, setToggleIdSelected] = useState(resultsGroupedOffId); - const [skippedColumns, setSkippedColumns] = useState([ - 'p-value', - 'Baseline rate', - 'Deviation rate', - ]); + 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); 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 f6f3ea0f4d9db..83be306e93f50 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 @@ -51,7 +51,15 @@ export const LogRateAnalysisResultsTable: FC = const euiTheme = useEuiTheme(); const primaryBackgroundColor = useEuiBackgroundColor('primary'); - const allSignificantItems = useAppSelector((s) => s.logRateAnalysisResults.significantItems); + const allItems = useAppSelector((s) => s.logRateAnalysisResults.significantItems); + + const allSignificantItems = useMemo(() => { + return allItems.map((item) => ({ + ...item, + logRateChangeSort: + item.bg_count > 0 ? item.doc_count / item.bg_count : Number.POSITIVE_INFINITY, + })); + }, [allItems]); const significantItems = useMemo(() => { if (!groupFilter) { @@ -85,7 +93,6 @@ export const LogRateAnalysisResultsTable: FC = ); const dispatch = useAppDispatch(); - const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [sortField, setSortField] = useState( @@ -135,8 +142,8 @@ export const LogRateAnalysisResultsTable: FC = ]; const sortDirections = [sortDirection]; - // Only if the table is sorted by p-value, add a secondary sort by doc count. - if (sortField === 'pValue') { + // If the table is sorted by p-value or log rate change, add a secondary sort by doc count. + if (sortField === 'pValue' || sortField === 'logRateChangeSort') { sortIteratees.push((item: SignificantItem) => item.doc_count); sortDirections.push(sortDirection); } @@ -242,12 +249,12 @@ export const LogRateAnalysisResultsTable: FC = pagination.pageSize ? pagination : undefined} - loading={false} sorting={sorting as EuiTableSortingType} + loading={false} + onChange={onChange} rowProps={(significantItem) => { return { 'data-test-subj': `aiopsLogRateAnalysisResultsTableRow row-${significantItem.fieldName}-${significantItem.fieldValue}`, 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 637aa0dc69b39..f3b8195767101 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { type EuiBasicTableColumn, EuiBadge, @@ -112,18 +112,6 @@ const groupLogRateHelpMessage = i18n.translate( 'A visual representation of the impact of the group on the message rate difference.', } ); -const groupImpactMessage = i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTableGroups.impactLabelColumnTooltip', - { - defaultMessage: 'The level of impact of the group on the message rate difference', - } -); -const impactMessage = i18n.translate( - 'xpack.aiops.logRateAnalysis.resultsTable.impactLabelColumnTooltip', - { - defaultMessage: 'The level of impact of the field on the message rate difference.', - } -); const logRateChangeMessage = i18n.translate( 'xpack.aiops.logRateAnalysis.resultsTableGroups.logRateChangeLabelColumnTooltip', { @@ -144,6 +132,69 @@ const deviationRateMessage = i18n.translate( } ); +// EuiInMemoryTable stores column `name` in its own memory as an object and computed columns may be updating dynamically which means the reference is no longer ===. +// Need to declare name react node outside to keep it static and maintain reference equality. +const LogRateColumnName = ( + <> + +   + + +); + +const ImpactColumnName = ( + <> + +   + + +); + +const GroupImpactColumnName = ( + <> + +   + + +); + export const useColumns = ( tableType: LogRateAnalysisResultsTableType, skippedColumns: string[], @@ -195,6 +246,31 @@ export const useColumns = ( return { baselineBuckets, deviationBuckets }; }, [currentAnalysisWindowParameters, interval]); + const logRateChangeNotAvailable = useMemo( + () => + interval === 0 || + currentAnalysisType === undefined || + currentAnalysisWindowParameters === undefined || + buckets === undefined || + isGroupsTable, + [interval, currentAnalysisType, currentAnalysisWindowParameters, buckets, isGroupsTable] + ); + + const getLogRateChangeValues = useCallback( + (docCount: number, bgCount: number) => { + const { baselineBucketRate, deviationBucketRate } = getBaselineAndDeviationRates( + currentAnalysisType!, + buckets!.baselineBuckets, + buckets!.deviationBuckets, + docCount, + bgCount + ); + + return getLogRateChange(currentAnalysisType!, baselineBucketRate, deviationBucketRate); + }, + [currentAnalysisType, buckets] + ); + const columnsMap: Record> = useMemo( () => ({ ['Field name']: { @@ -322,24 +398,8 @@ export const useColumns = ( 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnImpact', width: '8%', field: 'pValue', - name: ( - <> - -   - - - ), - render: (_, { pValue }) => { + name: isGroupsTable ? GroupImpactColumnName : ImpactColumnName, // content={isGroupsTable ? groupImpactMessage : impactMessage} + render: (_, { pValue }: SignificantItem) => { if (typeof pValue !== 'number') return NOT_AVAILABLE; const label = getFailedTransactionsCorrelationImpactLabel(pValue); return label ? {label.impact} : null; @@ -435,46 +495,12 @@ export const useColumns = ( }, ['Log rate change']: { 'data-test-subj': 'aiopsLogRateAnalysisResultsTableColumnLogRateChange', - name: ( - <> - -   - - - ), - render: ({ doc_count: docCount, bg_count: bgCount }: SignificantItem) => { - if ( - interval === 0 || - currentAnalysisType === undefined || - currentAnalysisWindowParameters === undefined || - buckets === undefined || - isGroupsTable - ) - return NOT_AVAILABLE; - - const { baselineBucketRate, deviationBucketRate } = getBaselineAndDeviationRates( - currentAnalysisType, - buckets.baselineBuckets, - buckets.deviationBuckets, - docCount, - bgCount - ); - - const logRateChange = getLogRateChange( - currentAnalysisType, - baselineBucketRate, - deviationBucketRate - ); + field: 'logRateChangeSort', + name: LogRateColumnName, + sortable: isGroupsTable ? false : true, + render: (_, { doc_count: docCount, bg_count: bgCount }: SignificantItem) => { + if (logRateChangeNotAvailable) return NOT_AVAILABLE; + const logRateChange = getLogRateChangeValues(docCount, bgCount); return ( <> diff --git a/x-pack/plugins/aiops/public/types/storage.ts b/x-pack/plugins/aiops/public/types/storage.ts index ea6fde6b06552..a4a29dda2f2c3 100644 --- a/x-pack/plugins/aiops/public/types/storage.ts +++ b/x-pack/plugins/aiops/public/types/storage.ts @@ -19,12 +19,14 @@ 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; @@ -37,6 +39,8 @@ 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 = [ @@ -44,4 +48,5 @@ 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;