mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -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.
|
* This data is sent directly to actions.
|
||||||
*/
|
*/
|
||||||
cellActionsMetadata?: Record<string, unknown>;
|
cellActionsMetadata?: Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* Optional extra props passed to the renderCellValue function/component.
|
||||||
|
*/
|
||||||
|
cellContext?: EuiDataGridProps['cellContext'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EuiDataGridMemoized = React.memo(EuiDataGrid);
|
export const EuiDataGridMemoized = React.memo(EuiDataGrid);
|
||||||
|
@ -438,6 +442,7 @@ export const UnifiedDataTable = ({
|
||||||
customGridColumnsConfiguration,
|
customGridColumnsConfiguration,
|
||||||
customControlColumnsConfiguration,
|
customControlColumnsConfiguration,
|
||||||
enableComparisonMode,
|
enableComparisonMode,
|
||||||
|
cellContext,
|
||||||
}: UnifiedDataTableProps) => {
|
}: UnifiedDataTableProps) => {
|
||||||
const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } =
|
const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } =
|
||||||
services;
|
services;
|
||||||
|
@ -1055,6 +1060,7 @@ export const UnifiedDataTable = ({
|
||||||
renderCustomGridBody={renderCustomGridBody}
|
renderCustomGridBody={renderCustomGridBody}
|
||||||
renderCustomToolbar={renderCustomToolbarFn}
|
renderCustomToolbar={renderCustomToolbarFn}
|
||||||
trailingControlColumns={customTrailingControlColumn}
|
trailingControlColumns={customTrailingControlColumn}
|
||||||
|
cellContext={cellContext}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
* 2.0.
|
* 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 { IFieldSubType } from '@kbn/es-query';
|
||||||
import type { FieldBrowserOptions } from '@kbn/triggers-actions-ui-plugin/public';
|
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 type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||||
import { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
import { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||||
|
@ -66,16 +66,7 @@ export interface HeaderActionProps {
|
||||||
|
|
||||||
export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>;
|
export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>;
|
||||||
|
|
||||||
type GenericActionRowCellRenderProps = Pick<
|
export type RowCellRender = EuiDataGridProps['renderCellValue'];
|
||||||
EuiDataGridCellValueElementProps,
|
|
||||||
'rowIndex' | 'columnId'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type RowCellRender =
|
|
||||||
| JSXElementConstructor<GenericActionRowCellRenderProps>
|
|
||||||
| ((props: GenericActionRowCellRenderProps) => JSX.Element)
|
|
||||||
| JSXElementConstructor<ActionProps>
|
|
||||||
| ((props: ActionProps) => JSX.Element);
|
|
||||||
|
|
||||||
export interface ActionProps {
|
export interface ActionProps {
|
||||||
action?: RowCellRender;
|
action?: RowCellRender;
|
||||||
|
|
|
@ -59,6 +59,7 @@ export interface HeaderActionProps {
|
||||||
onSelectAll: ({ isSelected }: { isSelected: boolean }) => void;
|
onSelectAll: ({ isSelected }: { isSelected: boolean }) => void;
|
||||||
showEventsSelect: boolean;
|
showEventsSelect: boolean;
|
||||||
showSelectAllCheckbox: boolean;
|
showSelectAllCheckbox: boolean;
|
||||||
|
showFullScreenToggle?: boolean;
|
||||||
sort: SortColumnTable[];
|
sort: SortColumnTable[];
|
||||||
tabType: string;
|
tabType: string;
|
||||||
timelineId: string;
|
timelineId: string;
|
||||||
|
@ -69,7 +70,8 @@ export type HeaderCellRender = ComponentType | ComponentType<HeaderActionProps>;
|
||||||
type GenericActionRowCellRenderProps = Pick<
|
type GenericActionRowCellRenderProps = Pick<
|
||||||
EuiDataGridCellValueElementProps,
|
EuiDataGridCellValueElementProps,
|
||||||
'rowIndex' | 'columnId'
|
'rowIndex' | 'columnId'
|
||||||
>;
|
> &
|
||||||
|
Partial<EuiDataGridCellValueElementProps>;
|
||||||
|
|
||||||
export type RowCellRender =
|
export type RowCellRender =
|
||||||
| JSXElementConstructor<GenericActionRowCellRenderProps>
|
| JSXElementConstructor<GenericActionRowCellRenderProps>
|
||||||
|
@ -114,7 +116,6 @@ interface AdditionalControlColumnProps {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onRowSelected: OnRowSelected;
|
onRowSelected: OnRowSelected;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
id: string;
|
|
||||||
columnId: string;
|
columnId: string;
|
||||||
loadingEventIds: Readonly<string[]>;
|
loadingEventIds: Readonly<string[]>;
|
||||||
onEventDetailsPanelOpened: () => void;
|
onEventDetailsPanelOpened: () => void;
|
||||||
|
|
|
@ -14,10 +14,10 @@ import { getDefaultControlColumn } from '../../../../timelines/components/timeli
|
||||||
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
||||||
|
|
||||||
jest.mock('../../../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;
|
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
|
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
|
||||||
|
|
||||||
const mockDispatch = jest.fn();
|
const mockDispatch = jest.fn();
|
||||||
jest.mock('react-redux', () => {
|
jest.mock('react-redux', () => {
|
||||||
|
|
|
@ -156,6 +156,7 @@ describe('Actions', () => {
|
||||||
describe('Guided Onboarding Step', () => {
|
describe('Guided Onboarding Step', () => {
|
||||||
const incrementStepMock = jest.fn();
|
const incrementStepMock = jest.fn();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
|
||||||
(useTourContext as jest.Mock).mockReturnValue({
|
(useTourContext as jest.Mock).mockReturnValue({
|
||||||
activeStep: 2,
|
activeStep: 2,
|
||||||
incrementStep: incrementStepMock,
|
incrementStep: incrementStepMock,
|
||||||
|
|
|
@ -68,6 +68,9 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||||
|
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||||
|
'unifiedComponentsInTimelineEnabled'
|
||||||
|
);
|
||||||
const emptyNotes: string[] = [];
|
const emptyNotes: string[] = [];
|
||||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||||
const timelineType = useShallowEqualSelector(
|
const timelineType = useShallowEqualSelector(
|
||||||
|
@ -224,6 +227,10 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
||||||
}
|
}
|
||||||
onEventDetailsPanelOpened();
|
onEventDetailsPanelOpened();
|
||||||
}, [activeStep, incrementStep, isTourAnchor, isTourShown, onEventDetailsPanelOpened]);
|
}, [activeStep, incrementStep, isTourAnchor, isTourShown, onEventDetailsPanelOpened]);
|
||||||
|
const showExpandEvent = useMemo(
|
||||||
|
() => !unifiedComponentsInTimelineEnabled || isEventViewer || timelineId !== TimelineId.active,
|
||||||
|
[isEventViewer, timelineId, unifiedComponentsInTimelineEnabled]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionsContainer>
|
<ActionsContainer>
|
||||||
|
@ -244,35 +251,38 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
||||||
</EventsTdContent>
|
</EventsTdContent>
|
||||||
</div>
|
</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 && (
|
{showExpandEvent && (
|
||||||
<InvestigateInTimelineAction
|
<GuidedOnboardingTourStep
|
||||||
ariaLabel={i18n.SEND_ALERT_TO_TIMELINE_FOR_ROW({ ariaRowindex, columnValues })}
|
isTourAnchor={isTourAnchor}
|
||||||
key="investigate-in-timeline"
|
onClick={onExpandEvent}
|
||||||
ecsRowData={ecsData}
|
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 && (
|
{!isEventViewer && toggleShowNotes && (
|
||||||
<>
|
<>
|
||||||
<AddEventNoteAction
|
<AddEventNoteAction
|
||||||
|
@ -281,6 +291,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
||||||
showNotes={showNotes ?? false}
|
showNotes={showNotes ?? false}
|
||||||
toggleShowNotes={toggleShowNotes}
|
toggleShowNotes={toggleShowNotes}
|
||||||
timelineType={timelineType}
|
timelineType={timelineType}
|
||||||
|
eventId={eventId}
|
||||||
/>
|
/>
|
||||||
<PinEventAction
|
<PinEventAction
|
||||||
ariaLabel={i18n.PIN_EVENT_FOR_ROW({ ariaRowindex, columnValues, isEventPinned })}
|
ariaLabel={i18n.PIN_EVENT_FOR_ROW({ ariaRowindex, columnValues, isEventPinned })}
|
||||||
|
|
|
@ -17,6 +17,7 @@ interface AddEventNoteActionProps {
|
||||||
showNotes: boolean;
|
showNotes: boolean;
|
||||||
timelineType: TimelineType;
|
timelineType: TimelineType;
|
||||||
toggleShowNotes: () => void;
|
toggleShowNotes: () => void;
|
||||||
|
eventId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
|
const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
|
||||||
|
@ -24,6 +25,7 @@ const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
|
||||||
showNotes,
|
showNotes,
|
||||||
timelineType,
|
timelineType,
|
||||||
toggleShowNotes,
|
toggleShowNotes,
|
||||||
|
eventId,
|
||||||
}) => {
|
}) => {
|
||||||
const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
|
const { kibanaSecuritySolutionsPrivileges } = useUserPrivileges();
|
||||||
|
|
||||||
|
@ -39,6 +41,7 @@ const AddEventNoteActionComponent: React.FC<AddEventNoteActionProps> = ({
|
||||||
toolTip={
|
toolTip={
|
||||||
timelineType === TimelineType.template ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP
|
timelineType === TimelineType.template ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP
|
||||||
}
|
}
|
||||||
|
eventId={eventId}
|
||||||
/>
|
/>
|
||||||
</ActionIconItem>
|
</ActionIconItem>
|
||||||
);
|
);
|
||||||
|
|
|
@ -68,6 +68,7 @@ const defaultProps: HeaderActionProps = {
|
||||||
tabType: TimelineTabs.query,
|
tabType: TimelineTabs.query,
|
||||||
timelineId,
|
timelineId,
|
||||||
width: 10,
|
width: 10,
|
||||||
|
fieldBrowserOptions: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('HeaderActions', () => {
|
describe('HeaderActions', () => {
|
||||||
|
|
|
@ -78,6 +78,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = memo(
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
showEventsSelect,
|
showEventsSelect,
|
||||||
showSelectAllCheckbox,
|
showSelectAllCheckbox,
|
||||||
|
showFullScreenToggle = true,
|
||||||
sort,
|
sort,
|
||||||
tabType,
|
tabType,
|
||||||
timelineId,
|
timelineId,
|
||||||
|
@ -222,17 +223,19 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = memo(
|
||||||
</EventsThContent>
|
</EventsThContent>
|
||||||
</EventsTh>
|
</EventsTh>
|
||||||
)}
|
)}
|
||||||
<EventsTh role="button">
|
{fieldBrowserOptions && (
|
||||||
<FieldBrowserContainer>
|
<EventsTh role="button">
|
||||||
{triggersActionsUi.getFieldBrowser({
|
<FieldBrowserContainer>
|
||||||
browserFields,
|
{triggersActionsUi.getFieldBrowser({
|
||||||
columnIds: columnHeaders.map(({ id }) => id),
|
browserFields,
|
||||||
onResetColumns,
|
columnIds: columnHeaders.map(({ id }) => id),
|
||||||
onToggleColumn,
|
onResetColumns,
|
||||||
options: fieldBrowserOptions,
|
onToggleColumn,
|
||||||
})}
|
options: fieldBrowserOptions,
|
||||||
</FieldBrowserContainer>
|
})}
|
||||||
</EventsTh>
|
</FieldBrowserContainer>
|
||||||
|
</EventsTh>
|
||||||
|
)}
|
||||||
|
|
||||||
<EventsTh role="button">
|
<EventsTh role="button">
|
||||||
<StatefulRowRenderersBrowser
|
<StatefulRowRenderersBrowser
|
||||||
|
@ -240,33 +243,34 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = memo(
|
||||||
timelineId={timelineId}
|
timelineId={timelineId}
|
||||||
/>
|
/>
|
||||||
</EventsTh>
|
</EventsTh>
|
||||||
|
{showFullScreenToggle && (
|
||||||
<EventsTh role="button">
|
<EventsTh role="button">
|
||||||
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
|
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
|
||||||
<EuiToolTip content={fullScreen ? EXIT_FULL_SCREEN : i18n.FULL_SCREEN}>
|
<EuiToolTip content={fullScreen ? EXIT_FULL_SCREEN : i18n.FULL_SCREEN}>
|
||||||
<EuiButtonIcon
|
<EuiButtonIcon
|
||||||
aria-label={
|
aria-label={
|
||||||
isFullScreen({
|
isFullScreen({
|
||||||
globalFullScreen,
|
globalFullScreen,
|
||||||
isActiveTimelines: isActiveTimeline(timelineId),
|
isActiveTimelines: isActiveTimeline(timelineId),
|
||||||
timelineFullScreen,
|
timelineFullScreen,
|
||||||
})
|
})
|
||||||
? EXIT_FULL_SCREEN
|
? EXIT_FULL_SCREEN
|
||||||
: i18n.FULL_SCREEN
|
: i18n.FULL_SCREEN
|
||||||
}
|
}
|
||||||
display={fullScreen ? 'fill' : 'empty'}
|
display={fullScreen ? 'fill' : 'empty'}
|
||||||
color="primary"
|
color="primary"
|
||||||
data-test-subj={
|
data-test-subj={
|
||||||
// a full screen button gets created for timeline and for the host page
|
// 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
|
// this sets the data-test-subj for each case so that tests can differentiate between them
|
||||||
isActiveTimeline(timelineId) ? 'full-screen-active' : 'full-screen'
|
isActiveTimeline(timelineId) ? 'full-screen-active' : 'full-screen'
|
||||||
}
|
}
|
||||||
iconType="fullScreen"
|
iconType="fullScreen"
|
||||||
onClick={toggleFullScreen}
|
onClick={toggleFullScreen}
|
||||||
/>
|
/>
|
||||||
</EuiToolTip>
|
</EuiToolTip>
|
||||||
</EventsThContent>
|
</EventsThContent>
|
||||||
</EventsTh>
|
</EventsTh>
|
||||||
|
)}
|
||||||
{tabType !== TimelineTabs.eql && (
|
{tabType !== TimelineTabs.eql && (
|
||||||
<EventsTh role="button" data-test-subj="timeline-sorting-fields">
|
<EventsTh role="button" data-test-subj="timeline-sorting-fields">
|
||||||
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
|
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
|
||||||
|
|
|
@ -44,26 +44,45 @@ NotesContainer.displayName = 'NotesContainer';
|
||||||
interface Props {
|
interface Props {
|
||||||
ariaRowindex: number;
|
ariaRowindex: number;
|
||||||
associateNote: AssociateNote;
|
associateNote: AssociateNote;
|
||||||
|
className?: string;
|
||||||
notes: TimelineResultNote[];
|
notes: TimelineResultNote[];
|
||||||
showAddNote: boolean;
|
showAddNote: boolean;
|
||||||
toggleShowAddNote: () => void;
|
toggleShowAddNote: (eventId?: string) => void;
|
||||||
|
eventId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A view for entering and reviewing notes */
|
/** A view for entering and reviewing notes */
|
||||||
export const NoteCards = React.memo<Props>(
|
export const NoteCards = React.memo<Props>(
|
||||||
({ ariaRowindex, associateNote, notes, showAddNote, toggleShowAddNote }) => {
|
({ ariaRowindex, associateNote, className, notes, showAddNote, toggleShowAddNote, eventId }) => {
|
||||||
const [newNote, setNewNote] = useState('');
|
const [newNote, setNewNote] = useState('');
|
||||||
|
|
||||||
const associateNoteAndToggleShow = useCallback(
|
const associateNoteAndToggleShow = useCallback(
|
||||||
(noteId: string) => {
|
(noteId: string) => {
|
||||||
associateNote(noteId);
|
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 (
|
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 ? (
|
{notes.length ? (
|
||||||
<NotePreviewsContainer data-test-subj="note-previews-container">
|
<NotePreviewsContainer data-test-subj="note-previews-container">
|
||||||
<NotesContainer
|
<NotesContainer
|
||||||
|
@ -85,7 +104,7 @@ export const NoteCards = React.memo<Props>(
|
||||||
<AddNote
|
<AddNote
|
||||||
associateNote={associateNoteAndToggleShow}
|
associateNote={associateNoteAndToggleShow}
|
||||||
newNote={newNote}
|
newNote={newNote}
|
||||||
onCancelAddNote={toggleShowAddNote}
|
onCancelAddNote={onCancelAddNote}
|
||||||
updateNewNote={setNewNote}
|
updateNewNote={setNewNote}
|
||||||
/>
|
/>
|
||||||
</AddNoteContainer>
|
</AddNoteContainer>
|
||||||
|
|
|
@ -96,6 +96,11 @@ describe('ColumnHeaders', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders the field browser', () => {
|
test('it renders the field browser', () => {
|
||||||
|
const mockCloseEditor = jest.fn();
|
||||||
|
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
|
||||||
|
editorActionsRef.current = { closeEditor: mockCloseEditor };
|
||||||
|
return {};
|
||||||
|
});
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<TestProviders>
|
<TestProviders>
|
||||||
<ColumnHeadersComponent {...defaultProps} />
|
<ColumnHeadersComponent {...defaultProps} />
|
||||||
|
|
|
@ -167,6 +167,7 @@ describe('EventColumnView', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<EventColumnView
|
<EventColumnView
|
||||||
{...props}
|
{...props}
|
||||||
|
timelineId={TimelineId.test}
|
||||||
leadingControlColumns={[testLeadingControlColumn, ...leadingControlColumns]}
|
leadingControlColumns={[testLeadingControlColumn, ...leadingControlColumns]}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
|
|
|
@ -120,9 +120,9 @@ export const isEvenEqlSequence = (event: Ecs): boolean => {
|
||||||
};
|
};
|
||||||
/** Return eventType raw or signal or eql */
|
/** Return eventType raw or signal or eql */
|
||||||
export const getEventType = (event: Ecs): Omit<TimelineEventsType, 'all'> => {
|
export const getEventType = (event: Ecs): Omit<TimelineEventsType, 'all'> => {
|
||||||
if (!isEmpty(event.kibana?.alert?.rule?.uuid)) {
|
if (!isEmpty(event?.kibana?.alert?.rule?.uuid)) {
|
||||||
return 'signal';
|
return 'signal';
|
||||||
} else if (!isEmpty(event.eql?.parentId)) {
|
} else if (!isEmpty(event?.eql?.parentId)) {
|
||||||
return 'eql';
|
return 'eql';
|
||||||
}
|
}
|
||||||
return 'raw';
|
return 'raw';
|
||||||
|
|
|
@ -126,7 +126,7 @@ const FormattedFieldValueComponent: React.FC<{
|
||||||
} else if (fieldType === GEO_FIELD_TYPE) {
|
} else if (fieldType === GEO_FIELD_TYPE) {
|
||||||
return <>{value}</>;
|
return <>{value}</>;
|
||||||
} else if (fieldType === DATE_FIELD_TYPE) {
|
} else if (fieldType === DATE_FIELD_TYPE) {
|
||||||
const classNames = truncate ? 'eui-textTruncate eui-alignMiddle' : undefined;
|
const classNames = truncate ? 'eui-textTruncate' : undefined;
|
||||||
const date = (
|
const date = (
|
||||||
<FormattedDate
|
<FormattedDate
|
||||||
className={classNames}
|
className={classNames}
|
||||||
|
|
|
@ -40,6 +40,10 @@ export const UnifiedTimelineBody = (props: UnifiedTimelineBodyProps) => {
|
||||||
onChangePage,
|
onChangePage,
|
||||||
activeTab,
|
activeTab,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
|
trailingControlColumns,
|
||||||
|
leadingControlColumns,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdToNoteIds,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [pageRows, setPageRows] = useState<TimelineItem[][]>([]);
|
const [pageRows, setPageRows] = useState<TimelineItem[][]>([]);
|
||||||
|
@ -85,6 +89,10 @@ export const UnifiedTimelineBody = (props: UnifiedTimelineBodyProps) => {
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
updatedAt={updatedAt}
|
updatedAt={updatedAt}
|
||||||
isTextBasedQuery={false}
|
isTextBasedQuery={false}
|
||||||
|
trailingControlColumns={trailingControlColumns}
|
||||||
|
leadingControlColumns={leadingControlColumns}
|
||||||
|
pinnedEventIds={pinnedEventIds}
|
||||||
|
eventIdToNoteIds={eventIdToNoteIds}
|
||||||
/>
|
/>
|
||||||
</RootDragDropProvider>
|
</RootDragDropProvider>
|
||||||
</StyledTableFlexItem>
|
</StyledTableFlexItem>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EuiBadge, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
import { EuiBadge, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import type { TimelineTypeLiteral } from '../../../../../common/api/timeline';
|
import type { TimelineTypeLiteral } from '../../../../../common/api/timeline';
|
||||||
|
@ -24,23 +24,32 @@ interface NotesButtonProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
showNotes: boolean;
|
showNotes: boolean;
|
||||||
toggleShowNotes: () => void;
|
toggleShowNotes: () => void | ((eventId: string) => void);
|
||||||
toolTip?: string;
|
toolTip?: string;
|
||||||
timelineType: TimelineTypeLiteral;
|
timelineType: TimelineTypeLiteral;
|
||||||
|
eventId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SmallNotesButtonProps {
|
interface SmallNotesButtonProps {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
toggleShowNotes: () => void;
|
toggleShowNotes: (eventId?: string) => void;
|
||||||
timelineType: TimelineTypeLiteral;
|
timelineType: TimelineTypeLiteral;
|
||||||
|
eventId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NOTES_BUTTON_CLASS_NAME = 'notes-button';
|
export const NOTES_BUTTON_CLASS_NAME = 'notes-button';
|
||||||
|
|
||||||
const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
||||||
({ ariaLabel = i18n.NOTES, isDisabled, toggleShowNotes, timelineType }) => {
|
({ ariaLabel = i18n.NOTES, isDisabled, toggleShowNotes, timelineType, eventId }) => {
|
||||||
const isTemplate = timelineType === TimelineType.template;
|
const isTemplate = timelineType === TimelineType.template;
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (eventId != null) {
|
||||||
|
toggleShowNotes(eventId);
|
||||||
|
} else {
|
||||||
|
toggleShowNotes();
|
||||||
|
}
|
||||||
|
}, [toggleShowNotes, eventId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiButtonIcon
|
<EuiButtonIcon
|
||||||
|
@ -49,7 +58,7 @@ const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
||||||
data-test-subj="timeline-notes-button-small"
|
data-test-subj="timeline-notes-button-small"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
iconType="editorComment"
|
iconType="editorComment"
|
||||||
onClick={toggleShowNotes}
|
onClick={onClick}
|
||||||
size="s"
|
size="s"
|
||||||
isDisabled={isTemplate}
|
isDisabled={isTemplate}
|
||||||
/>
|
/>
|
||||||
|
@ -59,13 +68,14 @@ const SmallNotesButton = React.memo<SmallNotesButtonProps>(
|
||||||
SmallNotesButton.displayName = 'SmallNotesButton';
|
SmallNotesButton.displayName = 'SmallNotesButton';
|
||||||
|
|
||||||
export const NotesButton = React.memo<NotesButtonProps>(
|
export const NotesButton = React.memo<NotesButtonProps>(
|
||||||
({ ariaLabel, isDisabled, showNotes, timelineType, toggleShowNotes, toolTip }) =>
|
({ ariaLabel, isDisabled, showNotes, timelineType, toggleShowNotes, toolTip, eventId }) =>
|
||||||
showNotes ? (
|
showNotes ? (
|
||||||
<SmallNotesButton
|
<SmallNotesButton
|
||||||
ariaLabel={ariaLabel}
|
ariaLabel={ariaLabel}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
toggleShowNotes={toggleShowNotes}
|
toggleShowNotes={toggleShowNotes}
|
||||||
timelineType={timelineType}
|
timelineType={timelineType}
|
||||||
|
eventId={eventId}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip">
|
<EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip">
|
||||||
|
@ -74,6 +84,7 @@ export const NotesButton = React.memo<NotesButtonProps>(
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
toggleShowNotes={toggleShowNotes}
|
toggleShowNotes={toggleShowNotes}
|
||||||
timelineType={timelineType}
|
timelineType={timelineType}
|
||||||
|
eventId={eventId}
|
||||||
/>
|
/>
|
||||||
</EuiToolTip>
|
</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"
|
end="2018-03-24T03:33:52.253Z"
|
||||||
eqlOptions={Object {}}
|
eqlOptions={Object {}}
|
||||||
|
eventIdToNoteIds={Object {}}
|
||||||
expandedDetail={Object {}}
|
expandedDetail={Object {}}
|
||||||
isLive={false}
|
isLive={false}
|
||||||
itemsPerPage={5}
|
itemsPerPage={5}
|
||||||
|
@ -170,6 +171,7 @@ In other use cases the message field can be used to concatenate different values
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
onEventClosed={[MockFunction]}
|
onEventClosed={[MockFunction]}
|
||||||
|
pinnedEventIds={Object {}}
|
||||||
renderCellValue={[Function]}
|
renderCellValue={[Function]}
|
||||||
rowRenderers={
|
rowRenderers={
|
||||||
Array [
|
Array [
|
||||||
|
|
|
@ -85,6 +85,8 @@ describe('Timeline', () => {
|
||||||
start: startDate,
|
start: startDate,
|
||||||
timelineId: TimelineId.test,
|
timelineId: TimelineId.test,
|
||||||
timerangeKind: 'absolute',
|
timerangeKind: 'absolute',
|
||||||
|
pinnedEventIds: {},
|
||||||
|
eventIdToNoteIds: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,11 @@ import type { ConnectedProps } from 'react-redux';
|
||||||
import { connect, useDispatch } from 'react-redux';
|
import { connect, useDispatch } from 'react-redux';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
import { InPortal } from 'react-reverse-portal';
|
import { InPortal } from 'react-reverse-portal';
|
||||||
|
import type { EuiDataGridControlColumn } from '@elastic/eui';
|
||||||
|
|
||||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||||
import { InputsModelId } from '../../../../../common/store/inputs/constants';
|
import { InputsModelId } from '../../../../../common/store/inputs/constants';
|
||||||
|
import type { ControlColumnProps } from '../../../../../../common/types';
|
||||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||||
import { timelineActions, timelineSelectors } from '../../../../store';
|
import { timelineActions, timelineSelectors } from '../../../../store';
|
||||||
|
@ -54,6 +56,7 @@ import type { TimelineTabCommonProps } from '../shared/types';
|
||||||
import { UnifiedTimelineBody } from '../../body/unified_timeline_body';
|
import { UnifiedTimelineBody } from '../../body/unified_timeline_body';
|
||||||
import { EqlTabHeader } from './header';
|
import { EqlTabHeader } from './header';
|
||||||
import { useTimelineColumns } from '../shared/use_timeline_columns';
|
import { useTimelineColumns } from '../shared/use_timeline_columns';
|
||||||
|
import { useTimelineControlColumn } from '../shared/use_timeline_control_columns';
|
||||||
|
|
||||||
export type Props = TimelineTabCommonProps & PropsFromRedux;
|
export type Props = TimelineTabCommonProps & PropsFromRedux;
|
||||||
|
|
||||||
|
@ -73,6 +76,8 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
||||||
showExpandedDetails,
|
showExpandedDetails,
|
||||||
start,
|
start,
|
||||||
timerangeKind,
|
timerangeKind,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdToNoteIds,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { query: eqlQuery = '', ...restEqlOption } = eqlOptions;
|
const { query: eqlQuery = '', ...restEqlOption } = eqlOptions;
|
||||||
|
@ -85,8 +90,9 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
||||||
runtimeMappings,
|
runtimeMappings,
|
||||||
selectedPatterns,
|
selectedPatterns,
|
||||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||||
const { augmentedColumnHeaders, getTimelineQueryFieldsFromColumns, leadingControlColumns } =
|
const { augmentedColumnHeaders, timelineQueryFieldsFromColumns } = useTimelineColumns(columns);
|
||||||
useTimelineColumns(columns);
|
|
||||||
|
const leadingControlColumns = useTimelineControlColumn(columns, TIMELINE_NO_SORTING);
|
||||||
|
|
||||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||||
'unifiedComponentsInTimelineEnabled'
|
'unifiedComponentsInTimelineEnabled'
|
||||||
|
@ -119,7 +125,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
||||||
dataViewId,
|
dataViewId,
|
||||||
endDate: end,
|
endDate: end,
|
||||||
eqlOptions: restEqlOption,
|
eqlOptions: restEqlOption,
|
||||||
fields: getTimelineQueryFieldsFromColumns(),
|
fields: timelineQueryFieldsFromColumns,
|
||||||
filterQuery: eqlQuery ?? '',
|
filterQuery: eqlQuery ?? '',
|
||||||
id: timelineId,
|
id: timelineId,
|
||||||
indexNames: selectedPatterns,
|
indexNames: selectedPatterns,
|
||||||
|
@ -195,6 +201,9 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
||||||
updatedAt={refreshedAt}
|
updatedAt={refreshedAt}
|
||||||
isTextBasedQuery={false}
|
isTextBasedQuery={false}
|
||||||
pageInfo={pageInfo}
|
pageInfo={pageInfo}
|
||||||
|
leadingControlColumns={leadingControlColumns as EuiDataGridControlColumn[]}
|
||||||
|
pinnedEventIds={pinnedEventIds}
|
||||||
|
eventIdToNoteIds={eventIdToNoteIds}
|
||||||
/>
|
/>
|
||||||
</ScrollableFlexItem>
|
</ScrollableFlexItem>
|
||||||
</FullWidthFlexGroup>
|
</FullWidthFlexGroup>
|
||||||
|
@ -238,7 +247,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
||||||
itemsCount: totalCount,
|
itemsCount: totalCount,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
})}
|
})}
|
||||||
leadingControlColumns={leadingControlColumns}
|
leadingControlColumns={leadingControlColumns as ControlColumnProps[]}
|
||||||
trailingControlColumns={timelineEmptyTrailingControlColumns}
|
trailingControlColumns={timelineEmptyTrailingControlColumns}
|
||||||
/>
|
/>
|
||||||
</StyledEuiFlyoutBody>
|
</StyledEuiFlyoutBody>
|
||||||
|
@ -293,8 +302,16 @@ const makeMapStateToProps = () => {
|
||||||
const mapStateToProps = (state: State, { timelineId }: TimelineTabCommonProps) => {
|
const mapStateToProps = (state: State, { timelineId }: TimelineTabCommonProps) => {
|
||||||
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
|
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
|
||||||
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
||||||
const { activeTab, columns, eqlOptions, expandedDetail, itemsPerPage, itemsPerPageOptions } =
|
const {
|
||||||
timeline;
|
activeTab,
|
||||||
|
columns,
|
||||||
|
eqlOptions,
|
||||||
|
expandedDetail,
|
||||||
|
itemsPerPage,
|
||||||
|
itemsPerPageOptions,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdToNoteIds,
|
||||||
|
} = timeline;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTab,
|
activeTab,
|
||||||
|
@ -306,6 +323,8 @@ const makeMapStateToProps = () => {
|
||||||
isLive: input.policy.kind === 'interval',
|
isLive: input.policy.kind === 'interval',
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
itemsPerPageOptions,
|
itemsPerPageOptions,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdToNoteIds,
|
||||||
showExpandedDetails:
|
showExpandedDetails:
|
||||||
!!expandedDetail[TimelineTabs.eql] && !!expandedDetail[TimelineTabs.eql]?.panelView,
|
!!expandedDetail[TimelineTabs.eql] && !!expandedDetail[TimelineTabs.eql]?.panelView,
|
||||||
|
|
||||||
|
@ -338,6 +357,8 @@ const EqlTabContent = connector(
|
||||||
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
||||||
prevProps.timelineId === nextProps.timelineId &&
|
prevProps.timelineId === nextProps.timelineId &&
|
||||||
deepEqual(prevProps.columns, nextProps.columns) &&
|
deepEqual(prevProps.columns, nextProps.columns) &&
|
||||||
|
deepEqual(prevProps.pinnedEventIds, nextProps.pinnedEventIds) &&
|
||||||
|
deepEqual(prevProps.eventIdToNoteIds, nextProps.eventIdToNoteIds) &&
|
||||||
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions)
|
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}
|
itemsPerPage={5}
|
||||||
itemsPerPageOptions={
|
itemsPerPageOptions={
|
||||||
Array [
|
Array [
|
||||||
|
|
|
@ -125,6 +125,8 @@ describe('PinnedTabContent', () => {
|
||||||
pinnedEventIds: {},
|
pinnedEventIds: {},
|
||||||
showExpandedDetails: false,
|
showExpandedDetails: false,
|
||||||
onEventClosed: jest.fn(),
|
onEventClosed: jest.fn(),
|
||||||
|
eventIdToNoteIds: {},
|
||||||
|
expandedDetail: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isEmpty } from 'lodash/fp';
|
import { isEmpty } from 'lodash/fp';
|
||||||
import React, { useMemo, useCallback } from 'react';
|
import React, { useMemo, useCallback, memo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
import type { ConnectedProps } from 'react-redux';
|
import type { ConnectedProps } from 'react-redux';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
|
import type { EuiDataGridControlColumn } from '@elastic/eui';
|
||||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||||
import type { ControlColumnProps } from '../../../../../../common/types';
|
import type { ControlColumnProps } from '../../../../../../common/types';
|
||||||
import { timelineActions, timelineSelectors } from '../../../../store';
|
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 { EventDetailsWidthProvider } from '../../../../../common/components/events_viewer/event_details_width_context';
|
||||||
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
||||||
import { timelineDefaults } from '../../../../store/defaults';
|
import { timelineDefaults } from '../../../../store/defaults';
|
||||||
|
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||||
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
||||||
import { useTimelineFullScreen } from '../../../../../common/containers/use_full_screen';
|
import { useTimelineFullScreen } from '../../../../../common/containers/use_full_screen';
|
||||||
import type { TimelineModel } from '../../../../store/model';
|
import type { TimelineModel } from '../../../../store/model';
|
||||||
|
@ -34,9 +35,7 @@ import type { ToggleDetailPanel } from '../../../../../../common/types/timeline'
|
||||||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||||
import { DetailsPanel } from '../../../side_panel';
|
import { DetailsPanel } from '../../../side_panel';
|
||||||
import { ExitFullScreen } from '../../../../../common/components/exit_full_screen';
|
import { ExitFullScreen } from '../../../../../common/components/exit_full_screen';
|
||||||
import { getDefaultControlColumn } from '../../body/control_columns';
|
import { UnifiedTimelineBody } from '../../body/unified_timeline_body';
|
||||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
|
||||||
import { HeaderActions } from '../../../../../common/components/header_actions/header_actions';
|
|
||||||
import {
|
import {
|
||||||
FullWidthFlexGroup,
|
FullWidthFlexGroup,
|
||||||
ScrollableFlexItem,
|
ScrollableFlexItem,
|
||||||
|
@ -45,10 +44,13 @@ import {
|
||||||
VerticalRule,
|
VerticalRule,
|
||||||
} from '../shared/layout';
|
} from '../shared/layout';
|
||||||
import type { TimelineTabCommonProps } from '../shared/types';
|
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`
|
const ExitFullScreenContainer = styled.div`
|
||||||
width: 180px;
|
width: 180px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface PinnedFilter {
|
interface PinnedFilter {
|
||||||
bool: {
|
bool: {
|
||||||
should: Array<{ match_phrase: { _id: string } }>;
|
should: Array<{ match_phrase: { _id: string } }>;
|
||||||
|
@ -60,6 +62,16 @@ export type Props = TimelineTabCommonProps & PropsFromRedux;
|
||||||
|
|
||||||
const trailingControlColumns: ControlColumnProps[] = []; // stable reference
|
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> = ({
|
export const PinnedTabContentComponent: React.FC<Props> = ({
|
||||||
columns,
|
columns,
|
||||||
timelineId,
|
timelineId,
|
||||||
|
@ -71,6 +83,8 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
||||||
rowRenderers,
|
rowRenderers,
|
||||||
showExpandedDetails,
|
showExpandedDetails,
|
||||||
sort,
|
sort,
|
||||||
|
expandedDetail,
|
||||||
|
eventIdToNoteIds,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
browserFields,
|
browserFields,
|
||||||
|
@ -80,8 +94,9 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
||||||
selectedPatterns,
|
selectedPatterns,
|
||||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||||
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
|
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
|
||||||
const isEnterprisePlus = useLicense().isEnterprise();
|
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||||
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
|
'unifiedComponentsInTimelineEnabled'
|
||||||
|
);
|
||||||
|
|
||||||
const filterQuery = useMemo(() => {
|
const filterQuery = useMemo(() => {
|
||||||
if (isEmpty(pinnedEventIds)) {
|
if (isEmpty(pinnedEventIds)) {
|
||||||
|
@ -138,6 +153,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
||||||
})),
|
})),
|
||||||
[sort]
|
[sort]
|
||||||
);
|
);
|
||||||
|
const { augmentedColumnHeaders } = useTimelineColumns(columns);
|
||||||
|
|
||||||
const [queryLoadingState, { events, totalCount, pageInfo, loadPage, refreshedAt, refetch }] =
|
const [queryLoadingState, { events, totalCount, pageInfo, loadPage, refreshedAt, refetch }] =
|
||||||
useTimelineEvents({
|
useTimelineEvents({
|
||||||
|
@ -155,6 +171,8 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
||||||
timerangeKind: undefined,
|
timerangeKind: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const leadingControlColumns = useTimelineControlColumn(columns, sort);
|
||||||
|
|
||||||
const isQueryLoading = useMemo(
|
const isQueryLoading = useMemo(
|
||||||
() => [DataLoadingState.loading, DataLoadingState.loadingMore].includes(queryLoadingState),
|
() => [DataLoadingState.loading, DataLoadingState.loadingMore].includes(queryLoadingState),
|
||||||
[queryLoadingState]
|
[queryLoadingState]
|
||||||
|
@ -164,14 +182,35 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
||||||
onEventClosed({ tabType: TimelineTabs.pinned, id: timelineId });
|
onEventClosed({ tabType: TimelineTabs.pinned, id: timelineId });
|
||||||
}, [timelineId, onEventClosed]);
|
}, [timelineId, onEventClosed]);
|
||||||
|
|
||||||
const leadingControlColumns = useMemo(
|
if (unifiedComponentsInTimelineEnabled) {
|
||||||
() =>
|
return (
|
||||||
getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
|
<UnifiedTimelineBody
|
||||||
...x,
|
header={<></>}
|
||||||
headerCellRender: HeaderActions,
|
columns={augmentedColumnHeaders}
|
||||||
})),
|
rowRenderers={rowRenderers}
|
||||||
[ACTION_BUTTON_COUNT]
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -204,7 +243,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
||||||
itemsCount: totalCount,
|
itemsCount: totalCount,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
})}
|
})}
|
||||||
leadingControlColumns={leadingControlColumns}
|
leadingControlColumns={leadingControlColumns as ControlColumnProps[]}
|
||||||
trailingControlColumns={trailingControlColumns}
|
trailingControlColumns={trailingControlColumns}
|
||||||
/>
|
/>
|
||||||
</StyledEuiFlyoutBody>
|
</StyledEuiFlyoutBody>
|
||||||
|
@ -252,8 +291,15 @@ const makeMapStateToProps = () => {
|
||||||
const getTimeline = timelineSelectors.getTimelineByIdSelector();
|
const getTimeline = timelineSelectors.getTimelineByIdSelector();
|
||||||
const mapStateToProps = (state: State, { timelineId }: TimelineTabCommonProps) => {
|
const mapStateToProps = (state: State, { timelineId }: TimelineTabCommonProps) => {
|
||||||
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
|
const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
|
||||||
const { columns, expandedDetail, itemsPerPage, itemsPerPageOptions, pinnedEventIds, sort } =
|
const {
|
||||||
timeline;
|
columns,
|
||||||
|
expandedDetail,
|
||||||
|
itemsPerPage,
|
||||||
|
itemsPerPageOptions,
|
||||||
|
pinnedEventIds,
|
||||||
|
sort,
|
||||||
|
eventIdToNoteIds,
|
||||||
|
} = timeline;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns,
|
columns,
|
||||||
|
@ -264,6 +310,8 @@ const makeMapStateToProps = () => {
|
||||||
showExpandedDetails:
|
showExpandedDetails:
|
||||||
!!expandedDetail[TimelineTabs.pinned] && !!expandedDetail[TimelineTabs.pinned]?.panelView,
|
!!expandedDetail[TimelineTabs.pinned] && !!expandedDetail[TimelineTabs.pinned]?.panelView,
|
||||||
sort,
|
sort,
|
||||||
|
expandedDetail,
|
||||||
|
eventIdToNoteIds,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
|
@ -280,7 +328,7 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps);
|
||||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
const PinnedTabContent = connector(
|
const PinnedTabContent = connector(
|
||||||
React.memo(
|
memo(
|
||||||
PinnedTabContentComponent,
|
PinnedTabContentComponent,
|
||||||
(prevProps, nextProps) =>
|
(prevProps, nextProps) =>
|
||||||
prevProps.itemsPerPage === nextProps.itemsPerPage &&
|
prevProps.itemsPerPage === nextProps.itemsPerPage &&
|
||||||
|
@ -288,6 +336,7 @@ const PinnedTabContent = connector(
|
||||||
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
||||||
prevProps.timelineId === nextProps.timelineId &&
|
prevProps.timelineId === nextProps.timelineId &&
|
||||||
deepEqual(prevProps.columns, nextProps.columns) &&
|
deepEqual(prevProps.columns, nextProps.columns) &&
|
||||||
|
deepEqual(prevProps.eventIdToNoteIds, nextProps.eventIdToNoteIds) &&
|
||||||
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
|
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
|
||||||
deepEqual(prevProps.pinnedEventIds, nextProps.pinnedEventIds) &&
|
deepEqual(prevProps.pinnedEventIds, nextProps.pinnedEventIds) &&
|
||||||
deepEqual(prevProps.sort, nextProps.sort)
|
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"
|
end="2018-03-24T03:33:52.253Z"
|
||||||
|
eventIdToNoteIds={Object {}}
|
||||||
expandedDetail={Object {}}
|
expandedDetail={Object {}}
|
||||||
filters={Array []}
|
filters={Array []}
|
||||||
isLive={false}
|
isLive={false}
|
||||||
|
@ -307,6 +308,7 @@ In other use cases the message field can be used to concatenate different values
|
||||||
kqlQueryExpression=" "
|
kqlQueryExpression=" "
|
||||||
kqlQueryLanguage="kuery"
|
kqlQueryLanguage="kuery"
|
||||||
onEventClosed={[MockFunction]}
|
onEventClosed={[MockFunction]}
|
||||||
|
pinnedEventIds={Object {}}
|
||||||
renderCellValue={[Function]}
|
renderCellValue={[Function]}
|
||||||
rowRenderers={
|
rowRenderers={
|
||||||
Array [
|
Array [
|
||||||
|
|
|
@ -113,6 +113,8 @@ describe('Timeline', () => {
|
||||||
timerangeKind: 'absolute',
|
timerangeKind: 'absolute',
|
||||||
activeTab: TimelineTabs.query,
|
activeTab: TimelineTabs.query,
|
||||||
show: true,
|
show: true,
|
||||||
|
pinnedEventIds: {},
|
||||||
|
eventIdToNoteIds: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type { Dispatch } from 'redux';
|
||||||
import type { ConnectedProps } from 'react-redux';
|
import type { ConnectedProps } from 'react-redux';
|
||||||
import { connect, useDispatch } from 'react-redux';
|
import { connect, useDispatch } from 'react-redux';
|
||||||
import deepEqual from 'fast-deep-equal';
|
import deepEqual from 'fast-deep-equal';
|
||||||
|
import type { EuiDataGridControlColumn } from '@elastic/eui';
|
||||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
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 { useInvalidFilterQuery } from '../../../../../common/hooks/use_invalid_filter_query';
|
||||||
import { timelineActions, timelineSelectors } from '../../../../store';
|
import { timelineActions, timelineSelectors } from '../../../../store';
|
||||||
import type { Direction } from '../../../../../../common/search_strategy';
|
import type { Direction } from '../../../../../../common/search_strategy';
|
||||||
|
import type { ControlColumnProps } from '../../../../../../common/types';
|
||||||
import { useTimelineEvents } from '../../../../containers';
|
import { useTimelineEvents } from '../../../../containers';
|
||||||
import { useKibana } from '../../../../../common/lib/kibana';
|
import { useKibana } from '../../../../../common/lib/kibana';
|
||||||
import { StatefulBody } from '../../body';
|
import { StatefulBody } from '../../body';
|
||||||
|
@ -55,6 +57,7 @@ import {
|
||||||
} from '../shared/utils';
|
} from '../shared/utils';
|
||||||
import type { TimelineTabCommonProps } from '../shared/types';
|
import type { TimelineTabCommonProps } from '../shared/types';
|
||||||
import { useTimelineColumns } from '../shared/use_timeline_columns';
|
import { useTimelineColumns } from '../shared/use_timeline_columns';
|
||||||
|
import { useTimelineControlColumn } from '../shared/use_timeline_control_columns';
|
||||||
|
|
||||||
const compareQueryProps = (prevProps: Props, nextProps: Props) =>
|
const compareQueryProps = (prevProps: Props, nextProps: Props) =>
|
||||||
prevProps.kqlMode === nextProps.kqlMode &&
|
prevProps.kqlMode === nextProps.kqlMode &&
|
||||||
|
@ -87,6 +90,8 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
||||||
sort,
|
sort,
|
||||||
timerangeKind,
|
timerangeKind,
|
||||||
expandedDetail,
|
expandedDetail,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdToNoteIds,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const {
|
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
|
// in order to include the exclude filters in the search that are not stored in the timeline
|
||||||
selectedPatterns,
|
selectedPatterns,
|
||||||
} = useSourcererDataView(SourcererScopeName.timeline);
|
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||||
const {
|
|
||||||
augmentedColumnHeaders,
|
|
||||||
defaultColumns,
|
|
||||||
getTimelineQueryFieldsFromColumns,
|
|
||||||
leadingControlColumns,
|
|
||||||
} = useTimelineColumns(columns);
|
|
||||||
|
|
||||||
const { uiSettings, timelineFilterManager } = useKibana().services;
|
const { uiSettings, timelineFilterManager } = useKibana().services;
|
||||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||||
|
@ -171,14 +170,8 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
||||||
type: columnType,
|
type: columnType,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
const { augmentedColumnHeaders, defaultColumns, timelineQueryFieldsFromColumns } =
|
||||||
dispatch(
|
useTimelineColumns(columns);
|
||||||
timelineActions.initializeTimelineSettings({
|
|
||||||
id: timelineId,
|
|
||||||
defaultColumns,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [dispatch, timelineId, defaultColumns]);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
dataLoadingState,
|
dataLoadingState,
|
||||||
|
@ -186,7 +179,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
||||||
] = useTimelineEvents({
|
] = useTimelineEvents({
|
||||||
dataViewId,
|
dataViewId,
|
||||||
endDate: end,
|
endDate: end,
|
||||||
fields: getTimelineQueryFieldsFromColumns(),
|
fields: timelineQueryFieldsFromColumns,
|
||||||
filterQuery: combinedQueries?.filterQuery,
|
filterQuery: combinedQueries?.filterQuery,
|
||||||
id: timelineId,
|
id: timelineId,
|
||||||
indexNames: selectedPatterns,
|
indexNames: selectedPatterns,
|
||||||
|
@ -199,6 +192,17 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
||||||
timerangeKind,
|
timerangeKind,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const leadingControlColumns = useTimelineControlColumn(columns, sort);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(
|
||||||
|
timelineActions.initializeTimelineSettings({
|
||||||
|
id: timelineId,
|
||||||
|
defaultColumns,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, timelineId, defaultColumns]);
|
||||||
|
|
||||||
const isQueryLoading = useMemo(
|
const isQueryLoading = useMemo(
|
||||||
() => [DataLoadingState.loading, DataLoadingState.loadingMore].includes(dataLoadingState),
|
() => [DataLoadingState.loading, DataLoadingState.loadingMore].includes(dataLoadingState),
|
||||||
[dataLoadingState]
|
[dataLoadingState]
|
||||||
|
@ -250,6 +254,9 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
||||||
onEventClosed={onEventClosed}
|
onEventClosed={onEventClosed}
|
||||||
expandedDetail={expandedDetail}
|
expandedDetail={expandedDetail}
|
||||||
showExpandedDetails={showExpandedDetails}
|
showExpandedDetails={showExpandedDetails}
|
||||||
|
leadingControlColumns={leadingControlColumns as EuiDataGridControlColumn[]}
|
||||||
|
eventIdToNoteIds={eventIdToNoteIds}
|
||||||
|
pinnedEventIds={pinnedEventIds}
|
||||||
onChangePage={loadPage}
|
onChangePage={loadPage}
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
updatedAt={refreshedAt}
|
updatedAt={refreshedAt}
|
||||||
|
@ -300,7 +307,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
||||||
itemsCount: totalCount,
|
itemsCount: totalCount,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
})}
|
})}
|
||||||
leadingControlColumns={leadingControlColumns}
|
leadingControlColumns={leadingControlColumns as ControlColumnProps[]}
|
||||||
trailingControlColumns={timelineEmptyTrailingControlColumns}
|
trailingControlColumns={timelineEmptyTrailingControlColumns}
|
||||||
/>
|
/>
|
||||||
</StyledEuiFlyoutBody>
|
</StyledEuiFlyoutBody>
|
||||||
|
@ -359,6 +366,8 @@ const makeMapStateToProps = () => {
|
||||||
activeTab,
|
activeTab,
|
||||||
columns,
|
columns,
|
||||||
dataProviders,
|
dataProviders,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdToNoteIds,
|
||||||
expandedDetail,
|
expandedDetail,
|
||||||
filters,
|
filters,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
|
@ -394,6 +403,8 @@ const makeMapStateToProps = () => {
|
||||||
expandedDetail,
|
expandedDetail,
|
||||||
filters: timelineFilter,
|
filters: timelineFilter,
|
||||||
timelineId,
|
timelineId,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdToNoteIds,
|
||||||
isLive: input.policy.kind === 'interval',
|
isLive: input.policy.kind === 'interval',
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
itemsPerPageOptions,
|
itemsPerPageOptions,
|
||||||
|
@ -437,8 +448,11 @@ const QueryTabContent = connector(
|
||||||
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
|
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
|
||||||
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
prevProps.showExpandedDetails === nextProps.showExpandedDetails &&
|
||||||
prevProps.status === nextProps.status &&
|
prevProps.status === nextProps.status &&
|
||||||
|
prevProps.status === nextProps.status &&
|
||||||
prevProps.timelineId === nextProps.timelineId &&
|
prevProps.timelineId === nextProps.timelineId &&
|
||||||
|
deepEqual(prevProps.eventIdToNoteIds, nextProps.eventIdToNoteIds) &&
|
||||||
deepEqual(prevProps.columns, nextProps.columns) &&
|
deepEqual(prevProps.columns, nextProps.columns) &&
|
||||||
|
deepEqual(prevProps.pinnedEventIds, nextProps.pinnedEventIds) &&
|
||||||
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
|
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
|
||||||
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
|
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
|
||||||
deepEqual(prevProps.sort, nextProps.sort)
|
deepEqual(prevProps.sort, nextProps.sort)
|
||||||
|
|
|
@ -472,26 +472,3 @@ Array [
|
||||||
"process.entry_leader.entity_id",
|
"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 { TestProviders } from '../../../../../common/mock';
|
||||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
|
||||||
import { useTimelineColumns } from './use_timeline_columns';
|
import { useTimelineColumns } from './use_timeline_columns';
|
||||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||||
import { defaultHeaders } from '../../body/column_headers/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;
|
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', () => {
|
describe('useTimelineColumns', () => {
|
||||||
const mockColumns: ColumnHeaderOptions[] = [
|
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', () => {
|
describe('getTimelineQueryFieldsFromColumns', () => {
|
||||||
it('should return the list of all the fields', () => {
|
it('should return the list of all the fields', () => {
|
||||||
const { result } = renderHook(() => useTimelineColumns([]), {
|
const { result } = renderHook(() => useTimelineColumns([]), {
|
||||||
wrapper: TestProviders,
|
wrapper: TestProviders,
|
||||||
});
|
});
|
||||||
expect(result.current.getTimelineQueryFieldsFromColumns()).toMatchSnapshot();
|
expect(result.current.timelineQueryFieldsFromColumns).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
it('should have a width of 152 for 5 actions', () => {
|
it('should have a width of 152 for 5 actions', () => {
|
||||||
const { result } = renderHook(() => useTimelineColumns(mockColumns), {
|
const { result } = renderHook(() => useTimelineColumns(mockColumns), {
|
||||||
wrapper: TestProviders,
|
wrapper: TestProviders,
|
||||||
});
|
});
|
||||||
expect(result.current.getTimelineQueryFieldsFromColumns()).toMatchSnapshot();
|
expect(result.current.timelineQueryFieldsFromColumns).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,15 +6,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isEmpty } from 'lodash/fp';
|
import { isEmpty } from 'lodash/fp';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
|
||||||
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
|
||||||
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
import { useSourcererDataView } from '../../../../../common/containers/sourcerer';
|
||||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||||
import { defaultHeaders } from '../../body/column_headers/default_headers';
|
import { defaultHeaders } from '../../body/column_headers/default_headers';
|
||||||
import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config';
|
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 { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||||
import type { ColumnHeaderOptions } from '../../../../../../common/types';
|
import type { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||||
import { memoizedGetTimelineColumnHeaders } from './utils';
|
import { memoizedGetTimelineColumnHeaders } from './utils';
|
||||||
|
@ -26,9 +23,6 @@ export const useTimelineColumns = (columns: ColumnHeaderOptions[]) => {
|
||||||
'unifiedComponentsInTimelineEnabled'
|
'unifiedComponentsInTimelineEnabled'
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEnterprisePlus = useLicense().isEnterprise();
|
|
||||||
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
|
|
||||||
|
|
||||||
const defaultColumns = useMemo(
|
const defaultColumns = useMemo(
|
||||||
() => (unifiedComponentsInTimelineEnabled ? defaultUdtHeaders : defaultHeaders),
|
() => (unifiedComponentsInTimelineEnabled ? defaultUdtHeaders : defaultHeaders),
|
||||||
[unifiedComponentsInTimelineEnabled]
|
[unifiedComponentsInTimelineEnabled]
|
||||||
|
@ -45,35 +39,19 @@ export const useTimelineColumns = (columns: ColumnHeaderOptions[]) => {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
const getTimelineQueryFieldsFromColumns = useCallback(() => {
|
const timelineQueryFieldsFromColumns = useMemo(() => {
|
||||||
const columnFields = augmentedColumnHeaders.map((c) => c.id);
|
const columnFields = augmentedColumnHeaders.map((c) => c.id);
|
||||||
|
|
||||||
return [...columnFields, ...requiredFieldsForActions];
|
return [...columnFields, ...requiredFieldsForActions];
|
||||||
}, [augmentedColumnHeaders]);
|
}, [augmentedColumnHeaders]);
|
||||||
|
|
||||||
const leadingControlColumns = useMemo(
|
|
||||||
() =>
|
|
||||||
getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
|
|
||||||
...x,
|
|
||||||
headerCellRender: HeaderActions,
|
|
||||||
})),
|
|
||||||
[ACTION_BUTTON_COUNT]
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
defaultColumns,
|
defaultColumns,
|
||||||
localColumns,
|
localColumns,
|
||||||
augmentedColumnHeaders,
|
augmentedColumnHeaders,
|
||||||
getTimelineQueryFieldsFromColumns,
|
timelineQueryFieldsFromColumns,
|
||||||
leadingControlColumns,
|
|
||||||
}),
|
}),
|
||||||
[
|
[augmentedColumnHeaders, defaultColumns, timelineQueryFieldsFromColumns, localColumns]
|
||||||
augmentedColumnHeaders,
|
|
||||||
defaultColumns,
|
|
||||||
getTimelineQueryFieldsFromColumns,
|
|
||||||
leadingControlColumns,
|
|
||||||
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
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`CustomTimelineDataGridBody should render exactly as snapshots 1`] = `
|
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 {
|
.c0 {
|
||||||
width: -webkit-fit-content;
|
width: -webkit-fit-content;
|
||||||
width: -moz-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;
|
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 {
|
.c1 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
display: -ms-flexbox;
|
display: -ms-flexbox;
|
||||||
display: flex;
|
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>
|
<div>
|
||||||
|
@ -34,6 +93,140 @@ exports[`CustomTimelineDataGridBody should render exactly as snapshots 1`] = `
|
||||||
Cell-0-2
|
Cell-0-2
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div>
|
||||||
Cell-0-3
|
Cell-0-3
|
||||||
</div>
|
</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 type { EuiDataGridColumn } from '@elastic/eui';
|
||||||
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
|
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
|
||||||
import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants';
|
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);
|
const testDataRows = structuredClone(mockTimelineData);
|
||||||
|
|
||||||
|
@ -37,6 +40,8 @@ const mockVisibleColumns = ['@timestamp', 'message', 'user.name']
|
||||||
.map((id) => defaultUdtHeaders.find((h) => h.id === id) as EuiDataGridColumn)
|
.map((id) => defaultUdtHeaders.find((h) => h.id === id) as EuiDataGridColumn)
|
||||||
.concat(additionalTrailingColumn);
|
.concat(additionalTrailingColumn);
|
||||||
|
|
||||||
|
const mockEventIdsAddingNotes = new Set<string>();
|
||||||
|
|
||||||
const defaultProps: CustomTimelineDataGridBodyProps = {
|
const defaultProps: CustomTimelineDataGridBodyProps = {
|
||||||
Cell: MockCellComponent,
|
Cell: MockCellComponent,
|
||||||
visibleRowData: { startRow: 0, endRow: 2, visibleRowCount: 2 },
|
visibleRowData: { startRow: 0, endRow: 2, visibleRowCount: 2 },
|
||||||
|
@ -44,6 +49,34 @@ const defaultProps: CustomTimelineDataGridBodyProps = {
|
||||||
enabledRowRenderers: [],
|
enabledRowRenderers: [],
|
||||||
setCustomGridBodyProps: jest.fn(),
|
setCustomGridBodyProps: jest.fn(),
|
||||||
visibleColumns: mockVisibleColumns,
|
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) => {
|
const renderTestComponents = (props?: CustomTimelineDataGridBodyProps) => {
|
||||||
|
@ -58,9 +91,34 @@ const renderTestComponents = (props?: CustomTimelineDataGridBodyProps) => {
|
||||||
|
|
||||||
describe('CustomTimelineDataGridBody', () => {
|
describe('CustomTimelineDataGridBody', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
const now = new Date();
|
||||||
(useStatefulRowRenderer as jest.Mock).mockReturnValue({
|
(useStatefulRowRenderer as jest.Mock).mockReturnValue({
|
||||||
canShowRowRenderer: true,
|
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(() => {
|
afterEach(() => {
|
||||||
|
@ -87,4 +145,18 @@ describe('CustomTimelineDataGridBody', () => {
|
||||||
expect(queryByText('Cell-0-3')).toBeFalsy();
|
expect(queryByText('Cell-0-3')).toBeFalsy();
|
||||||
expect(getByText('Cell-1-3')).toBeInTheDocument();
|
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 { EuiTheme } from '@kbn/react-kibana-context-styled';
|
||||||
import type { TimelineItem } from '@kbn/timelines-plugin/common';
|
import type { TimelineItem } from '@kbn/timelines-plugin/common';
|
||||||
import type { FC } from 'react';
|
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 styled from 'styled-components';
|
||||||
import type { RowRenderer } from '../../../../../../common/types';
|
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 { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants';
|
||||||
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
|
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
|
||||||
import { useGetEventTypeRowClassName } from './use_get_event_type_row_classname';
|
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 & {
|
export type CustomTimelineDataGridBodyProps = EuiDataGridCustomBodyProps & {
|
||||||
rows: Array<DataTableRecord & TimelineItem> | undefined;
|
rows: Array<DataTableRecord & TimelineItem> | undefined;
|
||||||
enabledRowRenderers: RowRenderer[];
|
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)
|
* 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(
|
export const CustomTimelineDataGridBody: FC<CustomTimelineDataGridBodyProps> = memo(
|
||||||
function CustomTimelineDataGridBody(props) {
|
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(
|
const visibleRows = useMemo(
|
||||||
() => (rows ?? []).slice(visibleRowData.startRow, visibleRowData.endRow),
|
() => (rows ?? []).slice(visibleRowData.startRow, visibleRowData.endRow),
|
||||||
[rows, visibleRowData]
|
[rows, visibleRowData]
|
||||||
);
|
);
|
||||||
|
const eventIds = useMemo(() => events.map((event) => event._id), [events]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{visibleRows.map((row, rowIndex) => (
|
{visibleRows.map((row, rowIndex) => {
|
||||||
<CustomDataGridSingleRow
|
const eventId = eventIds[rowIndex];
|
||||||
rowData={row}
|
const noteIds: string[] = (eventIdToNoteIds && eventIdToNoteIds[eventId]) || emptyNotes;
|
||||||
rowIndex={rowIndex}
|
const notes = noteIds
|
||||||
key={rowIndex}
|
.map((noteId) => {
|
||||||
visibleColumns={visibleColumns}
|
const note = notesById[noteId];
|
||||||
Cell={Cell}
|
if (note) {
|
||||||
enabledRowRenderers={enabledRowRenderers}
|
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;
|
width: fit-content;
|
||||||
border-bottom: 1px solid ${(props) => (props.theme as EuiTheme).eui.euiBorderThin};
|
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 */
|
/* 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',
|
role: 'row',
|
||||||
}))`
|
}))`
|
||||||
display: flex;
|
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 = {
|
type CustomTimelineDataGridSingleRowProps = {
|
||||||
rowData: DataTableRecord & TimelineItem;
|
rowData: DataTableRecord & TimelineItem;
|
||||||
rowIndex: number;
|
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(
|
const CustomDataGridSingleRow = memo(function CustomDataGridSingleRow(
|
||||||
props: CustomTimelineDataGridSingleRowProps
|
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({
|
const { canShowRowRenderer } = useStatefulRowRenderer({
|
||||||
data: rowData.ecs,
|
data: rowData.ecs,
|
||||||
rowRenderers: enabledRowRenderers,
|
rowRenderers: enabledRowRenderers,
|
||||||
|
@ -122,6 +227,26 @@ const CustomDataGridSingleRow = memo(function CustomDataGridSingleRow(
|
||||||
);
|
);
|
||||||
const eventTypeRowClassName = useGetEventTypeRowClassName(rowData.ecs);
|
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 (
|
return (
|
||||||
<CustomGridRow
|
<CustomGridRow
|
||||||
className={`${rowIndex % 2 === 0 ? 'euiDataGridRow--striped' : ''}`}
|
className={`${rowIndex % 2 === 0 ? 'euiDataGridRow--striped' : ''}`}
|
||||||
|
@ -143,6 +268,19 @@ const CustomDataGridSingleRow = memo(function CustomDataGridSingleRow(
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
</CustomGridRowCellWrapper>
|
</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 */}
|
{/* Timeline Expanded Row */}
|
||||||
{canShowRowRenderer ? (
|
{canShowRowRenderer ? (
|
||||||
<Cell
|
<Cell
|
||||||
|
|
|
@ -54,6 +54,7 @@ const TestComponent = (props: TestComponentProps) => {
|
||||||
itemsPerPage={50}
|
itemsPerPage={50}
|
||||||
itemsPerPageOptions={[10, 25, 50, 100]}
|
itemsPerPageOptions={[10, 25, 50, 100]}
|
||||||
rowRenderers={[]}
|
rowRenderers={[]}
|
||||||
|
leadingControlColumns={[]}
|
||||||
sort={[['@timestamp', 'desc']]}
|
sort={[['@timestamp', 'desc']]}
|
||||||
events={mockTimelineData}
|
events={mockTimelineData}
|
||||||
onFieldEdited={onFieldEditedMock}
|
onFieldEdited={onFieldEditedMock}
|
||||||
|
|
|
@ -68,7 +68,19 @@ type CommonDataTableProps = {
|
||||||
dataLoadingState: DataLoadingState;
|
dataLoadingState: DataLoadingState;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
isTextBasedQuery?: boolean;
|
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 {
|
interface DataTableProps extends CommonDataTableProps {
|
||||||
dataView: DataView;
|
dataView: DataView;
|
||||||
|
@ -100,6 +112,9 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
|
||||||
onSetColumns,
|
onSetColumns,
|
||||||
onSort,
|
onSort,
|
||||||
onFilter,
|
onFilter,
|
||||||
|
leadingControlColumns,
|
||||||
|
cellContext,
|
||||||
|
eventIdToNoteIds,
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
@ -369,11 +384,24 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
|
||||||
Cell={Cell}
|
Cell={Cell}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
visibleRowData={visibleRowData}
|
visibleRowData={visibleRowData}
|
||||||
|
eventIdToNoteIds={eventIdToNoteIds}
|
||||||
setCustomGridBodyProps={setCustomGridBodyProps}
|
setCustomGridBodyProps={setCustomGridBodyProps}
|
||||||
|
events={events}
|
||||||
enabledRowRenderers={enabledRowRenderers}
|
enabledRowRenderers={enabledRowRenderers}
|
||||||
|
eventIdsAddingNotes={cellContext?.eventIdsAddingNotes}
|
||||||
|
onToggleShowNotes={cellContext?.onToggleShowNotes}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[tableRows, enabledRowRenderers]
|
[
|
||||||
|
tableRows,
|
||||||
|
enabledRowRenderers,
|
||||||
|
events,
|
||||||
|
eventIdToNoteIds,
|
||||||
|
cellContext?.eventIdsAddingNotes,
|
||||||
|
cellContext?.onToggleShowNotes,
|
||||||
|
refetch,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -424,8 +452,10 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
|
||||||
showMultiFields={true}
|
showMultiFields={true}
|
||||||
cellActionsMetadata={cellActionsMetadata}
|
cellActionsMetadata={cellActionsMetadata}
|
||||||
externalAdditionalControls={additionalControls}
|
externalAdditionalControls={additionalControls}
|
||||||
trailingControlColumns={trailingControlColumns}
|
|
||||||
renderCustomGridBody={renderCustomBodyCallback}
|
renderCustomGridBody={renderCustomBodyCallback}
|
||||||
|
trailingControlColumns={trailingControlColumns}
|
||||||
|
externalControlColumns={leadingControlColumns}
|
||||||
|
cellContext={cellContext}
|
||||||
/>
|
/>
|
||||||
{showExpandedDetails && !isTimelineExpandableFlyoutEnabled && (
|
{showExpandedDetails && !isTimelineExpandableFlyoutEnabled && (
|
||||||
<DetailsPanel
|
<DetailsPanel
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
import type { EuiDataGridProps } from '@elastic/eui';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiHideFor } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiHideFor } from '@elastic/eui';
|
||||||
import React, { useMemo, useCallback, useState, useRef } from 'react';
|
import React, { useMemo, useCallback, useState, useRef } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
@ -46,6 +47,7 @@ import { DRAG_DROP_FIELD } from './data_table/translations';
|
||||||
import { TimelineResizableLayout } from './resizable_layout';
|
import { TimelineResizableLayout } from './resizable_layout';
|
||||||
import TimelineDataTable from './data_table';
|
import TimelineDataTable from './data_table';
|
||||||
import { timelineActions } from '../../../store';
|
import { timelineActions } from '../../../store';
|
||||||
|
import type { TimelineModel } from '../../../store/model';
|
||||||
import { getFieldsListCreationOptions } from './get_fields_list_creation_options';
|
import { getFieldsListCreationOptions } from './get_fields_list_creation_options';
|
||||||
import { defaultUdtHeaders } from './default_headers';
|
import { defaultUdtHeaders } from './default_headers';
|
||||||
|
|
||||||
|
@ -115,6 +117,10 @@ interface Props {
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
isTextBasedQuery?: boolean;
|
isTextBasedQuery?: boolean;
|
||||||
dataView: DataView;
|
dataView: DataView;
|
||||||
|
trailingControlColumns?: EuiDataGridProps['trailingControlColumns'];
|
||||||
|
leadingControlColumns?: EuiDataGridProps['leadingControlColumns'];
|
||||||
|
pinnedEventIds?: TimelineModel['pinnedEventIds'];
|
||||||
|
eventIdToNoteIds?: TimelineModel['eventIdToNoteIds'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const UnifiedTimelineComponent: React.FC<Props> = ({
|
const UnifiedTimelineComponent: React.FC<Props> = ({
|
||||||
|
@ -137,6 +143,10 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
||||||
updatedAt,
|
updatedAt,
|
||||||
isTextBasedQuery,
|
isTextBasedQuery,
|
||||||
dataView,
|
dataView,
|
||||||
|
trailingControlColumns,
|
||||||
|
leadingControlColumns,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdToNoteIds,
|
||||||
}) => {
|
}) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const unifiedFieldListContainerRef = useRef<UnifiedFieldListSidebarContainerApi>(null);
|
const unifiedFieldListContainerRef = useRef<UnifiedFieldListSidebarContainerApi>(null);
|
||||||
|
@ -156,7 +166,19 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
||||||
timelineFilterManager,
|
timelineFilterManager,
|
||||||
},
|
},
|
||||||
} = useKibana();
|
} = 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(
|
const fieldListSidebarServices: UnifiedFieldListSidebarContainerProps['services'] = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
fieldFormats,
|
fieldFormats,
|
||||||
|
@ -344,6 +366,17 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
||||||
onFieldEdited();
|
onFieldEdited();
|
||||||
}, [onFieldEdited]);
|
}, [onFieldEdited]);
|
||||||
|
|
||||||
|
const cellContext = useMemo(() => {
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
pinnedEventIds,
|
||||||
|
eventIdsAddingNotes,
|
||||||
|
onToggleShowNotes,
|
||||||
|
eventIdToNoteIds,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}, [events, pinnedEventIds, eventIdsAddingNotes, onToggleShowNotes, eventIdToNoteIds, refetch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TimelineBodyContainer className="timelineBodyContainer" ref={setSidebarContainer}>
|
<TimelineBodyContainer className="timelineBodyContainer" ref={setSidebarContainer}>
|
||||||
<TimelineResizableLayout
|
<TimelineResizableLayout
|
||||||
|
@ -418,6 +451,10 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
||||||
updatedAt={updatedAt}
|
updatedAt={updatedAt}
|
||||||
isTextBasedQuery={isTextBasedQuery}
|
isTextBasedQuery={isTextBasedQuery}
|
||||||
onFilter={onAddFilter as DocViewFilterFn}
|
onFilter={onAddFilter as DocViewFilterFn}
|
||||||
|
trailingControlColumns={trailingControlColumns}
|
||||||
|
leadingControlColumns={leadingControlColumns}
|
||||||
|
cellContext={cellContext}
|
||||||
|
eventIdToNoteIds={eventIdToNoteIds}
|
||||||
/>
|
/>
|
||||||
</EventDetailsWidthProvider>
|
</EventDetailsWidthProvider>
|
||||||
</DropOverlayWrapper>
|
</DropOverlayWrapper>
|
||||||
|
|
|
@ -67,6 +67,11 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.udtTimeline .euiDataGridHeaderCell.euiDataGridHeaderCell--controlColumn {
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.udtTimeline .euiDataGridRowCell--controlColumn {
|
.udtTimeline .euiDataGridRowCell--controlColumn {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
@ -100,8 +105,9 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
||||||
}
|
}
|
||||||
.udtTimeline .euiDataGridRow:has(.eqlSequence) {
|
.udtTimeline .euiDataGridRow:has(.eqlSequence) {
|
||||||
.euiDataGridRowCell--firstColumn,
|
.euiDataGridRowCell--firstColumn,
|
||||||
.euiDataGridRowCell--lastColumn {
|
.euiDataGridRowCell--lastColumn,
|
||||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorPrimary};`}
|
.udt--customRow {
|
||||||
|
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorPrimary}`};
|
||||||
}
|
}
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
127deg,
|
127deg,
|
||||||
|
@ -113,7 +119,8 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
||||||
}
|
}
|
||||||
.udtTimeline .euiDataGridRow:has(.eqlNonSequence) {
|
.udtTimeline .euiDataGridRow:has(.eqlNonSequence) {
|
||||||
.euiDataGridRowCell--firstColumn,
|
.euiDataGridRowCell--firstColumn,
|
||||||
.euiDataGridRowCell--lastColumn {
|
.euiDataGridRowCell--lastColumn,
|
||||||
|
.udt--customRow {
|
||||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorAccent};`}
|
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorAccent};`}
|
||||||
}
|
}
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
|
@ -126,13 +133,15 @@ export const StyledTimelineUnifiedDataTable = styled.div.attrs(({ className = ''
|
||||||
}
|
}
|
||||||
.udtTimeline .euiDataGridRow:has(.nonRawEvent) {
|
.udtTimeline .euiDataGridRow:has(.nonRawEvent) {
|
||||||
.euiDataGridRowCell--firstColumn,
|
.euiDataGridRowCell--firstColumn,
|
||||||
.euiDataGridRowCell--lastColumn {
|
.euiDataGridRowCell--lastColumn,
|
||||||
|
.udt--customRow {
|
||||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorWarning};`}
|
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorWarning};`}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.udtTimeline .euiDataGridRow:has(.rawEvent) {
|
.udtTimeline .euiDataGridRow:has(.rawEvent) {
|
||||||
.euiDataGridRowCell--firstColumn,
|
.euiDataGridRowCell--firstColumn,
|
||||||
.euiDataGridRowCell--lastColumn {
|
.euiDataGridRowCell--lastColumn,
|
||||||
|
.udt--customRow {
|
||||||
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorLightShade};`}
|
${({ theme }) => `border-left: 4px solid ${theme.eui.euiColorLightShade};`}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,12 @@ import {
|
||||||
TIMELINE_DETAILS_FLYOUT,
|
TIMELINE_DETAILS_FLYOUT,
|
||||||
USER_DETAILS_FLYOUT,
|
USER_DETAILS_FLYOUT,
|
||||||
} from '../../../../screens/unified_timeline';
|
} 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 { GET_DISCOVER_DATA_GRID_CELL_HEADER } from '../../../../screens/discover';
|
||||||
import { addFieldToTable, removeFieldFromTable } from '../../../../tasks/discover';
|
import { addFieldToTable, removeFieldFromTable } from '../../../../tasks/discover';
|
||||||
import { login } from '../../../../tasks/login';
|
import { login } from '../../../../tasks/login';
|
||||||
|
@ -57,6 +63,16 @@ describe.skip(
|
||||||
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER('agent.type')).should('not.exist');
|
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
|
// these tests are skipped until we implement the expandable flyout in the unified table for timeline
|
||||||
context('flyout', () => {
|
context('flyout', () => {
|
||||||
it.skip('should be able to open/close details details/host/user 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 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_CARD_CONTENT = '[data-test-subj="notes"]';
|
||||||
|
|
||||||
export const NOTE_DESCRIPTION = '[data-test-subj="note-preview-description"]';
|
export const NOTE_DESCRIPTION = '[data-test-subj="note-preview-description"]';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue