mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Unified Timeline - Fix Flaky tests (#184747)
## Summary fixes flaky tests and below issues : - https://github.com/elastic/kibana/issues/179831 - https://github.com/elastic/kibana/issues/179843 - https://github.com/elastic/kibana/issues/180956 - https://github.com/elastic/kibana/issues/180937 - https://github.com/elastic/security-team/issues/9549 - https://github.com/elastic/security-team/issues/9548
This commit is contained in:
parent
c9bd32623a
commit
089c61efb0
22 changed files with 493 additions and 225 deletions
|
@ -19,7 +19,9 @@ import React, { memo, useEffect } from 'react';
|
|||
*
|
||||
* */
|
||||
export const getCustomCellPopoverRenderer = () => {
|
||||
return memo(function RenderCustomCellPopover(props: EuiDataGridCellPopoverElementProps) {
|
||||
const RenderCustomCellPopoverMemoized = memo(function RenderCustomCellPopoverMemoized(
|
||||
props: EuiDataGridCellPopoverElementProps
|
||||
) {
|
||||
const { setCellPopoverProps, DefaultCellPopover } = props;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -30,4 +32,10 @@ export const getCustomCellPopoverRenderer = () => {
|
|||
|
||||
return <DefaultCellPopover {...props} />;
|
||||
});
|
||||
|
||||
// Components passed to EUI DataGrid cannot be memoized components
|
||||
// otherwise EUI throws an error `typeof Component !== 'function'`
|
||||
return (props: EuiDataGridCellPopoverElementProps) => (
|
||||
<RenderCustomCellPopoverMemoized {...props} />
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { memo, useEffect, useContext } from 'react';
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
|
@ -47,10 +47,7 @@ export const getRenderCellValueFn = ({
|
|||
externalCustomRenderers?: CustomCellRenderer;
|
||||
isPlainRecord?: boolean;
|
||||
}) => {
|
||||
/**
|
||||
* memo is imperative here otherwise the cell will re-render on every hover on every cell
|
||||
*/
|
||||
return memo(function UnifiedDataTableRenderCellValue({
|
||||
return function UnifiedDataTableRenderCellValue({
|
||||
rowIndex,
|
||||
columnId,
|
||||
isDetails,
|
||||
|
@ -149,7 +146,7 @@ export const getRenderCellValueFn = ({
|
|||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,6 +11,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { TimelineTabs, TableId } from '@kbn/securitysolution-data-table';
|
||||
import { selectTimelineById } from '../../../timelines/store/selectors';
|
||||
import {
|
||||
eventHasNotes,
|
||||
getEventType,
|
||||
|
@ -18,7 +19,7 @@ import {
|
|||
} from '../../../timelines/components/timeline/body/helpers';
|
||||
import { getScopedActions, isTimelineScope } from '../../../helpers';
|
||||
import { useIsInvestigateInResolverActionEnabled } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver';
|
||||
import { timelineActions, timelineSelectors } from '../../../timelines/store';
|
||||
import { timelineActions } from '../../../timelines/store';
|
||||
import type { ActionProps, OnPinEvent } from '../../../../common/types';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import { AddEventNoteAction } from './add_note_icon_item';
|
||||
|
@ -66,11 +67,10 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
'unifiedComponentsInTimelineEnabled'
|
||||
);
|
||||
const emptyNotes: string[] = [];
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const timelineType = useShallowEqualSelector(
|
||||
(state) =>
|
||||
(isTimelineScope(timelineId) ? getTimeline(state, timelineId) : timelineDefaults).timelineType
|
||||
const { timelineType } = useShallowEqualSelector((state) =>
|
||||
isTimelineScope(timelineId) ? selectTimelineById(state, timelineId) : timelineDefaults
|
||||
);
|
||||
|
||||
const { startTransaction } = useStartTransaction();
|
||||
|
||||
const isEnterprisePlus = useLicense().isEnterprise();
|
||||
|
@ -213,8 +213,8 @@ const ActionsComponent: React.FC<ActionProps> = ({
|
|||
onEventDetailsPanelOpened();
|
||||
}, [activeStep, incrementStep, isTourAnchor, isTourShown, onEventDetailsPanelOpened]);
|
||||
const showExpandEvent = useMemo(
|
||||
() => !unifiedComponentsInTimelineEnabled || isEventViewer || timelineId !== TimelineId.active,
|
||||
[isEventViewer, timelineId, unifiedComponentsInTimelineEnabled]
|
||||
() => !unifiedComponentsInTimelineEnabled || isEventViewer,
|
||||
[isEventViewer, unifiedComponentsInTimelineEnabled]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -16,8 +16,7 @@ import { TimelineTabs, TimelineId } from '../../../../common/types';
|
|||
import { isFullScreen } from '../../../timelines/components/timeline/body/column_headers';
|
||||
import { isActiveTimeline } from '../../../helpers';
|
||||
import { getColumnHeader } from '../../../timelines/components/timeline/body/column_headers/helpers';
|
||||
import { timelineActions, timelineSelectors } from '../../../timelines/store';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { timelineActions } from '../../../timelines/store';
|
||||
import { useGlobalFullScreen, useTimelineFullScreen } from '../../containers/use_full_screen';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { DEFAULT_ACTION_BUTTON_WIDTH } from '.';
|
||||
|
@ -27,6 +26,8 @@ import { EXIT_FULL_SCREEN } from '../exit_full_screen/translations';
|
|||
import { EventsSelect } from '../../../timelines/components/timeline/body/column_headers/events_select';
|
||||
import * as i18n from './translations';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { selectTimelineById } from '../../../timelines/store/selectors';
|
||||
|
||||
const SortingColumnsContainer = styled.div`
|
||||
button {
|
||||
|
@ -90,14 +91,14 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = memo(
|
|||
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getManageTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const { defaultColumns } = useDeepEqualSelector((state) =>
|
||||
getManageTimeline(state, timelineId)
|
||||
);
|
||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||
'unifiedComponentsInTimelineEnabled'
|
||||
);
|
||||
|
||||
const { defaultColumns } = useDeepEqualSelector((state) =>
|
||||
selectTimelineById(state, timelineId)
|
||||
);
|
||||
|
||||
const toggleFullScreen = useCallback(() => {
|
||||
if (timelineId === TimelineId.active) {
|
||||
setTimelineFullScreen(!timelineFullScreen);
|
||||
|
|
|
@ -152,8 +152,9 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
|
|||
refetchQuery([timelineQuery]);
|
||||
} else {
|
||||
refetchQuery(globalQuery);
|
||||
if (refetch) refetch();
|
||||
}
|
||||
|
||||
if (refetch) refetch();
|
||||
}, [scopeId, globalQuery, timelineQuery, refetch]);
|
||||
|
||||
const ruleIndex =
|
||||
|
|
|
@ -145,14 +145,16 @@ describe('EventColumnView', () => {
|
|||
});
|
||||
|
||||
test('it renders correct tooltip for NotesButton - timeline template', () => {
|
||||
(useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.template);
|
||||
(useShallowEqualSelector as jest.Mock).mockReturnValue({
|
||||
timelineType: TimelineType.template,
|
||||
});
|
||||
|
||||
const wrapper = mount(<EventColumnView {...props} />, { wrappingComponent: TestProviders });
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual(
|
||||
NOTES_DISABLE_TOOLTIP
|
||||
);
|
||||
(useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default);
|
||||
(useShallowEqualSelector as jest.Mock).mockReturnValue({ timelineType: TimelineType.default });
|
||||
});
|
||||
|
||||
test('it does NOT render a pin button when isEventViewer is true', () => {
|
||||
|
|
|
@ -46,6 +46,8 @@ const defaultProps: UnifiedTimelineBodyProps = {
|
|||
activePage: 0,
|
||||
querySize: 0,
|
||||
},
|
||||
eventIdToNoteIds: {} as Record<string, string[]>,
|
||||
pinnedEventIds: {} as Record<string, boolean>,
|
||||
};
|
||||
|
||||
const renderTestComponents = (props?: UnifiedTimelineBodyProps) => {
|
||||
|
|
|
@ -92,8 +92,6 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
|||
} = useSourcererDataView(SourcererScopeName.timeline);
|
||||
const { augmentedColumnHeaders, timelineQueryFieldsFromColumns } = useTimelineColumns(columns);
|
||||
|
||||
const leadingControlColumns = useTimelineControlColumn(columns, TIMELINE_NO_SORTING);
|
||||
|
||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||
'unifiedComponentsInTimelineEnabled'
|
||||
);
|
||||
|
@ -137,6 +135,14 @@ export const EqlTabContentComponent: React.FC<Props> = ({
|
|||
timerangeKind,
|
||||
});
|
||||
|
||||
const leadingControlColumns = useTimelineControlColumn({
|
||||
columns,
|
||||
sort: TIMELINE_NO_SORTING,
|
||||
timelineId,
|
||||
activeTab: TimelineTabs.eql,
|
||||
refetch,
|
||||
});
|
||||
|
||||
const isQueryLoading = useMemo(
|
||||
() =>
|
||||
dataLoadingState === DataLoadingState.loading ||
|
||||
|
|
|
@ -171,7 +171,13 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
timerangeKind: undefined,
|
||||
});
|
||||
|
||||
const leadingControlColumns = useTimelineControlColumn(columns, sort);
|
||||
const leadingControlColumns = useTimelineControlColumn({
|
||||
columns,
|
||||
sort,
|
||||
timelineId,
|
||||
activeTab: TimelineTabs.pinned,
|
||||
refetch,
|
||||
});
|
||||
|
||||
const isQueryLoading = useMemo(
|
||||
() => [DataLoadingState.loading, DataLoadingState.loadingMore].includes(queryLoadingState),
|
||||
|
|
|
@ -203,7 +203,13 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
timerangeKind,
|
||||
});
|
||||
|
||||
const leadingControlColumns = useTimelineControlColumn(columns, sort);
|
||||
const leadingControlColumns = useTimelineControlColumn({
|
||||
columns,
|
||||
sort,
|
||||
timelineId,
|
||||
activeTab: TimelineTabs.query,
|
||||
refetch,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import type { ComponentProps, FunctionComponent } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import QueryTabContent from '.';
|
||||
import { defaultRowRenderers } from '../../body/renderers';
|
||||
|
@ -15,7 +15,9 @@ import { useTimelineEventsDetails } from '../../../../containers/details';
|
|||
import { useSourcererDataView } from '../../../../../sourcerer/containers';
|
||||
import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks';
|
||||
import {
|
||||
createMockStore,
|
||||
createSecuritySolutionStorageMock,
|
||||
mockGlobalState,
|
||||
mockTimelineData,
|
||||
TestProviders,
|
||||
} from '../../../../../common/mock';
|
||||
|
@ -29,7 +31,13 @@ import { timelineActions } from '../../../../store';
|
|||
import type { ExperimentalFeatures } from '../../../../../../common';
|
||||
import { allowedExperimentalValues } from '../../../../../../common';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { cloneDeep, flatten } from 'lodash';
|
||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../../common/components/user_privileges');
|
||||
|
||||
jest.mock('../../../../containers', () => ({
|
||||
useTimelineEvents: jest.fn(),
|
||||
|
@ -54,9 +62,17 @@ jest.mock('../../../../../common/lib/kuery');
|
|||
|
||||
jest.mock('../../../../../common/hooks/use_experimental_features');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '',
|
||||
search: '',
|
||||
})),
|
||||
}));
|
||||
|
||||
// These tests can take more than standard timeout of 5s
|
||||
// that is why we are setting it to 15s
|
||||
const SPECIAL_TEST_TIMEOUT = 15000;
|
||||
// that is why we are increasing it.
|
||||
const SPECIAL_TEST_TIMEOUT = 50000;
|
||||
|
||||
const useIsExperimentalFeatureEnabledMock = jest.fn((feature: keyof ExperimentalFeatures) => {
|
||||
if (feature === 'unifiedComponentsInTimelineEnabled') {
|
||||
|
@ -67,7 +83,7 @@ const useIsExperimentalFeatureEnabledMock = jest.fn((feature: keyof Experimental
|
|||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
|
||||
// unified-field-list is is reporiting multiple analytics events
|
||||
// unified-field-list is reporting multiple analytics events
|
||||
jest.mock(`@kbn/analytics-client`);
|
||||
|
||||
const TestComponent = (props: Partial<ComponentProps<typeof QueryTabContent>>) => {
|
||||
|
@ -98,44 +114,41 @@ const TestComponent = (props: Partial<ComponentProps<typeof QueryTabContent>>) =
|
|||
return <QueryTabContent {...testComponentDefaultProps} {...props} />;
|
||||
};
|
||||
|
||||
const customColumnOrder = [
|
||||
...defaultUdtHeaders,
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.severity',
|
||||
},
|
||||
];
|
||||
|
||||
const mockState = {
|
||||
...structuredClone(mockGlobalState),
|
||||
};
|
||||
|
||||
mockState.timeline.timelineById[TimelineId.test].columns = customColumnOrder;
|
||||
|
||||
const TestWrapper: FunctionComponent = ({ children }) => {
|
||||
return <TestProviders store={createMockStore(mockState)}>{children}</TestProviders>;
|
||||
};
|
||||
|
||||
const renderTestComponents = (props?: Partial<ComponentProps<typeof TestComponent>>) => {
|
||||
return render(<TestComponent {...props} />, {
|
||||
wrapper: TestProviders,
|
||||
wrapper: TestWrapper,
|
||||
});
|
||||
};
|
||||
|
||||
const changeItemsPerPageTo = (newItemsPerPage: number) => {
|
||||
fireEvent.click(screen.getByTestId('tablePaginationPopoverButton'));
|
||||
fireEvent.click(screen.getByTestId(`tablePagination-${newItemsPerPage}-rows`));
|
||||
expect(screen.getByTestId('tablePaginationPopoverButton')).toHaveTextContent(
|
||||
`Rows per page: ${newItemsPerPage}`
|
||||
);
|
||||
};
|
||||
|
||||
const loadPageMock = jest.fn();
|
||||
|
||||
const useTimelineEventsMock = jest.fn(() => [
|
||||
false,
|
||||
{
|
||||
events: cloneDeep(mockTimelineData),
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
totalPages: 10,
|
||||
},
|
||||
refreshedAt: Date.now(),
|
||||
totalCount: 70,
|
||||
loadPage: loadPageMock,
|
||||
},
|
||||
]);
|
||||
|
||||
const useSourcererDataViewMocked = jest.fn().mockReturnValue({
|
||||
...mockSourcererScope,
|
||||
});
|
||||
|
||||
const { storage: storageMock } = createSecuritySolutionStorageMock();
|
||||
|
||||
// Flaky : See https://github.com/elastic/kibana/issues/179831
|
||||
describe.skip('query tab with unified timeline', () => {
|
||||
let useTimelineEventsMock = jest.fn();
|
||||
|
||||
describe('query tab with unified timeline', () => {
|
||||
const kibanaServiceMock: StartServices = {
|
||||
...createStartServicesMock(),
|
||||
storage: storageMock,
|
||||
|
@ -149,9 +162,20 @@ describe.skip('query tab with unified timeline', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// increase timeout for these tests as they are rendering a complete table with ~30 rows which can take time.
|
||||
const ONE_SECOND = 1000;
|
||||
jest.setTimeout(10 * ONE_SECOND);
|
||||
useTimelineEventsMock = jest.fn(() => [
|
||||
false,
|
||||
{
|
||||
events: structuredClone(mockTimelineData.slice(0, 1)),
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
totalPages: 3,
|
||||
},
|
||||
refreshedAt: Date.now(),
|
||||
totalCount: 3,
|
||||
loadPage: loadPageMock,
|
||||
},
|
||||
]);
|
||||
|
||||
HTMLElement.prototype.getBoundingClientRect = jest.fn(() => {
|
||||
return {
|
||||
width: 1000,
|
||||
|
@ -176,6 +200,12 @@ describe.skip('query tab with unified timeline', () => {
|
|||
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(
|
||||
useIsExperimentalFeatureEnabledMock
|
||||
);
|
||||
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue({
|
||||
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
|
||||
endpointPrivileges: getEndpointPrivilegesInitialStateMock(),
|
||||
detectionEnginePrivileges: { loading: false, error: undefined, result: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
|
@ -235,15 +265,39 @@ describe.skip('query tab with unified timeline', () => {
|
|||
|
||||
fireEvent.click(screen.getByLabelText('Closes this modal window'));
|
||||
|
||||
expect(screen.queryByTestId('row-renderers-modal')).toBeFalsy();
|
||||
expect(screen.queryByTestId('row-renderers-modal')).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByTestId('timeline-row-renderer-0')).toBeFalsy();
|
||||
expect(screen.queryByTestId('timeline-row-renderer-0')).not.toBeInTheDocument();
|
||||
},
|
||||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
beforeEach(() => {
|
||||
// should return all the records instead just 3
|
||||
// as the case in the default mock
|
||||
useTimelineEventsMock = jest.fn(() => [
|
||||
false,
|
||||
{
|
||||
events: structuredClone(mockTimelineData),
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
totalPages: 10,
|
||||
},
|
||||
refreshedAt: Date.now(),
|
||||
totalCount: 70,
|
||||
loadPage: loadPageMock,
|
||||
},
|
||||
]);
|
||||
|
||||
(useTimelineEvents as jest.Mock).mockImplementation(useTimelineEventsMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it(
|
||||
'should paginate correctly',
|
||||
async () => {
|
||||
|
@ -296,9 +350,14 @@ describe.skip('query tab with unified timeline', () => {
|
|||
await waitFor(() => {
|
||||
expect(screen.getByTestId('discoverDocTable')).toBeVisible();
|
||||
});
|
||||
|
||||
const messageColumnIndex =
|
||||
customColumnOrder.findIndex((header) => header.id === 'message') + 3;
|
||||
// 3 is the offset for additional leading columns on left
|
||||
|
||||
expect(container.querySelector('[data-gridcell-column-id="message"]')).toHaveAttribute(
|
||||
'data-gridcell-column-index',
|
||||
'12'
|
||||
String(messageColumnIndex)
|
||||
);
|
||||
|
||||
expect(container.querySelector('[data-gridcell-column-id="message"]')).toBeInTheDocument();
|
||||
|
@ -318,7 +377,7 @@ describe.skip('query tab with unified timeline', () => {
|
|||
await waitFor(() => {
|
||||
expect(container.querySelector('[data-gridcell-column-id="message"]')).toHaveAttribute(
|
||||
'data-gridcell-column-index',
|
||||
'11'
|
||||
String(messageColumnIndex - 1)
|
||||
);
|
||||
});
|
||||
},
|
||||
|
@ -391,7 +450,7 @@ describe.skip('query tab with unified timeline', () => {
|
|||
sort: [
|
||||
{
|
||||
direction: 'asc',
|
||||
esTypes: [],
|
||||
esTypes: ['date'],
|
||||
field: '@timestamp',
|
||||
type: 'date',
|
||||
},
|
||||
|
@ -439,7 +498,7 @@ describe.skip('query tab with unified timeline', () => {
|
|||
sort: [
|
||||
{
|
||||
direction: 'desc',
|
||||
esTypes: [],
|
||||
esTypes: ['date'],
|
||||
field: '@timestamp',
|
||||
type: 'date',
|
||||
},
|
||||
|
@ -498,7 +557,7 @@ describe.skip('query tab with unified timeline', () => {
|
|||
sort: [
|
||||
{
|
||||
direction: 'desc',
|
||||
esTypes: [],
|
||||
esTypes: ['date'],
|
||||
field: '@timestamp',
|
||||
type: 'date',
|
||||
},
|
||||
|
@ -547,7 +606,6 @@ describe.skip('query tab with unified timeline', () => {
|
|||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
|
||||
// Failing: See https://github.com/elastic/kibana/issues/179831
|
||||
it(
|
||||
'should be able to sort by multiple columns',
|
||||
async () => {
|
||||
|
@ -608,7 +666,7 @@ describe.skip('query tab with unified timeline', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fieldListGroupedSelectedFields-count')).toHaveTextContent(
|
||||
'11'
|
||||
String(customColumnOrder.length)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -620,7 +678,7 @@ describe.skip('query tab with unified timeline', () => {
|
|||
// column not longer exists in the table
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fieldListGroupedSelectedFields-count')).toHaveTextContent(
|
||||
'10'
|
||||
String(customColumnOrder.length - 1)
|
||||
);
|
||||
});
|
||||
expect(screen.queryAllByTestId(`dataGridHeaderCell-${field.name}`)).toHaveLength(0);
|
||||
|
@ -640,7 +698,7 @@ describe.skip('query tab with unified timeline', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fieldListGroupedSelectedFields-count')).toHaveTextContent(
|
||||
'11'
|
||||
String(customColumnOrder.length)
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -653,7 +711,7 @@ describe.skip('query tab with unified timeline', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fieldListGroupedSelectedFields-count')).toHaveTextContent(
|
||||
'12'
|
||||
String(customColumnOrder.length + 1)
|
||||
);
|
||||
});
|
||||
expect(screen.queryAllByTestId(`dataGridHeaderCell-${field.name}`)).toHaveLength(1);
|
||||
|
@ -693,50 +751,64 @@ describe.skip('query tab with unified timeline', () => {
|
|||
async () => {
|
||||
renderTestComponents();
|
||||
expect(await screen.findByTestId('timeline-sidebar')).toBeVisible();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('fieldListGroupedAvailableFields-count')).toHaveTextContent(
|
||||
'37'
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('fieldListGroupedFieldGroups')).toBeVisible();
|
||||
|
||||
fireEvent.click(screen.getByTitle('Hide sidebar'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('fieldListGroupedAvailableFields-count')).toHaveLength(0);
|
||||
expect(screen.queryByTestId('fieldListGroupedFieldGroups')).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe('row leading actions', () => {
|
||||
it(
|
||||
'should be able to add notes',
|
||||
async () => {
|
||||
renderTestComponents();
|
||||
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('timeline-notes-button-small'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('add-note-container')).toBeVisible();
|
||||
});
|
||||
},
|
||||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should have all populated fields in Available fields section',
|
||||
'should be cancel adding notes',
|
||||
async () => {
|
||||
const listOfPopulatedFields = new Set(
|
||||
flatten(
|
||||
mockTimelineData.map((dataItem) =>
|
||||
dataItem.data.map((item) =>
|
||||
item.value && item.value.length > 0 ? item.field : undefined
|
||||
)
|
||||
)
|
||||
).filter((item) => typeof item !== 'undefined')
|
||||
);
|
||||
|
||||
renderTestComponents();
|
||||
|
||||
expect(await screen.findByTestId('timeline-sidebar')).toBeVisible();
|
||||
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
|
||||
|
||||
changeItemsPerPageTo(100);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
const availableFields = screen.getByTestId('fieldListGroupedAvailableFields');
|
||||
fireEvent.click(screen.getByTestId('timeline-notes-button-small'));
|
||||
|
||||
for (const field of listOfPopulatedFields) {
|
||||
fireEvent.change(screen.getByTestId('fieldListFiltersFieldSearch'), {
|
||||
target: { value: field },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('add-note-container')).toBeVisible();
|
||||
});
|
||||
|
||||
expect(within(availableFields).getByTestId(`field-${field}`));
|
||||
}
|
||||
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), 'Test Note 1');
|
||||
|
||||
expect(screen.getByTestId('cancel')).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(screen.getByTestId('cancel'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('add-note-container')).not.toBeInTheDocument();
|
||||
});
|
||||
},
|
||||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
|
|
|
@ -5,11 +5,7 @@ Array [
|
|||
Object {
|
||||
"headerCellRender": [Function],
|
||||
"id": "default-timeline-control-column",
|
||||
"rowCellRender": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
"rowCellRender": [Function],
|
||||
"width": 152,
|
||||
},
|
||||
]
|
||||
|
|
|
@ -10,6 +10,8 @@ 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';
|
||||
import { TimelineId } from '@kbn/timelines-plugin/public/store/timeline';
|
||||
import { TimelineTabs } from '../../../../../../common/types';
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
|
||||
useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
|
@ -37,20 +39,42 @@ describe('useTimelineColumns', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const refetchMock = jest.fn();
|
||||
|
||||
describe('leadingControlColumns', () => {
|
||||
it('should return the leading control columns', () => {
|
||||
const { result } = renderHook(() => useTimelineControlColumn(mockColumns, []), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useTimelineControlColumn({
|
||||
columns: mockColumns,
|
||||
sort: [],
|
||||
timelineId: TimelineId.test,
|
||||
activeTab: TimelineTabs.query,
|
||||
refetch: refetchMock,
|
||||
}),
|
||||
{
|
||||
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 { result } = renderHook(
|
||||
() =>
|
||||
useTimelineControlColumn({
|
||||
columns: mockColumns,
|
||||
sort: [],
|
||||
timelineId: TimelineId.test,
|
||||
activeTab: TimelineTabs.query,
|
||||
refetch: refetchMock,
|
||||
}),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
const controlColumn = result.current[0] as EuiDataGridControlColumn;
|
||||
expect(controlColumn.width).toBe(124);
|
||||
});
|
||||
|
@ -58,9 +82,19 @@ describe('useTimelineColumns', () => {
|
|||
useLicenseMock.mockReturnValue({
|
||||
isEnterprise: () => true,
|
||||
});
|
||||
const { result } = renderHook(() => useTimelineControlColumn(mockColumns, []), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useTimelineControlColumn({
|
||||
columns: mockColumns,
|
||||
sort: [],
|
||||
timelineId: TimelineId.test,
|
||||
activeTab: TimelineTabs.query,
|
||||
refetch: refetchMock,
|
||||
}),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
const controlColumn = result.current[0] as EuiDataGridControlColumn;
|
||||
expect(controlColumn.width).toBe(152);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import type { EuiDataGridControlColumn } from '@elastic/eui';
|
||||
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
||||
import type { SortColumnTable } from '@kbn/securitysolution-data-table';
|
||||
import { useLicense } from '../../../../../common/hooks/use_license';
|
||||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
|
@ -14,17 +14,32 @@ import { useSourcererDataView } from '../../../../../sourcerer/containers';
|
|||
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 type { 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';
|
||||
import type { TimelineDataGridCellContext } from '../../types';
|
||||
|
||||
interface UseTimelineControlColumnArgs {
|
||||
columns: ColumnHeaderOptions[];
|
||||
sort: SortColumnTable[];
|
||||
timelineId: string;
|
||||
activeTab: TimelineTabs;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
const EMPTY_STRING_ARRAY: string[] = [];
|
||||
|
||||
const noOp = () => {};
|
||||
const noSelectAll = ({ isSelected }: { isSelected: boolean }) => {};
|
||||
export const useTimelineControlColumn = (
|
||||
columns: ColumnHeaderOptions[],
|
||||
sort: SortColumnTable[]
|
||||
) => {
|
||||
export const useTimelineControlColumn = ({
|
||||
columns,
|
||||
sort,
|
||||
timelineId,
|
||||
activeTab,
|
||||
refetch,
|
||||
}: UseTimelineControlColumnArgs) => {
|
||||
const { browserFields } = useSourcererDataView(SourcererScopeName.timeline);
|
||||
|
||||
const unifiedComponentsInTimelineEnabled = useIsExperimentalFeatureEnabled(
|
||||
|
@ -55,14 +70,35 @@ export const useTimelineControlColumn = (
|
|||
showSelectAllCheckbox={false}
|
||||
showFullScreenToggle={false}
|
||||
sort={sort}
|
||||
tabType={TimelineTabs.pinned}
|
||||
tabType={activeTab}
|
||||
{...props}
|
||||
timelineId={TimelineId.active}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
rowCellRender: ControlColumnCellRender,
|
||||
})) as unknown as EuiDataGridControlColumn[];
|
||||
rowCellRender: (props: EuiDataGridCellValueElementProps & TimelineDataGridCellContext) => {
|
||||
return (
|
||||
<ControlColumnCellRender
|
||||
{...props}
|
||||
timelineId={timelineId}
|
||||
ariaRowindex={props.rowIndex}
|
||||
checked={false}
|
||||
columnValues=""
|
||||
data={props.events[props.rowIndex].data}
|
||||
ecsData={props.events[props.rowIndex].ecs}
|
||||
loadingEventIds={EMPTY_STRING_ARRAY}
|
||||
eventId={props.events[props.rowIndex]?._id}
|
||||
index={props.rowIndex}
|
||||
onEventDetailsPanelOpened={noOp}
|
||||
onRowSelected={noOp}
|
||||
refetch={refetch}
|
||||
showCheckboxes={false}
|
||||
setEventsLoading={noOp}
|
||||
setEventsDeleted={noOp}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
return getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
|
||||
...x,
|
||||
|
@ -76,5 +112,8 @@ export const useTimelineControlColumn = (
|
|||
localColumns,
|
||||
sort,
|
||||
unifiedComponentsInTimelineEnabled,
|
||||
timelineId,
|
||||
activeTab,
|
||||
refetch,
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { TimelineItem } from '@kbn/timelines-plugin/common';
|
||||
import type { TimelineModel } from '../../store/model';
|
||||
|
||||
export interface TimelineDataGridCellContext {
|
||||
events: TimelineItem[];
|
||||
pinnedEventIds: TimelineModel['pinnedEventIds'];
|
||||
eventIdsAddingNotes: Set<string>;
|
||||
onToggleShowNotes: (eventId?: string) => void;
|
||||
eventIdToNoteIds: Record<string, string[]>;
|
||||
refetch: () => void;
|
||||
}
|
|
@ -9,7 +9,6 @@ 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';
|
||||
|
||||
|
@ -20,8 +19,12 @@ export interface UnifiedActionProps extends ActionProps {
|
|||
pinnedEventIds: TimelineModel['pinnedEventIds'];
|
||||
}
|
||||
|
||||
export const ControlColumnCellRender = memo(function RowCellRender(props: UnifiedActionProps) {
|
||||
const { rowIndex, events, ecsData, pinnedEventIds, onToggleShowNotes, eventIdToNoteIds } = props;
|
||||
export const ControlColumnCellRender = memo(function ControlColumnCellRender(
|
||||
props: UnifiedActionProps
|
||||
) {
|
||||
const { rowIndex, events, pinnedEventIds, onToggleShowNotes, eventIdToNoteIds, timelineId } =
|
||||
props;
|
||||
|
||||
const event = useMemo(() => events && events[rowIndex], [events, rowIndex]);
|
||||
const isPinned = useMemo(
|
||||
() => eventIsPinned({ eventId: event?._id, pinnedEventIds }),
|
||||
|
@ -32,17 +35,14 @@ export const ControlColumnCellRender = memo(function RowCellRender(props: Unifie
|
|||
{...props}
|
||||
ariaRowindex={rowIndex}
|
||||
columnValues="columnValues"
|
||||
ecsData={ecsData ?? event.ecs}
|
||||
eventId={event?._id}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
isEventPinned={isPinned}
|
||||
isEventViewer={false}
|
||||
onEventDetailsPanelOpened={noOp}
|
||||
onRuleChange={noOp}
|
||||
showNotes={true}
|
||||
timelineId={TimelineId.active}
|
||||
timelineId={timelineId}
|
||||
toggleShowNotes={onToggleShowNotes}
|
||||
refetch={noOp}
|
||||
rowIndex={rowIndex}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -17,15 +17,29 @@ import type { ComponentProps } from 'react';
|
|||
import { getColumnHeaders } from '../../body/column_headers/helpers';
|
||||
import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks';
|
||||
import { timelineActions } from '../../../../store';
|
||||
import type { ExpandedDetailTimeline } from '../../../../../../common/types';
|
||||
import { useUnifiedTableExpandableFlyout } from '../hooks/use_unified_timeline_expandable_flyout';
|
||||
|
||||
jest.mock('../../../../../sourcerer/containers');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '',
|
||||
search: '',
|
||||
})),
|
||||
}));
|
||||
|
||||
const onFieldEditedMock = jest.fn();
|
||||
const refetchMock = jest.fn();
|
||||
const onEventClosedMock = jest.fn();
|
||||
const onChangePageMock = jest.fn();
|
||||
|
||||
const openFlyoutMock = jest.fn();
|
||||
const closeFlyoutMock = jest.fn();
|
||||
const isExpandableFlyoutDisabled = false;
|
||||
|
||||
jest.mock('../hooks/use_unified_timeline_expandable_flyout');
|
||||
|
||||
const initialEnrichedColumns = getColumnHeaders(
|
||||
defaultUdtHeaders,
|
||||
mockSourcererScope.browserFields
|
||||
|
@ -39,7 +53,7 @@ type TestComponentProps = Partial<ComponentProps<typeof TimelineDataTable>> & {
|
|||
|
||||
// These tests can take more than standard timeout of 5s
|
||||
// that is why we are setting it to 10s
|
||||
const SPECIAL_TEST_TIMEOUT = 10000;
|
||||
const SPECIAL_TEST_TIMEOUT = 50000;
|
||||
|
||||
const TestComponent = (props: TestComponentProps) => {
|
||||
const { store = createMockStore(), ...restProps } = props;
|
||||
|
@ -81,10 +95,17 @@ const getTimelineFromStore = (
|
|||
return store.getState().timeline.timelineById[timelineId];
|
||||
};
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/179843
|
||||
describe.skip('unified data table', () => {
|
||||
describe('unified data table', () => {
|
||||
beforeEach(() => {
|
||||
(useSourcererDataView as jest.Mock).mockReturnValue(mockSourcererScope);
|
||||
(useUnifiedTableExpandableFlyout as jest.Mock).mockReturnValue({
|
||||
isExpandableFlyoutDisabled,
|
||||
openFlyout: openFlyoutMock,
|
||||
closeFlyout: closeFlyoutMock,
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it(
|
||||
|
@ -269,86 +290,11 @@ describe.skip('unified data table', () => {
|
|||
fireEvent.click(screen.getAllByTestId('docTableExpandToggleColumn')[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('timeline:details-panel:flyout')).toBeVisible();
|
||||
expect(openFlyoutMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
},
|
||||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
'should show details flyout when expandedDetails state is set',
|
||||
async () => {
|
||||
const customMockStore = createMockStore();
|
||||
const mockExpandedDetail: ExpandedDetailTimeline = {
|
||||
query: {
|
||||
params: {
|
||||
eventId: 'some_id',
|
||||
indexName: 'security-*',
|
||||
},
|
||||
panelView: 'eventDetail',
|
||||
},
|
||||
};
|
||||
customMockStore.dispatch(
|
||||
timelineActions.toggleDetailPanel({
|
||||
id: TimelineId.test,
|
||||
tabType: TimelineTabs.query,
|
||||
...mockExpandedDetail.query,
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<TestComponent
|
||||
store={customMockStore}
|
||||
showExpandedDetails={true}
|
||||
expandedDetail={mockExpandedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('timeline:details-panel:flyout')).toBeVisible();
|
||||
});
|
||||
},
|
||||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
it(
|
||||
'should close details flyout when close icon is clicked',
|
||||
async () => {
|
||||
const customMockStore = createMockStore();
|
||||
const mockExpandedDetail: ExpandedDetailTimeline = {
|
||||
query: {
|
||||
params: {
|
||||
eventId: 'some_id',
|
||||
indexName: 'security-*',
|
||||
},
|
||||
panelView: 'eventDetail',
|
||||
},
|
||||
};
|
||||
|
||||
customMockStore.dispatch(
|
||||
timelineActions.toggleDetailPanel({
|
||||
id: TimelineId.test,
|
||||
tabType: TimelineTabs.query,
|
||||
...mockExpandedDetail.query,
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<TestComponent
|
||||
store={customMockStore}
|
||||
showExpandedDetails={true}
|
||||
expandedDetail={mockExpandedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('euiFlyoutCloseButton')).toBeVisible();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('euiFlyoutCloseButton'));
|
||||
expect(onEventClosedMock).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
|
|
|
@ -58,6 +58,14 @@ jest.mock('../../../../common/lib/kuery');
|
|||
|
||||
jest.mock('../../../../common/hooks/use_experimental_features');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '',
|
||||
search: '',
|
||||
})),
|
||||
}));
|
||||
|
||||
const useIsExperimentalFeatureEnabledMock = jest.fn((feature: keyof ExperimentalFeatures) => {
|
||||
if (feature === 'unifiedComponentsInTimelineEnabled') {
|
||||
return true;
|
||||
|
@ -79,8 +87,8 @@ const columnsToDisplay = [
|
|||
];
|
||||
|
||||
// These tests can take more than standard timeout of 5s
|
||||
// that is why we are setting it to 10s
|
||||
const SPECIAL_TEST_TIMEOUT = 10000;
|
||||
// that is why we are increasing the timeout
|
||||
const SPECIAL_TEST_TIMEOUT = 50000;
|
||||
|
||||
const localMockedTimelineData = structuredClone(mockTimelineData);
|
||||
|
||||
|
@ -110,6 +118,8 @@ const TestComponent = (props: Partial<ComponentProps<typeof UnifiedTimeline>>) =
|
|||
dataLoadingState: DataLoadingState.loaded,
|
||||
updatedAt: Date.now(),
|
||||
isTextBasedQuery: false,
|
||||
eventIdToNoteIds: {} as Record<string, string[]>,
|
||||
pinnedEventIds: {} as Record<string, boolean>,
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
@ -188,8 +198,6 @@ describe('unified timeline', () => {
|
|||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const ONE_SECOND = 1000;
|
||||
jest.setTimeout(10 * ONE_SECOND);
|
||||
HTMLElement.prototype.getBoundingClientRect = jest.fn(() => {
|
||||
return {
|
||||
width: 1000,
|
||||
|
@ -216,9 +224,7 @@ describe('unified timeline', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// Flaky : See https://github.com/elastic/kibana/issues/179831
|
||||
// removing/moving column current leads to infitinite loop, will be fixed in further PRs.
|
||||
describe.skip('columns', () => {
|
||||
describe('columns', () => {
|
||||
it(
|
||||
'should move column left correctly ',
|
||||
async () => {
|
||||
|
@ -297,7 +303,7 @@ describe('unified timeline', () => {
|
|||
SPECIAL_TEST_TIMEOUT
|
||||
);
|
||||
|
||||
it.skip(
|
||||
it(
|
||||
'should remove column ',
|
||||
async () => {
|
||||
const field = {
|
||||
|
@ -539,9 +545,7 @@ describe('unified timeline', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/180937
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/180956
|
||||
describe.skip('unified field list', () => {
|
||||
describe('unified field list', () => {
|
||||
it(
|
||||
'should be able to add filters',
|
||||
async () => {
|
||||
|
|
|
@ -50,6 +50,7 @@ import { timelineActions } from '../../../store';
|
|||
import type { TimelineModel } from '../../../store/model';
|
||||
import { getFieldsListCreationOptions } from './get_fields_list_creation_options';
|
||||
import { defaultUdtHeaders } from './default_headers';
|
||||
import type { TimelineDataGridCellContext } from '../types';
|
||||
|
||||
const TimelineBodyContainer = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `${className}`,
|
||||
|
@ -119,8 +120,8 @@ interface Props {
|
|||
dataView: DataView;
|
||||
trailingControlColumns?: EuiDataGridProps['trailingControlColumns'];
|
||||
leadingControlColumns?: EuiDataGridProps['leadingControlColumns'];
|
||||
pinnedEventIds?: TimelineModel['pinnedEventIds'];
|
||||
eventIdToNoteIds?: TimelineModel['eventIdToNoteIds'];
|
||||
pinnedEventIds: TimelineModel['pinnedEventIds'];
|
||||
eventIdToNoteIds: TimelineModel['eventIdToNoteIds'];
|
||||
}
|
||||
|
||||
const UnifiedTimelineComponent: React.FC<Props> = ({
|
||||
|
@ -170,8 +171,10 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
|||
} = timelineDataService;
|
||||
|
||||
const [eventIdsAddingNotes, setEventIdsAddingNotes] = useState<Set<string>>(new Set());
|
||||
|
||||
const onToggleShowNotes = useCallback(
|
||||
(eventId: string) => {
|
||||
(eventId?: string) => {
|
||||
if (!eventId) return;
|
||||
const newSet = new Set(eventIdsAddingNotes);
|
||||
if (newSet.has(eventId)) {
|
||||
newSet.delete(eventId);
|
||||
|
@ -370,7 +373,7 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
|||
onFieldEdited();
|
||||
}, [onFieldEdited]);
|
||||
|
||||
const cellContext = useMemo(() => {
|
||||
const cellContext: TimelineDataGridCellContext = useMemo(() => {
|
||||
return {
|
||||
events,
|
||||
pinnedEventIds,
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { getNewRule } from '../../../objects/rule';
|
||||
import { deleteAlertsAndRules } from '../../../tasks/api_calls/common';
|
||||
import { createRule } from '../../../tasks/api_calls/rules';
|
||||
import { login } from '../../../tasks/login';
|
||||
import { visitWithTimeRange } from '../../../tasks/navigation';
|
||||
import { openTimelineUsingToggle } from '../../../tasks/security_main';
|
||||
import { ALERTS_URL } from '../../../urls/navigation';
|
||||
import {
|
||||
createNewTimeline,
|
||||
executeTimelineKQL,
|
||||
executeTimelineSearch,
|
||||
openTimelineEventContextMenu,
|
||||
} from '../../../tasks/timeline';
|
||||
import { MARK_ALERT_ACKNOWLEDGED_BTN } from '../../../screens/alerts';
|
||||
import { GET_TIMELINE_GRID_CELL } from '../../../screens/timeline';
|
||||
|
||||
describe(
|
||||
'Timeline table Row Actions',
|
||||
{
|
||||
tags: ['@ess', '@serverless', '@skipInServerlessMKI'],
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
createRule(getNewRule());
|
||||
login();
|
||||
visitWithTimeRange(ALERTS_URL);
|
||||
openTimelineUsingToggle();
|
||||
createNewTimeline();
|
||||
executeTimelineSearch('*');
|
||||
});
|
||||
|
||||
it('should refresh the table when alert status is changed', () => {
|
||||
executeTimelineKQL('kibana.alert.workflow_status:open');
|
||||
cy.get(GET_TIMELINE_GRID_CELL('@timestamp')).should('have.length', 1);
|
||||
openTimelineEventContextMenu();
|
||||
cy.get(MARK_ALERT_ACKNOWLEDGED_BTN).click();
|
||||
cy.get(GET_TIMELINE_GRID_CELL('@timestamp')).should('have.length', 0);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { deleteAlertsAndRules } from '../../../../tasks/api_calls/common';
|
||||
import { getNewRule } from '../../../../objects/rule';
|
||||
import { createRule } from '../../../../tasks/api_calls/rules';
|
||||
import { MARK_ALERT_ACKNOWLEDGED_BTN } from '../../../../screens/alerts';
|
||||
import { GET_UNIFIED_DATA_GRID_CELL } from '../../../../screens/unified_timeline';
|
||||
import { login } from '../../../../tasks/login';
|
||||
import { visitWithTimeRange } from '../../../../tasks/navigation';
|
||||
import { openTimelineUsingToggle } from '../../../../tasks/security_main';
|
||||
import {
|
||||
createNewTimeline,
|
||||
executeTimelineKQL,
|
||||
executeTimelineSearch,
|
||||
openTimelineEventContextMenu,
|
||||
} from '../../../../tasks/timeline';
|
||||
import { ALERTS_URL } from '../../../../urls/navigation';
|
||||
|
||||
describe(
|
||||
'Unified Timeline table Row Actions',
|
||||
{
|
||||
tags: ['@ess', '@serverless', '@skipInServerlessMKI'],
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'unifiedComponentsInTimelineEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
deleteAlertsAndRules();
|
||||
createRule(getNewRule());
|
||||
login();
|
||||
visitWithTimeRange(ALERTS_URL);
|
||||
openTimelineUsingToggle();
|
||||
createNewTimeline();
|
||||
executeTimelineSearch('*');
|
||||
});
|
||||
|
||||
it('should refresh the table when alert status is changed', () => {
|
||||
executeTimelineKQL('kibana.alert.workflow_status:open');
|
||||
cy.get(GET_UNIFIED_DATA_GRID_CELL('@timestamp', 0)).should('be.visible');
|
||||
openTimelineEventContextMenu();
|
||||
cy.get(MARK_ALERT_ACKNOWLEDGED_BTN).click();
|
||||
cy.get(GET_UNIFIED_DATA_GRID_CELL('@timestamp', 0)).should('not.exist');
|
||||
});
|
||||
}
|
||||
);
|
|
@ -89,12 +89,14 @@ import {
|
|||
BOTTOM_BAR_TIMELINE_PLUS_ICON,
|
||||
BOTTOM_BAR_CREATE_NEW_TIMELINE,
|
||||
BOTTOM_BAR_CREATE_NEW_TIMELINE_TEMPLATE,
|
||||
TIMELINE_FLYOUT,
|
||||
} from '../screens/timeline';
|
||||
|
||||
import { REFRESH_BUTTON, TIMELINE, TIMELINES_TAB_TEMPLATE } from '../screens/timelines';
|
||||
import { drag, drop, waitForTabToBeLoaded } from './common';
|
||||
|
||||
import { closeFieldsBrowser, filterFieldsBrowser } from './fields_browser';
|
||||
import { TIMELINE_CONTEXT_MENU_BTN } from '../screens/alerts';
|
||||
|
||||
const hostExistsQuery = 'host.name: *';
|
||||
|
||||
|
@ -505,3 +507,23 @@ export const selectKqlSearchMode = () => {
|
|||
cy.get(TIMELINE_SEARCH_OR_FILTER).click();
|
||||
cy.get(TIMELINE_KQLMODE_SEARCH).click();
|
||||
};
|
||||
|
||||
export const openTimelineEventContextMenu = (rowIndex: number = 0) => {
|
||||
cy.get(TIMELINE_FLYOUT).within(() => {
|
||||
const togglePopover = () => {
|
||||
cy.get(TIMELINE_CONTEXT_MENU_BTN).eq(rowIndex).should('be.visible');
|
||||
cy.get(TIMELINE_CONTEXT_MENU_BTN).eq(rowIndex).click();
|
||||
cy.get(TIMELINE_CONTEXT_MENU_BTN)
|
||||
.first()
|
||||
.should('be.visible')
|
||||
.then(($btnEl) => {
|
||||
if ($btnEl.attr('data-popover-open') !== 'true') {
|
||||
cy.log(`${TIMELINE_CONTEXT_MENU_BTN} was flaky, attempting to re-open popover`);
|
||||
togglePopover();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
togglePopover();
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue