Skip to content

Commit

Permalink
[ Security Solution ] - Better row indicators with getRowIndicator
Browse files Browse the repository at this point in the history
…callback (elastic#206736)

## Summary

Recently unified table introduced `getRowIndicator` callback to add row
highlighting. Today Security solution achieves that by using
`border-left` style.

This PR replaces that `border-left` with `getRowIndicator` . 

> [!Note]
> One thing to note is that `Event/Row Renderers` will still make use of
`border-left` as it is a cell and `getRowIndicator` applies only to a
complete `row`.

### Without Row Renderers

|| Before | After |
|---|---|---|
|Query Tab |
![image](https://github.com/user-attachments/assets/bb5405f6-9403-40b3-9cec-4dab1aeb4606)
|
![image](https://github.com/user-attachments/assets/38fd410f-9d2e-4ed6-a194-e3681ed07c3e)|
|Correlation Tab|
![image](https://github.com/user-attachments/assets/f8914ade-5e5f-4d0c-9bfc-dd4667f252e7)|![image](https://github.com/user-attachments/assets/d86fdf46-0fd9-4a28-bec1-381783a3641c)|

### With Row Renderers

|| Before | After |
|---|---|---|
|Query Tab |
![image](https://github.com/user-attachments/assets/4f0d2777-9e5e-4685-abaa-5d5eece655b4)|![image](https://github.com/user-attachments/assets/8ce6b8a3-bbc8-4919-941a-fa0b2ab5254e)|
|Correlation
Tab|![image](https://github.com/user-attachments/assets/560ef16e-abe0-45f9-8c47-f1cde43facc1)|![image](https://github.com/user-attachments/assets/576ee2eb-258b-4d51-90ce-1848944aea2a)|



### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] 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/src/platform/packages/shared/kbn-i18n/README.md)
- [x] [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
  • Loading branch information
logeekal authored Jan 21, 2025
1 parent 41a03ee commit 3d37119
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,12 @@ export const isEvenEqlSequence = (event: Ecs): boolean => {
};
/** Return eventType raw or signal or eql */
export const getEventType = (event: Ecs): Omit<TimelineEventsType, 'all'> => {
if (!isEmpty(event?.kibana?.alert?.rule?.uuid)) {
return 'signal';
} else if (!isEmpty(event?.eql?.parentId)) {
if (!isEmpty(event?.eql?.parentId)) {
return 'eql';
} else if (!isEmpty(event?.kibana?.alert?.rule?.uuid)) {
return 'signal';
}

return 'raw';
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,9 @@ describe('query tab with unified timeline', () => {
});

const messageColumnIndex =
customColumnOrder.findIndex((header) => header.id === 'message') + 3;
// 3 is the offset for additional leading columns on left
customColumnOrder.findIndex((header) => header.id === 'message') +
// offset for additional leading columns on left
4;

expect(container.querySelector('[data-gridcell-column-id="message"]')).toHaveAttribute(
'data-gridcell-column-index',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { getEventType, isEvenEqlSequence, isEventBuildingBlockType } from '../..
export const getEventTypeRowClassName = (ecsData: TimelineItem['ecs']) => {
const eventType = getEventType(ecsData);
const eventTypeClassName =
eventType === 'raw'
? 'rawEvent'
: eventType === 'eql'
eventType === 'eql'
? isEvenEqlSequence(ecsData)
? 'eqlSequence'
: 'eqlNonSequence'
: eventType === 'raw'
? 'rawEvent'
: 'nonRawEvent';

const buildingBlockTypeClassName = isEventBuildingBlockType(ecsData) ? 'buildingBlockType' : '';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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 type { DataTableRecord } from '@kbn/discover-utils';
import { getTimelineRowTypeIndicator } from './get_row_indicator';
import type { EuiThemeComputed } from '@elastic/eui';

const mockEuiTheme = {
colors: {
primary: 'primary',
accent: 'accent',
warning: 'warning',
lightShade: 'lightShade',
},
} as EuiThemeComputed;

describe('getTimelineRowTypeIndicator', () => {
describe('Alert', () => {
it('should return correct label and color for EQL Event', () => {
const row = {
flattened: {
'event.kind': 'signal',
'eql.parentId': '123',
'eql.sequenceNumber': '1-3',
},
} as unknown as DataTableRecord;
const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme);
expect(rowIndicator).toEqual({
color: 'accent',
label: 'EQL Sequence',
});
});
it('should return correct label and color for non-EQL Event', () => {
const row = {
flattened: {
'event.kind': 'signal',
},
} as unknown as DataTableRecord;
const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme);
expect(rowIndicator).toEqual({
color: 'warning',
label: 'Alert',
});
});
});

describe('Event', () => {
it('should return correct label and color for EQL Event', () => {
const row = {
flattened: {
'eql.parentId': '123',
'eql.sequenceNumber': '1-3',
},
} as unknown as DataTableRecord;
const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme);
expect(rowIndicator).toEqual({
color: 'accent',
label: 'EQL Sequence',
});
});
it('should return correct label and color for non-EQL Event', () => {
const row = {
flattened: {},
} as unknown as DataTableRecord;
const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme);
expect(rowIndicator).toMatchObject({
color: 'lightShade',
label: 'Event',
});
});
});

describe('EQL Event Type', () => {
it('should return correct label and color for Even EQL Sequence', () => {
const row = {
flattened: {
'eql.parentId': '123',
'eql.sequenceNumber': '2-4',
},
} as unknown as DataTableRecord;
const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme);
expect(rowIndicator).toEqual({
color: 'primary',
label: 'EQL Sequence',
});
});
it('should return correct label and color for Non-Even EQL Sequence', () => {
const row = {
flattened: {
'eql.parentId': '123',
'eql.sequenceNumber': '1-4',
},
} as unknown as DataTableRecord;
const rowIndicator = getTimelineRowTypeIndicator(row, mockEuiTheme);
expect(rowIndicator).toEqual({
color: 'accent',
label: 'EQL Sequence',
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 type { EuiThemeComputed } from '@elastic/eui';
import type { DataTableRecord } from '@kbn/discover-utils';
import { getFieldValue } from '@kbn/discover-utils';
import type { UnifiedDataTableProps } from '@kbn/unified-data-table';
import { isEmpty } from 'lodash';

export const getTimelineRowTypeIndicator: NonNullable<UnifiedDataTableProps['getRowIndicator']> = (
row: DataTableRecord,
euiTheme: EuiThemeComputed
) => {
const isAlert = getFieldValue(row, 'event.kind') === 'signal';

const isEql =
!isEmpty(getFieldValue(row, 'eql.parentId')) &&
!isEmpty(getFieldValue(row, 'eql.sequenceNumber'));

if (isEql) {
const sequenceNumber = ((getFieldValue(row, 'eql.sequenceNumber') as string) ?? '').split(
'-'
)[0];

const isEvenSequence = parseInt(sequenceNumber, 10) % 2 === 0;

return {
/* alternating colors to differentiate consecutive sequences */
color: isEvenSequence ? euiTheme.colors.primary : euiTheme.colors.accent,
label: 'EQL Sequence',
};
}

if (isAlert) {
return {
color: euiTheme.colors.warning,
label: 'Alert',
};
}

return {
color: euiTheme.colors.lightShade,
label: 'Event',
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { TimelineEventDetailRow } from './timeline_event_detail_row';
import { CustomTimelineDataGridBody } from './custom_timeline_data_grid_body';
import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants';
import { DocumentEventTypes } from '../../../../../common/lib/telemetry/types';
import { getTimelineRowTypeIndicator } from './get_row_indicator';

export const SAMPLE_SIZE_SETTING = 500;
const DataGridMemoized = React.memo(UnifiedDataTable);
Expand Down Expand Up @@ -423,6 +424,7 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
trailingControlColumns={finalTrailControlColumns}
externalControlColumns={leadingControlColumns}
onUpdatePageIndex={onUpdatePageIndex}
getRowIndicator={getTimelineRowTypeIndicator}
/>
</StyledTimelineUnifiedDataTable>
</StatefulEventContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe('unified timeline', () => {
});
expect(
container.querySelector(`[data-gridcell-column-id="${field.name}"]`)
).toHaveAttribute('data-gridcell-column-index', '3');
).toHaveAttribute('data-gridcell-column-index', '4');

expect(
container.querySelector(`[data-gridcell-column-id="${field.name}"]`)
Expand Down Expand Up @@ -268,7 +268,7 @@ describe('unified timeline', () => {
});
expect(
container.querySelector(`[data-gridcell-column-id="${field.name}"]`)
).toHaveAttribute('data-gridcell-column-index', '3');
).toHaveAttribute('data-gridcell-column-index', '4');

expect(
container.querySelector(`[data-gridcell-column-id="${field.name}"]`)
Expand Down Expand Up @@ -482,7 +482,7 @@ describe('unified timeline', () => {
});
expect(
container.querySelector(`[data-gridcell-column-id="${field.name}"]`)
).toHaveAttribute('data-gridcell-column-index', '3');
).toHaveAttribute('data-gridcell-column-index', '4');

expect(
container.querySelector(`[data-gridcell-column-id="${field.name}"]`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,19 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
.udtTimeline [data-gridcell-column-id|='select'] {
border-right: none;
}
.udtTimeline [data-gridcell-column-id|='openDetails'] .euiDataGridRowCell__contentByHeight {
margin-top: 3px;
.udtTimeline [data-gridcell-column-id|='openDetails'] {
/* custom row height based on number of lines */
.euiDataGridRowCell__content--lineCountHeight,
/* auto row height */
.euiDataGridRowCell__content--autoHeight {
margin-top: 9px;
}
/* single row height */
.euiDataGridRowCell__content--defaultHeight {
margin-top: 3px;
}
}
.udtTimeline
Expand All @@ -90,11 +101,24 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
overflow: visible;
}
.udtTimeline [data-gridcell-column-id|='select'] .euiDataGridRowCell__contentByHeight {
margin-top: 5px;
.udtTimeline [data-gridcell-column-id|='select'] {
/* custom row height based on number of lines */
.euiDataGridRowCell__content--lineCountHeight,
/* auto row height */
.euiDataGridRowCell__content--autoHeight {
margin-top: 6px;
}
/* single row height */
.euiDataGridRowCell__content--defaultHeight {
margin-top: 3px;
}
}
.udtTimeline
[data-gridcell-column-id|='select']
.udtTimeline
.euiDataGridRow:hover
.euiDataGridRowCell--lastColumn.euiDataGridRowCell--controlColumn {
${({ theme }) => `background-color: ${theme.eui.colorLightShade};`};
Expand All @@ -120,7 +144,6 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
}
.udtTimeline .euiDataGridRow:has(.eqlSequence),
.udtTimeline .euiDataGridRow.eqlSequence {
.euiDataGridRowCell--firstColumn,
.euiDataGridRowCell--controlColumn.euiDataGridRowCell--lastColumn,
.udt--customRow {
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorPrimary}`};
Expand All @@ -135,7 +158,6 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
}
.udtTimeline .euiDataGridRow:has(.eqlNonSequence),
.udtTimeline .euiDataGridRow.eqlNonSequence {
.euiDataGridRowCell--firstColumn,
.euiDataGridRowCell--controlColumn.euiDataGridRowCell--lastColumn,
.udt--customRow {
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorAccent};`}
Expand All @@ -150,15 +172,13 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
}
.udtTimeline .euiDataGridRow:has(.nonRawEvent),
.udtTimeline .euiDataGridRow.nonRawEvent {
.euiDataGridRowCell--firstColumn,
.euiDataGridRowCell--controlColumn.euiDataGridRowCell--lastColumn,
.udt--customRow {
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorWarning};`}
}
}
.udtTimeline .euiDataGridRow:has(.rawEvent),
.udtTimeline .euiDataGridRow.rawEvent {
.euiDataGridRowCell--firstColumn,
.euiDataGridRowCell--controlColumn.euiDataGridRowCell--lastColumn,
.udt--customRow {
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorLightShade};`}
Expand Down

0 comments on commit 3d37119

Please sign in to comment.