From 40c3563e0330965fe35253f04777dd31871eff79 Mon Sep 17 00:00:00 2001 From: mgiota Date: Wed, 18 Aug 2021 13:41:39 +0200 Subject: [PATCH] [Observability RAC] add filter for value action (#108648) * filter for value * code clean up * fix i18n tests * fix type errors * revert changes to reason field to make reason field clickable again * [RAC Observability] fix reason field * fix type issues * filter my kibana.alert. status on load (will refactor) * refactor filter for alert status on load * remove rest params * fix eslint errors * hard code alert status for now, will be fixed in another PR * move filter_for button in a separate file * fix errors * comply with kibana i18n guideines * simpler implementation for default filtering * fix syntax error * fix type errors * fix eslint errors * fix eslint errors Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/alerts/alerts_table_t_grid.tsx | 15 ++-- .../pages/alerts/default_cell_actions.tsx | 69 ++++++------------- .../public/pages/alerts/filter_for_value.tsx | 64 +++++++++++++++++ .../public/pages/alerts/index.tsx | 22 +++++- .../public/pages/alerts/render_cell_value.tsx | 8 +-- 5 files changed, 119 insertions(+), 59 deletions(-) create mode 100644 x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index d8ba7a96ad2c6..cc32b3a6dfa30 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -14,13 +14,13 @@ import { AlertConsumers as AlertConsumersTyped, ALERT_DURATION as ALERT_DURATION_TYPED, ALERT_STATUS as ALERT_STATUS_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, + ALERT_REASON as ALERT_REASON_TYPED, ALERT_RULE_CONSUMER, } from '@kbn/rule-data-utils'; import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, ALERT_STATUS as ALERT_STATUS_NON_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + ALERT_REASON as ALERT_REASON_NON_TYPED, TIMESTAMP, // @ts-expect-error importing from a place other than root because we want to limit what we import from this package } from '@kbn/rule-data-utils/target_node/technical_field_names'; @@ -39,7 +39,6 @@ import { import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import React, { Suspense, useMemo, useState, useCallback } from 'react'; - import { get } from 'lodash'; import { useGetUserAlertsPermissions } from '../../hooks/use_alert_permission'; import type { TimelinesUIStart, TGridType, SortDirection } from '../../../../timelines/public'; @@ -65,7 +64,7 @@ import { CoreStart } from '../../../../../../src/core/public'; const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; -const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; +const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; interface AlertsTableTGridProps { indexName: string; @@ -74,6 +73,7 @@ interface AlertsTableTGridProps { kuery: string; status: string; setRefetch: (ref: () => void) => void; + addToQuery: (value: string) => void; } interface ObservabilityActionsProps extends ActionProps { @@ -136,8 +136,8 @@ export const columns: Array< displayAsText: i18n.translate('xpack.observability.alertsTGrid.reasonColumnDescription', { defaultMessage: 'Reason', }), + id: ALERT_REASON, linkField: '*', - id: ALERT_RULE_NAME, }, ]; @@ -288,7 +288,7 @@ function ObservabilityActions({ } export function AlertsTableTGrid(props: AlertsTableTGridProps) { - const { indexName, rangeFrom, rangeTo, kuery, status, setRefetch } = props; + const { indexName, rangeFrom, rangeTo, kuery, status, setRefetch, addToQuery } = props; const { timelines } = useKibana<{ timelines: TimelinesUIStart }>().services; const [flyoutAlert, setFlyoutAlert] = useState(undefined); @@ -332,7 +332,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { type, columns, deletedEventIds: [], - defaultCellActions: getDefaultCellActions({ enableFilterActions: false }), + defaultCellActions: getDefaultCellActions({ addToQuery }), end: rangeTo, filters: [], indexNames: [indexName], @@ -377,6 +377,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { rangeTo, setRefetch, status, + addToQuery, ]); const handleFlyoutClose = () => setFlyoutAlert(undefined); const { observabilityRuleTypeRegistry } = usePluginContext(); diff --git a/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx index 3056b026fc27a..7e166ac99c05f 100644 --- a/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/default_cell_actions.tsx @@ -6,16 +6,18 @@ */ import React from 'react'; - +import { i18n } from '@kbn/i18n'; import { ObservabilityPublicPluginsStart } from '../..'; import { getMappedNonEcsValue } from './render_cell_value'; +import FilterForValueButton from './filter_for_value'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { TimelineNonEcsData } from '../../../../timelines/common/search_strategy'; import { TGridCellAction } from '../../../../timelines/common/types/timeline'; import { TimelinesUIStart } from '../../../../timelines/public'; -/** a noop required by the filter in / out buttons */ -const onFilterAdded = () => {}; +export const FILTER_FOR_VALUE = i18n.translate('xpack.observability.hoverActions.filterForValue', { + defaultMessage: 'Filter for value', +}); /** a hook to eliminate the verbose boilerplate required to use common services */ const useKibanaServices = () => { @@ -31,32 +33,10 @@ const useKibanaServices = () => { return { timelines, filterManager }; }; -/** actions for adding filters to the search bar */ -const filterCellActions: TGridCellAction[] = [ - ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { - const { timelines, filterManager } = useKibanaServices(); - - const value = getMappedNonEcsValue({ - data: data[rowIndex], - fieldName: columnId, - }); - - return ( - <> - {timelines.getHoverActions().getFilterForValueButton({ - Component, - field: columnId, - filterManager, - onFilterAdded, - ownFocus: false, - showTooltip: false, - value, - })} - - ); - }, +/** actions common to all cells (e.g. copy to clipboard) */ +const commonCellActions: TGridCellAction[] = [ ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { - const { timelines, filterManager } = useKibanaServices(); + const { timelines } = useKibanaServices(); const value = getMappedNonEcsValue({ data: data[rowIndex], @@ -65,11 +45,10 @@ const filterCellActions: TGridCellAction[] = [ return ( <> - {timelines.getHoverActions().getFilterOutValueButton({ + {timelines.getHoverActions().getCopyButton({ Component, field: columnId, - filterManager, - onFilterAdded, + isHoverAction: false, ownFocus: false, showTooltip: false, value, @@ -79,31 +58,27 @@ const filterCellActions: TGridCellAction[] = [ }, ]; -/** actions common to all cells (e.g. copy to clipboard) */ -const commonCellActions: TGridCellAction[] = [ +/** actions for adding filters to the search bar */ +const buildFilterCellActions = (addToQuery: (value: string) => void): TGridCellAction[] => [ ({ data }: { data: TimelineNonEcsData[][] }) => ({ rowIndex, columnId, Component }) => { - const { timelines } = useKibanaServices(); - const value = getMappedNonEcsValue({ data: data[rowIndex], fieldName: columnId, }); return ( - <> - {timelines.getHoverActions().getCopyButton({ - Component, - field: columnId, - isHoverAction: false, - ownFocus: false, - showTooltip: false, - value, - })} - + ); }, ]; /** returns the default actions shown in `EuiDataGrid` cells */ -export const getDefaultCellActions = ({ enableFilterActions }: { enableFilterActions: boolean }) => - enableFilterActions ? [...filterCellActions, ...commonCellActions] : [...commonCellActions]; +export const getDefaultCellActions = ({ addToQuery }: { addToQuery: (value: string) => void }) => [ + ...buildFilterCellActions(addToQuery), + ...commonCellActions, +]; diff --git a/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx b/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx new file mode 100644 index 0000000000000..77cac9d482a37 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; + +export const filterForValueButtonLabel = i18n.translate( + 'xpack.observability.hoverActions.filterForValueButtonLabel', + { + defaultMessage: 'Filter for value', + } +); + +import { EuiButtonIcon, EuiButtonEmpty } from '@elastic/eui'; + +interface FilterForValueProps { + Component?: typeof EuiButtonEmpty | typeof EuiButtonIcon; + field: string; + value: string[] | string | null | undefined; + addToQuery: (value: string) => void; +} + +const FilterForValueButton: React.FC = React.memo( + ({ Component, field, value, addToQuery }) => { + const text = useMemo(() => `${field}${value != null ? `: "${value}"` : ''}`, [field, value]); + const onClick = useCallback(() => { + addToQuery(text); + }, [text, addToQuery]); + const button = useMemo( + () => + Component ? ( + + {filterForValueButtonLabel} + + ) : ( + + ), + [Component, onClick] + ); + return button; + } +); + +FilterForValueButton.displayName = 'FilterForValueButton'; + +// eslint-disable-next-line import/no-default-export +export { FilterForValueButton as default }; diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index baed76d49aac8..b3ff3f94dc4db 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -40,7 +40,12 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { const history = useHistory(); const refetch = useRef<() => void>(); const { - query: { rangeFrom = 'now-15m', rangeTo = 'now', kuery = '', status = 'open' }, + query: { + rangeFrom = 'now-15m', + rangeTo = 'now', + kuery = 'kibana.alert.status: "open"', // TODO change hardcoded values as part of another PR + status = 'open', + }, } = routeParams; useBreadcrumbs([ @@ -98,6 +103,20 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { [history, rangeFrom, rangeTo, kuery] ); + const addToQuery = useCallback( + (value: string) => { + let output = value; + if (kuery !== '') { + output = `${kuery} and ${value}`; + } + onQueryChange({ + dateRange: { from: rangeFrom, to: rangeTo }, + query: output, + }); + }, + [kuery, onQueryChange, rangeFrom, rangeTo] + ); + const setRefetch = useCallback((ref) => { refetch.current = ref; }, []); @@ -170,6 +189,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { kuery={kuery} status={status} setRefetch={setRefetch} + addToQuery={addToQuery} /> diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index 03b79a77baadc..87abe8da8da2e 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -16,13 +16,13 @@ import type { ALERT_DURATION as ALERT_DURATION_TYPED, ALERT_SEVERITY as ALERT_SEVERITY_TYPED, ALERT_STATUS as ALERT_STATUS_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, + ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, ALERT_STATUS as ALERT_STATUS_NON_TYPED, - ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + ALERT_REASON as ALERT_REASON_NON_TYPED, TIMESTAMP, // @ts-expect-error importing from a place other than root because we want to limit what we import from this package } from '@kbn/rule-data-utils/target_node/technical_field_names'; @@ -38,7 +38,7 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; -const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; +const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; export const getMappedNonEcsValue = ({ data, @@ -110,7 +110,7 @@ export const getRenderCellValue = ({ return asDuration(Number(value)); case ALERT_SEVERITY: return ; - case ALERT_RULE_NAME: + case ALERT_REASON: const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const alert = parseAlert(observabilityRuleTypeRegistry)(dataFieldEs);