[Security Solution] Restores the @timestamp column actions and fixes an @timestamp alignment issue (#118990) (#119184)

## [Security Solution] Restores the `@timestamp` column actions and fixes an `@timestamp` alignment issue

This PR fixes the `@timestamp` column issues described in https://github.com/elastic/kibana/issues/118989

The PR:

- Fixes an issue where the `Filter in`, `Filter out`, and `Investigate in timeline` actions were disabled in the `Security > Alerts` table, per the before / after screenshots below:

**Before**

![alerts-actions-before](https://user-images.githubusercontent.com/4459398/142335094-141fe435-eb9a-4920-a6d3-2c7673f31664.png)

_Above: The `Filter in`, `Filter out`, and `Investigate in timeline` actions were disabled for `@timestamp` in Security > Alerts_

**After**

![alerts-actions-after](https://user-images.githubusercontent.com/4459398/142302794-c2c3684b-92cc-483c-a02b-33f2624ddc79.png)

_Above: The `Filter in`, `Filter out`, and `Investigate in timeline` actions are enabled for `@timestamp` in Security > Alerts_

- Fixes a CSS issue where text truncation styles were causing the `@timestamp` column to be mis-aligned in Timeline, per the before / after screenshots below:

**Before**

![timestamp-alignment-before](https://user-images.githubusercontent.com/4459398/142334676-3a95bb9b-3be9-47c6-8828-efe5f8cbfe2c.png)

_Above: The `@timestamp` column in Timeline was vertically mis-aligned_

**After**

![timestamp-alignment-after](https://user-images.githubusercontent.com/4459398/142334495-c4997aa0-225e-4373-bee2-44b8ec05312b.png)

_Above: The `@timestamp` column in Timeline is correctly (vertically) aligned_

### No changes to the o11y alert actions

There are no changes to the actions shown in the `o11y` alerts table, per the before / after screenshots below:

![o11y-alerts-before](https://user-images.githubusercontent.com/4459398/142335532-5678c55c-b8a1-4122-accb-47e4f71e761f.png)

_Above: Before - the `@timestamp` field in the `o11y` alerts table does NOT have actions_

![o11y-alerts-after-no-change](https://user-images.githubusercontent.com/4459398/142321916-fd7ffbd6-c2db-4820-b055-b9f3fe546cc9.png)

_Above: After - the `@timestamp` field in the `o11y` alerts table (still) does NOT have actions_

### Field browser search input auto-focus

- Fixed an issue where the `Fields` browser search input was not auto-focused, per the screenshot above:

![fields-browser-auto-focused](https://user-images.githubusercontent.com/4459398/142300763-21d0fc50-e1c1-477b-be15-4f367e800af8.png)

_Above: The search input is auto-focused when the `Fields` browser is opened_

### Details

The fix that re-enables the `Filter in`, `Filter out`, and `Investigate in timeline` actions in the `Security > Alerts` required removing a recently-introduced `TODO` in `x-pack/plugins/timelines/public/components/t_grid/body/index.tsx`, which defined a common set of disabled actions for both the `o11y` and `Security` solutions.

The `TODO` was replaced by a `disabledCellActions` prop, which enables each solution to configure the disabled cell actions independently.

### Desk testing

While desk testing the `@timestamp` alignment issue:

> - Fixes a CSS issue where text truncation styles were causing the `@timestamp` column to be mis-aligned in Timeline

You may find it informative to locally edit `x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.tsx` to add the following styles:

```css
background-color: red;
```

and

```css
background-color: green;
```

to `ProviderContentWrapper`, as shown in the code below:

```typescript
export const ProviderContentWrapper = styled.span`
  > span.euiToolTipAnchor {
    background-color: red;
    display: block; /* allow EuiTooltip content to be truncatable */
  }

  > span.euiToolTipAnchor.eui-textTruncate {
    background-color: green;
    display: inline-block; /* do not override display when a tooltip is truncated via eui-textTruncate */
  }
`;
```

as illustrated by the `diff` below:

![background-colors](https://user-images.githubusercontent.com/4459398/142300526-c61f1de7-f695-4b96-99d9-428763405d99.png)

_Above: `background-color: green` and `background-color: red` styles added locally for desk testing_

With the (temporary) style changes above, the effect (and scope) of the new style is easily seen, as shown in the screenshot below:

![compare-styles](https://user-images.githubusercontent.com/4459398/142300502-dbc44572-6066-4cb1-a045-26a15086d02d.png)

_Above: The effect of the (green) style changes compared with the (red) unchanged styles_

When the new style introduced in this PR is commented-out for desk testing, as shown in the code below:

```typescript
export const ProviderContentWrapper = styled.span`
  > span.euiToolTipAnchor {
    background-color: red;
    display: block; /* allow EuiTooltip content to be truncatable */
  }

  /*
  > span.euiToolTipAnchor.eui-textTruncate {
    background-color: green;
    display: inline-block; /* do not override display when a tooltip is truncated via eui-textTruncate */
  }
  */
`;
```

the behavior of `@timestamp` reverts to the behavior prior to this PR, as shown in the screenshot below:

![reverted-fix](https://user-images.githubusercontent.com/4459398/142300380-095c4b58-0417-46c4-98b4-10387c4220b8.png)

_Above: The `@timestamp` column defaults to the old (red) unchanged behavior when the new style is commented-out for desk testing_

Co-authored-by: Andrew Goldstein <andrew-goldstein@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-11-19 12:11:18 -05:00 committed by GitHub
parent c517030fee
commit b627dca3ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 87 additions and 16 deletions

View file

@ -313,6 +313,14 @@ function ObservabilityActions({
);
}
const FIELDS_WITHOUT_CELL_ACTIONS = [
'@timestamp',
'signal.rule.risk_score',
'signal.reason',
'kibana.alert.duration.us',
'kibana.alert.reason',
];
export function AlertsTableTGrid(props: AlertsTableTGridProps) {
const { indexNames, rangeFrom, rangeTo, kuery, workflowStatus, setRefetch, addToQuery } = props;
const prevWorkflowStatus = usePrevious(workflowStatus);
@ -382,6 +390,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
columns,
deletedEventIds,
defaultCellActions: getDefaultCellActions({ addToQuery }),
disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS,
end: rangeTo,
filters: [],
hasAlertsCrudPermissions,

View file

@ -85,6 +85,10 @@ export const ProviderContentWrapper = styled.span`
> span.euiToolTipAnchor {
display: block; /* allow EuiTooltip content to be truncatable */
}
> span.euiToolTipAnchor.eui-textTruncate {
display: inline-block; /* do not override display when a tooltip is truncated via eui-textTruncate */
}
`;
type RenderFunctionProp = (

View file

@ -26,6 +26,7 @@ import type { EntityType } from '../../../../../timelines/common';
import { TGridCellAction } from '../../../../../timelines/common/types';
import { DetailsPanel } from '../../../timelines/components/side_panel';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants';
import { useKibana } from '../../lib/kibana';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { useCreateFieldButton } from '../../../timelines/components/create_field_button';
@ -180,6 +181,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
dataProviders,
defaultCellActions,
deletedEventIds,
disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS,
docValueFields,
end,
entityType,

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
/** actions are disabled for these fields in tables and popovers */
export const FIELDS_WITHOUT_CELL_ACTIONS = ['signal.rule.risk_score', 'signal.reason'];

View file

@ -13,14 +13,14 @@ import { getColumnRenderer } from '../body/renderers/get_column_renderer';
import { CellValueElementProps } from '.';
import { getLink } from '../../../../common/lib/cell_actions/helpers';
import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../../../common/lib/cell_actions/constants';
import {
ExpandedCellValueActions,
StyledContent,
} from '../../../../common/lib/cell_actions/expanded_cell_value_actions';
const FIELDS_WITHOUT_CELL_ACTIONS = ['signal.rule.risk_score', 'signal.reason'];
const hasCellActions = (columnId?: string) => {
return columnId && FIELDS_WITHOUT_CELL_ACTIONS.indexOf(columnId) < 0;
return columnId && !FIELDS_WITHOUT_CELL_ACTIONS.includes(columnId);
};
export const DefaultCellRenderer: React.FC<CellValueElementProps> = ({

View file

@ -11,6 +11,7 @@ import { ColumnHeaderOptions } from '../../../../common';
import { Ecs } from '../../../../common/ecs';
import {
allowSorting,
hasCellActions,
mapSortDirectionToDirection,
mapSortingColumns,
stringifyEvent,
@ -420,4 +421,20 @@ describe('helpers', () => {
expect(mockedSetCellProps).toBeCalledWith({ style: { backgroundColor: 'inherit' } });
});
});
describe('hasCellActions', () => {
const columnId = '@timestamp';
test('it returns false when the columnId is included in `disabledCellActions` ', () => {
const disabledCellActions = ['foo', '@timestamp', 'bar', 'baz']; // includes @timestamp
expect(hasCellActions({ columnId, disabledCellActions })).toBe(false);
});
test('it returns true when the columnId is NOT included in `disabledCellActions` ', () => {
const disabledCellActions = ['foo', 'bar', 'baz'];
expect(hasCellActions({ columnId, disabledCellActions })).toBe(true);
});
});
});

View file

@ -239,3 +239,12 @@ export const addBuildingBlockStyle = (
});
}
};
/** Returns true when the specified column has cell actions */
export const hasCellActions = ({
columnId,
disabledCellActions,
}: {
columnId: string;
disabledCellActions: string[];
}) => !disabledCellActions.includes(columnId);

View file

@ -68,6 +68,8 @@ describe('Body', () => {
clearSelected: jest.fn() as unknown as StatefulBodyProps['clearSelected'],
columnHeaders: defaultHeaders,
data: mockTimelineData,
defaultCellActions: [],
disabledCellActions: ['signal.rule.risk_score', 'signal.reason'],
excludedRowRendererIds: [],
id: 'timeline-test',
isSelectAllChecked: false,
@ -156,7 +158,7 @@ describe('Body', () => {
).toEqual(mockTimelineData[0].ecs.timestamp);
});
test("timestamp column doesn't render cell actions", () => {
test('timestamp column renders cell actions', () => {
const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp');
const testProps = {
...props,
@ -176,7 +178,7 @@ describe('Body', () => {
.first()
.prop<EuiDataGridColumn[]>('columns')
.find((c) => c.id === '@timestamp')?.cellActions
).toBeUndefined();
).toBeDefined();
});
test("signal.rule.risk_score column doesn't render cell actions", () => {

View file

@ -59,6 +59,7 @@ import { getColumnHeaders } from './column_headers/helpers';
import {
addBuildingBlockStyle,
getEventIdToDataMapping,
hasCellActions,
mapSortDirectionToDirection,
mapSortingColumns,
} from './helpers';
@ -92,6 +93,7 @@ interface OwnProps {
createFieldComponent?: CreateFieldComponentType;
data: TimelineItem[];
defaultCellActions?: TGridCellAction[];
disabledCellActions: string[];
filters?: Filter[];
filterQuery?: string;
filterStatus?: AlertStatus;
@ -145,16 +147,6 @@ const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>`
}
`;
// TODO: accept extra list of column ids without actions from callsites
const FIELDS_WITHOUT_CELL_ACTIONS = [
'@timestamp',
'signal.rule.risk_score',
'signal.reason',
'kibana.alert.duration.us',
'kibana.alert.reason',
];
const hasCellActions = (columnId?: string) =>
columnId && FIELDS_WITHOUT_CELL_ACTIONS.indexOf(columnId) < 0;
const transformControlColumns = ({
columnHeaders,
controlColumns,
@ -182,6 +174,7 @@ const transformControlColumns = ({
controlColumns: ControlColumnProps[];
createFieldComponent?: CreateFieldComponentType;
data: TimelineItem[];
disabledCellActions: string[];
isEventViewer?: boolean;
loadingEventIds: string[];
onRowSelected: OnRowSelected;
@ -311,6 +304,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
createFieldComponent,
data,
defaultCellActions,
disabledCellActions,
filterQuery,
filters,
filterStatus,
@ -621,6 +615,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
controlColumns,
createFieldComponent,
data,
disabledCellActions,
isEventViewer,
loadingEventIds,
onRowSelected,
@ -647,6 +642,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
columnHeaders,
createFieldComponent,
data,
disabledCellActions,
isEventViewer,
id,
loadingEventIds,
@ -692,7 +688,10 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
},
],
},
...(hasCellActions(header.id)
...(hasCellActions({
columnId: header.id,
disabledCellActions,
})
? {
cellActions:
header.tGridCellActions?.map(buildAction) ??
@ -701,7 +700,16 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
: {}),
};
}),
[columnHeaders, defaultCellActions, browserFields, data, pageSize, id, dispatch]
[
browserFields,
columnHeaders,
data,
defaultCellActions,
disabledCellActions,
dispatch,
id,
pageSize,
]
);
const renderTGridCellValue = useMemo(() => {

View file

@ -102,6 +102,7 @@ export interface TGridIntegratedProps {
dataProviders: DataProvider[];
defaultCellActions?: TGridCellAction[];
deletedEventIds: Readonly<string[]>;
disabledCellActions: string[];
docValueFields: DocValueFields[];
end: string;
entityType: EntityType;
@ -144,6 +145,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
dataProviders,
defaultCellActions,
deletedEventIds,
disabledCellActions,
docValueFields,
end,
entityType,
@ -353,6 +355,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
createFieldComponent={createFieldComponent}
data={nonDeletedEvents}
defaultCellActions={defaultCellActions}
disabledCellActions={disabledCellActions}
filterQuery={filterQuery}
filters={filters}
filterStatus={filterStatus}

View file

@ -86,6 +86,7 @@ export interface TGridStandaloneProps {
columns: ColumnHeaderOptions[];
defaultCellActions?: TGridCellAction[];
deletedEventIds: Readonly<string[]>;
disabledCellActions: string[];
end: string;
entityType?: EntityType;
loadingText: React.ReactNode;
@ -126,6 +127,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
columns,
defaultCellActions,
deletedEventIds,
disabledCellActions,
end,
entityType = 'alerts',
loadingText,
@ -381,6 +383,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
browserFields={browserFields}
data={nonDeletedEvents}
defaultCellActions={defaultCellActions}
disabledCellActions={disabledCellActions}
filterQuery={filterQuery}
hasAlertsCrud={hasAlertsCrud}
hasAlertsCrudPermissions={hasAlertsCrudPermissions}

View file

@ -57,12 +57,15 @@ const CountRow = React.memo<Pick<Props, 'filteredBrowserFields'>>(({ filteredBro
CountRow.displayName = 'CountRow';
const inputRef = (node: HTMLInputElement | null) => node?.focus();
export const Search = React.memo<Props>(
({ isSearching, filteredBrowserFields, onSearchInputChange, searchInput, timelineId }) => (
<>
<EuiFieldSearch
className={getFieldBrowserSearchInputClassName(timelineId)}
data-test-subj="field-search"
inputRef={inputRef}
isLoading={isSearching}
onChange={onSearchInputChange}
placeholder={i18n.FILTER_PLACEHOLDER}

View file

@ -92,6 +92,7 @@ export const tGridIntegratedProps: TGridIntegratedProps = {
columns: columnHeaders,
dataProviders: mockDataProviders,
deletedEventIds: [],
disabledCellActions: [],
docValueFields: mockDocValueFields,
end: '2021-08-19T00:30:00.000Z',
entityType: 'alerts',

View file

@ -75,6 +75,7 @@ const AppRoot = React.memo(
columns: [],
indexNames: [],
deletedEventIds: [],
disabledCellActions: [],
end: '',
footerText: 'Events',
filters: [],