From ec0a87c5ccd3f6785538bdcba69b927a1e22555d Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 17 Apr 2023 13:50:04 -0600 Subject: [PATCH] [ML] Explain Log Rate Spikes: add popover to analysis table for viewing other field values (#154689) ## Summary Related meta issue: https://github.com/elastic/kibana/issues/146065 Adds unified field list popover to show top values for fields in the Explain Log Rate Spikes analysis table. image Field pairs table: image image Expanded row in grouped table: image ### 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 - [ ] 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: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/aiops/kibana.jsonc | 3 +- .../explain_log_rate_spikes_analysis.tsx | 15 +- .../field_stats_content.tsx | 191 ++++++++++++++++++ .../field_stats_popover.tsx | 89 ++++++++ .../components/field_stats_popover/index.ts | 8 + .../spike_analysis_table.tsx | 57 +++++- .../spike_analysis_table_groups.tsx | 16 +- x-pack/plugins/aiops/tsconfig.json | 1 + 8 files changed, 361 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_content.tsx create mode 100644 x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_popover.tsx create mode 100644 x-pack/plugins/aiops/public/components/field_stats_popover/index.ts diff --git a/x-pack/plugins/aiops/kibana.jsonc b/x-pack/plugins/aiops/kibana.jsonc index a343af521ed80..019c77cc155fa 100644 --- a/x-pack/plugins/aiops/kibana.jsonc +++ b/x-pack/plugins/aiops/kibana.jsonc @@ -11,7 +11,8 @@ "charts", "data", "lens", - "licensing" + "licensing", + "unifiedFieldList", ], "requiredBundles": [ "fieldFormats", diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index f1bcdb18fbaec..7ddef239cf2f4 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useEffect, useMemo, useState, type FC } from 'react'; +import React, { useEffect, useMemo, useState, FC } from 'react'; import { isEqual, uniq } from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiButton, @@ -25,7 +26,6 @@ import { useFetchStream } from '@kbn/aiops-utils'; import type { WindowParameters } from '@kbn/aiops-utils'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { Query } from '@kbn/es-query'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { initialState, streamReducer } from '../../../common/api/stream_reducer'; @@ -67,7 +67,7 @@ interface ExplainLogRateSpikesAnalysisProps { /** Window parameters for the analysis */ windowParameters: WindowParameters; /** The search query to be applied to the analysis as a filter */ - searchQuery: Query['query']; + searchQuery: estypes.QueryDslQueryContainer; /** Sample probability to be applied to random sampler aggregations */ sampleProbability: number; } @@ -215,6 +215,7 @@ export const ExplainLogRateSpikesAnalysis: FC return p + c.groupItemsSortedByUniqueness.length; }, 0); const foundGroups = groupTableItems.length > 0 && groupItemCount > 0; + const timeRangeMs = { from: earliest, to: latest }; // Disable the grouping switch toggle only if no groups were found, // the toggle wasn't enabled already and no fields were selected to be skipped. @@ -326,14 +327,18 @@ export const ExplainLogRateSpikesAnalysis: FC significantTerms={data.significantTerms} groupTableItems={groupTableItems} loading={isRunning} - dataViewId={dataView.id} + dataView={dataView} + timeRangeMs={timeRangeMs} + searchQuery={searchQuery} /> ) : null} {showSpikeAnalysisTable && !groupResults ? ( ) : null} diff --git a/x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_content.tsx b/x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_content.tsx new file mode 100644 index 0000000000000..e3ea0d5bb01c0 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_content.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useMemo, useState, useCallback } from 'react'; +import { + FieldStats, + FieldStatsProps, + FieldStatsServices, + FieldStatsState, + FieldTopValuesBucketParams, + FieldTopValuesBucket, +} from '@kbn/unified-field-list-plugin/public'; +import { isDefined } from '@kbn/ml-is-defined'; +import type { DataView, DataViewField } from '@kbn/data-plugin/common'; +import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; +import moment from 'moment'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiHorizontalRule, euiPaletteColorBlind, EuiSpacer, EuiText } from '@elastic/eui'; + +const DEFAULT_COLOR = euiPaletteColorBlind()[0]; +const HIGHLIGHTED_FIELD_PROPS = { + color: 'accent', + textProps: { + color: 'accent', + }, +}; + +function getPercentValue( + currentValue: number, + totalCount: number, + digitsRequired: boolean +): number { + const percentageString = + totalCount > 0 + ? `${(Math.round((currentValue / totalCount) * 1000) / 10).toFixed(digitsRequired ? 1 : 0)}` + : ''; + return Number(percentageString); +} + +interface FieldStatsContentProps { + dataView: DataView; + field: DataViewField; + fieldName: string; + fieldValue: string | number; + fieldStatsServices: FieldStatsServices; + timeRangeMs?: TimeRangeMs; + dslQuery?: FieldStatsProps['dslQuery']; +} + +export const FieldStatsContent: FC = ({ + dataView: currentDataView, + field, + fieldName, + fieldValue, + fieldStatsServices, + timeRangeMs, + dslQuery, +}) => { + const [fieldStatsState, setFieldStatsState] = useState(); + + // Format timestamp to ISO formatted date strings + const timeRange = useMemo(() => { + // Use the provided timeRange if available + if (timeRangeMs) { + return { + from: moment(timeRangeMs.from).toISOString(), + to: moment(timeRangeMs.to).toISOString(), + }; + } + + const now = moment(); + return { from: now.toISOString(), to: now.toISOString() }; + }, [timeRangeMs]); + + const onStateChange = useCallback((nextState: FieldStatsState) => { + setFieldStatsState(nextState); + }, []); + + const individualStatForDisplay = useMemo((): { + needToDisplayIndividualStat: boolean; + percentage: string; + } => { + const defaultIndividualStatForDisplay = { + needToDisplayIndividualStat: false, + percentage: '< 1%', + }; + if (fieldStatsState === undefined) return defaultIndividualStatForDisplay; + + const { topValues: currentTopValues, sampledValues } = fieldStatsState; + + const idxToHighlight = + currentTopValues?.buckets && Array.isArray(currentTopValues.buckets) + ? currentTopValues.buckets.findIndex((value) => value.key === fieldValue) + : null; + + const needToDisplayIndividualStat = + idxToHighlight === -1 && fieldName !== undefined && fieldValue !== undefined; + + if (needToDisplayIndividualStat) { + defaultIndividualStatForDisplay.needToDisplayIndividualStat = true; + + const buckets = + currentTopValues?.buckets && Array.isArray(currentTopValues.buckets) + ? currentTopValues.buckets + : []; + let lowestPercentage: number | undefined; + + // Taken from the unifiedFieldList plugin + const digitsRequired = buckets.some( + (bucket) => !Number.isInteger(bucket.count / (sampledValues ?? 5000)) + ); + + buckets.forEach((bucket) => { + const currentPercentage = getPercentValue( + bucket.count, + sampledValues ?? 5000, + digitsRequired + ); + + if (lowestPercentage === undefined || currentPercentage < lowestPercentage) { + lowestPercentage = currentPercentage; + } + }); + + defaultIndividualStatForDisplay.percentage = `< ${lowestPercentage ?? 1}%`; + } + + return defaultIndividualStatForDisplay; + }, [fieldStatsState, fieldName, fieldValue]); + + const overrideFieldTopValueBar = useCallback( + (fieldTopValuesBucketParams: FieldTopValuesBucketParams) => { + if (fieldTopValuesBucketParams.type === 'other') { + return { color: 'primary' }; + } + return fieldValue === fieldTopValuesBucketParams.fieldValue ? HIGHLIGHTED_FIELD_PROPS : {}; + }, + [fieldValue] + ); + + const showFieldStats = timeRange && isDefined(currentDataView) && field; + + if (!showFieldStats) return null; + const formatter = currentDataView.getFormatterForField(field); + + return ( + <> + + {individualStatForDisplay.needToDisplayIndividualStat ? ( + <> + + + + + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_popover.tsx b/x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_popover.tsx new file mode 100644 index 0000000000000..ba0d1c29e8965 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_popover.tsx @@ -0,0 +1,89 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isDefined } from '@kbn/ml-is-defined'; +import { + FieldPopover, + FieldPopoverHeader, + FieldStatsServices, + FieldStatsProps, +} from '@kbn/unified-field-list-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/common'; +import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; +import { useEuiTheme } from '../../hooks/use_eui_theme'; +import { FieldStatsContent } from './field_stats_content'; + +export function FieldStatsPopover({ + dataView, + dslQuery, + fieldName, + fieldValue, + fieldStatsServices, + timeRangeMs, +}: { + dataView: DataView; + dslQuery?: FieldStatsProps['dslQuery']; + fieldName: string; + fieldValue: string | number; + fieldStatsServices: FieldStatsServices; + timeRangeMs?: TimeRangeMs; +}) { + const [infoIsOpen, setInfoOpen] = useState(false); + const euiTheme = useEuiTheme(); + + const closePopover = useCallback(() => setInfoOpen(false), []); + + const fieldForStats = useMemo( + () => (isDefined(fieldName) ? dataView.getFieldByName(fieldName) : undefined), + [fieldName, dataView] + ); + + const trigger = ( + + ) => { + setInfoOpen(!infoIsOpen); + }} + aria-label={i18n.translate('xpack.aiops.fieldContextPopover.topFieldValuesAriaLabel', { + defaultMessage: 'Show top field values', + })} + data-test-subj={'aiopsContextPopoverButton'} + style={{ marginLeft: euiTheme.euiSizeXS }} + /> + + ); + + if (!fieldForStats) return null; + + return ( + } + renderContent={() => ( + + )} + /> + ); +} diff --git a/x-pack/plugins/aiops/public/components/field_stats_popover/index.ts b/x-pack/plugins/aiops/public/components/field_stats_popover/index.ts new file mode 100644 index 0000000000000..c982a7dd397ea --- /dev/null +++ b/x-pack/plugins/aiops/public/components/field_stats_popover/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 { FieldStatsPopover } from './field_stats_popover'; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx index afbe7b693bfda..1a5b7460b62fe 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx @@ -7,6 +7,7 @@ import React, { FC, useCallback, useMemo, useState } from 'react'; import { sortBy } from 'lodash'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { useEuiBackgroundColor, @@ -19,16 +20,22 @@ import { EuiToolTip, } from '@elastic/eui'; +import { FieldStatsServices } from '@kbn/unified-field-list-plugin/public'; + +import type { DataView } from '@kbn/data-views-plugin/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { SignificantTerm } from '@kbn/ml-agg-utils'; +import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; import { useEuiTheme } from '../../hooks/use_eui_theme'; import { MiniHistogram } from '../mini_histogram'; +import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { getFailedTransactionsCorrelationImpactLabel } from './get_failed_transactions_correlation_impact_label'; import { useSpikeAnalysisTableRowContext } from './spike_analysis_table_row_provider'; +import { FieldStatsPopover } from '../field_stats_popover'; import { useCopyToClipboardAction } from './use_copy_to_clipboard_action'; import { useViewInDiscoverAction } from './use_view_in_discover_action'; @@ -43,19 +50,24 @@ const DEFAULT_SORT_DIRECTION = 'asc'; interface SpikeAnalysisTableProps { significantTerms: SignificantTerm[]; - dataViewId?: string; + dataView: DataView; loading: boolean; isExpandedRow?: boolean; + searchQuery: estypes.QueryDslQueryContainer; + timeRangeMs: TimeRangeMs; } export const SpikeAnalysisTable: FC = ({ significantTerms, - dataViewId, + dataView, loading, isExpandedRow, + searchQuery, + timeRangeMs, }) => { const euiTheme = useEuiTheme(); const primaryBackgroundColor = useEuiBackgroundColor('primary'); + const dataViewId = dataView.id; const { pinnedSignificantTerm, @@ -69,6 +81,18 @@ export const SpikeAnalysisTable: FC = ({ const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); + const { data, uiSettings, fieldFormats, charts } = useAiopsAppContext(); + + const fieldStatsServices: FieldStatsServices = useMemo(() => { + return { + uiSettings, + dataViews: data.dataViews, + data, + fieldFormats, + charts, + }; + }, [uiSettings, data, fieldFormats, charts]); + const copyToClipBoardAction = useCopyToClipboardAction(); const viewInDiscoverAction = useViewInDiscoverAction(dataViewId); @@ -79,8 +103,21 @@ export const SpikeAnalysisTable: FC = ({ name: i18n.translate('xpack.aiops.explainLogRateSpikes.spikeAnalysisTable.fieldNameLabel', { defaultMessage: 'Field name', }), + render: (_, { fieldName, fieldValue }) => ( + <> + + {fieldName} + + ), sortable: true, - valign: 'top', + valign: 'middle', }, { 'data-test-subj': 'aiopsSpikeAnalysisTableColumnFieldValue', @@ -91,7 +128,7 @@ export const SpikeAnalysisTable: FC = ({ render: (_, { fieldValue }) => String(fieldValue), sortable: true, textOnly: true, - valign: 'top', + valign: 'middle', }, { 'data-test-subj': 'aiopsSpikeAnalysisTableColumnLogRate', @@ -125,7 +162,7 @@ export const SpikeAnalysisTable: FC = ({ /> ), sortable: false, - valign: 'top', + valign: 'middle', }, { 'data-test-subj': 'aiopsSpikeAnalysisTableColumnDocCount', @@ -135,7 +172,7 @@ export const SpikeAnalysisTable: FC = ({ defaultMessage: 'Doc count', }), sortable: true, - valign: 'top', + valign: 'middle', }, { 'data-test-subj': 'aiopsSpikeAnalysisTableColumnPValue', @@ -163,7 +200,7 @@ export const SpikeAnalysisTable: FC = ({ ), render: (pValue: number | null) => pValue?.toPrecision(3) ?? NOT_AVAILABLE, sortable: true, - valign: 'top', + valign: 'middle', }, { 'data-test-subj': 'aiopsSpikeAnalysisTableColumnImpact', @@ -194,7 +231,7 @@ export const SpikeAnalysisTable: FC = ({ return label ? {label.impact} : null; }, sortable: true, - valign: 'top', + valign: 'middle', }, { 'data-test-subj': 'aiOpsSpikeAnalysisTableColumnAction', @@ -203,7 +240,7 @@ export const SpikeAnalysisTable: FC = ({ }), actions: [viewInDiscoverAction, copyToClipBoardAction], width: ACTIONS_COLUMN_WIDTH, - valign: 'top', + valign: 'middle', }, ]; @@ -231,7 +268,7 @@ export const SpikeAnalysisTable: FC = ({ return ''; }, sortable: false, - valign: 'top', + valign: 'middle', }); } diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index 22c6f717c3a0a..b319db0088d4d 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -25,9 +25,12 @@ import { euiPaletteColorBlind, } from '@elastic/eui'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { SignificantTerm } from '@kbn/ml-agg-utils'; +import type { TimeRange as TimeRangeMs } from '@kbn/ml-date-picker'; +import type { DataView } from '@kbn/data-views-plugin/public'; import { MiniHistogram } from '../mini_histogram'; @@ -51,15 +54,19 @@ const DEFAULT_SORT_DIRECTION = 'asc'; interface SpikeAnalysisTableProps { significantTerms: SignificantTerm[]; groupTableItems: GroupTableItem[]; - dataViewId?: string; loading: boolean; + searchQuery: estypes.QueryDslQueryContainer; + timeRangeMs: TimeRangeMs; + dataView: DataView; } export const SpikeAnalysisGroupsTable: FC = ({ significantTerms, groupTableItems, - dataViewId, loading, + dataView, + timeRangeMs, + searchQuery, }) => { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); @@ -75,6 +82,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ const { pinnedGroup, selectedGroup, setPinnedGroup, setSelectedGroup } = useSpikeAnalysisTableRowContext(); + const dataViewId = dataView.id; const toggleDetails = (item: GroupTableItem) => { const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; @@ -101,8 +109,10 @@ export const SpikeAnalysisGroupsTable: FC = ({ [] )} loading={loading} - dataViewId={dataViewId} isExpandedRow + dataView={dataView} + timeRangeMs={timeRangeMs} + searchQuery={searchQuery} /> ); } diff --git a/x-pack/plugins/aiops/tsconfig.json b/x-pack/plugins/aiops/tsconfig.json index 9398c0c189e04..b9a6ff5408eda 100644 --- a/x-pack/plugins/aiops/tsconfig.json +++ b/x-pack/plugins/aiops/tsconfig.json @@ -49,6 +49,7 @@ "@kbn/ml-query-utils", "@kbn/ml-is-defined", "@kbn/ml-route-utils", + "@kbn/unified-field-list-plugin", "@kbn/ml-random-sampler-utils", ], "exclude": [