Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Update icons and timeline tabs when visualization in flyout is enabled #195687

Merged
merged 1 commit into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import styled from 'styled-components';

import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
import { TimelineTabs, TableId } from '@kbn/securitysolution-data-table';
import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../common/constants';
import {
selectNotesByDocumentId,
selectDocumentNotesBySavedObjectId,
Expand Down Expand Up @@ -46,6 +47,8 @@ import { isDetectionsAlertsTable } from '../top_n/helpers';
import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step';
import { DEFAULT_ACTION_BUTTON_WIDTH, isAlert } from './helpers';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { useNavigateToAnalyzer } from '../../../flyout/document_details/shared/hooks/use_navigate_to_analyzer';
import { useNavigateToSessionView } from '../../../flyout/document_details/shared/hooks/use_navigate_to_session_view';

const ActionsContainer = styled.div`
align-items: center;
Expand Down Expand Up @@ -111,25 +114,48 @@ const ActionsComponent: React.FC<ActionProps> = ({
);
}, [ecsData, eventType]);

const [visualizationInFlyoutEnabled] = useUiSetting$<boolean>(
ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING
);

const { navigateToAnalyzer } = useNavigateToAnalyzer({
Copy link
Contributor

@logeekal logeekal Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking if It makes sense to delegate the responsibility of opening Analyzer either in tab or in flyout to useNavigateToAnalyzer.

With that logic becomes consolidated in one place and easy to refactor. But i am not too strongly opiniated about it. Just something to think about. I also understand if you would want to keep the current code as it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@logeekal thank you for reviewing! i could see your point, the intention of the hooks was to reduce the duplication of openFlyout calls. i would think when it comes to removing the existing analyzer overlay, the code removal would be the same in either places. But I'll think more about it!

Given there is another PR depending on this one, I will merge it as is

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. 👍

isFlyoutOpen: false,
eventId,
indexName: ecsData._index,
scopeId: timelineId,
});

const { navigateToSessionView } = useNavigateToSessionView({
isFlyoutOpen: false,
eventId,
indexName: ecsData._index,
scopeId: timelineId,
});

const isDisabled = !useIsInvestigateInResolverActionEnabled(ecsData);
const { setGlobalFullScreen } = useGlobalFullScreen();
const { setTimelineFullScreen } = useTimelineFullScreen();
const handleClick = useCallback(() => {
startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER });
const scopedActions = getScopedActions(timelineId);

const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen');
if (scopedActions) {
dispatch(scopedActions.updateGraphEventId({ id: timelineId, graphEventId: ecsData._id }));
}
if (timelineId === TimelineId.active) {
if (dataGridIsFullScreen) {
setTimelineFullScreen(true);
}
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph }));
if (visualizationInFlyoutEnabled) {
navigateToAnalyzer();
} else {
if (dataGridIsFullScreen) {
setGlobalFullScreen(true);
const scopedActions = getScopedActions(timelineId);

const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen');
if (scopedActions) {
dispatch(scopedActions.updateGraphEventId({ id: timelineId, graphEventId: ecsData._id }));
}
if (timelineId === TimelineId.active) {
if (dataGridIsFullScreen) {
setTimelineFullScreen(true);
}
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph }));
} else {
if (dataGridIsFullScreen) {
setGlobalFullScreen(true);
}
}
}
}, [
Expand All @@ -139,6 +165,8 @@ const ActionsComponent: React.FC<ActionProps> = ({
ecsData._id,
setTimelineFullScreen,
setGlobalFullScreen,
visualizationInFlyoutEnabled,
navigateToAnalyzer,
]);

const sessionViewConfig = useMemo(() => {
Expand Down Expand Up @@ -169,23 +197,32 @@ const ActionsComponent: React.FC<ActionProps> = ({
const openSessionView = useCallback(() => {
const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen');
startTransaction({ name: ALERTS_ACTIONS.OPEN_SESSION_VIEW });
const scopedActions = getScopedActions(timelineId);

if (timelineId === TimelineId.active) {
if (dataGridIsFullScreen) {
setTimelineFullScreen(true);
}
if (sessionViewConfig !== null) {
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session }));
}
if (
visualizationInFlyoutEnabled &&
sessionViewConfig !== null &&
timelineId !== TableId.kubernetesPageSessions
) {
navigateToSessionView();
} else {
if (dataGridIsFullScreen) {
setGlobalFullScreen(true);
const scopedActions = getScopedActions(timelineId);

if (timelineId === TimelineId.active) {
if (dataGridIsFullScreen) {
setTimelineFullScreen(true);
}
if (sessionViewConfig !== null) {
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.session }));
}
} else {
if (dataGridIsFullScreen) {
setGlobalFullScreen(true);
}
}
}
if (sessionViewConfig !== null) {
if (scopedActions) {
dispatch(scopedActions.updateSessionViewConfig({ id: timelineId, sessionViewConfig }));
if (sessionViewConfig !== null) {
if (scopedActions) {
dispatch(scopedActions.updateSessionViewConfig({ id: timelineId, sessionViewConfig }));
}
}
}
}, [
Expand All @@ -195,6 +232,8 @@ const ActionsComponent: React.FC<ActionProps> = ({
setTimelineFullScreen,
dispatch,
setGlobalFullScreen,
visualizationInFlyoutEnabled,
navigateToSessionView,
]);

const { activeStep, isTourShown, incrementStep } = useTourContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const mockedUseKibana = mockUseKibana();
(useKibana as jest.Mock).mockReturnValue(mockedUseKibana);

const mockUseWhichFlyout = useWhichFlyout as jest.Mock;
const FLYOUT_KEY = 'securitySolution';
const FLYOUT_KEY = 'SecuritySolution';
const TIMELINE_FLYOUT_KEY = 'Timeline';

const eventId = 'eventId1';
const indexName = 'index1';
Expand All @@ -36,11 +37,11 @@ const scopeId = 'scopeId1';
describe('useNavigateToAnalyzer', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseWhichFlyout.mockReturnValue(FLYOUT_KEY);
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
});

it('when isFlyoutOpen is true, should return callback that opens left and preview panels', () => {
mockUseWhichFlyout.mockReturnValue(FLYOUT_KEY);
const hookResult = renderHook(() =>
useNavigateToAnalyzer({ isFlyoutOpen: true, eventId, indexName, scopeId })
);
Expand Down Expand Up @@ -68,7 +69,9 @@ describe('useNavigateToAnalyzer', () => {
});
});

it('when isFlyoutOpen is false, should return callback that opens a new flyout', () => {
it('when isFlyoutOpen is false and scopeId is not timeline, should return callback that opens a new flyout', () => {
mockUseWhichFlyout.mockReturnValue(null);

const hookResult = renderHook(() =>
useNavigateToAnalyzer({ isFlyoutOpen: false, eventId, indexName, scopeId })
);
Expand Down Expand Up @@ -103,4 +106,42 @@ describe('useNavigateToAnalyzer', () => {
},
});
});

it('when isFlyoutOpen is false and scopeId is current timeline, should return callback that opens a new flyout in timeline', () => {
mockUseWhichFlyout.mockReturnValue(null);
const timelineId = 'timeline-1';
const hookResult = renderHook(() =>
useNavigateToAnalyzer({ isFlyoutOpen: false, eventId, indexName, scopeId: timelineId })
);
hookResult.result.current.navigateToAnalyzer();
expect(mockFlyoutApi.openFlyout).toHaveBeenCalledWith({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName,
scopeId: timelineId,
},
},
left: {
id: DocumentDetailsLeftPanelKey,
path: {
tab: 'visualize',
subTab: ANALYZE_GRAPH_ID,
},
params: {
id: eventId,
indexName,
scopeId: timelineId,
},
},
preview: {
id: DocumentDetailsAnalyzerPanelKey,
params: {
resolverComponentInstanceID: `${TIMELINE_FLYOUT_KEY}-${timelineId}`,
banner: ANALYZER_PREVIEW_BANNER,
},
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
DocumentDetailsRightPanelKey,
DocumentDetailsAnalyzerPanelKey,
} from '../constants/panel_keys';
import { Flyouts } from '../constants/flyouts';
import { isTimelineScope } from '../../../../helpers';

export interface UseNavigateToAnalyzerParams {
/**
Expand Down Expand Up @@ -56,7 +58,11 @@ export const useNavigateToAnalyzer = ({
}: UseNavigateToAnalyzerParams): UseNavigateToAnalyzerResult => {
const { telemetry } = useKibana().services;
const { openLeftPanel, openPreviewPanel, openFlyout } = useExpandableFlyoutApi();
const key = useWhichFlyout() ?? 'memory';
let key = useWhichFlyout() ?? 'memory';

if (!isFlyoutOpen) {
key = isTimelineScope(scopeId) ? Flyouts.timeline : Flyouts.securitySolution;
}

const right: FlyoutPanelProps = useMemo(
() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
import { TimelineTypeEnum } from '../../../../../common/api/timeline';
import { useEsqlAvailability } from '../../../../common/hooks/esql/use_esql_availability';
import { render, screen, waitFor } from '@testing-library/react';
import { useLicense } from '../../../../common/hooks/use_license';

jest.mock('../../../../common/hooks/use_license');

const mockUseUiSetting = jest.fn().mockReturnValue([false]);
jest.mock('@kbn/kibana-react-plugin/public', () => {
const original = jest.requireActual('@kbn/kibana-react-plugin/public');
return {
...original,
useUiSetting$: () => mockUseUiSetting(),
};
});

jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
Expand All @@ -33,18 +45,19 @@ jest.mock('../../../../common/hooks/esql/use_esql_availability', () => ({

const useEsqlAvailabilityMock = useEsqlAvailability as jest.Mock;

const defaultProps = {
renderCellValue: () => {
return null;
},
rowRenderers: [],
timelineId: TimelineId.test,
timelineType: TimelineTypeEnum.default,
timelineDescription: '',
};

describe('Timeline', () => {
describe('esql tab', () => {
const esqlTabSubj = `timelineTabs-${TimelineTabs.esql}`;
const defaultProps = {
renderCellValue: () => {
return null;
},
rowRenderers: [],
timelineId: TimelineId.test,
timelineType: TimelineTypeEnum.default,
timelineDescription: '',
};

it('should show the esql tab', () => {
render(
Expand Down Expand Up @@ -131,4 +144,31 @@ describe('Timeline', () => {
});
});
});

describe('analyzer tab and session view tab', () => {
const analyzerTabSubj = `timelineTabs-${TimelineTabs.graph}`;
const sessionViewTabSubj = `timelineTabs-${TimelineTabs.session}`;
it('should show the analyzer tab when the advanced setting is disabled', () => {
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
render(
<TestProviders>
<TabsContent {...defaultProps} />
</TestProviders>
);
expect(screen.getByTestId(analyzerTabSubj)).toBeInTheDocument();
expect(screen.getByTestId(sessionViewTabSubj)).toBeInTheDocument();
});

it('should not show the analyzer tab when the advanced setting is enabled', async () => {
mockUseUiSetting.mockReturnValue([true]);
(useLicense as jest.Mock).mockReturnValue({ isEnterprise: () => true });
render(
<TestProviders>
<TabsContent {...defaultProps} />
</TestProviders>
);
expect(screen.queryByTestId(analyzerTabSubj)).not.toBeInTheDocument();
expect(screen.queryByTestId(sessionViewTabSubj)).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { Ref, ReactElement, ComponentType } from 'react';
import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';

import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import type { State } from '../../../../common/store';
Expand Down Expand Up @@ -42,6 +43,7 @@ import { useLicense } from '../../../../common/hooks/use_license';
import { initializeTimelineSettings } from '../../../store/actions';
import { selectTimelineById, selectTimelineESQLSavedSearchId } from '../../../store/selectors';
import { fetchNotesBySavedObjectIds, selectSortedNotesBySavedObjectId } from '../../../../notes';
import { ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING } from '../../../../../common/constants';

const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScroll: boolean }>(
({ $isVisible = false, isOverflowYScroll = false }) => ({
Expand Down Expand Up @@ -255,6 +257,10 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
'securitySolutionNotesEnabled'
);

const [visualizationInFlyoutEnabled] = useUiSetting$<boolean>(
ENABLE_VISUALIZATIONS_IN_FLYOUT_SETTING
);

const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId));
const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId));
const shouldShowESQLTab = useMemo(() => {
Expand Down Expand Up @@ -409,16 +415,18 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
{showTimeline && <EqlEventsCountBadge />}
</StyledEuiTab>
)}
<EuiTab
data-test-subj={`timelineTabs-${TimelineTabs.graph}`}
onClick={setGraphAsActiveTab}
isSelected={activeTab === TimelineTabs.graph}
disabled={!graphEventId}
key={TimelineTabs.graph}
>
{i18n.ANALYZER_TAB}
</EuiTab>
{isEnterprisePlus && (
{!visualizationInFlyoutEnabled && (
<EuiTab
data-test-subj={`timelineTabs-${TimelineTabs.graph}`}
onClick={setGraphAsActiveTab}
isSelected={activeTab === TimelineTabs.graph}
disabled={!graphEventId}
key={TimelineTabs.graph}
>
{i18n.ANALYZER_TAB}
</EuiTab>
)}
{isEnterprisePlus && !visualizationInFlyoutEnabled && (
<EuiTab
data-test-subj={`timelineTabs-${TimelineTabs.session}`}
onClick={setSessionAsActiveTab}
Expand Down