Skip to content

Commit

Permalink
[Security Solution] [Unified Timeline] Notes, Pinned events, and row …
Browse files Browse the repository at this point in the history
…actions in timeline (elastic#181376)

## Summary 

This pr adds pinned events, notes, and row actions to the unified data
table within timeline. Uses the existing shared hooks and components
from timeline, with only a few casts to make everything work. As with
the other parts of the unified timeline, this is hidden behind the
feature flag 'unifiedComponentsInTimelineEnabled'.

![timeline_notes_pinned](https://github.com/elastic/kibana/assets/56408403/6aa5d951-a98e-4a84-9fc5-8546db3e9167)

Correlation/EQL tab:
<img width="862" alt="image"
src="https://github.com/elastic/kibana/assets/56408403/5b7facfc-e385-41a2-b14c-e36cf134fe00">

Improved header controls positioning:
<img width="1456" alt="image"
src="https://github.com/elastic/kibana/assets/56408403/a87e39d3-3f53-4266-9a2d-5bc33a37cfdc">



### Checklist


- [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

---------

Co-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>
  • Loading branch information
kqualters-elastic and michaelolo24 committed May 3, 2024
1 parent ca18196 commit 1f04b5f
Show file tree
Hide file tree
Showing 41 changed files with 1,044 additions and 249 deletions.
6 changes: 6 additions & 0 deletions packages/kbn-unified-data-table/src/components/data_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ export interface UnifiedDataTableProps {
* This data is sent directly to actions.
*/
cellActionsMetadata?: Record<string, unknown>;
/**
* Optional extra props passed to the renderCellValue function/component.
*/
cellContext?: EuiDataGridProps['cellContext'];
}

export const EuiDataGridMemoized = React.memo(EuiDataGrid);
Expand Down Expand Up @@ -438,6 +442,7 @@ export const UnifiedDataTable = ({
customGridColumnsConfiguration,
customControlColumnsConfiguration,
enableComparisonMode,
cellContext,
}: UnifiedDataTableProps) => {
const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } =
services;
Expand Down Expand Up @@ -1055,6 +1060,7 @@ export const UnifiedDataTable = ({
renderCustomGridBody={renderCustomGridBody}
renderCustomToolbar={renderCustomToolbarFn}
trailingControlColumns={customTrailingControlColumn}
cellContext={cellContext}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* 2.0.
*/

import type { EuiDataGridCellValueElementProps, EuiDataGridColumn } from '@elastic/eui';
import type { EuiDataGridColumn, EuiDataGridProps } from '@elastic/eui';
import type { IFieldSubType } from '@kbn/es-query';
import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public';
import type { ComponentType, JSXElementConstructor } from 'react';
import type { ComponentType } from 'react';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
import { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
Expand Down Expand Up @@ -66,16 +66,7 @@ export interface HeaderActionProps {

export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>;

type GenericActionRowCellRenderProps = Pick<
EuiDataGridCellValueElementProps,
'rowIndex' | 'columnId'
>;

export type RowCellRender =
| JSXElementConstructor<GenericActionRowCellRenderProps>
| ((props: GenericActionRowCellRenderProps) => JSX.Element)
| JSXElementConstructor<ActionProps>
| ((props: ActionProps) => JSX.Element);
export type RowCellRender = EuiDataGridProps['renderCellValue'];

export interface ActionProps {
action?: RowCellRender;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface HeaderActionProps {
onSelectAll: ({ isSelected }: { isSelected: boolean }) => void;
showEventsSelect: boolean;
showSelectAllCheckbox: boolean;
showFullScreenToggle?: boolean;
sort: SortColumnTable[];
tabType: string;
timelineId: string;
Expand All @@ -69,7 +70,8 @@ export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>;
type GenericActionRowCellRenderProps = Pick<
EuiDataGridCellValueElementProps,
'rowIndex' | 'columnId'
>;
> &
Partial<EuiDataGridCellValueElementProps>;

export type RowCellRender =
| JSXElementConstructor<GenericActionRowCellRenderProps>
Expand Down Expand Up @@ -114,7 +116,6 @@ interface AdditionalControlColumnProps {
checked: boolean;
onRowSelected: OnRowSelected;
eventId: string;
id: string;
columnId: string;
loadingEventIds: Readonly<string[]>;
onEventDetailsPanelOpened: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { getDefaultControlColumn } from '../../../../timelines/components/timeli
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';

jest.mock('../../../hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(false),
}));
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);

const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ describe('Actions', () => {
describe('Guided Onboarding Step', () => {
const incrementStepMock = jest.fn();
beforeEach(() => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
(useTourContext as jest.Mock).mockReturnValue({
activeStep: 2,
incrementStep: incrementStepMock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ const ActionsComponent: React.FC<ActionProps> = ({
}) => {
const dispatch = useDispatch();
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
'unifiedComponentsInTimelineEnabled'
);
const emptyNotes: string[] = [];
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const timelineType = useShallowEqualSelector(
Expand Down Expand Up @@ -224,6 +227,10 @@ const ActionsComponent: React.FC<ActionProps> = ({
}
onEventDetailsPanelOpened();
}, [activeStep, incrementStep, isTourAnchor, isTourShown, onEventDetailsPanelOpened]);
const showExpandEvent = useMemo(
() => !unifiedComponentsInTimelineEnabled || isEventViewer || timelineId !== TimelineId.active,
[isEventViewer, timelineId, unifiedComponentsInTimelineEnabled]
);

return (
<ActionsContainer>
Expand All @@ -244,35 +251,38 @@ const ActionsComponent: React.FC<ActionProps> = ({
</EventsTdContent>
</div>
)}
<GuidedOnboardingTourStep
isTourAnchor={isTourAnchor}
onClick={onExpandEvent}
step={AlertsCasesTourSteps.expandEvent}
tourId={SecurityStepId.alertsCases}
>
<div key="expand-event">
<EventsTdContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip data-test-subj="expand-event-tool-tip" content={i18n.VIEW_DETAILS}>
<EuiButtonIcon
aria-label={i18n.VIEW_DETAILS_FOR_ROW({ ariaRowindex, columnValues })}
data-test-subj="expand-event"
iconType="expand"
onClick={onExpandEvent}
size="s"
/>
</EuiToolTip>
</EventsTdContent>
</div>
</GuidedOnboardingTourStep>
<>
{timelineId !== TimelineId.active && (
<InvestigateInTimelineAction
ariaLabel={i18n.SEND_ALERT_TO_TIMELINE_FOR_ROW({ ariaRowindex, columnValues })}
key="investigate-in-timeline"
ecsRowData={ecsData}
/>
{showExpandEvent && (
<GuidedOnboardingTourStep
isTourAnchor={isTourAnchor}
onClick={onExpandEvent}
step={AlertsCasesTourSteps.expandEvent}
tourId={SecurityStepId.alertsCases}
>
<div key="expand-event">
<EventsTdContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip data-test-subj="expand-event-tool-tip" content={i18n.VIEW_DETAILS}>
<EuiButtonIcon
aria-label={i18n.VIEW_DETAILS_FOR_ROW({ ariaRowindex, columnValues })}
data-test-subj="expand-event"
iconType="expand"
onClick={onExpandEvent}
size="s"
/>
</EuiToolTip>
</EventsTdContent>
</div>
</GuidedOnboardingTourStep>
)}

<>
{timelineId !== TimelineId.active && (
<InvestigateInTimelineAction
ariaLabel={i18n.SEND_ALERT_TO_TIMELINE_FOR_ROW({ ariaRowindex, columnValues })}
key="investigate-in-timeline"
ecsRowData={ecsData}
/>
)}
</>
{!isEventViewer && toggleShowNotes && (
<>
<AddEventNoteAction
Expand All @@ -281,6 +291,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
showNotes={showNotes ?? false}
toggleShowNotes={toggleShowNotes}
timelineType={timelineType}
eventId={eventId}
/>
<PinEventAction
ariaLabel={i18n.PIN_EVENT_FOR_ROW({ ariaRowindex, columnValues, isEventPinned })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ interface AddEventNoteActionProps {
showNotes: boolean;
timelineType: TimelineType;
toggleShowNotes: () => void;
eventId?: string;
}

const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
ariaLabel,
showNotes,
timelineType,
toggleShowNotes,
eventId,
}) => {
const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();

Expand All @@ -39,6 +41,7 @@ const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
toolTip={
timelineType === TimelineType.template ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP
}
eventId={eventId}
/>
</ActionIconItem>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const defaultProps: HeaderActionProps = {
tabType: TimelineTabs.query,
timelineId,
width: 10,
fieldBrowserOptions: {},
};

describe('HeaderActions', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = memo(
onSelectAll,
showEventsSelect,
showSelectAllCheckbox,
showFullScreenToggle = true,
sort,
tabType,
timelineId,
Expand Down Expand Up @@ -222,51 +223,54 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = memo(
</EventsThContent>
</EventsTh>
)}
<EventsTh role="button">
<FieldBrowserContainer>
{triggersActionsUi.getFieldBrowser({
browserFields,
columnIds: columnHeaders.map(({ id }) => id),
onResetColumns,
onToggleColumn,
options: fieldBrowserOptions,
})}
</FieldBrowserContainer>
</EventsTh>
{fieldBrowserOptions && (
<EventsTh role="button">
<FieldBrowserContainer>
{triggersActionsUi.getFieldBrowser({
browserFields,
columnIds: columnHeaders.map(({ id }) => id),
onResetColumns,
onToggleColumn,
options: fieldBrowserOptions,
})}
</FieldBrowserContainer>
</EventsTh>
)}

<EventsTh role="button">
<StatefulRowRenderersBrowser
data-test-subj="row-renderers-browser"
timelineId={timelineId}
/>
</EventsTh>

<EventsTh role="button">
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip content={fullScreen ? EXIT_FULL_SCREEN : i18n.FULL_SCREEN}>
<EuiButtonIcon
aria-label={
isFullScreen({
globalFullScreen,
isActiveTimelines: isActiveTimeline(timelineId),
timelineFullScreen,
})
? EXIT_FULL_SCREEN
: i18n.FULL_SCREEN
}
display={fullScreen ? 'fill' : 'empty'}
color="primary"
data-test-subj={
// a full screen button gets created for timeline and for the host page
// this sets the data-test-subj for each case so that tests can differentiate between them
isActiveTimeline(timelineId) ? 'full-screen-active' : 'full-screen'
}
iconType="fullScreen"
onClick={toggleFullScreen}
/>
</EuiToolTip>
</EventsThContent>
</EventsTh>
{showFullScreenToggle && (
<EventsTh role="button">
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip content={fullScreen ? EXIT_FULL_SCREEN : i18n.FULL_SCREEN}>
<EuiButtonIcon
aria-label={
isFullScreen({
globalFullScreen,
isActiveTimelines: isActiveTimeline(timelineId),
timelineFullScreen,
})
? EXIT_FULL_SCREEN
: i18n.FULL_SCREEN
}
display={fullScreen ? 'fill' : 'empty'}
color="primary"
data-test-subj={
// a full screen button gets created for timeline and for the host page
// this sets the data-test-subj for each case so that tests can differentiate between them
isActiveTimeline(timelineId) ? 'full-screen-active' : 'full-screen'
}
iconType="fullScreen"
onClick={toggleFullScreen}
/>
</EuiToolTip>
</EventsThContent>
</EventsTh>
)}
{tabType !== TimelineTabs.eql && (
<EventsTh role="button" data-test-subj="timeline-sorting-fields">
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,45 @@ NotesContainer.displayName = 'NotesContainer';
interface Props {
ariaRowindex: number;
associateNote: AssociateNote;
className?: string;
notes: TimelineResultNote[];
showAddNote: boolean;
toggleShowAddNote: () => void;
toggleShowAddNote: (eventId?: string) => void;
eventId?: string;
}

/** A view for entering and reviewing notes */
export const NoteCards = React.memo<Props>(
({ ariaRowindex, associateNote, notes, showAddNote, toggleShowAddNote }) => {
({ ariaRowindex, associateNote, className, notes, showAddNote, toggleShowAddNote, eventId }) => {
const [newNote, setNewNote] = useState('');

const associateNoteAndToggleShow = useCallback(
(noteId: string) => {
associateNote(noteId);
toggleShowAddNote();
if (eventId != null) {
toggleShowAddNote(eventId);
} else {
toggleShowAddNote();
}
},
[associateNote, toggleShowAddNote]
[associateNote, toggleShowAddNote, eventId]
);

const onCancelAddNote = useCallback(() => {
if (eventId != null) {
toggleShowAddNote(eventId);
} else {
toggleShowAddNote();
}
}, [eventId, toggleShowAddNote]);

return (
<NoteCardsCompContainer data-test-subj="note-cards" hasShadow={false} paddingSize="none">
<NoteCardsCompContainer
className={className}
data-test-subj="note-cards"
hasShadow={false}
paddingSize="none"
>
{notes.length ? (
<NotePreviewsContainer data-test-subj="note-previews-container">
<NotesContainer
Expand All @@ -85,7 +104,7 @@ export const NoteCards = React.memo<Props>(
<AddNote
associateNote={associateNoteAndToggleShow}
newNote={newNote}
onCancelAddNote={toggleShowAddNote}
onCancelAddNote={onCancelAddNote}
updateNewNote={setNewNote}
/>
</AddNoteContainer>
Expand Down
Loading

0 comments on commit 1f04b5f

Please sign in to comment.