mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] [Unified Timeline] Notes, Pinned events, and row actions in timeline (#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'.  Correlation/EQL tab: <img width="862" alt="image" src="5b7facfc
-e385-41a2-b14c-e36cf134fe00"> Improved header controls positioning: <img width="1456" alt="image" src="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>
This commit is contained in:
parent
ca181965ca
commit
1f04b5f174
41 changed files with 1044 additions and 249 deletions
|
@ -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);
|
||||
|
@ -438,6 +442,7 @@ export const UnifiedDataTable = ({
|
|||
customGridColumnsConfiguration,
|
||||
customControlColumnsConfiguration,
|
||||
enableComparisonMode,
|
||||
cellContext,
|
||||
}: UnifiedDataTableProps) => {
|
||||
const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } =
|
||||
services;
|
||||
|
@ -1055,6 +1060,7 @@ export const UnifiedDataTable = ({
|
|||
renderCustomGridBody={renderCustomGridBody}
|
||||
renderCustomToolbar={renderCustomToolbarFn}
|
||||
trailingControlColumns={customTrailingControlColumn}
|
||||
cellContext={cellContext}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
|
|
|
@ -59,6 +59,7 @@ export interface HeaderActionProps {
|
|||
onSelectAll: ({ isSelected }: { isSelected: boolean }) => void;
|
||||
showEventsSelect: boolean;
|
||||
showSelectAllCheckbox: boolean;
|
||||
showFullScreenToggle?: boolean;
|
||||
sort: SortColumnTable[];
|
||||
tabType: string;
|
||||
timelineId: string;
|
||||
|
@ -69,7 +70,8 @@ export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>;
|
|||
type GenericActionRowCellRenderProps = Pick<
|
||||
EuiDataGridCellValueElementProps,
|
||||
'rowIndex' | 'columnId'
|
||||
>;
|
||||
> &
|
||||
Partial<EuiDataGridCellValueElementProps>;
|
||||
|
||||
export type RowCellRender =
|
||||
| JSXElementConstructor<GenericActionRowCellRenderProps>
|
||||
|
@ -114,7 +116,6 @@ interface AdditionalControlColumnProps {
|
|||
checked: boolean;
|
||||
onRowSelected: OnRowSelected;
|
||||
eventId: string;
|
||||
id: string;
|
||||
columnId: string;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onEventDetailsPanelOpened: () => void;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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 })}
|
||||
|
|
|
@ -17,6 +17,7 @@ interface AddEventNoteActionProps {
|
|||
showNotes: boolean;
|
||||
timelineType: TimelineType;
|
||||
toggleShowNotes: () => void;
|
||||
eventId?: string;
|
||||
}
|
||||
|
||||
const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
|
||||
|
@ -24,6 +25,7 @@ const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
|
|||
showNotes,
|
||||
timelineType,
|
||||
toggleShowNotes,
|
||||
eventId,
|
||||
}) => {
|
||||
const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
|
||||
|
||||
|
@ -39,6 +41,7 @@ const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
|
|||
toolTip={
|
||||
timelineType === TimelineType.template ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP
|
||||
}
|
||||
eventId={eventId}
|
||||
/>
|
||||
</ActionIconItem>
|
||||
);
|
||||
|
|
|
@ -68,6 +68,7 @@ const defaultProps: HeaderActionProps = {
|
|||
tabType: TimelineTabs.query,
|
||||
timelineId,
|
||||
width: 10,
|
||||
fieldBrowserOptions: {},
|
||||
};
|
||||
|
||||
describe('HeaderActions', () => {
|
||||
|
|
|
@ -78,6 +78,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = memo(
|
|||
onSelectAll,
|
||||
showEventsSelect,
|
||||
showSelectAllCheckbox,
|
||||
showFullScreenToggle = true,
|
||||
sort,
|
||||
tabType,
|
||||
timelineId,
|
||||
|
@ -222,17 +223,19 @@ 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
|
||||
|
@ -240,33 +243,34 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = memo(
|
|||
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}>
|
||||
|
|
|
@ -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
|
||||
|
@ -85,7 +104,7 @@ export const NoteCards = React.memo<Props>(
|
|||
<AddNote
|
||||
associateNote={associateNoteAndToggleShow}
|
||||
newNote={newNote}
|
||||
onCancelAddNote={toggleShowAddNote}
|
||||
onCancelAddNote={onCancelAddNote}
|
||||
updateNewNote={setNewNote}
|
||||
/>
|
||||
</AddNoteContainer>
|
||||
|
|
|
@ -96,6 +96,11 @@ describe('ColumnHeaders', () => {
|
|||
});
|
||||
|
||||
test('it renders the field browser', () => {
|
||||
const mockCloseEditor = jest.fn();
|
||||
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
|
||||
editorActionsRef.current = { closeEditor: mockCloseEditor };
|
||||
return {};
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent {...defaultProps} />
|
||||
|
|
|
@ -167,6 +167,7 @@ describe('EventColumnView', () => {
|
|||
const wrapper = mount(
|
||||
<EventColumnView
|
||||
{...props}
|
||||
timelineId={TimelineId.test}
|
||||
leadingControlColumns={[testLeadingControlColumn, ...leadingControlColumns]}
|
||||
/>,
|
||||
{
|
||||
|
|
|
@ -120,9 +120,9 @@ 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)) {
|
||||
if (!isEmpty(event?.kibana?.alert?.rule?.uuid)) {
|
||||
return 'signal';
|
||||
} else if (!isEmpty(event.eql?.parentId)) {
|
||||
} else if (!isEmpty(event?.eql?.parentId)) {
|
||||
return 'eql';
|
||||
}
|
||||
return 'raw';
|
||||
|
|
|
@ -126,7 +126,7 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
} else if (fieldType === GEO_FIELD_TYPE) {
|
||||
return <>{value}</>;
|
||||
} else if (fieldType === DATE_FIELD_TYPE) {
|
||||
const classNames = truncate ? 'eui-textTruncate eui-alignMiddle' : undefined;
|
||||
const classNames = truncate ? 'eui-textTruncate' : undefined;
|
||||
const date = (
|
||||
<FormattedDate
|
||||
className={classNames}
|
||||
|
|
|
@ -40,6 +40,10 @@ export const UnifiedTimelineBody = (props: UnifiedTimelineBodyProps) => {
|
|||
onChangePage,
|
||||
activeTab,
|
||||
updatedAt,
|
||||
trailingControlColumns,
|
||||
leadingControlColumns,
|
||||
pinnedEventIds,
|
||||
eventIdToNoteIds,
|
||||
} = props;
|
||||
|
||||
const [pageRows, setPageRows] = useState<TimelineItem[][]>([]);
|
||||
|
@ -85,6 +89,10 @@ export const UnifiedTimelineBody = (props: UnifiedTimelineBodyProps) => {
|
|||
activeTab={activeTab}
|
||||
updatedAt={updatedAt}
|
||||
isTextBasedQuery={false}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
pinnedEventIds={pinnedEventIds}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
/>
|
||||
</RootDragDropProvider>
|
||||
</StyledTableFlexItem>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiBadge, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { TimelineTypeLiteral } from '../../../../../common/api/timeline';
|
||||
|
@ -24,23 +24,32 @@ interface NotesButtonProps {
|
|||
ariaLabel?: string;
|
||||
isDisabled?: boolean;
|
||||
showNotes: boolean;
|
||||
toggleShowNotes: () => void;
|
||||
toggleShowNotes: () => void | ((eventId: string) => void);
|
||||
toolTip?: string;
|
||||
timelineType: TimelineTypeLiteral;
|
||||
eventId?: string;
|
||||
}
|
||||
|
||||
interface SmallNotesButtonProps {
|
||||
ariaLabel?: string;
|
||||
isDisabled?: boolean;
|
||||
toggleShowNotes: () => void;
|
||||
toggleShowNotes: (eventId?: string) => void;
|
||||
timelineType: TimelineTypeLiteral;
|
||||
eventId?: string;
|
||||
}
|
||||
|
||||
export const NOTES_BUTTON_CLASS_NAME = 'notes-button';
|
||||
|
||||
const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
||||
({ ariaLabel = i18n.NOTES, isDisabled, toggleShowNotes, timelineType }) => {
|
||||
({ ariaLabel = i18n.NOTES, isDisabled, toggleShowNotes, timelineType, eventId }) => {
|
||||
const isTemplate = timelineType === TimelineType.template;
|
||||
const onClick = useCallback(() => {
|
||||
if (eventId != null) {
|
||||
toggleShowNotes(eventId);
|
||||
} else {
|
||||
toggleShowNotes();
|
||||
}
|
||||
}, [toggleShowNotes, eventId]);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
|
@ -49,7 +58,7 @@ const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
|||
data-test-subj="timeline-notes-button-small"
|
||||
disabled={isDisabled}
|
||||
iconType="editorComment"
|
||||
onClick={toggleShowNotes}
|
||||
onClick={onClick}
|
||||
size="s"
|
||||
isDisabled={isTemplate}
|
||||
/>
|
||||
|
@ -59,13 +68,14 @@ const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
|||
SmallNotesButton.displayName = 'SmallNotesButton';
|
||||
|
||||
export const NotesButton = React.memo<NotesButtonProps>(
|
||||
({ ariaLabel, isDisabled, showNotes, timelineType, toggleShowNotes, toolTip }) =>
|
||||
({ ariaLabel, isDisabled, showNotes, timelineType, toggleShowNotes, toolTip, eventId }) =>
|
||||
showNotes ? (
|
||||
<SmallNotesButton
|
||||
ariaLabel={ariaLabel}
|
||||
isDisabled={isDisabled}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
timelineType={timelineType}
|
||||
eventId={eventId}
|
||||
/>
|
||||
) : (
|
||||
<EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip">
|
||||
|
@ -74,6 +84,7 @@ export const NotesButton = React.memo<NotesButtonProps>(
|
|||
isDisabled={isDisabled}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
timelineType={timelineType}
|
||||
eventId={eventId}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)
|
||||
|
|
|
@ -159,6 +159,7 @@ In other use cases the message field can be used to concatenate different values
|
|||
}
|
||||
end="2018-03-24T03:33:52.253Z"
|
||||
eqlOptions={Object {}}
|
||||
eventIdToNoteIds={Object {}}
|
||||
expandedDetail={Object {}}
|
||||
isLive={false}
|
||||
itemsPerPage={5}
|
||||
|
@ -170,6 +171,7 @@ In other use cases the message field can be used to concatenate different values
|
|||
]
|
||||
}
|
||||
onEventClosed={[MockFunction]}
|
||||
pinnedEventIds={Object {}}
|
||||
renderCellValue={[Function]}
|
||||
rowRenderers={
|
||||
Array [
|
||||
|
|
|
@ -85,6 +85,8 @@ describe('Timeline', () => {
|
|||
start: startDate,
|
||||
timelineId: TimelineId.test,
|
||||
timerangeKind: 'absolute',
|
||||
pinnedEventIds: {},
|
||||
eventIdToNoteIds: {},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -13,9 +13,11 @@ import type { ConnectedProps } from 'react-redux';
|
|||
import { connect, useDispatch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { InPortal } from 'react-reverse-portal';
|
||||
import type { EuiDataGridControlColumn } from '@elastic/eui';
|
||||
|
||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||
import { InputsModelId } from '../../../../../common/store/inputs/constants';
|
||||
import type { ControlColumnProps } from '../../../../../../common/types';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { timelineActions, timelineSelectors } from '../../../../store';
|
||||
|
@ -54,6 +56,7 @@ import type { TimelineTabCommonProps } from '../shared/types';
|
|||
import { UnifiedTimelineBody } from '../../body/unified_timeline_body';
|
||||
import { EqlTabHeader } from './header';
|
||||
import { useTimelineColumns } from '../shared/use_timeline_columns';
|
||||
import { useTimelineControlColumn } from '../shared/use_timeline_control_columns';
|
||||
|
||||
export type Props = TimelineTabCommonProps & PropsFromRedux;
|
||||
|
||||
|
@ -73,6 +76,8 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
|||
showExpandedDetails,
|
||||
start,
|
||||
timerangeKind,
|
||||
pinnedEventIds,
|
||||
eventIdToNoteIds,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { query: eqlQuery = '', ...restEqlOption } = eqlOptions;
|
||||
|
@ -85,8 +90,9 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
|||
runtimeMappings,
|
||||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
const { augmentedColumnHeaders, getTimelineQueryFieldsFromColumns, leadingControlColumns } =
|
||||
useTimelineColumns(columns);
|
||||
const { augmentedColumnHeaders, timelineQueryFieldsFromColumns } = useTimelineColumns(columns);
|
||||
|
||||
const leadingControlColumns = useTimelineControlColumn(columns, TIMELINE_NO_SORTING);
|
||||
|
||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||
'unifiedComponentsInTimelineEnabled'
|
||||
|
@ -119,7 +125,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
|||
dataViewId,
|
||||
endDate: end,
|
||||
eqlOptions: restEqlOption,
|
||||
fields: getTimelineQueryFieldsFromColumns(),
|
||||
fields: timelineQueryFieldsFromColumns,
|
||||
filterQuery: eqlQuery ?? '',
|
||||
id: timelineId,
|
||||
indexNames: selectedPatterns,
|
||||
|
@ -195,6 +201,9 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
|||
updatedAt={refreshedAt}
|
||||
isTextBasedQuery={false}
|
||||
pageInfo={pageInfo}
|
||||
leadingControlColumns={leadingControlColumns as EuiDataGridControlColumn[]}
|
||||
pinnedEventIds={pinnedEventIds}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
/>
|
||||
</ScrollableFlexItem>
|
||||
</FullWidthFlexGroup>
|
||||
|
@ -238,7 +247,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
|||
itemsCount: totalCount,
|
||||
itemsPerPage,
|
||||
})}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns as ControlColumnProps[]}
|
||||
trailingControlColumns={timelineEmptyTrailingControlColumns}
|
||||
/>
|
||||
</StyledEuiFlyoutBody>
|
||||
|
@ -293,8 +302,16 @@ const makeMapStateToProps = () => {
|
|||
const mapStateToProps = (state: State, { timelineId }: TimelineTabCommonProps) => {
|
||||
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
|
||||
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
||||
const { activeTab, columns, eqlOptions, expandedDetail, itemsPerPage, itemsPerPageOptions } =
|
||||
timeline;
|
||||
const {
|
||||
activeTab,
|
||||
columns,
|
||||
eqlOptions,
|
||||
expandedDetail,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
pinnedEventIds,
|
||||
eventIdToNoteIds,
|
||||
} = timeline;
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
|
@ -306,6 +323,8 @@ const makeMapStateToProps = () => {
|
|||
isLive: input.policy.kind === 'interval',
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
pinnedEventIds,
|
||||
eventIdToNoteIds,
|
||||
showExpandedDetails:
|
||||
!!expandedDetail[TimelineTabs.eql] && !!expandedDetail[TimelineTabs.eql]?.panelView,
|
||||
|
||||
|
@ -338,6 +357,8 @@ const EqlTabContent = connector(
|
|||
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
||||
prevProps.timelineId === nextProps.timelineId &&
|
||||
deepEqual(prevProps.columns, nextProps.columns) &&
|
||||
deepEqual(prevProps.pinnedEventIds, nextProps.pinnedEventIds) &&
|
||||
deepEqual(prevProps.eventIdToNoteIds, nextProps.eventIdToNoteIds) &&
|
||||
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -156,6 +156,8 @@ In other use cases the message field can be used to concatenate different values
|
|||
},
|
||||
]
|
||||
}
|
||||
eventIdToNoteIds={Object {}}
|
||||
expandedDetail={Object {}}
|
||||
itemsPerPage={5}
|
||||
itemsPerPageOptions={
|
||||
Array [
|
||||
|
|
|
@ -125,6 +125,8 @@ describe('PinnedTabContent', () => {
|
|||
pinnedEventIds: {},
|
||||
showExpandedDetails: false,
|
||||
onEventClosed: jest.fn(),
|
||||
eventIdToNoteIds: {},
|
||||
expandedDetail: {},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
*/
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import React, { useMemo, useCallback, memo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { Dispatch } from 'redux';
|
||||
import type { ConnectedProps } from 'react-redux';
|
||||
import { connect } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import type { EuiDataGridControlColumn } from '@elastic/eui';
|
||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||
import type { ControlColumnProps } from '../../../../../../common/types';
|
||||
import { timelineActions, timelineSelectors } from '../../../../store';
|
||||
|
@ -25,6 +25,7 @@ import { requiredFieldsForActions } from '../../../../../detections/components/a
|
|||
import { EventDetailsWidthProvider } from '../../../../../common/components/events_viewer/event_details_width_context';
|
||||
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
||||
import { timelineDefaults } from '../../../../store/defaults';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
||||
import { useTimelineFullScreen } from '../../../../../common/containers/use_full_screen';
|
||||
import type { TimelineModel } from '../../../../store/model';
|
||||
|
@ -34,9 +35,7 @@ import type { ToggleDetailPanel } from '../../../../../../common/types/timeline'
|
|||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { DetailsPanel } from '../../../side_panel';
|
||||
import { ExitFullScreen } from '../../../../../common/components/exit_full_screen';
|
||||
import { getDefaultControlColumn } from '../../body/control_columns';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { HeaderActions } from '../../../../../common/components/header_actions/header_actions';
|
||||
import { UnifiedTimelineBody } from '../../body/unified_timeline_body';
|
||||
import {
|
||||
FullWidthFlexGroup,
|
||||
ScrollableFlexItem,
|
||||
|
@ -45,10 +44,13 @@ import {
|
|||
VerticalRule,
|
||||
} from '../shared/layout';
|
||||
import type { TimelineTabCommonProps } from '../shared/types';
|
||||
import { useTimelineColumns } from '../shared/use_timeline_columns';
|
||||
import { useTimelineControlColumn } from '../shared/use_timeline_control_columns';
|
||||
|
||||
const ExitFullScreenContainer = styled.div`
|
||||
width: 180px;
|
||||
`;
|
||||
|
||||
interface PinnedFilter {
|
||||
bool: {
|
||||
should: Array<{ match_phrase: { _id: string } }>;
|
||||
|
@ -60,6 +62,16 @@ export type Props = TimelineTabCommonProps & PropsFromRedux;
|
|||
|
||||
const trailingControlColumns: ControlColumnProps[] = []; // stable reference
|
||||
|
||||
const rowDetailColumn = [
|
||||
{
|
||||
id: 'row-details',
|
||||
columnHeaderType: 'not-filtered',
|
||||
width: 0,
|
||||
headerCellRender: () => null,
|
||||
rowCellRender: () => null,
|
||||
},
|
||||
];
|
||||
|
||||
export const PinnedTabContentComponent: React.FC<Props> = ({
|
||||
columns,
|
||||
timelineId,
|
||||
|
@ -71,6 +83,8 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
rowRenderers,
|
||||
showExpandedDetails,
|
||||
sort,
|
||||
expandedDetail,
|
||||
eventIdToNoteIds,
|
||||
}) => {
|
||||
const {
|
||||
browserFields,
|
||||
|
@ -80,8 +94,9 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
|
||||
const isEnterprisePlus = useLicense().isEnterprise();
|
||||
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
|
||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||
'unifiedComponentsInTimelineEnabled'
|
||||
);
|
||||
|
||||
const filterQuery = useMemo(() => {
|
||||
if (isEmpty(pinnedEventIds)) {
|
||||
|
@ -138,6 +153,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
})),
|
||||
[sort]
|
||||
);
|
||||
const { augmentedColumnHeaders } = useTimelineColumns(columns);
|
||||
|
||||
const [queryLoadingState, { events, totalCount, pageInfo, loadPage, refreshedAt, refetch }] =
|
||||
useTimelineEvents({
|
||||
|
@ -155,6 +171,8 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
timerangeKind: undefined,
|
||||
});
|
||||
|
||||
const leadingControlColumns = useTimelineControlColumn(columns, sort);
|
||||
|
||||
const isQueryLoading = useMemo(
|
||||
() => [DataLoadingState.loading, DataLoadingState.loadingMore].includes(queryLoadingState),
|
||||
[queryLoadingState]
|
||||
|
@ -164,14 +182,35 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
onEventClosed({ tabType: TimelineTabs.pinned, id: timelineId });
|
||||
}, [timelineId, onEventClosed]);
|
||||
|
||||
const leadingControlColumns = useMemo(
|
||||
() =>
|
||||
getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
|
||||
...x,
|
||||
headerCellRender: HeaderActions,
|
||||
})),
|
||||
[ACTION_BUTTON_COUNT]
|
||||
);
|
||||
if (unifiedComponentsInTimelineEnabled) {
|
||||
return (
|
||||
<UnifiedTimelineBody
|
||||
header={<></>}
|
||||
columns={augmentedColumnHeaders}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={timelineId}
|
||||
itemsPerPage={itemsPerPage}
|
||||
itemsPerPageOptions={itemsPerPageOptions}
|
||||
sort={sort}
|
||||
events={events}
|
||||
refetch={refetch}
|
||||
dataLoadingState={queryLoadingState}
|
||||
pinnedEventIds={pinnedEventIds}
|
||||
totalCount={events.length}
|
||||
onEventClosed={onEventClosed}
|
||||
expandedDetail={expandedDetail}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
showExpandedDetails={showExpandedDetails}
|
||||
onChangePage={loadPage}
|
||||
activeTab={TimelineTabs.pinned}
|
||||
updatedAt={refreshedAt}
|
||||
isTextBasedQuery={false}
|
||||
pageInfo={pageInfo}
|
||||
leadingControlColumns={leadingControlColumns as EuiDataGridControlColumn[]}
|
||||
trailingControlColumns={rowDetailColumn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -204,7 +243,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
itemsCount: totalCount,
|
||||
itemsPerPage,
|
||||
})}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns as ControlColumnProps[]}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
/>
|
||||
</StyledEuiFlyoutBody>
|
||||
|
@ -252,8 +291,15 @@ const makeMapStateToProps = () => {
|
|||
const getTimeline = timelineSelectors.getTimelineByIdSelector();
|
||||
const mapStateToProps = (state: State, { timelineId }: TimelineTabCommonProps) => {
|
||||
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
|
||||
const { columns, expandedDetail, itemsPerPage, itemsPerPageOptions, pinnedEventIds, sort } =
|
||||
timeline;
|
||||
const {
|
||||
columns,
|
||||
expandedDetail,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
pinnedEventIds,
|
||||
sort,
|
||||
eventIdToNoteIds,
|
||||
} = timeline;
|
||||
|
||||
return {
|
||||
columns,
|
||||
|
@ -264,6 +310,8 @@ const makeMapStateToProps = () => {
|
|||
showExpandedDetails:
|
||||
!!expandedDetail[TimelineTabs.pinned] && !!expandedDetail[TimelineTabs.pinned]?.panelView,
|
||||
sort,
|
||||
expandedDetail,
|
||||
eventIdToNoteIds,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
@ -280,7 +328,7 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps);
|
|||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
const PinnedTabContent = connector(
|
||||
React.memo(
|
||||
memo(
|
||||
PinnedTabContentComponent,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.itemsPerPage === nextProps.itemsPerPage &&
|
||||
|
@ -288,6 +336,7 @@ const PinnedTabContent = connector(
|
|||
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
||||
prevProps.timelineId === nextProps.timelineId &&
|
||||
deepEqual(prevProps.columns, nextProps.columns) &&
|
||||
deepEqual(prevProps.eventIdToNoteIds, nextProps.eventIdToNoteIds) &&
|
||||
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
|
||||
deepEqual(prevProps.pinnedEventIds, nextProps.pinnedEventIds) &&
|
||||
deepEqual(prevProps.sort, nextProps.sort)
|
||||
|
|
|
@ -292,6 +292,7 @@ In other use cases the message field can be used to concatenate different values
|
|||
]
|
||||
}
|
||||
end="2018-03-24T03:33:52.253Z"
|
||||
eventIdToNoteIds={Object {}}
|
||||
expandedDetail={Object {}}
|
||||
filters={Array []}
|
||||
isLive={false}
|
||||
|
@ -307,6 +308,7 @@ In other use cases the message field can be used to concatenate different values
|
|||
kqlQueryExpression=" "
|
||||
kqlQueryLanguage="kuery"
|
||||
onEventClosed={[MockFunction]}
|
||||
pinnedEventIds={Object {}}
|
||||
renderCellValue={[Function]}
|
||||
rowRenderers={
|
||||
Array [
|
||||
|
|
|
@ -113,6 +113,8 @@ describe('Timeline', () => {
|
|||
timerangeKind: 'absolute',
|
||||
activeTab: TimelineTabs.query,
|
||||
show: true,
|
||||
pinnedEventIds: {},
|
||||
eventIdToNoteIds: {},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { Dispatch } from 'redux';
|
|||
import type { ConnectedProps } from 'react-redux';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import type { EuiDataGridControlColumn } from '@elastic/eui';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
|
@ -19,6 +20,7 @@ import { InputsModelId } from '../../../../../common/store/inputs/constants';
|
|||
import { useInvalidFilterQuery } from '../../../../../common/hooks/use_invalid_filter_query';
|
||||
import { timelineActions, timelineSelectors } from '../../../../store';
|
||||
import type { Direction } from '../../../../../../common/search_strategy';
|
||||
import type { ControlColumnProps } from '../../../../../../common/types';
|
||||
import { useTimelineEvents } from '../../../../containers';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import { StatefulBody } from '../../body';
|
||||
|
@ -55,6 +57,7 @@ import {
|
|||
} from '../shared/utils';
|
||||
import type { TimelineTabCommonProps } from '../shared/types';
|
||||
import { useTimelineColumns } from '../shared/use_timeline_columns';
|
||||
import { useTimelineControlColumn } from '../shared/use_timeline_control_columns';
|
||||
|
||||
const compareQueryProps = (prevProps: Props, nextProps: Props) =>
|
||||
prevProps.kqlMode === nextProps.kqlMode &&
|
||||
|
@ -87,6 +90,8 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
sort,
|
||||
timerangeKind,
|
||||
expandedDetail,
|
||||
pinnedEventIds,
|
||||
eventIdToNoteIds,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
|
@ -99,12 +104,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
// in order to include the exclude filters in the search that are not stored in the timeline
|
||||
selectedPatterns,
|
||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
const {
|
||||
augmentedColumnHeaders,
|
||||
defaultColumns,
|
||||
getTimelineQueryFieldsFromColumns,
|
||||
leadingControlColumns,
|
||||
} = useTimelineColumns(columns);
|
||||
|
||||
const { uiSettings, timelineFilterManager } = useKibana().services;
|
||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||
|
@ -171,14 +170,8 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
type: columnType,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
timelineActions.initializeTimelineSettings({
|
||||
id: timelineId,
|
||||
defaultColumns,
|
||||
})
|
||||
);
|
||||
}, [dispatch, timelineId, defaultColumns]);
|
||||
const { augmentedColumnHeaders, defaultColumns, timelineQueryFieldsFromColumns } =
|
||||
useTimelineColumns(columns);
|
||||
|
||||
const [
|
||||
dataLoadingState,
|
||||
|
@ -186,7 +179,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
] = useTimelineEvents({
|
||||
dataViewId,
|
||||
endDate: end,
|
||||
fields: getTimelineQueryFieldsFromColumns(),
|
||||
fields: timelineQueryFieldsFromColumns,
|
||||
filterQuery: combinedQueries?.filterQuery,
|
||||
id: timelineId,
|
||||
indexNames: selectedPatterns,
|
||||
|
@ -199,6 +192,17 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
timerangeKind,
|
||||
});
|
||||
|
||||
const leadingControlColumns = useTimelineControlColumn(columns, sort);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
timelineActions.initializeTimelineSettings({
|
||||
id: timelineId,
|
||||
defaultColumns,
|
||||
})
|
||||
);
|
||||
}, [dispatch, timelineId, defaultColumns]);
|
||||
|
||||
const isQueryLoading = useMemo(
|
||||
() => [DataLoadingState.loading, DataLoadingState.loadingMore].includes(dataLoadingState),
|
||||
[dataLoadingState]
|
||||
|
@ -250,6 +254,9 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
onEventClosed={onEventClosed}
|
||||
expandedDetail={expandedDetail}
|
||||
showExpandedDetails={showExpandedDetails}
|
||||
leadingControlColumns={leadingControlColumns as EuiDataGridControlColumn[]}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
pinnedEventIds={pinnedEventIds}
|
||||
onChangePage={loadPage}
|
||||
activeTab={activeTab}
|
||||
updatedAt={refreshedAt}
|
||||
|
@ -300,7 +307,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
itemsCount: totalCount,
|
||||
itemsPerPage,
|
||||
})}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns as ControlColumnProps[]}
|
||||
trailingControlColumns={timelineEmptyTrailingControlColumns}
|
||||
/>
|
||||
</StyledEuiFlyoutBody>
|
||||
|
@ -359,6 +366,8 @@ const makeMapStateToProps = () => {
|
|||
activeTab,
|
||||
columns,
|
||||
dataProviders,
|
||||
pinnedEventIds,
|
||||
eventIdToNoteIds,
|
||||
expandedDetail,
|
||||
filters,
|
||||
itemsPerPage,
|
||||
|
@ -394,6 +403,8 @@ const makeMapStateToProps = () => {
|
|||
expandedDetail,
|
||||
filters: timelineFilter,
|
||||
timelineId,
|
||||
pinnedEventIds,
|
||||
eventIdToNoteIds,
|
||||
isLive: input.policy.kind === 'interval',
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
|
@ -437,8 +448,11 @@ const QueryTabContent = connector(
|
|||
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
|
||||
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
||||
prevProps.status === nextProps.status &&
|
||||
prevProps.status === nextProps.status &&
|
||||
prevProps.timelineId === nextProps.timelineId &&
|
||||
deepEqual(prevProps.eventIdToNoteIds, nextProps.eventIdToNoteIds) &&
|
||||
deepEqual(prevProps.columns, nextProps.columns) &&
|
||||
deepEqual(prevProps.pinnedEventIds, nextProps.pinnedEventIds) &&
|
||||
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
|
||||
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
|
||||
deepEqual(prevProps.sort, nextProps.sort)
|
||||
|
|
|
@ -472,26 +472,3 @@ Array [
|
|||
"process.entry_leader.entity_id",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`useTimelineColumns leadingControlColumns should return the leading control columns 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"headerCellRender": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
},
|
||||
"id": "default-timeline-control-column",
|
||||
"rowCellRender": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
"width": 180,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`useTimelineColumns leadingControlColumns should return the leading control columns 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"headerCellRender": [Function],
|
||||
"id": "default-timeline-control-column",
|
||||
"rowCellRender": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
"width": 152,
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -8,7 +8,6 @@
|
|||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { useTimelineColumns } from './use_timeline_columns';
|
||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||
import { defaultHeaders } from '../../body/column_headers/default_headers';
|
||||
|
@ -20,14 +19,6 @@ jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
|
|||
|
||||
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_license', () => ({
|
||||
useLicense: jest.fn().mockReturnValue({
|
||||
isEnterprise: () => true,
|
||||
}),
|
||||
}));
|
||||
|
||||
const useLicenseMock = useLicense as jest.Mock;
|
||||
|
||||
describe('useTimelineColumns', () => {
|
||||
const mockColumns: ColumnHeaderOptions[] = [
|
||||
{
|
||||
|
@ -108,45 +99,18 @@ describe('useTimelineColumns', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('leadingControlColumns', () => {
|
||||
it('should return the leading control columns', () => {
|
||||
const { result } = renderHook(() => useTimelineColumns([]), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current.leadingControlColumns).toMatchSnapshot();
|
||||
});
|
||||
it('should have a width of 152 for 5 actions', () => {
|
||||
useLicenseMock.mockReturnValue({
|
||||
isEnterprise: () => false,
|
||||
});
|
||||
const { result } = renderHook(() => useTimelineColumns([]), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current.leadingControlColumns[0].width).toBe(152);
|
||||
});
|
||||
it('should have a width of 180 for 6 actions', () => {
|
||||
useLicenseMock.mockReturnValue({
|
||||
isEnterprise: () => true,
|
||||
});
|
||||
const { result } = renderHook(() => useTimelineColumns([]), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current.leadingControlColumns[0].width).toBe(180);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimelineQueryFieldsFromColumns', () => {
|
||||
it('should return the list of all the fields', () => {
|
||||
const { result } = renderHook(() => useTimelineColumns([]), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current.getTimelineQueryFieldsFromColumns()).toMatchSnapshot();
|
||||
expect(result.current.timelineQueryFieldsFromColumns).toMatchSnapshot();
|
||||
});
|
||||
it('should have a width of 152 for 5 actions', () => {
|
||||
const { result } = renderHook(() => useTimelineColumns(mockColumns), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current.getTimelineQueryFieldsFromColumns()).toMatchSnapshot();
|
||||
expect(result.current.timelineQueryFieldsFromColumns).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,15 +6,12 @@
|
|||
*/
|
||||
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { useMemo } from 'react';
|
||||
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { defaultHeaders } from '../../body/column_headers/default_headers';
|
||||
import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config';
|
||||
import { getDefaultControlColumn } from '../../body/control_columns';
|
||||
import { HeaderActions } from '../../../../../common/components/header_actions/header_actions';
|
||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||
import { memoizedGetTimelineColumnHeaders } from './utils';
|
||||
|
@ -26,9 +23,6 @@ export const useTimelineColumns = (columns: ColumnHeaderOptions[]) => {
|
|||
'unifiedComponentsInTimelineEnabled'
|
||||
);
|
||||
|
||||
const isEnterprisePlus = useLicense().isEnterprise();
|
||||
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
|
||||
|
||||
const defaultColumns = useMemo(
|
||||
() => (unifiedComponentsInTimelineEnabled ? defaultUdtHeaders : defaultHeaders),
|
||||
[unifiedComponentsInTimelineEnabled]
|
||||
|
@ -45,35 +39,19 @@ export const useTimelineColumns = (columns: ColumnHeaderOptions[]) => {
|
|||
false
|
||||
);
|
||||
|
||||
const getTimelineQueryFieldsFromColumns = useCallback(() => {
|
||||
const timelineQueryFieldsFromColumns = useMemo(() => {
|
||||
const columnFields = augmentedColumnHeaders.map((c) => c.id);
|
||||
|
||||
return [...columnFields, ...requiredFieldsForActions];
|
||||
}, [augmentedColumnHeaders]);
|
||||
|
||||
const leadingControlColumns = useMemo(
|
||||
() =>
|
||||
getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
|
||||
...x,
|
||||
headerCellRender: HeaderActions,
|
||||
})),
|
||||
[ACTION_BUTTON_COUNT]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
defaultColumns,
|
||||
localColumns,
|
||||
augmentedColumnHeaders,
|
||||
getTimelineQueryFieldsFromColumns,
|
||||
leadingControlColumns,
|
||||
timelineQueryFieldsFromColumns,
|
||||
}),
|
||||
[
|
||||
augmentedColumnHeaders,
|
||||
defaultColumns,
|
||||
getTimelineQueryFieldsFromColumns,
|
||||
leadingControlColumns,
|
||||
localColumns,
|
||||
]
|
||||
[augmentedColumnHeaders, defaultColumns, timelineQueryFieldsFromColumns, localColumns]
|
||||
);
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { EuiDataGridControlColumn } from '@elastic/eui';
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { useTimelineControlColumn } from './use_timeline_control_columns';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline/columns';
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_license', () => ({
|
||||
useLicense: jest.fn().mockReturnValue({
|
||||
isEnterprise: () => true,
|
||||
}),
|
||||
}));
|
||||
|
||||
const useLicenseMock = useLicense as jest.Mock;
|
||||
|
||||
describe('useTimelineColumns', () => {
|
||||
const mockColumns: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'source.ip',
|
||||
initialWidth: 150,
|
||||
},
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'agent.type',
|
||||
initialWidth: 150,
|
||||
},
|
||||
];
|
||||
|
||||
describe('leadingControlColumns', () => {
|
||||
it('should return the leading control columns', () => {
|
||||
const { result } = renderHook(() => useTimelineControlColumn(mockColumns, []), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
expect(result.current).toMatchSnapshot();
|
||||
});
|
||||
it('should have a width of 124 for 5 actions', () => {
|
||||
useLicenseMock.mockReturnValue({
|
||||
isEnterprise: () => false,
|
||||
});
|
||||
const { result } = renderHook(() => useTimelineControlColumn(mockColumns, []), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
const controlColumn = result.current[0] as EuiDataGridControlColumn;
|
||||
expect(controlColumn.width).toBe(124);
|
||||
});
|
||||
it('should have a width of 152 for 6 actions', () => {
|
||||
useLicenseMock.mockReturnValue({
|
||||
isEnterprise: () => true,
|
||||
});
|
||||
const { result } = renderHook(() => useTimelineControlColumn(mockColumns, []), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
const controlColumn = result.current[0] as EuiDataGridControlColumn;
|
||||
expect(controlColumn.width).toBe(152);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import type { EuiDataGridControlColumn } from '@elastic/eui';
|
||||
import type { SortColumnTable } from '@kbn/securitysolution-data-table';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { getDefaultControlColumn } from '../../body/control_columns';
|
||||
import type { UnifiedActionProps } from '../../unified_components/data_table/control_column_cell_render';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { HeaderActions } from '../../../../../common/components/header_actions/header_actions';
|
||||
import { ControlColumnCellRender } from '../../unified_components/data_table/control_column_cell_render';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||
import { useTimelineColumns } from './use_timeline_columns';
|
||||
|
||||
const noSelectAll = ({ isSelected }: { isSelected: boolean }) => {};
|
||||
export const useTimelineControlColumn = (
|
||||
columns: ColumnHeaderOptions[],
|
||||
sort: SortColumnTable[]
|
||||
) => {
|
||||
const { browserFields } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||
'unifiedComponentsInTimelineEnabled'
|
||||
);
|
||||
|
||||
const isEnterprisePlus = useLicense().isEnterprise();
|
||||
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
|
||||
const { localColumns } = useTimelineColumns(columns);
|
||||
|
||||
// We need one less when the unified components are enabled because the document expand is provided by the unified data table
|
||||
const UNIFIED_COMPONENTS_ACTION_BUTTON_COUNT = ACTION_BUTTON_COUNT - 1;
|
||||
|
||||
return useMemo(() => {
|
||||
if (unifiedComponentsInTimelineEnabled) {
|
||||
return getDefaultControlColumn(UNIFIED_COMPONENTS_ACTION_BUTTON_COUNT).map((x) => ({
|
||||
...x,
|
||||
headerCellRender: function HeaderCellRender(props: UnifiedActionProps) {
|
||||
return (
|
||||
<HeaderActions
|
||||
width={x.width}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={localColumns}
|
||||
isEventViewer={false}
|
||||
isSelectAllChecked={false}
|
||||
onSelectAll={noSelectAll}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
showFullScreenToggle={false}
|
||||
sort={sort}
|
||||
tabType={TimelineTabs.pinned}
|
||||
{...props}
|
||||
timelineId={TimelineId.active}
|
||||
/>
|
||||
);
|
||||
},
|
||||
rowCellRender: ControlColumnCellRender,
|
||||
})) as unknown as EuiDataGridControlColumn[];
|
||||
} else {
|
||||
return getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
|
||||
...x,
|
||||
headerCellRender: HeaderActions,
|
||||
})) as unknown as ColumnHeaderOptions[];
|
||||
}
|
||||
}, [
|
||||
ACTION_BUTTON_COUNT,
|
||||
UNIFIED_COMPONENTS_ACTION_BUTTON_COUNT,
|
||||
browserFields,
|
||||
localColumns,
|
||||
sort,
|
||||
unifiedComponentsInTimelineEnabled,
|
||||
]);
|
||||
};
|
|
@ -1,6 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CustomTimelineDataGridBody should render exactly as snapshots 1`] = `
|
||||
.c3 {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.c2.euiPanel--plain {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
|
@ -8,11 +26,52 @@ exports[`CustomTimelineDataGridBody should render exactly as snapshots 1`] = `
|
|||
border-bottom: 1px solid 1px solid #343741;
|
||||
}
|
||||
|
||||
.c0 . euiDataGridRowCell--controlColumn {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.c0 .udt--customRow {
|
||||
border-radius: 0;
|
||||
padding: 6px;
|
||||
max-width: 1200px;
|
||||
width: 85vw;
|
||||
}
|
||||
|
||||
.c0 .euiCommentEvent__body {
|
||||
background-color: #1d1e24;
|
||||
}
|
||||
|
||||
.c0:has(.unifiedDataTable__cell--expanded) .euiDataGridRowCell--firstColumn,
|
||||
.c0:has(.unifiedDataTable__cell--expanded) .euiDataGridRowCell--lastColumn,
|
||||
.c0:has(.unifiedDataTable__cell--expanded) .euiDataGridRowCell--controlColumn,
|
||||
.c0:has(.unifiedDataTable__cell--expanded) .udt--customRow {
|
||||
background-color: #2e2d25;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.c1 .euiDataGridRowCell,
|
||||
.c1 .euiDataGridRowCell__content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.c1 .euiDataGridRowCell .unifiedDataTable__rowControl,
|
||||
.c1 .euiDataGridRowCell__content .unifiedDataTable__rowControl {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.c1 .euiDataGridRowCell--controlColumn .euiDataGridRowCell__content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
<div>
|
||||
|
@ -34,6 +93,140 @@ exports[`CustomTimelineDataGridBody should render exactly as snapshots 1`] = `
|
|||
Cell-0-2
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiPanel euiPanel--plain c2 udt--customRow emotion-euiPanel-grow-m-plain"
|
||||
data-test-subj="note-cards"
|
||||
>
|
||||
<section
|
||||
class="c3"
|
||||
data-test-subj="note-previews-container"
|
||||
>
|
||||
<div
|
||||
class="euiFlexGroup c4 notes-container-0 emotion-euiFlexGroup-none-flexStart-stretch-column"
|
||||
data-test-subj="notes"
|
||||
>
|
||||
<p
|
||||
class="emotion-euiScreenReaderOnly"
|
||||
>
|
||||
You are viewing notes for the event in row 0. Press the up arrow key when finished to return to the event.
|
||||
</p>
|
||||
<ol
|
||||
class="euiTimeline euiCommentList emotion-euiTimeline-l"
|
||||
data-test-subj="note-comment-list"
|
||||
role="list"
|
||||
>
|
||||
<li
|
||||
class="euiComment emotion-euiTimelineItem-top"
|
||||
data-test-subj="note-preview-id"
|
||||
>
|
||||
<div
|
||||
class="emotion-euiTimelineItemIcon-top"
|
||||
>
|
||||
<div
|
||||
class="emotion-euiTimelineItemIcon__content"
|
||||
>
|
||||
<div
|
||||
aria-label="test"
|
||||
class="euiAvatar euiAvatar--l euiAvatar--user emotion-euiAvatar-user-l-uppercase"
|
||||
data-test-subj="avatar"
|
||||
role="img"
|
||||
style="background-color: rgb(228, 166, 199); color: rgb(0, 0, 0);"
|
||||
title="test"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
t
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="emotion-euiTimelineItemEvent-top"
|
||||
>
|
||||
<figure
|
||||
class="euiCommentEvent emotion-euiCommentEvent-border-subdued"
|
||||
data-type="regular"
|
||||
>
|
||||
<figcaption
|
||||
class="euiCommentEvent__header emotion-euiCommentEvent__header-border-subdued"
|
||||
>
|
||||
<div
|
||||
class="euiPanel euiPanel--subdued euiPanel--paddingSmall emotion-euiPanel-grow-none-s-subdued"
|
||||
>
|
||||
<div
|
||||
class="euiCommentEvent__headerMain emotion-euiCommentEvent__headerMain"
|
||||
>
|
||||
<div
|
||||
class="euiCommentEvent__headerData emotion-euiCommentEvent__headerData"
|
||||
>
|
||||
<div
|
||||
class="euiCommentEvent__headerUsername emotion-euiCommentEvent__headerUsername"
|
||||
>
|
||||
test
|
||||
</div>
|
||||
<div
|
||||
class="euiCommentEvent__headerEvent emotion-euiCommentEvent__headerEvent"
|
||||
>
|
||||
added a note
|
||||
</div>
|
||||
<div
|
||||
class="euiCommentEvent__headerTimestamp"
|
||||
>
|
||||
<time>
|
||||
now
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiCommentEvent__headerActions emotion-euiCommentEvent__headerActions"
|
||||
>
|
||||
<button
|
||||
aria-label="Delete Note"
|
||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-text"
|
||||
data-test-subj="delete-note"
|
||||
title="Delete Note"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="trash"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</figcaption>
|
||||
<div
|
||||
class="euiCommentEvent__body emotion-euiCommentEvent__body-regular"
|
||||
>
|
||||
<div
|
||||
class="note-content"
|
||||
tabindex="0"
|
||||
>
|
||||
<p
|
||||
class="emotion-euiScreenReaderOnly"
|
||||
>
|
||||
test added a note
|
||||
</p>
|
||||
<div
|
||||
class="euiText euiMarkdownFormat emotion-euiText-m-euiMarkdownFormat-m"
|
||||
>
|
||||
<p>
|
||||
note
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div>
|
||||
Cell-0-3
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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, { memo, useMemo } from 'react';
|
||||
import type { TimelineItem } from '@kbn/timelines-plugin/common';
|
||||
import { eventIsPinned } from '../../body/helpers';
|
||||
import { Actions } from '../../../../../common/components/header_actions';
|
||||
import { TimelineId } from '../../../../../../common/types';
|
||||
import type { TimelineModel } from '../../../../store/model';
|
||||
import type { ActionProps } from '../../../../../../common/types';
|
||||
|
||||
const noOp = () => {};
|
||||
const emptyLoadingEventIds: string[] = [];
|
||||
export interface UnifiedActionProps extends ActionProps {
|
||||
onToggleShowNotes: (eventId?: string) => void;
|
||||
events: TimelineItem[];
|
||||
pinnedEventIds: TimelineModel['pinnedEventIds'];
|
||||
}
|
||||
|
||||
export const ControlColumnCellRender = memo(function RowCellRender(props: UnifiedActionProps) {
|
||||
const { rowIndex, events, ecsData, pinnedEventIds, onToggleShowNotes, eventIdToNoteIds } = props;
|
||||
const event = useMemo(() => events && events[rowIndex], [events, rowIndex]);
|
||||
const isPinned = useMemo(
|
||||
() => eventIsPinned({ eventId: event?._id, pinnedEventIds }),
|
||||
[event?._id, pinnedEventIds]
|
||||
);
|
||||
return (
|
||||
<Actions
|
||||
{...props}
|
||||
ecsData={ecsData ?? event.ecs}
|
||||
ariaRowindex={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
checked={false}
|
||||
columnValues="columnValues"
|
||||
eventId={event?._id}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
isEventPinned={isPinned}
|
||||
isEventViewer={false}
|
||||
loadingEventIds={emptyLoadingEventIds}
|
||||
onEventDetailsPanelOpened={noOp}
|
||||
onRowSelected={noOp}
|
||||
onRuleChange={noOp}
|
||||
showCheckboxes={false}
|
||||
showNotes={true}
|
||||
timelineId={TimelineId.active}
|
||||
toggleShowNotes={onToggleShowNotes}
|
||||
refetch={noOp}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -16,6 +16,9 @@ import { defaultUdtHeaders } from '../default_headers';
|
|||
import type { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
|
||||
import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_selector');
|
||||
|
||||
const testDataRows = structuredClone(mockTimelineData);
|
||||
|
||||
|
@ -37,6 +40,8 @@ const mockVisibleColumns = ['@timestamp', 'message', 'user.name']
|
|||
.map((id) => defaultUdtHeaders.find((h) => h.id === id) as EuiDataGridColumn)
|
||||
.concat(additionalTrailingColumn);
|
||||
|
||||
const mockEventIdsAddingNotes = new Set<string>();
|
||||
|
||||
const defaultProps: CustomTimelineDataGridBodyProps = {
|
||||
Cell: MockCellComponent,
|
||||
visibleRowData: { startRow: 0, endRow: 2, visibleRowCount: 2 },
|
||||
|
@ -44,6 +49,34 @@ const defaultProps: CustomTimelineDataGridBodyProps = {
|
|||
enabledRowRenderers: [],
|
||||
setCustomGridBodyProps: jest.fn(),
|
||||
visibleColumns: mockVisibleColumns,
|
||||
eventIdsAddingNotes: mockEventIdsAddingNotes,
|
||||
eventIdToNoteIds: {
|
||||
event1: ['noteId1', 'noteId2'],
|
||||
event2: ['noteId3'],
|
||||
},
|
||||
events: [
|
||||
{
|
||||
_id: 'event1',
|
||||
_index: 'logs-*',
|
||||
data: [],
|
||||
ecs: { _id: 'event1', _index: 'logs-*' },
|
||||
},
|
||||
{
|
||||
_id: 'event2',
|
||||
_index: 'logs-*',
|
||||
data: [],
|
||||
ecs: { _id: 'event2', _index: 'logs-*' },
|
||||
},
|
||||
],
|
||||
onToggleShowNotes: (eventId?: string) => {
|
||||
if (eventId) {
|
||||
if (mockEventIdsAddingNotes.has(eventId)) {
|
||||
mockEventIdsAddingNotes.delete(eventId);
|
||||
} else {
|
||||
mockEventIdsAddingNotes.add(eventId);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const renderTestComponents = (props?: CustomTimelineDataGridBodyProps) => {
|
||||
|
@ -58,9 +91,34 @@ const renderTestComponents = (props?: CustomTimelineDataGridBodyProps) => {
|
|||
|
||||
describe('CustomTimelineDataGridBody', () => {
|
||||
beforeEach(() => {
|
||||
const now = new Date();
|
||||
(useStatefulRowRenderer as jest.Mock).mockReturnValue({
|
||||
canShowRowRenderer: true,
|
||||
});
|
||||
(useDeepEqualSelector as jest.Mock).mockReturnValue({
|
||||
noteId1: {
|
||||
created: now,
|
||||
eventId: 'event1',
|
||||
id: 'test',
|
||||
lastEdit: now,
|
||||
note: 'note',
|
||||
user: 'test',
|
||||
saveObjectId: 'id',
|
||||
timelineId: 'timeline-1',
|
||||
version: '',
|
||||
},
|
||||
noteId2: {
|
||||
created: now,
|
||||
eventId: 'event1',
|
||||
id: 'test',
|
||||
lastEdit: now,
|
||||
note: 'note',
|
||||
user: 'test',
|
||||
saveObjectId: 'id',
|
||||
timelineId: 'timeline-1',
|
||||
version: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -87,4 +145,18 @@ describe('CustomTimelineDataGridBody', () => {
|
|||
expect(queryByText('Cell-0-3')).toBeFalsy();
|
||||
expect(getByText('Cell-1-3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a note when notes are present', () => {
|
||||
const { getByText } = renderTestComponents();
|
||||
expect(getByText('note')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the note creation form when the set of eventIds adding a note includes the eventId', () => {
|
||||
const { getByTestId } = renderTestComponents({
|
||||
...defaultProps,
|
||||
eventIdsAddingNotes: new Set(['event1']),
|
||||
});
|
||||
|
||||
expect(getByTestId('new-note-tabs')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,9 +10,16 @@ import type { DataTableRecord } from '@kbn/discover-utils/types';
|
|||
import type { EuiTheme } from '@kbn/react-kibana-context-styled';
|
||||
import type { TimelineItem } from '@kbn/timelines-plugin/common';
|
||||
import type { FC } from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import React, { memo, useMemo, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import type { RowRenderer } from '../../../../../../common/types';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { appSelectors } from '../../../../../common/store';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { timelineActions } from '../../../../store';
|
||||
import { NoteCards } from '../../../notes/note_cards';
|
||||
import type { TimelineResultNote } from '../../../open_timeline/types';
|
||||
import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants';
|
||||
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
|
||||
import { useGetEventTypeRowClassName } from './use_get_event_type_row_classname';
|
||||
|
@ -20,8 +27,15 @@ import { useGetEventTypeRowClassName } from './use_get_event_type_row_classname'
|
|||
export type CustomTimelineDataGridBodyProps = EuiDataGridCustomBodyProps & {
|
||||
rows: Array<DataTableRecord & TimelineItem> | undefined;
|
||||
enabledRowRenderers: RowRenderer[];
|
||||
events: TimelineItem[];
|
||||
eventIdToNoteIds?: Record<string, string[]> | null;
|
||||
eventIdsAddingNotes?: Set<string>;
|
||||
onToggleShowNotes: (eventId?: string) => void;
|
||||
refetch?: () => void;
|
||||
};
|
||||
|
||||
const emptyNotes: string[] = [];
|
||||
|
||||
/**
|
||||
*
|
||||
* In order to render the additional row with every event ( which displays the row-renderer, notes and notes editor)
|
||||
|
@ -37,25 +51,63 @@ export type CustomTimelineDataGridBodyProps = EuiDataGridCustomBodyProps & {
|
|||
* */
|
||||
export const CustomTimelineDataGridBody: FC<CustomTimelineDataGridBodyProps> = memo(
|
||||
function CustomTimelineDataGridBody(props) {
|
||||
const { Cell, visibleColumns, visibleRowData, rows, enabledRowRenderers } = props;
|
||||
|
||||
const {
|
||||
Cell,
|
||||
visibleColumns,
|
||||
visibleRowData,
|
||||
rows,
|
||||
enabledRowRenderers,
|
||||
events = [],
|
||||
eventIdToNoteIds = {},
|
||||
eventIdsAddingNotes = new Set<string>(),
|
||||
onToggleShowNotes,
|
||||
refetch,
|
||||
} = props;
|
||||
const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []);
|
||||
const notesById = useDeepEqualSelector(getNotesByIds);
|
||||
const visibleRows = useMemo(
|
||||
() => (rows ?? []).slice(visibleRowData.startRow, visibleRowData.endRow),
|
||||
[rows, visibleRowData]
|
||||
);
|
||||
const eventIds = useMemo(() => events.map((event) => event._id), [events]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{visibleRows.map((row, rowIndex) => (
|
||||
<CustomDataGridSingleRow
|
||||
rowData={row}
|
||||
rowIndex={rowIndex}
|
||||
key={rowIndex}
|
||||
visibleColumns={visibleColumns}
|
||||
Cell={Cell}
|
||||
enabledRowRenderers={enabledRowRenderers}
|
||||
/>
|
||||
))}
|
||||
{visibleRows.map((row, rowIndex) => {
|
||||
const eventId = eventIds[rowIndex];
|
||||
const noteIds: string[] = (eventIdToNoteIds && eventIdToNoteIds[eventId]) || emptyNotes;
|
||||
const notes = noteIds
|
||||
.map((noteId) => {
|
||||
const note = notesById[noteId];
|
||||
if (note) {
|
||||
return {
|
||||
savedObjectId: note.saveObjectId,
|
||||
note: note.note,
|
||||
noteId: note.id,
|
||||
updated: (note.lastEdit ?? note.created).getTime(),
|
||||
updatedBy: note.user,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((note) => note !== null) as TimelineResultNote[];
|
||||
return (
|
||||
<CustomDataGridSingleRow
|
||||
rowData={row}
|
||||
rowIndex={rowIndex}
|
||||
key={rowIndex}
|
||||
visibleColumns={visibleColumns}
|
||||
Cell={Cell}
|
||||
enabledRowRenderers={enabledRowRenderers}
|
||||
notes={notes}
|
||||
eventIdsAddingNotes={eventIdsAddingNotes}
|
||||
eventId={eventId}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
refetch={refetch}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -74,6 +126,29 @@ const CustomGridRow = styled.div.attrs<{
|
|||
}))`
|
||||
width: fit-content;
|
||||
border-bottom: 1px solid ${(props) => (props.theme as EuiTheme).eui.euiBorderThin};
|
||||
. euiDataGridRowCell--controlColumn {
|
||||
height: 40px;
|
||||
}
|
||||
.udt--customRow {
|
||||
border-radius: 0;
|
||||
padding: ${(props) => (props.theme as EuiTheme).eui.euiDataGridCellPaddingM};
|
||||
max-width: ${(props) => (props.theme as EuiTheme).eui.euiPageDefaultMaxWidth};
|
||||
width: 85vw;
|
||||
}
|
||||
|
||||
.euiCommentEvent__body {
|
||||
background-color: ${(props) => (props.theme as EuiTheme).eui.euiColorEmptyShade};
|
||||
}
|
||||
|
||||
&:has(.unifiedDataTable__cell--expanded) {
|
||||
.euiDataGridRowCell--firstColumn,
|
||||
.euiDataGridRowCell--lastColumn,
|
||||
.euiDataGridRowCell--controlColumn,
|
||||
.udt--customRow {
|
||||
${({ theme }) => `background-color: ${theme.eui.euiColorHighlight};`}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/* below styles as per : https://eui.elastic.co/#/tabular-content/data-grid-advanced#custom-body-renderer */
|
||||
|
@ -84,12 +159,31 @@ const CustomGridRowCellWrapper = styled.div.attrs<{
|
|||
role: 'row',
|
||||
}))`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
.euiDataGridRowCell,
|
||||
.euiDataGridRowCell__content {
|
||||
height: 100%;
|
||||
.unifiedDataTable__rowControl {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.euiDataGridRowCell--controlColumn .euiDataGridRowCell__content {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
type CustomTimelineDataGridSingleRowProps = {
|
||||
rowData: DataTableRecord & TimelineItem;
|
||||
rowIndex: number;
|
||||
} & Pick<CustomTimelineDataGridBodyProps, 'visibleColumns' | 'Cell' | 'enabledRowRenderers'>;
|
||||
notes?: TimelineResultNote[] | null;
|
||||
eventId?: string;
|
||||
eventIdsAddingNotes?: Set<string>;
|
||||
onToggleShowNotes: (eventId?: string) => void;
|
||||
} & Pick<
|
||||
CustomTimelineDataGridBodyProps,
|
||||
'visibleColumns' | 'Cell' | 'enabledRowRenderers' | 'refetch'
|
||||
>;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -99,8 +193,19 @@ type CustomTimelineDataGridSingleRowProps = {
|
|||
const CustomDataGridSingleRow = memo(function CustomDataGridSingleRow(
|
||||
props: CustomTimelineDataGridSingleRowProps
|
||||
) {
|
||||
const { rowIndex, rowData, enabledRowRenderers, visibleColumns, Cell } = props;
|
||||
|
||||
const {
|
||||
rowIndex,
|
||||
rowData,
|
||||
enabledRowRenderers,
|
||||
visibleColumns,
|
||||
Cell,
|
||||
notes,
|
||||
eventIdsAddingNotes,
|
||||
eventId = '',
|
||||
onToggleShowNotes,
|
||||
refetch,
|
||||
} = props;
|
||||
const dispatch = useDispatch();
|
||||
const { canShowRowRenderer } = useStatefulRowRenderer({
|
||||
data: rowData.ecs,
|
||||
rowRenderers: enabledRowRenderers,
|
||||
|
@ -122,6 +227,26 @@ const CustomDataGridSingleRow = memo(function CustomDataGridSingleRow(
|
|||
);
|
||||
const eventTypeRowClassName = useGetEventTypeRowClassName(rowData.ecs);
|
||||
|
||||
const associateNote = useCallback(
|
||||
(noteId: string) => {
|
||||
dispatch(
|
||||
timelineActions.addNoteToEvent({
|
||||
eventId,
|
||||
id: TimelineId.active,
|
||||
noteId,
|
||||
})
|
||||
);
|
||||
if (refetch) {
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[dispatch, eventId, refetch]
|
||||
);
|
||||
|
||||
const renderNotesContainer = useMemo(() => {
|
||||
return ((notes && notes.length > 0) || eventIdsAddingNotes?.has(eventId)) ?? false;
|
||||
}, [notes, eventIdsAddingNotes, eventId]);
|
||||
|
||||
return (
|
||||
<CustomGridRow
|
||||
className={`${rowIndex % 2 === 0 ? 'euiDataGridRow--striped' : ''}`}
|
||||
|
@ -143,6 +268,19 @@ const CustomDataGridSingleRow = memo(function CustomDataGridSingleRow(
|
|||
return null;
|
||||
})}
|
||||
</CustomGridRowCellWrapper>
|
||||
{renderNotesContainer && (
|
||||
<NoteCards
|
||||
ariaRowindex={rowIndex}
|
||||
associateNote={associateNote}
|
||||
className="udt--customRow"
|
||||
data-test-subj="note-cards"
|
||||
notes={notes ?? []}
|
||||
showAddNote={eventIdsAddingNotes?.has(eventId) ?? false}
|
||||
toggleShowAddNote={onToggleShowNotes}
|
||||
eventId={eventId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Timeline Expanded Row */}
|
||||
{canShowRowRenderer ? (
|
||||
<Cell
|
||||
|
|
|
@ -54,6 +54,7 @@ const TestComponent = (props: TestComponentProps) => {
|
|||
itemsPerPage={50}
|
||||
itemsPerPageOptions={[10, 25, 50, 100]}
|
||||
rowRenderers={[]}
|
||||
leadingControlColumns={[]}
|
||||
sort={[['@timestamp', 'desc']]}
|
||||
events={mockTimelineData}
|
||||
onFieldEdited={onFieldEditedMock}
|
||||
|
|
|
@ -68,7 +68,19 @@ type CommonDataTableProps = {
|
|||
dataLoadingState: DataLoadingState;
|
||||
updatedAt: number;
|
||||
isTextBasedQuery?: boolean;
|
||||
} & Pick<UnifiedDataTableProps, 'onSort' | 'onSetColumns' | 'sort' | 'onFilter' | 'isSortEnabled'>;
|
||||
leadingControlColumns: EuiDataGridProps['leadingControlColumns'];
|
||||
cellContext?: EuiDataGridProps['cellContext'];
|
||||
eventIdToNoteIds?: Record<string, string[]>;
|
||||
} & Pick<
|
||||
UnifiedDataTableProps,
|
||||
| 'onSort'
|
||||
| 'onSetColumns'
|
||||
| 'sort'
|
||||
| 'onFilter'
|
||||
| 'renderCustomGridBody'
|
||||
| 'trailingControlColumns'
|
||||
| 'isSortEnabled'
|
||||
>;
|
||||
|
||||
interface DataTableProps extends CommonDataTableProps {
|
||||
dataView: DataView;
|
||||
|
@ -100,6 +112,9 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
|
|||
onSetColumns,
|
||||
onSort,
|
||||
onFilter,
|
||||
leadingControlColumns,
|
||||
cellContext,
|
||||
eventIdToNoteIds,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -369,11 +384,24 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
|
|||
Cell={Cell}
|
||||
visibleColumns={visibleColumns}
|
||||
visibleRowData={visibleRowData}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
setCustomGridBodyProps={setCustomGridBodyProps}
|
||||
events={events}
|
||||
enabledRowRenderers={enabledRowRenderers}
|
||||
eventIdsAddingNotes={cellContext?.eventIdsAddingNotes}
|
||||
onToggleShowNotes={cellContext?.onToggleShowNotes}
|
||||
refetch={refetch}
|
||||
/>
|
||||
),
|
||||
[tableRows, enabledRowRenderers]
|
||||
[
|
||||
tableRows,
|
||||
enabledRowRenderers,
|
||||
events,
|
||||
eventIdToNoteIds,
|
||||
cellContext?.eventIdsAddingNotes,
|
||||
cellContext?.onToggleShowNotes,
|
||||
refetch,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -424,8 +452,10 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
|
|||
showMultiFields={true}
|
||||
cellActionsMetadata={cellActionsMetadata}
|
||||
externalAdditionalControls={additionalControls}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
renderCustomGridBody={renderCustomBodyCallback}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
externalControlColumns={leadingControlColumns}
|
||||
cellContext={cellContext}
|
||||
/>
|
||||
{showExpandedDetails && !isTimelineExpandableFlyoutEnabled && (
|
||||
<DetailsPanel
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { EuiDataGridProps } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHideFor } from '@elastic/eui';
|
||||
import React, { useMemo, useCallback, useState, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -46,6 +47,7 @@ import { DRAG_DROP_FIELD } from './data_table/translations';
|
|||
import { TimelineResizableLayout } from './resizable_layout';
|
||||
import TimelineDataTable from './data_table';
|
||||
import { timelineActions } from '../../../store';
|
||||
import type { TimelineModel } from '../../../store/model';
|
||||
import { getFieldsListCreationOptions } from './get_fields_list_creation_options';
|
||||
import { defaultUdtHeaders } from './default_headers';
|
||||
|
||||
|
@ -115,6 +117,10 @@ interface Props {
|
|||
updatedAt: number;
|
||||
isTextBasedQuery?: boolean;
|
||||
dataView: DataView;
|
||||
trailingControlColumns?: EuiDataGridProps['trailingControlColumns'];
|
||||
leadingControlColumns?: EuiDataGridProps['leadingControlColumns'];
|
||||
pinnedEventIds?: TimelineModel['pinnedEventIds'];
|
||||
eventIdToNoteIds?: TimelineModel['eventIdToNoteIds'];
|
||||
}
|
||||
|
||||
const UnifiedTimelineComponent: React.FC<Props> = ({
|
||||
|
@ -137,6 +143,10 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
|||
updatedAt,
|
||||
isTextBasedQuery,
|
||||
dataView,
|
||||
trailingControlColumns,
|
||||
leadingControlColumns,
|
||||
pinnedEventIds,
|
||||
eventIdToNoteIds,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const unifiedFieldListContainerRef = useRef<UnifiedFieldListSidebarContainerApi>(null);
|
||||
|
@ -156,7 +166,19 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
|||
timelineFilterManager,
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const [eventIdsAddingNotes, setEventIdsAddingNotes] = useState<Set<string>>(new Set());
|
||||
const onToggleShowNotes = useCallback(
|
||||
(eventId: string) => {
|
||||
const newSet = new Set(eventIdsAddingNotes);
|
||||
if (newSet.has(eventId)) {
|
||||
newSet.delete(eventId);
|
||||
setEventIdsAddingNotes(newSet);
|
||||
} else {
|
||||
setEventIdsAddingNotes(newSet.add(eventId));
|
||||
}
|
||||
},
|
||||
[eventIdsAddingNotes]
|
||||
);
|
||||
const fieldListSidebarServices: UnifiedFieldListSidebarContainerProps['services'] = useMemo(
|
||||
() => ({
|
||||
fieldFormats,
|
||||
|
@ -344,6 +366,17 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
|||
onFieldEdited();
|
||||
}, [onFieldEdited]);
|
||||
|
||||
const cellContext = useMemo(() => {
|
||||
return {
|
||||
events,
|
||||
pinnedEventIds,
|
||||
eventIdsAddingNotes,
|
||||
onToggleShowNotes,
|
||||
eventIdToNoteIds,
|
||||
refetch,
|
||||
};
|
||||
}, [events, pinnedEventIds, eventIdsAddingNotes, onToggleShowNotes, eventIdToNoteIds, refetch]);
|
||||
|
||||
return (
|
||||
<TimelineBodyContainer className="timelineBodyContainer" ref={setSidebarContainer}>
|
||||
<TimelineResizableLayout
|
||||
|
@ -418,6 +451,10 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
|||
updatedAt={updatedAt}
|
||||
isTextBasedQuery={isTextBasedQuery}
|
||||
onFilter={onAddFilter as DocViewFilterFn}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
cellContext={cellContext}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
/>
|
||||
</EventDetailsWidthProvider>
|
||||
</DropOverlayWrapper>
|
||||
|
|
|
@ -67,6 +67,11 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
|||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.udtTimeline .euiDataGridHeaderCell.euiDataGridHeaderCell--controlColumn {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.udtTimeline .euiDataGridRowCell--controlColumn {
|
||||
overflow: visible;
|
||||
}
|
||||
|
@ -100,8 +105,9 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
|||
}
|
||||
.udtTimeline .euiDataGridRow:has(.eqlSequence) {
|
||||
.euiDataGridRowCell--firstColumn,
|
||||
.euiDataGridRowCell--lastColumn {
|
||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorPrimary};`}
|
||||
.euiDataGridRowCell--lastColumn,
|
||||
.udt--customRow {
|
||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorPrimary}`};
|
||||
}
|
||||
background: repeating-linear-gradient(
|
||||
127deg,
|
||||
|
@ -113,7 +119,8 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
|||
}
|
||||
.udtTimeline .euiDataGridRow:has(.eqlNonSequence) {
|
||||
.euiDataGridRowCell--firstColumn,
|
||||
.euiDataGridRowCell--lastColumn {
|
||||
.euiDataGridRowCell--lastColumn,
|
||||
.udt--customRow {
|
||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorAccent};`}
|
||||
}
|
||||
background: repeating-linear-gradient(
|
||||
|
@ -126,13 +133,15 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
|||
}
|
||||
.udtTimeline .euiDataGridRow:has(.nonRawEvent) {
|
||||
.euiDataGridRowCell--firstColumn,
|
||||
.euiDataGridRowCell--lastColumn {
|
||||
.euiDataGridRowCell--lastColumn,
|
||||
.udt--customRow {
|
||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorWarning};`}
|
||||
}
|
||||
}
|
||||
.udtTimeline .euiDataGridRow:has(.rawEvent) {
|
||||
.euiDataGridRowCell--firstColumn,
|
||||
.euiDataGridRowCell--lastColumn {
|
||||
.euiDataGridRowCell--lastColumn,
|
||||
.udt--customRow {
|
||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorLightShade};`}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,12 @@ import {
|
|||
TIMELINE_DETAILS_FLYOUT,
|
||||
USER_DETAILS_FLYOUT,
|
||||
} from '../../../../screens/unified_timeline';
|
||||
import {
|
||||
ROW_ADD_NOTES_BUTTON,
|
||||
ADD_NOTE_CONTAINER,
|
||||
RESOLVER_GRAPH_CONTAINER,
|
||||
} from '../../../../screens/timeline';
|
||||
import { OPEN_ANALYZER_BTN } from '../../../../screens/alerts';
|
||||
import { GET_DISCOVER_DATA_GRID_CELL_HEADER } from '../../../../screens/discover';
|
||||
import { addFieldToTable, removeFieldFromTable } from '../../../../tasks/discover';
|
||||
import { login } from '../../../../tasks/login';
|
||||
|
@ -57,6 +63,16 @@ describe.skip(
|
|||
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER('agent.type')).should('not.exist');
|
||||
});
|
||||
|
||||
it('should render the add note button and display the markdown editor', () => {
|
||||
cy.get(ROW_ADD_NOTES_BUTTON).should('be.visible').realClick();
|
||||
cy.get(ADD_NOTE_CONTAINER).should('be.visible');
|
||||
});
|
||||
|
||||
it('should render the analyze event button and display the process analyzer visualization', () => {
|
||||
cy.get(OPEN_ANALYZER_BTN).should('be.visible').realClick();
|
||||
cy.get(RESOLVER_GRAPH_CONTAINER).should('be.visible');
|
||||
});
|
||||
|
||||
// these tests are skipped until we implement the expandable flyout in the unified table for timeline
|
||||
context('flyout', () => {
|
||||
it.skip('should be able to open/close details details/host/user flyout', () => {
|
||||
|
|
|
@ -61,6 +61,10 @@ export const UNLOCKED_ICON = '[data-test-subj="timeline-date-picker-unlock-butto
|
|||
|
||||
export const ROW_ADD_NOTES_BUTTON = '[data-test-subj="timeline-notes-button-small"]';
|
||||
|
||||
export const ADD_NOTE_CONTAINER = '[data-test-subj="add-note-container"]';
|
||||
|
||||
export const RESOLVER_GRAPH_CONTAINER = '[data-test-subj="resolver:graph"]';
|
||||
|
||||
export const NOTE_CARD_CONTENT = '[data-test-subj="notes"]';
|
||||
|
||||
export const NOTE_DESCRIPTION = '[data-test-subj="note-preview-description"]';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue