[Security Solution] - remove expandableFlyoutDisabled feature flag (#187759)

This commit is contained in:
Philippe Oberti 2024-07-12 01:27:31 +02:00 committed by GitHub
parent 4b9a9d704c
commit 1f82d5d68c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 340 additions and 1649 deletions

View file

@ -105,11 +105,6 @@ export const allowedExperimentalValues = Object.freeze({
*/
alertTypeEnabled: false,
/**
* Disables expandable flyout
*/
expandableFlyoutDisabled: false,
/**
* Enables new notes
*/
@ -132,7 +127,6 @@ export const allowedExperimentalValues = Object.freeze({
/**
* Enables the Managed User section inside the new user details flyout.
* To see this section you also need expandableFlyoutDisabled flag set to false.
*/
newUserDetailsFlyoutManagedUser: false,

View file

@ -11,7 +11,6 @@ import type { CaseViewRefreshPropInterface } from '@kbn/cases-plugin/common';
import { CaseMetricsFeature } from '@kbn/cases-plugin/common';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { CaseDetailsRefreshContext } from '../../common/components/endpoint';
import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features';
import { DocumentDetailsRightPanelKey } from '../../flyout/document_details/shared/constants/panel_keys';
import { useTourContext } from '../../common/components/guided_onboarding_tour';
import {
@ -58,7 +57,6 @@ const CaseContainerComponent: React.FC = () => {
SecurityPageName.rules
);
const { openFlyout } = useExpandableFlyoutApi();
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const getDetectionsRuleDetailsHref = useCallback(
(ruleId) => detectionsFormatUrl(getRuleDetailsUrl(ruleId ?? '', detectionsUrlSearch)),
@ -69,38 +67,22 @@ const CaseContainerComponent: React.FC = () => {
const showAlertDetails = useCallback(
(alertId: string, index: string) => {
if (!expandableFlyoutDisabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: alertId,
indexName: index,
scopeId: TimelineId.casePage,
},
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: alertId,
indexName: index,
scopeId: TimelineId.casePage,
},
});
telemetry.reportDetailsFlyoutOpened({
location: TimelineId.casePage,
panel: 'right',
});
}
// TODO remove when https://github.com/elastic/security-team/issues/7462 is merged
// support of old flyout in cases page
else {
dispatch(
timelineActions.toggleDetailPanel({
panelView: 'eventDetail',
id: TimelineId.casePage,
params: {
eventId: alertId,
indexName: index,
},
})
);
}
},
});
telemetry.reportDetailsFlyoutOpened({
location: TimelineId.casePage,
panel: 'right',
});
},
[dispatch, expandableFlyoutDisabled, openFlyout, telemetry]
[openFlyout, telemetry]
);
const endpointDetailsHref = (endpointId: string) =>

View file

@ -7,14 +7,9 @@
import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { dataTableActions, TableId } from '@kbn/securitysolution-data-table';
import { LeftPanelNotesTab } from '../../../../flyout/document_details/left';
import { useRouteSpy } from '../../../utils/route/use_route_spy';
import { useKibana } from '../../../lib/kibana';
import { timelineActions } from '../../../../timelines/store';
import { SecurityPageName } from '../../../../../common/constants';
import {
DocumentDetailsLeftPanelKey,
DocumentDetailsRightPanelKey,
@ -23,12 +18,10 @@ import type {
SetEventsDeleted,
SetEventsLoading,
ControlColumnProps,
ExpandedDetailType,
} from '../../../../../common/types';
import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline';
import { TimelineId } from '../../../../../common/types';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { useTourContext } from '../../guided_onboarding_tour';
import { AlertsCasesTourSteps, SecurityStepId } from '../../guided_onboarding_tour/tour_config';
@ -79,8 +72,6 @@ const RowActionComponent = ({
const { telemetry } = useKibana().services;
const { openFlyout } = useExpandableFlyoutApi();
const dispatch = useDispatch();
const [{ pageName }] = useRouteSpy();
const { activeStep, isTourShown } = useTourContext();
const shouldFocusOnOverviewTab =
(activeStep === AlertsCasesTourSteps.expandEvent ||
@ -102,72 +93,27 @@ const RowActionComponent = ({
[columnHeaders, timelineNonEcsData]
);
// TODO remove when https://github.com/elastic/security-team/issues/7462 is merged
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
'securitySolutionNotesEnabled'
);
const showExpandableFlyout =
pageName === SecurityPageName.attackDiscovery ? true : !expandableFlyoutDisabled;
const handleOnEventDetailPanelOpened = useCallback(() => {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'eventDetail',
params: {
eventId: eventId ?? '',
indexName: indexName ?? '',
},
};
if (showExpandableFlyout) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
path: shouldFocusOnOverviewTab ? { tab: 'overview' } : undefined,
params: {
id: eventId,
indexName,
scopeId: tableId,
},
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
path: shouldFocusOnOverviewTab ? { tab: 'overview' } : undefined,
params: {
id: eventId,
indexName,
scopeId: tableId,
},
});
telemetry.reportDetailsFlyoutOpened({
location: tableId,
panel: 'right',
});
}
// TODO remove when https://github.com/elastic/security-team/issues/7462 is merged
// support of old flyout in cases page
else if (tableId === TableId.alertsOnCasePage) {
dispatch(
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
id: TimelineId.casePage,
})
);
}
// TODO remove when https://github.com/elastic/security-team/issues/7462 is merged
// support of old flyout
else {
dispatch(
dataTableActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType,
id: tableId,
})
);
}
}, [
eventId,
indexName,
showExpandableFlyout,
tableId,
openFlyout,
shouldFocusOnOverviewTab,
telemetry,
dispatch,
tabType,
]);
},
});
telemetry.reportDetailsFlyoutOpened({
location: tableId,
panel: 'right',
});
}, [eventId, indexName, tableId, openFlyout, shouldFocusOnOverviewTab, telemetry]);
const toggleShowNotes = useCallback(() => {
openFlyout({
@ -229,14 +175,12 @@ const RowActionComponent = ({
showCheckboxes={showCheckboxes}
tabType={tabType}
timelineId={tableId}
toggleShowNotes={
!expandableFlyoutDisabled && securitySolutionNotesEnabled ? toggleShowNotes : undefined
}
toggleShowNotes={securitySolutionNotesEnabled ? toggleShowNotes : undefined}
width={width}
setEventsLoading={setEventsLoading}
setEventsDeleted={setEventsDeleted}
refetch={refetch}
showNotes={!expandableFlyoutDisabled && securitySolutionNotesEnabled ? true : false}
showNotes={securitySolutionNotesEnabled ? true : false}
/>
)}
</>

View file

@ -220,8 +220,6 @@ const ActionsComponent: React.FC<ActionProps> = ({
'securitySolutionNotesEnabled'
);
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
/* only applicable for new event based notes */
const documentBasedNotes = useSelector((state: State) => selectNotesByDocumentId(state, eventId));
@ -232,18 +230,15 @@ const ActionsComponent: React.FC<ActionProps> = ({
);
const notesCount = useMemo(
() =>
securitySolutionNotesEnabled && !expandableFlyoutDisabled
? documentBasedNotes.length
: timelineNoteIds.length,
[documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled, expandableFlyoutDisabled]
() => (securitySolutionNotesEnabled ? documentBasedNotes.length : timelineNoteIds.length),
[documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled]
);
const noteIds = useMemo(() => {
return securitySolutionNotesEnabled && !expandableFlyoutDisabled
return securitySolutionNotesEnabled
? documentBasedNotes.map((note) => note.noteId)
: timelineNoteIds;
}, [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled, expandableFlyoutDisabled]);
}, [documentBasedNotes, timelineNoteIds, securitySolutionNotesEnabled]);
return (
<ActionsContainer>

View file

@ -1,10 +0,0 @@
/*
* 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 { ExpandedDetailType } from '../../../../common/types';
export type FlyoutUrlState = ExpandedDetailType;

View file

@ -1,103 +0,0 @@
/*
* 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 { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import {
dataTableSelectors,
TableId,
tableDefaults,
dataTableActions,
} from '@kbn/securitysolution-data-table';
import { useIsExperimentalFeatureEnabled } from '../use_experimental_features';
import { useInitializeUrlParam } from '../../utils/global_query_string';
import { URL_PARAM_KEY } from '../use_url_state';
import type { FlyoutUrlState } from './types';
import { useShallowEqualSelector } from '../use_selector';
import { getQueryStringKeyValue } from '../timeline/use_query_timeline_by_id_on_url_change';
/**
* The state of the old flyout of the table in the Alerts page is stored in local storage.
* This hook was created to initialize things and populate the url with the correct param and its value.
* This is only be needed with the old flyout.
* // TODO remove this hook entirely when we delete the old flyout code
*/
export const useInitFlyoutFromUrlParam = () => {
const { search } = useLocation();
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const [urlDetails, setUrlDetails] = useState<FlyoutUrlState | null>(null);
const [hasLoadedUrlDetails, updateHasLoadedUrlDetails] = useState(false);
const dispatch = useDispatch();
const getDataTable = dataTableSelectors.getTableByIdSelector();
// Only allow the alerts page for now to be saved in the url state.
// Allowing only one makes the transition to the expanded flyout much easier as well
const dataTableCurrent = useShallowEqualSelector(
(state) => getDataTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults
);
const onInitialize = useCallback(
(initialState: FlyoutUrlState | null) => {
if (expandableFlyoutDisabled && initialState != null && initialState.panelView) {
setUrlDetails(initialState);
}
},
[expandableFlyoutDisabled]
);
const loadExpandedDetailFromUrl = useCallback(() => {
if (!expandableFlyoutDisabled) return;
const { initialized, isLoading, totalCount, additionalFilters } = dataTableCurrent;
const isTableLoaded = initialized && !isLoading && totalCount > 0;
if (urlDetails) {
if (!additionalFilters || !additionalFilters.showBuildingBlockAlerts) {
// We want to show building block alerts when loading the flyout in case the alert is a building block alert
dispatch(
dataTableActions.updateShowBuildingBlockAlertsFilter({
id: TableId.alertsOnAlertsPage,
showBuildingBlockAlerts: true,
})
);
}
if (isTableLoaded) {
updateHasLoadedUrlDetails(true);
dispatch(
dataTableActions.toggleDetailPanel({
id: TableId.alertsOnAlertsPage,
...urlDetails,
})
);
}
}
}, [dataTableCurrent, dispatch, expandableFlyoutDisabled, urlDetails]);
// The alert page creates a default dataTable slice in redux initially that is later overriden when data is retrieved
// We use the below to store the urlDetails on app load, and then set it when the table is done loading and has data
useEffect(() => {
if (expandableFlyoutDisabled && !hasLoadedUrlDetails) {
loadExpandedDetailFromUrl();
}
}, [hasLoadedUrlDetails, expandableFlyoutDisabled, loadExpandedDetailFromUrl]);
// We check the url for the presence of the old `evenFlyout` parameter. If it exists replace it with the new `flyout` key.
const eventFlyoutKey = getQueryStringKeyValue({ urlKey: URL_PARAM_KEY.eventFlyout, search });
let currentKey = '';
let newKey = '';
if (expandableFlyoutDisabled) {
if (eventFlyoutKey) {
currentKey = URL_PARAM_KEY.eventFlyout;
newKey = URL_PARAM_KEY.flyout;
} else {
currentKey = URL_PARAM_KEY.flyout;
}
}
useInitializeUrlParam(currentKey, onInitialize, newKey);
};

View file

@ -1,59 +0,0 @@
/*
* 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 { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import {
dataTableActions,
dataTableSelectors,
tableDefaults,
TableId,
} from '@kbn/securitysolution-data-table';
import { useIsExperimentalFeatureEnabled } from '../use_experimental_features';
import { ALERTS_PATH } from '../../../../common/constants';
import { useUpdateUrlParam } from '../../utils/global_query_string';
import { useShallowEqualSelector } from '../use_selector';
import { URL_PARAM_KEY } from '../use_url_state';
import type { FlyoutUrlState } from './types';
/**
* The state of the old flyout of the table in the Alerts page is stored in local storage.
* This hook was created to sync the data stored in local storage to the url.
* This is only be needed with the old flyout.
* // TODO remove this hook entirely when we delete the old flyout code
*/
export const useSyncFlyoutUrlParam = () => {
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const updateUrlParam = useUpdateUrlParam<FlyoutUrlState>(URL_PARAM_KEY.flyout);
const { pathname } = useLocation();
const dispatch = useDispatch();
const getDataTable = dataTableSelectors.getTableByIdSelector();
// Only allow the alerts page for now to be saved in the url state
const { expandedDetail } = useShallowEqualSelector(
(state) => getDataTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults
);
useEffect(() => {
if (!expandableFlyoutDisabled) return;
const isOnAlertsPage = pathname === ALERTS_PATH;
if (isOnAlertsPage && expandedDetail != null && expandedDetail?.query) {
updateUrlParam(expandedDetail.query.panelView ? expandedDetail.query : null);
} else if (!isOnAlertsPage && expandedDetail?.query?.panelView) {
// Close the detail panel as it's stored in a top level redux store maintained across views
dispatch(
dataTableActions.toggleDetailPanel({
id: TableId.alertsOnAlertsPage,
})
);
// Clear the reference from the url
updateUrlParam(null);
}
}, [dispatch, expandedDetail, expandableFlyoutDisabled, pathname, updateUrlParam]);
};

View file

@ -12,8 +12,6 @@ import { useUpdateTimerangeOnPageChange } from './search_bar/use_update_timerang
import { useInitTimelineFromUrlParam } from './timeline/use_init_timeline_url_param';
import { useSyncTimelineUrlParam } from './timeline/use_sync_timeline_url_param';
import { useQueryTimelineByIdOnUrlChange } from './timeline/use_query_timeline_by_id_on_url_change';
import { useInitFlyoutFromUrlParam } from './flyout/use_init_flyout_url_param';
import { useSyncFlyoutUrlParam } from './flyout/use_sync_flyout_url_param';
export const useUrlState = () => {
useSyncGlobalQueryString();
@ -23,8 +21,6 @@ export const useUrlState = () => {
useInitTimelineFromUrlParam();
useSyncTimelineUrlParam();
useQueryTimelineByIdOnUrlChange();
useInitFlyoutFromUrlParam();
useSyncFlyoutUrlParam();
};
export const URL_PARAM_KEY = {

View file

@ -29,11 +29,10 @@ export const getUseActionColumnHook =
// we only want to show the note icon if the expandable flyout and the new notes system are enabled
// TODO delete most likely in 8.16
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
'securitySolutionNotesEnabled'
);
if (expandableFlyoutDisabled || !securitySolutionNotesEnabled) {
if (!securitySolutionNotesEnabled) {
ACTION_BUTTON_COUNT--;
}

View file

@ -11,9 +11,6 @@ import { AlertDetailsRedirect } from './alert_details_redirect';
import { TestProviders } from '../../../common/mock';
import { ALERTS_PATH, ALERT_DETAILS_REDIRECT_PATH } from '../../../../common/constants';
import { mockHistory } from '../../../common/utils/route/mocks';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
jest.mock('../../../common/hooks/use_experimental_features');
jest.mock('../../../common/lib/kibana');
@ -149,37 +146,4 @@ describe('AlertDetailsRedirect', () => {
});
});
});
describe('When expandable flyout is enabled', () => {
beforeEach(() => {
jest.mocked(useIsExperimentalFeatureEnabled).mockReturnValue(true);
});
describe('when eventFlyout or flyout are not in the query', () => {
it('redirects to the expected path with the correct query parameters', () => {
const testSearch = `?index=${testIndex}&timestamp=${testTimestamp}`;
const historyMock = {
...mockHistory,
location: {
hash: '',
pathname: mockPathname,
search: testSearch,
state: '',
},
};
render(
<TestProviders>
<Router history={historyMock}>
<AlertDetailsRedirect />
</Router>
</TestProviders>
);
const [{ search, pathname }] = historyMock.replace.mock.lastCall;
expect(search as string).toMatch(/flyout.*/);
expect(pathname).toEqual(ALERTS_PATH);
});
});
});
});

View file

@ -13,7 +13,6 @@ import moment from 'moment';
import { encode } from '@kbn/rison';
import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils';
import type { FilterControlConfig } from '@kbn/alerts-ui-shared';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { ALERTS_PATH, DEFAULT_ALERTS_INDEX } from '../../../../common/constants';
import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state';
import { inputsSelectors } from '../../../common/store';
@ -69,16 +68,11 @@ export const AlertDetailsRedirect = () => {
const currentFlyoutParams = searchParams.get(URL_PARAM_KEY.flyout);
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const urlParams = new URLSearchParams({
[URL_PARAM_KEY.appQuery]: kqlAppQuery,
[URL_PARAM_KEY.timerange]: timerange,
[URL_PARAM_KEY.pageFilter]: pageFiltersQuery,
[URL_PARAM_KEY.flyout]: resolveFlyoutParams(
{ index, alertId, expandableFlyoutDisabled },
currentFlyoutParams
),
[URL_PARAM_KEY.flyout]: resolveFlyoutParams({ index, alertId }, currentFlyoutParams),
});
const url = `${ALERTS_PATH}?${urlParams.toString()}`;

View file

@ -11,7 +11,6 @@ import { expandableFlyoutStateFromEventMeta } from '../../../flyout/document_det
export interface ResolveFlyoutParamsConfig {
index: string;
alertId: string;
expandableFlyoutDisabled: boolean;
}
/**
@ -21,27 +20,14 @@ export interface ResolveFlyoutParamsConfig {
* with Share Button on the Expandable Flyout
*/
export const resolveFlyoutParams = (
{ index, alertId, expandableFlyoutDisabled }: ResolveFlyoutParamsConfig,
{ index, alertId }: ResolveFlyoutParamsConfig,
currentParamsString: string | null
) => {
if (expandableFlyoutDisabled) {
const legacyFlyoutString = encode({
panelView: 'eventDetail',
params: {
eventId: alertId,
indexName: index,
},
});
return legacyFlyoutString;
}
if (currentParamsString) {
return currentParamsString;
}
const modernFlyoutString = encode(
return encode(
expandableFlyoutStateFromEventMeta({ index, eventId: alertId, scopeId: 'alerts-page' })
);
return modernFlyoutString;
};

View file

@ -13,7 +13,6 @@ import { UsersQueries } from '../../../../../../common/search_strategy/security_
import type { UserItem } from '../../../../../../common/search_strategy/security_solution/users/common';
import { NOT_EVENT_KIND_ASSET_FILTER } from '../../../../../../common/search_strategy/security_solution/users/common';
import { useSearchStrategy } from '../../../../../common/containers/use_search_strategy';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
export const OBSERVED_USER_QUERY_ID = 'observedUsersDetailsQuery';
@ -43,7 +42,6 @@ export const useObservedUserDetails = ({
skip = false,
startDate,
}: UseUserDetails): [boolean, UserDetailsArgs] => {
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const {
loading,
result: response,
@ -81,9 +79,9 @@ export const useObservedUserDetails = ({
from: startDate,
to: endDate,
},
filterQuery: !expandableFlyoutDisabled ? NOT_EVENT_KIND_ASSET_FILTER : undefined,
filterQuery: NOT_EVENT_KIND_ASSET_FILTER,
}),
[endDate, indexNames, startDate, userName, expandableFlyoutDisabled]
[endDate, indexNames, startDate, userName]
);
useEffect(() => {

View file

@ -20,7 +20,6 @@ import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useKibana } from '../../../../common/lib/kibana';
import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys';
import type { TimelineResultNote } from '../types';
@ -29,7 +28,7 @@ import { MarkdownRenderer } from '../../../../common/components/markdown_editor'
import { timelineActions, timelineSelectors } from '../../../store';
import { NOTE_CONTENT_CLASS_NAME } from '../../timeline/body/helpers';
import * as i18n from './translations';
import { TimelineTabs, TimelineId } from '../../../../../common/types/timeline';
import { TimelineId } from '../../../../../common/types/timeline';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
import { useSourcererDataView } from '../../../../sourcerer/containers';
@ -51,53 +50,27 @@ const ToggleEventDetailsButtonComponent: React.FC<ToggleEventDetailsButtonProps>
eventId,
timelineId,
}) => {
const dispatch = useDispatch();
const { selectedPatterns } = useSourcererDataView(SourcererScopeName.timeline);
const { telemetry } = useKibana().services;
const { openFlyout } = useExpandableFlyoutApi();
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const handleClick = useCallback(() => {
const indexName = selectedPatterns.join(',');
if (!expandableFlyoutDisabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName,
scopeId: timelineId,
},
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName: selectedPatterns.join(','),
scopeId: timelineId,
},
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
} else {
dispatch(
timelineActions.toggleDetailPanel({
panelView: 'eventDetail',
tabType: TimelineTabs.notes,
id: timelineId,
params: {
eventId,
indexName,
},
})
);
}
}, [
dispatch,
eventId,
expandableFlyoutDisabled,
openFlyout,
selectedPatterns,
telemetry,
timelineId,
]);
},
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
}, [eventId, openFlyout, selectedPatterns, telemetry, timelineId]);
return (
<EuiButtonIcon

View file

@ -8,7 +8,6 @@ import { renderHook, act } from '@testing-library/react-hooks';
import React from 'react';
import type { UseDetailPanelConfig } from './use_detail_panel';
import { useDetailPanel } from './use_detail_panel';
import { timelineActions } from '../../../store';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { SourcererScopeName } from '../../../../sourcerer/store/model';
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
@ -33,14 +32,6 @@ jest.mock('../../../../common/lib/kibana', () => {
jest.mock('../../../../common/hooks/use_selector');
jest.mock('../../../store');
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('../../../../sourcerer/containers', () => {
const mockSourcererReturn = {
browserFields: {},
@ -88,18 +79,6 @@ describe('useDetailPanel', () => {
});
});
test('should fire redux action to open event details panel', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderUseDetailPanel();
await waitForNextUpdate();
result.current?.openEventDetailsPanel('123');
expect(mockDispatch).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
test('should show the details panel', async () => {
mockGetExpandedDetail.mockImplementation(() => ({
[TimelineTabs.session]: {

View file

@ -5,15 +5,12 @@
* 2.0.
*/
import React, { useMemo, useCallback, useRef } from 'react';
import { useDispatch } from 'react-redux';
import React, { useMemo, useCallback } from 'react';
import type { EntityType } from '@kbn/timelines-plugin/common';
import { dataTableSelectors } from '@kbn/securitysolution-data-table';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useKibana } from '../../../../common/lib/kibana';
import type { ExpandedDetailType } from '../../../../../common/types';
import { getScopedActions, isInTableScope, isTimelineScope } from '../../../../helpers';
import { isInTableScope, isTimelineScope } from '../../../../helpers';
import { timelineSelectors } from '../../../store';
import { useSourcererDataView } from '../../../../sourcerer/containers';
import type { SourcererScopeName } from '../../../../sourcerer/store/model';
@ -45,10 +42,8 @@ export const useDetailPanel = ({
}: UseDetailPanelConfig): UseDetailPanelReturn => {
const { telemetry } = useKibana().services;
const { browserFields, selectedPatterns, runtimeMappings } = useSourcererDataView(sourcererScope);
const dispatch = useDispatch();
const { openFlyout } = useExpandableFlyoutApi();
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const getScope = useMemo(() => {
if (isTimelineScope(scopeId)) {
@ -62,8 +57,6 @@ export const useDetailPanel = ({
const expandedDetail = useDeepEqualSelector(
(state) => ((getScope && getScope(state, scopeId)) ?? timelineDefaults)?.expandedDetail
);
const onPanelClose = useRef(() => {});
const noopPanelClose = () => {};
const shouldShowDetailsPanel = useMemo(() => {
if (
@ -76,68 +69,34 @@ export const useDetailPanel = ({
}
return false;
}, [expandedDetail, tabType]);
const scopedActions = getScopedActions(scopeId);
// We could just surface load details panel, but rather than have users be concerned
// of the config for a panel, they can just pass the base necessary values to a panel specific function
const loadDetailsPanel = useCallback(
(panelConfig?: ExpandedDetailType) => {
if (panelConfig && scopedActions) {
dispatch(
scopedActions.toggleDetailPanel({
...panelConfig,
tabType,
id: scopeId,
})
);
}
},
[scopedActions, scopeId, dispatch, tabType]
);
const openEventDetailsPanel = useCallback(
(eventId?: string, onClose?: () => void) => {
if (!expandableFlyoutDisabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName: eventDetailsIndex,
scopeId,
},
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName: eventDetailsIndex,
scopeId,
},
});
telemetry.reportDetailsFlyoutOpened({
location: scopeId,
panel: 'right',
});
} else if (eventId) {
loadDetailsPanel({
panelView: 'eventDetail',
params: { eventId, indexName: eventDetailsIndex },
});
onPanelClose.current = onClose ?? noopPanelClose;
}
},
});
telemetry.reportDetailsFlyoutOpened({
location: scopeId,
panel: 'right',
});
},
[expandableFlyoutDisabled, openFlyout, eventDetailsIndex, scopeId, telemetry, loadDetailsPanel]
[openFlyout, eventDetailsIndex, scopeId, telemetry]
);
const handleOnDetailsPanelClosed = useCallback(() => {
if (!expandableFlyoutDisabled) return;
if (onPanelClose.current) onPanelClose.current();
if (scopedActions) {
dispatch(scopedActions.toggleDetailPanel({ tabType, id: scopeId }));
}
}, [expandableFlyoutDisabled, scopedActions, dispatch, tabType, scopeId]);
const DetailsPanel = useMemo(
() =>
shouldShowDetailsPanel ? (
<DetailsPanelComponent
browserFields={browserFields}
entityType={entityType}
handleOnPanelClosed={handleOnDetailsPanelClosed}
handleOnPanelClosed={() => {}}
isFlyoutView={isFlyoutView}
runtimeMappings={runtimeMappings}
tabType={tabType}
@ -147,7 +106,6 @@ export const useDetailPanel = ({
[
browserFields,
entityType,
handleOnDetailsPanelClosed,
isFlyoutView,
runtimeMappings,
shouldShowDetailsPanel,

View file

@ -11,7 +11,6 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { isEventBuildingBlockType } from '@kbn/securitysolution-data-table';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import { useKibana } from '../../../../../common/lib/kibana';
@ -43,7 +42,6 @@ import { useGetMappedNonEcsValue } from '../data_driven_columns';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import type {
ControlColumnProps,
ExpandedDetailType,
SetEventsDeleted,
SetEventsLoading,
} from '../../../../../../common/types';
@ -112,7 +110,6 @@ const StatefulEventComponent: React.FC<Props> = ({
const trGroupRef = useRef<HTMLDivElement | null>(null);
const dispatch = useDispatch();
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const { openFlyout } = useExpandableFlyoutApi();
// Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created
@ -206,51 +203,21 @@ const StatefulEventComponent: React.FC<Props> = ({
);
const handleOnEventDetailPanelOpened = useCallback(() => {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'eventDetail',
params: {
eventId,
indexName,
refetch,
},
};
if (!expandableFlyoutDisabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName,
scopeId: timelineId,
},
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName,
scopeId: timelineId,
},
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
} else {
// opens the panel when clicking on the table row action
dispatch(
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType,
id: timelineId,
})
);
}
}, [
eventId,
indexName,
refetch,
expandableFlyoutDisabled,
openFlyout,
timelineId,
telemetry,
dispatch,
tabType,
]);
},
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
}, [eventId, indexName, openFlyout, timelineId, telemetry]);
const setEventsLoading = useCallback<SetEventsLoading>(
({ eventIds, isLoading }) => {

View file

@ -11,17 +11,13 @@ import { waitFor } from '@testing-library/react';
import { HostName } from './host_name';
import { TestProviders } from '../../../../../common/mock';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import { timelineActions } from '../../../../store';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { dataTableActions, TableId } from '@kbn/securitysolution-data-table';
import { TableId } from '@kbn/securitysolution-data-table';
const mockedTelemetry = createTelemetryServiceMock();
const mockOpenRightPanel = jest.fn();
jest.mock('../../../../../common/hooks/use_experimental_features');
jest.mock('@kbn/expandable-flyout', () => {
return {
useExpandableFlyoutApi: () => ({
@ -31,14 +27,6 @@ jest.mock('@kbn/expandable-flyout', () => {
};
});
jest.mock('react-redux', () => {
const origin = jest.requireActual('react-redux');
return {
...origin,
useDispatch: jest.fn().mockReturnValue(jest.fn()),
};
});
jest.mock('../../../../../common/lib/kibana/kibana_react', () => {
return {
useKibana: () => ({
@ -57,28 +45,6 @@ jest.mock('../../../../../common/components/draggables', () => ({
DefaultDraggable: () => <div data-test-subj="DefaultDraggable" />,
}));
jest.mock('../../../../store', () => {
const original = jest.requireActual('../../../../store');
return {
...original,
timelineActions: {
...original.timelineActions,
toggleDetailPanel: jest.fn(),
},
};
});
jest.mock('@kbn/securitysolution-data-table', () => {
const original = jest.requireActual('@kbn/securitysolution-data-table');
return {
...original,
dataTableActions: {
...original.dataTableActions,
toggleDetailPanel: jest.fn(),
},
};
});
describe('HostName', () => {
afterEach(() => {
jest.clearAllMocks();
@ -129,8 +95,6 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
@ -153,8 +117,6 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
@ -177,82 +139,11 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open old flyout on table', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'expandableFlyoutDisabled') return true;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TableId.alertsOnAlertsPage,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<HostName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).toHaveBeenCalledWith({
id: context.timelineID,
panelView: 'hostDetail',
params: {
hostName: props.value,
},
tabType: context.tabType,
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open old flyout in timeline', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'expandableFlyoutDisabled') return true;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TimelineId.active,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<HostName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({
id: context.timelineID,
panelView: 'hostDetail',
params: {
hostName: props.value,
},
tabType: context.tabType,
});
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open expandable flyout on table', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'expandableFlyoutDisabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
@ -278,15 +169,10 @@ describe('HostName', () => {
isDraggable: false,
},
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
test('should open expandable flyout in timeline', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'expandableFlyoutDisabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
@ -312,8 +198,6 @@ describe('HostName', () => {
isDraggable: false,
},
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
});

View file

@ -7,16 +7,11 @@
import React, { useCallback, useContext, useMemo } from 'react';
import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
import { useDispatch } from 'react-redux';
import { isString } from 'lodash/fp';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { HostPanelKey } from '../../../../../flyout/entity_details/host_right';
import type { ExpandedDetailType } from '../../../../../../common/types';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import { getScopedActions } from '../../../../../helpers';
import { HostDetailsLink } from '../../../../../common/components/links';
import type { TimelineTabs } from '../../../../../../common/types/timeline';
import { DefaultDraggable } from '../../../../../common/components/draggables';
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
import { TruncatableText } from '../../../../../common/components/truncatable_text';
@ -48,10 +43,8 @@ const HostNameComponent: React.FC<Props> = ({
title,
value,
}) => {
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const { openRightPanel } = useExpandableFlyoutApi();
const dispatch = useDispatch();
const eventContext = useContext(StatefulEventContext);
const hostName = `${value}`;
const isInTimelineContext =
@ -69,48 +62,19 @@ const HostNameComponent: React.FC<Props> = ({
return;
}
const { timelineID, tabType } = eventContext;
const { timelineID } = eventContext;
if (!expandableFlyoutDisabled) {
openRightPanel({
id: HostPanelKey,
params: {
hostName,
contextID: contextId,
scopeId: timelineID,
isDraggable,
},
});
} else {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'hostDetail',
params: {
hostName,
},
};
const scopedActions = getScopedActions(timelineID);
if (scopedActions) {
dispatch(
scopedActions.toggleDetailPanel({
...updatedExpandedDetail,
id: timelineID,
tabType: tabType as TimelineTabs,
})
);
}
}
openRightPanel({
id: HostPanelKey,
params: {
hostName,
contextID: contextId,
scopeId: timelineID,
isDraggable,
},
});
},
[
contextId,
dispatch,
eventContext,
expandableFlyoutDisabled,
hostName,
isDraggable,
isInTimelineContext,
onClick,
openRightPanel,
]
[contextId, eventContext, hostName, isDraggable, isInTimelineContext, onClick, openRightPanel]
);
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined

View file

@ -10,18 +10,14 @@ import { waitFor } from '@testing-library/react';
import { TestProviders } from '../../../../../common/mock';
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
import { timelineActions } from '../../../../store';
import { UserName } from './user_name';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock';
import { dataTableActions, TableId } from '@kbn/securitysolution-data-table';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { TableId } from '@kbn/securitysolution-data-table';
const mockedTelemetry = createTelemetryServiceMock();
const mockOpenRightPanel = jest.fn();
jest.mock('../../../../../common/hooks/use_experimental_features');
jest.mock('@kbn/expandable-flyout', () => {
return {
useExpandableFlyoutApi: () => ({
@ -31,14 +27,6 @@ jest.mock('@kbn/expandable-flyout', () => {
};
});
jest.mock('react-redux', () => {
const origin = jest.requireActual('react-redux');
return {
...origin,
useDispatch: jest.fn().mockReturnValue(jest.fn()),
};
});
jest.mock('../../../../../common/lib/kibana/kibana_react', () => {
return {
useKibana: () => ({
@ -57,28 +45,6 @@ jest.mock('../../../../../common/components/draggables', () => ({
DefaultDraggable: () => <div data-test-subj="DefaultDraggable" />,
}));
jest.mock('../../../../store', () => {
const original = jest.requireActual('../../../../store');
return {
...original,
timelineActions: {
...original.timelineActions,
toggleDetailPanel: jest.fn(),
},
};
});
jest.mock('@kbn/securitysolution-data-table', () => {
const original = jest.requireActual('@kbn/securitysolution-data-table');
return {
...original,
dataTableActions: {
...original.dataTableActions,
toggleDetailPanel: jest.fn(),
},
};
});
describe('UserName', () => {
afterEach(() => {
jest.clearAllMocks();
@ -127,8 +93,6 @@ describe('UserName', () => {
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
@ -151,82 +115,11 @@ describe('UserName', () => {
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open old flyout on table', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'expandableFlyoutDisabled') return true;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TableId.alertsOnAlertsPage,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<UserName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).toHaveBeenCalledWith({
id: context.timelineID,
panelView: 'userDetail',
params: {
userName: props.value,
},
tabType: context.tabType,
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open old flyout in timeline', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'expandableFlyoutDisabled') return true;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TimelineId.active,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<UserName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({
id: context.timelineID,
panelView: 'userDetail',
params: {
userName: props.value,
},
tabType: context.tabType,
});
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open expandable flyout on table', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'expandableFlyoutDisabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
@ -252,15 +145,10 @@ describe('UserName', () => {
isDraggable: false,
},
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
test('should open expandable flyout in timeline', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'expandableFlyoutDisabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
@ -286,8 +174,6 @@ describe('UserName', () => {
isDraggable: false,
},
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
});

View file

@ -7,15 +7,10 @@
import React, { useCallback, useContext, useMemo } from 'react';
import type { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui';
import { useDispatch } from 'react-redux';
import { isString } from 'lodash/fp';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { UserPanelKey } from '../../../../../flyout/entity_details/user_right';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import type { ExpandedDetailType } from '../../../../../../common/types';
import { getScopedActions } from '../../../../../helpers';
import type { TimelineTabs } from '../../../../../../common/types/timeline';
import { DefaultDraggable } from '../../../../../common/components/draggables';
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
import { UserDetailsLink } from '../../../../../common/components/links';
@ -48,9 +43,7 @@ const UserNameComponent: React.FC<Props> = ({
title,
value,
}) => {
const dispatch = useDispatch();
const eventContext = useContext(StatefulEventContext);
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const userName = `${value}`;
const isInTimelineContext = userName && eventContext?.timelineID;
const { openRightPanel } = useExpandableFlyoutApi();
@ -67,48 +60,19 @@ const UserNameComponent: React.FC<Props> = ({
return;
}
const { timelineID, tabType } = eventContext;
const { timelineID } = eventContext;
if (!expandableFlyoutDisabled) {
openRightPanel({
id: UserPanelKey,
params: {
userName,
contextID: contextId,
scopeId: timelineID,
isDraggable,
},
});
} else {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'userDetail',
params: {
userName,
},
};
const scopedActions = getScopedActions(timelineID);
if (scopedActions) {
dispatch(
scopedActions.toggleDetailPanel({
...updatedExpandedDetail,
id: timelineID,
tabType: tabType as TimelineTabs,
})
);
}
}
openRightPanel({
id: UserPanelKey,
params: {
userName,
contextID: contextId,
scopeId: timelineID,
isDraggable,
},
});
},
[
contextId,
dispatch,
eventContext,
expandableFlyoutDisabled,
isDraggable,
isInTimelineContext,
onClick,
openRightPanel,
userName,
]
[contextId, eventContext, isDraggable, isInTimelineContext, onClick, openRightPanel, userName]
);
// The below is explicitly defined this way as the onClick takes precedence when it and the href are both defined

View file

@ -145,7 +145,6 @@ export const EqlTabContentComponent: React.FC<Props> = ({
timerangeKind,
});
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const { openFlyout } = useExpandableFlyoutApi();
const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
'securitySolutionNotesEnabled'
@ -168,7 +167,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({
const onToggleShowNotes = useCallback(
(eventId?: string) => {
const indexName = selectedPatterns.join(',');
if (eventId && !expandableFlyoutDisabled && securitySolutionNotesEnabled) {
if (eventId && securitySolutionNotesEnabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
@ -205,7 +204,6 @@ export const EqlTabContentComponent: React.FC<Props> = ({
}
},
[
expandableFlyoutDisabled,
openFlyout,
securitySolutionNotesEnabled,
selectedPatterns,

View file

@ -181,7 +181,6 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
timerangeKind: undefined,
});
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const { openFlyout } = useExpandableFlyoutApi();
const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
'securitySolutionNotesEnabled'
@ -204,7 +203,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
const onToggleShowNotes = useCallback(
(eventId?: string) => {
const indexName = selectedPatterns.join(',');
if (eventId && !expandableFlyoutDisabled && securitySolutionNotesEnabled) {
if (eventId && securitySolutionNotesEnabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
@ -241,7 +240,6 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
}
},
[
expandableFlyoutDisabled,
openFlyout,
securitySolutionNotesEnabled,
selectedPatterns,

View file

@ -211,7 +211,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
timerangeKind,
});
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const { openFlyout } = useExpandableFlyoutApi();
const securitySolutionNotesEnabled = useIsExperimentalFeatureEnabled(
'securitySolutionNotesEnabled'
@ -234,7 +233,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
const onToggleShowNotes = useCallback(
(eventId?: string) => {
const indexName = selectedPatterns.join(',');
if (eventId && !expandableFlyoutDisabled && securitySolutionNotesEnabled) {
if (eventId && securitySolutionNotesEnabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
@ -271,7 +270,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
}
},
[
expandableFlyoutDisabled,
openFlyout,
securitySolutionNotesEnabled,
selectedPatterns,

View file

@ -776,332 +776,148 @@ describe('query tab with unified timeline', () => {
describe('Leading actions - notes', () => {
describe('securitySolutionNotesEnabled = true', () => {
describe('expandableFlyoutDisabled = false', () => {
beforeEach(() => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(
jest.fn((feature: keyof ExperimentalFeatures) => {
if (feature === 'unifiedComponentsInTimelineEnabled') {
return true;
}
if (feature === 'securitySolutionNotesEnabled') {
return true;
}
return allowedExperimentalValues[feature];
})
);
});
it(
'should have the notification dot & correct tooltip',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1);
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible();
fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small'));
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible();
expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent(
'1 Note available. Click to view it & add more.'
);
});
},
SPECIAL_TEST_TIMEOUT
);
it(
'should be able to add notes through expandable flyout',
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(mockOpenFlyout).toHaveBeenCalled();
});
},
SPECIAL_TEST_TIMEOUT
beforeEach(() => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(
jest.fn((feature: keyof ExperimentalFeatures) => {
if (feature === 'unifiedComponentsInTimelineEnabled') {
return true;
}
if (feature === 'securitySolutionNotesEnabled') {
return true;
}
return allowedExperimentalValues[feature];
})
);
});
describe('expandableFlyoutDisabled = true', () => {
beforeEach(() => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(
jest.fn((feature: keyof ExperimentalFeatures) => {
if (feature === 'unifiedComponentsInTimelineEnabled') {
return true;
}
if (feature === 'expandableFlyoutDisabled') {
return true;
}
if (feature === 'securitySolutionNotesEnabled') {
return true;
}
return allowedExperimentalValues[feature];
})
);
});
it(
'should have the notification dot & correct tooltip',
async () => {
renderTestComponents();
it(
'should have the notification dot & correct tooltip',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1);
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1);
expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible();
fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small'));
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible();
expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent(
'1 Note available. Click to view it & add more.'
);
});
},
SPECIAL_TEST_TIMEOUT
);
it(
'should be able to add notes through expandable flyout',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
});
expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible();
fireEvent.click(screen.getByTestId('timeline-notes-button-small'));
fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small'));
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible();
expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent(
'1 Note available. Click to view it & add more.'
);
});
},
SPECIAL_TEST_TIMEOUT
);
it(
'should be able to add notes using EuiFlyout',
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 be cancel adding 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();
});
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
);
});
await waitFor(() => {
expect(mockOpenFlyout).toHaveBeenCalled();
});
},
SPECIAL_TEST_TIMEOUT
);
});
describe('securitySolutionNotesEnabled = false', () => {
describe('expandableFlyoutDisabled = false', () => {
beforeEach(() => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(
jest.fn((feature: keyof ExperimentalFeatures) => {
if (feature === 'unifiedComponentsInTimelineEnabled') {
return true;
}
if (feature === 'securitySolutionNotesEnabled') {
return false;
}
return allowedExperimentalValues[feature];
})
);
});
it(
'should have the notification dot & correct tooltip',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1);
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible();
fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small'));
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible();
expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent(
'1 Note available. Click to view it & add more.'
);
});
},
SPECIAL_TEST_TIMEOUT
);
it(
'should be able to add notes using EuiFlyout',
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 be cancel adding 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();
});
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
beforeEach(() => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(
jest.fn((feature: keyof ExperimentalFeatures) => {
if (feature === 'unifiedComponentsInTimelineEnabled') {
return true;
}
if (feature === 'securitySolutionNotesEnabled') {
return false;
}
return allowedExperimentalValues[feature];
})
);
});
describe('expandableFlyoutDisabled = true', () => {
beforeEach(() => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(
jest.fn((feature: keyof ExperimentalFeatures) => {
if (feature === 'unifiedComponentsInTimelineEnabled') {
return true;
}
if (feature === 'expandableFlyoutDisabled') {
return true;
}
if (feature === 'securitySolutionNotesEnabled') {
return true;
}
return allowedExperimentalValues[feature];
})
);
});
it(
'should have the notification dot & correct tooltip',
async () => {
renderTestComponents();
it(
'should have the notification dot & correct tooltip',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1);
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
expect(screen.getAllByTestId('timeline-notes-button-small')).toHaveLength(1);
expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible();
fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small'));
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible();
expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent(
'1 Note available. Click to view it & add more.'
);
});
},
SPECIAL_TEST_TIMEOUT
);
it(
'should be able to add notes using EuiFlyout',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
});
expect(screen.getByTestId('timeline-notes-notification-dot')).toBeVisible();
fireEvent.click(screen.getByTestId('timeline-notes-button-small'));
fireEvent.mouseOver(screen.getByTestId('timeline-notes-button-small'));
await waitFor(() => {
expect(screen.getByTestId('add-note-container')).toBeVisible();
});
},
SPECIAL_TEST_TIMEOUT
);
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-tool-tip')).toBeVisible();
expect(screen.getByTestId('timeline-notes-tool-tip')).toHaveTextContent(
'1 Note available. Click to view it & add more.'
);
});
},
SPECIAL_TEST_TIMEOUT
);
it(
'should be able to add notes using EuiFlyout',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
it(
'should be cancel adding notes',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
});
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
});
fireEvent.click(screen.getByTestId('timeline-notes-button-small'));
fireEvent.click(screen.getByTestId('timeline-notes-button-small'));
await waitFor(() => {
expect(screen.getByTestId('add-note-container')).toBeVisible();
});
},
SPECIAL_TEST_TIMEOUT
);
await waitFor(() => {
expect(screen.getByTestId('add-note-container')).toBeVisible();
});
it(
'should be cancel adding notes',
async () => {
renderTestComponents();
expect(await screen.findByTestId('discoverDocTable')).toBeVisible();
userEvent.type(screen.getByTestId('euiMarkdownEditorTextArea'), 'Test Note 1');
await waitFor(() => {
expect(screen.getByTestId('timeline-notes-button-small')).not.toBeDisabled();
});
expect(screen.getByTestId('cancel')).not.toBeDisabled();
fireEvent.click(screen.getByTestId('timeline-notes-button-small'));
fireEvent.click(screen.getByTestId('cancel'));
await waitFor(() => {
expect(screen.getByTestId('add-note-container')).toBeVisible();
});
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
);
});
await waitFor(() => {
expect(screen.queryByTestId('add-note-container')).not.toBeInTheDocument();
});
},
SPECIAL_TEST_TIMEOUT
);
});
});

View file

@ -36,7 +36,6 @@ const onChangePageMock = jest.fn();
const openFlyoutMock = jest.fn();
const closeFlyoutMock = jest.fn();
const isExpandableFlyoutDisabled = false;
jest.mock('../hooks/use_unified_timeline_expandable_flyout');
@ -99,7 +98,6 @@ describe('unified data table', () => {
beforeEach(() => {
(useSourcererDataView as jest.Mock).mockReturnValue(mockSourcererScope);
(useUnifiedTableExpandableFlyout as jest.Mock).mockReturnValue({
isExpandableFlyoutDisabled,
openFlyout: openFlyoutMock,
closeFlyout: closeFlyoutMock,
});

View file

@ -19,7 +19,7 @@ import { RowRendererCount } from '../../../../../../common/api/timeline';
import { EmptyComponent } from '../../../../../common/lib/cell_actions/helpers';
import { withDataView } from '../../../../../common/components/with_data_view';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import type { ExpandedDetailTimeline, ExpandedDetailType } from '../../../../../../common/types';
import type { ExpandedDetailTimeline } from '../../../../../../common/types';
import type { TimelineItem } from '../../../../../../common/search_strategy';
import { useKibana } from '../../../../../common/lib/kibana';
import type {
@ -29,12 +29,7 @@ import type {
ToggleDetailPanel,
TimelineTabs,
} from '../../../../../../common/types/timeline';
import { TimelineId } from '../../../../../../common/types/timeline';
import type { State, inputsModel } from '../../../../../common/store';
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
import { useSourcererDataView } from '../../../../../sourcerer/containers';
import { activeTimeline } from '../../../../containers/active_timeline_context';
import { DetailsPanel } from '../../../side_panel';
import { SecurityCellActionsTrigger } from '../../../../../app/actions/constants';
import { getFormattedFields } from '../../body/renderers/formatted_field_udt';
import ToolbarAdditionalControls from './toolbar_additional_controls';
@ -147,13 +142,9 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
setExpandedDoc((prev) => (!prev ? prev : undefined));
}, []);
const { openFlyout, closeFlyout, isExpandableFlyoutDisabled } = useUnifiedTableExpandableFlyout(
{
onClose: onCloseExpandableFlyout,
}
);
const { browserFields, runtimeMappings } = useSourcererDataView(SourcererScopeName.timeline);
const { openFlyout, closeFlyout } = useUnifiedTableExpandableFlyout({
onClose: onCloseExpandableFlyout,
});
const showTimeCol = useMemo(() => !!dataView && !!dataView.timeFieldName, [dataView]);
@ -168,57 +159,24 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
const handleOnEventDetailPanelOpened = useCallback(
(eventData: DataTableRecord & TimelineItem) => {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'eventDetail',
params: {
eventId: eventData._id,
indexName: eventData.ecs._index ?? '', // TODO: fix type error
refetch,
},
};
if (!isExpandableFlyoutDisabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventData._id,
indexName: eventData.ecs._index ?? '',
scopeId: timelineId,
},
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventData._id,
indexName: eventData.ecs._index ?? '',
scopeId: timelineId,
},
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
} else {
dispatch(
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType: activeTab,
id: timelineId,
})
);
}
activeTimeline.toggleExpandedDetail({ ...updatedExpandedDetail });
},
});
telemetry.reportDetailsFlyoutOpened({
location: timelineId,
panel: 'right',
});
},
[refetch, isExpandableFlyoutDisabled, openFlyout, timelineId, telemetry, dispatch, activeTab]
[openFlyout, timelineId, telemetry]
);
const onTimelineLegacyFlyoutClose = useCallback(() => {
if (
expandedDetail[activeTab]?.panelView &&
timelineId === TimelineId.active &&
showExpandedDetails
) {
activeTimeline.toggleExpandedDetail({});
}
setExpandedDoc(undefined);
onEventClosed({ tabType: activeTab, id: timelineId });
}, [expandedDetail, activeTab, timelineId, showExpandedDetails, onEventClosed]);
const onSetExpandedDoc = useCallback(
(newDoc?: DataTableRecord) => {
if (newDoc) {
@ -228,20 +186,10 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
handleOnEventDetailPanelOpened(timelineDoc);
}
} else {
if (!isExpandableFlyoutDisabled) {
closeFlyout();
return;
}
onTimelineLegacyFlyoutClose();
closeFlyout();
}
},
[
tableRows,
handleOnEventDetailPanelOpened,
onTimelineLegacyFlyoutClose,
closeFlyout,
isExpandableFlyoutDisabled,
]
[tableRows, handleOnEventDetailPanelOpened, closeFlyout]
);
const onColumnResize = useCallback(
@ -468,16 +416,6 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
externalControlColumns={leadingControlColumns}
cellContext={cellContext}
/>
{showExpandedDetails && isExpandableFlyoutDisabled && (
<DetailsPanel
browserFields={browserFields}
handleOnPanelClosed={onTimelineLegacyFlyoutClose}
runtimeMappings={runtimeMappings}
tabType={activeTab}
scopeId={timelineId}
isFlyoutView
/>
)}
</StyledTimelineUnifiedDataTable>
</StatefulEventContext.Provider>
);

View file

@ -5,14 +5,12 @@
* 2.0.
*/
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { renderHook } from '@testing-library/react-hooks';
import { useUnifiedTableExpandableFlyout } from './use_unified_timeline_expandable_flyout';
import { useLocation } from 'react-router-dom';
import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
jest.mock('../../../../../common/hooks/use_experimental_features');
jest.mock('@kbn/kibana-react-plugin/public');
jest.mock('react-router-dom', () => {
return {
@ -24,62 +22,22 @@ jest.mock('@kbn/expandable-flyout');
const onFlyoutCloseMock = jest.fn();
describe('useUnifiedTimelineExpandableFlyout', () => {
it('should disable expandable flyout when expandableFlyoutDisabled flag is true', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
(useLocation as jest.Mock).mockReturnValue({
search: `?${URL_PARAM_KEY.timelineFlyout}=(test:value)`,
});
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({
openFlyout: jest.fn(),
closeFlyout: jest.fn(),
});
const { result } = renderHook(() =>
useUnifiedTableExpandableFlyout({
onClose: onFlyoutCloseMock,
})
);
expect(result.current.isExpandableFlyoutDisabled).toBe(true);
});
describe('when expandable flyout is enabled', () => {
beforeEach(() => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
(useExpandableFlyoutApi as jest.Mock).mockReturnValue({
openFlyout: jest.fn(),
closeFlyout: jest.fn(),
});
(useLocation as jest.Mock).mockReturnValue({
search: `${URL_PARAM_KEY.timelineFlyout}=(test:value)`,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should mark flyout as closed when location is empty', () => {
(useLocation as jest.Mock).mockReturnValue({
search: '',
});
const { result } = renderHook(() =>
useUnifiedTableExpandableFlyout({
onClose: onFlyoutCloseMock,
})
);
expect(result.current.isExpandableFlyoutDisabled).toBe(false);
});
it('should mark flyout as open when location has `timelineFlyout`', () => {
(useLocation as jest.Mock).mockReturnValue({
search: `${URL_PARAM_KEY.timelineFlyout}=(test:value)`,
});
const { result } = renderHook(() =>
useUnifiedTableExpandableFlyout({
onClose: onFlyoutCloseMock,
})
);
expect(result.current.isExpandableFlyoutDisabled).toBe(false);
});
it('should mark flyout as close when location has empty `timelineFlyout`', () => {
const { result, rerender } = renderHook(() =>
useUnifiedTableExpandableFlyout({
@ -87,8 +45,6 @@ describe('useUnifiedTimelineExpandableFlyout', () => {
})
);
expect(result.current.isExpandableFlyoutDisabled).toBe(false);
(useLocation as jest.Mock).mockReturnValue({
search: `${URL_PARAM_KEY.timelineFlyout}=()`,
});

View file

@ -8,7 +8,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useLocation } from 'react-router-dom';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { URL_PARAM_KEY } from '../../../../../common/hooks/use_url_state';
const EMPTY_TIMELINE_FLYOUT_SEARCH_PARAMS = '()';
@ -20,8 +19,6 @@ interface UseUnifiedTableExpandableFlyoutArgs {
export const useUnifiedTableExpandableFlyout = ({
onClose,
}: UseUnifiedTableExpandableFlyoutArgs) => {
const expandableFlyoutDisabled = useIsExperimentalFeatureEnabled('expandableFlyoutDisabled');
const location = useLocation();
const { openFlyout, closeFlyout } = useExpandableFlyoutApi();
@ -33,7 +30,7 @@ export const useUnifiedTableExpandableFlyout = ({
const isFlyoutOpen = useMemo(() => {
/**
* Currently, if new expanable flyout is closed, there is not way for
* Currently, if new expandable flyout is closed, there is no way for
* consumer to trigger an effect `onClose` of flyout. So, we are using
* this hack to know if flyout is open or not.
*
@ -71,6 +68,5 @@ export const useUnifiedTableExpandableFlyout = ({
isTimelineExpandableFlyoutOpen,
openFlyout,
closeFlyout: closeFlyoutWithEffect,
isExpandableFlyoutDisabled: expandableFlyoutDisabled,
};
};

View file

@ -33,109 +33,95 @@ import { mockRiskEngineEnabled } from '../../tasks/entity_analytics';
const CURRENT_HOST_RISK_LEVEL = 'Current host risk level';
const ORIGINAL_HOST_RISK_LEVEL = 'Original host risk level';
describe(
'Enrichment',
{
tags: ['@ess'],
env: {
ftrConfig: {
kbnServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'expandableFlyoutDisabled',
])}`,
],
},
},
},
() => {
before(() => {
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' });
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new_updated' });
cy.task('esArchiverLoad', { archiveName: 'risk_users' });
});
// this whole suite is failing on main
describe.skip('Enrichment', { tags: ['@ess'] }, () => {
before(() => {
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' });
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new_updated' });
cy.task('esArchiverLoad', { archiveName: 'risk_users' });
});
after(() => {
cy.task('esArchiverUnload', { archiveName: 'risk_users' });
});
after(() => {
cy.task('esArchiverUnload', { archiveName: 'risk_users' });
});
describe('Custom query rule', () => {
// FLAKY: https://github.com/elastic/kibana/issues/176965
describe.skip('from legacy risk scores', () => {
beforeEach(() => {
cy.task('esArchiverLoad', { archiveName: 'risk_hosts' });
deleteAlertsAndRules();
createRule(getNewRule({ rule_id: 'rule1' }));
login();
visitWithTimeRange(ALERTS_URL);
waitForAlertsToPopulate();
});
afterEach(() => {
cy.task('esArchiverUnload', { archiveName: 'risk_hosts' });
cy.task('esArchiverUnload', { archiveName: 'risk_hosts_updated' });
});
it('Should has enrichment fields from legacy risk', function () {
cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level');
cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level');
scrollAlertTableColumnIntoView(HOST_RISK_COLUMN);
cy.get(HOST_RISK_COLUMN).contains('Low');
scrollAlertTableColumnIntoView(USER_RISK_COLUMN);
cy.get(USER_RISK_COLUMN).contains('Low');
scrollAlertTableColumnIntoView(ACTION_COLUMN);
expandFirstAlert();
cy.get(ENRICHED_DATA_ROW).contains('Low');
cy.get(ENRICHED_DATA_ROW).contains(CURRENT_HOST_RISK_LEVEL);
cy.get(ENRICHED_DATA_ROW).contains('Critical').should('not.exist');
cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL).should('not.exist');
closeAlertFlyout();
cy.task('esArchiverUnload', { archiveName: 'risk_hosts' });
cy.task('esArchiverLoad', { archiveName: 'risk_hosts_updated' });
expandFirstAlert();
cy.get(ENRICHED_DATA_ROW).contains('Critical');
cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL);
});
describe('Custom query rule', () => {
// FLAKY: https://github.com/elastic/kibana/issues/176965
describe.skip('from legacy risk scores', () => {
beforeEach(() => {
cy.task('esArchiverLoad', { archiveName: 'risk_hosts' });
deleteAlertsAndRules();
createRule(getNewRule({ rule_id: 'rule1' }));
login();
visitWithTimeRange(ALERTS_URL);
waitForAlertsToPopulate();
});
describe('from new risk scores', () => {
beforeEach(() => {
cy.task('esArchiverLoad', { archiveName: 'risk_scores_new' });
deleteAlertsAndRules();
createRule(getNewRule({ rule_id: 'rule1' }));
login();
mockRiskEngineEnabled();
visitWithTimeRange(ALERTS_URL);
waitForAlertsToPopulate();
});
afterEach(() => {
cy.task('esArchiverUnload', { archiveName: 'risk_hosts' });
cy.task('esArchiverUnload', { archiveName: 'risk_hosts_updated' });
});
afterEach(() => {
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' });
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new_updated' });
});
it('Should has enrichment fields from legacy risk', function () {
cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level');
cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level');
scrollAlertTableColumnIntoView(HOST_RISK_COLUMN);
cy.get(HOST_RISK_COLUMN).contains('Low');
scrollAlertTableColumnIntoView(USER_RISK_COLUMN);
cy.get(USER_RISK_COLUMN).contains('Low');
scrollAlertTableColumnIntoView(ACTION_COLUMN);
expandFirstAlert();
cy.get(ENRICHED_DATA_ROW).contains('Low');
cy.get(ENRICHED_DATA_ROW).contains(CURRENT_HOST_RISK_LEVEL);
cy.get(ENRICHED_DATA_ROW).contains('Critical').should('not.exist');
cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL).should('not.exist');
it('Should has enrichment fields from legacy risk', function () {
cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level');
cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level');
scrollAlertTableColumnIntoView(HOST_RISK_COLUMN);
cy.get(HOST_RISK_COLUMN).contains('Critical');
scrollAlertTableColumnIntoView(USER_RISK_COLUMN);
cy.get(USER_RISK_COLUMN).contains('High');
scrollAlertTableColumnIntoView(ACTION_COLUMN);
expandFirstAlert();
cy.get(ENRICHED_DATA_ROW).contains('Critical');
cy.get(ENRICHED_DATA_ROW).contains(CURRENT_HOST_RISK_LEVEL);
cy.get(ENRICHED_DATA_ROW).contains('Low').should('not.exist');
cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL).should('not.exist');
closeAlertFlyout();
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' });
cy.task('esArchiverLoad', { archiveName: 'risk_scores_new_updated' });
expandFirstAlert();
cy.get(ENRICHED_DATA_ROW).contains('Low');
cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL);
});
closeAlertFlyout();
cy.task('esArchiverUnload', { archiveName: 'risk_hosts' });
cy.task('esArchiverLoad', { archiveName: 'risk_hosts_updated' });
expandFirstAlert();
cy.get(ENRICHED_DATA_ROW).contains('Critical');
cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL);
});
});
}
);
describe('from new risk scores', () => {
beforeEach(() => {
cy.task('esArchiverLoad', { archiveName: 'risk_scores_new' });
deleteAlertsAndRules();
createRule(getNewRule({ rule_id: 'rule1' }));
login();
mockRiskEngineEnabled();
visitWithTimeRange(ALERTS_URL);
waitForAlertsToPopulate();
});
afterEach(() => {
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' });
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new_updated' });
});
it('Should has enrichment fields from legacy risk', function () {
cy.get(HOST_RISK_HEADER_COLUMN).contains('Host Risk Level');
cy.get(USER_RISK_HEADER_COLUMN).contains('User Risk Level');
scrollAlertTableColumnIntoView(HOST_RISK_COLUMN);
cy.get(HOST_RISK_COLUMN).contains('Critical');
scrollAlertTableColumnIntoView(USER_RISK_COLUMN);
cy.get(USER_RISK_COLUMN).contains('High');
scrollAlertTableColumnIntoView(ACTION_COLUMN);
expandFirstAlert();
cy.get(ENRICHED_DATA_ROW).contains('Critical');
cy.get(ENRICHED_DATA_ROW).contains(CURRENT_HOST_RISK_LEVEL);
cy.get(ENRICHED_DATA_ROW).contains('Low').should('not.exist');
cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL).should('not.exist');
closeAlertFlyout();
cy.task('esArchiverUnload', { archiveName: 'risk_scores_new' });
cy.task('esArchiverLoad', { archiveName: 'risk_scores_new_updated' });
expandFirstAlert();
cy.get(ENRICHED_DATA_ROW).contains('Low');
cy.get(ENRICHED_DATA_ROW).contains(ORIGINAL_HOST_RISK_LEVEL);
});
});
});
});

View file

@ -1,248 +0,0 @@
/*
* 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 {
ALERT_FLYOUT,
CELL_TEXT,
COPY_ALERT_FLYOUT_LINK,
JSON_TEXT,
OVERVIEW_RULE,
SUMMARY_VIEW,
TABLE_CONTAINER,
TABLE_ROWS,
} from '../../../screens/alerts_details';
import { closeAlertFlyout, expandFirstAlert } from '../../../tasks/alerts';
import {
changeAlertStatusTo,
filterBy,
openJsonView,
openTable,
} from '../../../tasks/alerts_details';
import { createRule } from '../../../tasks/api_calls/rules';
import { deleteAlertsAndRules } from '../../../tasks/api_calls/common';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
import { login } from '../../../tasks/login';
import { visit, visitWithTimeRange } from '../../../tasks/navigation';
import { getNewRule, getUnmappedRule } from '../../../objects/rule';
import { ALERTS_URL } from '../../../urls/navigation';
import { tablePageSelector } from '../../../screens/table_pagination';
import { ALERTS_TABLE_COUNT } from '../../../screens/timeline';
import { ALERT_SUMMARY_SEVERITY_DONUT_CHART } from '../../../screens/alerts';
import {
visitRuleDetailsPage,
waitForPageToBeLoaded as waitForRuleDetailsPageToBeLoaded,
} from '../../../tasks/rule_details';
// this functionality is now not used anymore (though still accessible if customers turn the `expandableFlyoutDisabled` feature flag on
// the code will be removed entirely for `8.16 (see https://github.com/elastic/security-team/issues/7462)
describe.skip('Alert details flyout', { tags: ['@ess', '@serverless'] }, () => {
describe('Basic functions', () => {
beforeEach(() => {
deleteAlertsAndRules();
createRule(getNewRule());
login();
visit(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlert();
});
it('should update the table when status of the alert is updated', () => {
cy.get(OVERVIEW_RULE).should('be.visible');
cy.get(ALERTS_TABLE_COUNT).should('have.text', '1 alert');
cy.get(ALERT_SUMMARY_SEVERITY_DONUT_CHART).should('contain.text', '1alert');
expandFirstAlert();
changeAlertStatusTo('acknowledged');
cy.get(ALERTS_TABLE_COUNT).should('have.text', '1 alert');
cy.get(ALERT_SUMMARY_SEVERITY_DONUT_CHART).should('contain.text', '1alert');
});
});
describe('With unmapped fields', () => {
before(() => {
cy.task('esArchiverLoad', { archiveName: 'unmapped_fields' });
});
beforeEach(() => {
deleteAlertsAndRules();
createRule({ ...getUnmappedRule(), investigation_fields: { field_names: ['event.kind'] } });
login();
visit(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlert();
});
after(() => {
cy.task('esArchiverUnload', { archiveName: 'unmapped_fields' });
});
it.skip('should display user and system defined highlighted fields', () => {
cy.get(SUMMARY_VIEW)
.should('be.visible')
.and('contain.text', 'event.kind')
.and('contain.text', 'Rule type');
});
it('should display the unmapped field on the JSON view', () => {
const expectedUnmappedValue = 'This is the unmapped field';
openJsonView();
cy.get(JSON_TEXT).then((x) => {
const parsed = JSON.parse(x.text());
expect(parsed.fields.unmapped[0]).to.equal(expectedUnmappedValue);
});
});
it('should displays the unmapped field on the table', () => {
const expectedUnmappedField = {
field: 'unmapped',
text: 'This is the unmapped field',
};
openTable();
cy.get(ALERT_FLYOUT).find(tablePageSelector(6)).click({ force: true });
cy.get(ALERT_FLYOUT)
.find(TABLE_ROWS)
.last()
.within(() => {
cy.get(CELL_TEXT).should('contain', expectedUnmappedField.field);
cy.get(CELL_TEXT).should('contain', expectedUnmappedField.text);
});
});
// This test makes sure that the table does not overflow horizontally
it('table should not scroll horizontally', () => {
openTable();
cy.get(ALERT_FLYOUT)
.find(TABLE_CONTAINER)
.within(($tableContainer) => {
expect($tableContainer[0].scrollLeft).to.equal(0);
// Due to the introduction of pagination on the table, a slight horizontal overflow has been introduced.
// scroll ignores the `overflow-x:hidden` attribute and will still scroll the element if there is a hidden overflow
// Updated the below to < 5 to account for this and keep a test to make sure it doesn't grow
$tableContainer[0].scroll({ left: 1000 });
expect($tableContainer[0].scrollLeft).to.be.lessThan(5);
});
});
});
describe('Url state management', () => {
before(() => {
cy.task('esArchiverLoad', { archiveName: 'query_alert', useCreate: true, docsOnly: true });
});
beforeEach(() => {
deleteAlertsAndRules();
createRule(getNewRule());
login();
visitWithTimeRange(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlert();
});
it('should store the flyout state in the url when it is opened and remove it when closed', () => {
cy.get(OVERVIEW_RULE).should('be.visible');
cy.url().should('include', 'flyout=');
closeAlertFlyout();
cy.url().should('not.include', 'flyout=');
});
it.skip('should open the alert flyout when the page is refreshed', () => {
cy.get(OVERVIEW_RULE).should('be.visible');
cy.reload();
cy.get(OVERVIEW_RULE).should('be.visible');
cy.get(OVERVIEW_RULE).should('contain', 'Endpoint Security');
});
it('should show the copy link button for the flyout', () => {
cy.get(COPY_ALERT_FLYOUT_LINK).should('be.visible');
});
it.skip('should have the `kibana.alert.url` field set', () => {
openTable();
filterBy('kibana.alert.url');
cy.get('[data-test-subj="formatted-field-kibana.alert.url"]').should(
'have.text',
'http://localhost:5601/app/security/alerts/redirect/eabbdefc23da981f2b74ab58b82622a97bb9878caa11bc914e2adfacc94780f1?index=.alerts-security.alerts-default&timestamp=2023-04-27T11:03:57.906Z'
);
});
});
describe('Localstorage management', () => {
const ARCHIVED_RULE_ID = '7015a3e2-e4ea-11ed-8c11-49608884878f';
const ARCHIVED_RULE_NAME = 'Endpoint Security';
before(() => {
deleteAlertsAndRules();
// It just imports an alert without a rule but rule details page should work anyway
cy.task('esArchiverLoad', { archiveName: 'query_alert', useCreate: true, docsOnly: true });
});
after(() => {
cy.task('esArchiverUnload', { archiveName: 'query_alert' });
});
beforeEach(() => {
createRule(getNewRule());
login();
visitWithTimeRange(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlert();
});
/**
* Localstorage is updated after a delay here x-pack/plugins/security_solution/public/common/store/data_table/epic_local_storage.ts
* We create this config to re-check localStorage 3 times, every 500ms to avoid any potential flakyness from that delay
*/
it('should store the flyout state in localstorage', () => {
cy.get(OVERVIEW_RULE).should('be.visible');
cy.window().then((win) => {
const securityDataTableStorage = win.localStorage.getItem('securityDataTable');
cy.wrap(securityDataTableStorage).should('contain', '"panelView":"eventDetail"');
});
});
it('should remove the flyout details from local storage when closed', () => {
cy.get(OVERVIEW_RULE).should('be.visible');
closeAlertFlyout();
cy.window().then((win) => {
const securityDataTableStorage = win.localStorage.getItem('securityDataTable');
cy.wrap(securityDataTableStorage).should('not.contain', '"panelView"');
});
});
it('should remove the flyout state from localstorage when navigating away without closing the flyout', () => {
cy.get(OVERVIEW_RULE).should('be.visible');
visitRuleDetailsPage(ARCHIVED_RULE_ID);
waitForRuleDetailsPageToBeLoaded(ARCHIVED_RULE_NAME);
cy.window().then((win) => {
const securityDataTableStorage = win.localStorage.getItem('securityDataTable');
cy.wrap(securityDataTableStorage).should('not.contain', '"panelView"');
});
});
it('should not reopen the flyout when navigating away from the alerts page and returning to it', () => {
cy.get(OVERVIEW_RULE).should('be.visible');
visitRuleDetailsPage(ARCHIVED_RULE_ID);
waitForRuleDetailsPageToBeLoaded(ARCHIVED_RULE_NAME);
visit(ALERTS_URL);
cy.get(OVERVIEW_RULE).should('not.exist');
});
});
});