-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ML] Explain Log Rate Spikes: add popover to analysis table for viewi…
…ng other field values (#154689) ## Summary Related meta issue: #146065 Adds unified field list popover to show top values for fields in the Explain Log Rate Spikes analysis table. <img width="1350" alt="image" src="https://user-images.githubusercontent.com/6446462/231584775-cade3e53-560f-4622-811a-c56747c2e469.png"> Field pairs table: <img width="1175" alt="image" src="https://user-images.githubusercontent.com/6446462/231584835-1b82a0bb-eff0-477b-a060-c8b74a067500.png"> <img width="976" alt="image" src="https://user-images.githubusercontent.com/6446462/231578276-9d99ed13-852b-4744-a9c3-5dd11a35c46d.png"> Expanded row in grouped table: <img width="1166" alt="image" src="https://user-images.githubusercontent.com/6446462/231584916-51271006-94fa-4a25-ac14-e7a99f9728f9.png"> ### 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 <[email protected]>
- Loading branch information
1 parent
5a81817
commit ec0a87c
Showing
8 changed files
with
361 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
191 changes: 191 additions & 0 deletions
191
x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_content.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<FieldStatsContentProps> = ({ | ||
dataView: currentDataView, | ||
field, | ||
fieldName, | ||
fieldValue, | ||
fieldStatsServices, | ||
timeRangeMs, | ||
dslQuery, | ||
}) => { | ||
const [fieldStatsState, setFieldStatsState] = useState<FieldStatsState | undefined>(); | ||
|
||
// 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 ( | ||
<> | ||
<FieldStats | ||
key={field.name} | ||
services={fieldStatsServices} | ||
dslQuery={dslQuery ?? { match_all: {} }} | ||
fromDate={timeRange.from} | ||
toDate={timeRange.to} | ||
dataViewOrDataViewId={currentDataView} | ||
field={field} | ||
data-test-subj={`mlAiOpsFieldStatsPopoverContent ${field.name}`} | ||
color={DEFAULT_COLOR} | ||
onStateChange={onStateChange} | ||
overrideFieldTopValueBar={overrideFieldTopValueBar} | ||
/> | ||
{individualStatForDisplay.needToDisplayIndividualStat ? ( | ||
<> | ||
<EuiHorizontalRule margin="s" /> | ||
<EuiText size="xs" color="subdued"> | ||
<FormattedMessage | ||
id="xpack.aiops.fieldContextPopover.notTopTenValueMessage" | ||
defaultMessage="Selected term is not in the top 10" | ||
/> | ||
</EuiText> | ||
<EuiSpacer size="s" /> | ||
<FieldTopValuesBucket | ||
field={field} | ||
fieldValue={fieldValue} | ||
formattedPercentage={individualStatForDisplay.percentage} | ||
formattedFieldValue={formatter.convert(fieldValue)} | ||
// Always set as completed since calc is done once we're here | ||
progressValue={100} | ||
count={0} | ||
overrideFieldTopValueBar={overrideFieldTopValueBar} | ||
{...{ 'data-test-subj': 'aiopsNotInTopTenFieldTopValueBucket' }} | ||
{...HIGHLIGHTED_FIELD_PROPS} | ||
/> | ||
</> | ||
) : null} | ||
</> | ||
); | ||
}; |
89 changes: 89 additions & 0 deletions
89
x-pack/plugins/aiops/public/components/field_stats_popover/field_stats_popover.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = ( | ||
<EuiToolTip | ||
content={i18n.translate('xpack.aiops.fieldContextPopover.descriptionTooltipContent', { | ||
defaultMessage: 'Show top field values', | ||
})} | ||
> | ||
<EuiButtonIcon | ||
iconType="inspect" | ||
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => { | ||
setInfoOpen(!infoIsOpen); | ||
}} | ||
aria-label={i18n.translate('xpack.aiops.fieldContextPopover.topFieldValuesAriaLabel', { | ||
defaultMessage: 'Show top field values', | ||
})} | ||
data-test-subj={'aiopsContextPopoverButton'} | ||
style={{ marginLeft: euiTheme.euiSizeXS }} | ||
/> | ||
</EuiToolTip> | ||
); | ||
|
||
if (!fieldForStats) return null; | ||
|
||
return ( | ||
<FieldPopover | ||
isOpen={infoIsOpen} | ||
closePopover={closePopover} | ||
button={trigger} | ||
renderHeader={() => <FieldPopoverHeader field={fieldForStats} closePopover={closePopover} />} | ||
renderContent={() => ( | ||
<FieldStatsContent | ||
field={fieldForStats} | ||
fieldName={fieldName} | ||
fieldValue={fieldValue} | ||
dataView={dataView} | ||
fieldStatsServices={fieldStatsServices} | ||
timeRangeMs={timeRangeMs} | ||
dslQuery={dslQuery} | ||
/> | ||
)} | ||
/> | ||
); | ||
} |
8 changes: 8 additions & 0 deletions
8
x-pack/plugins/aiops/public/components/field_stats_popover/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'; |
Oops, something went wrong.