[Security Solution][Endpoint] Set show= search param for endpoint details flyout tabs (#105123)

* add `show=` param when details flyout opens

fixes elastic/security-team/issues/1346

* set `show=` param correctly for `activity_log` and `details` flyout tabs

fixes elastic/security-team/issues/1346

* update tests to reflect change to endpoint details URL

fixes elastic/security-team/issues/1346

* Update endpoint_details_tabs.tsx

* review changes

* avoid redundant hostname refresh on header

review changes

* review changes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Ashokaditya 2021-07-14 16:17:35 +02:00 committed by GitHub
parent dd081bad71
commit 25b549202a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 117 additions and 111 deletions

View file

@ -81,6 +81,9 @@ export const getEndpointDetailsPath = (
const queryParams: EndpointDetailsUrlProps = { ...rest };
switch (props.name) {
case 'endpointDetails':
queryParams.show = 'details';
break;
case 'endpointIsolate':
queryParams.show = 'isolate';
break;

View file

@ -15,7 +15,7 @@ import {
} from '../../../../../common/endpoint/types';
import { ServerApiError } from '../../../../common/types';
import { GetPolicyListResponse } from '../../policy/types';
import { EndpointIndexUIQueryParams, EndpointState } from '../types';
import { EndpointState } from '../types';
import { IIndexPattern } from '../../../../../../../../src/plugins/data/public';
export interface ServerReturnedEndpointList {
@ -173,11 +173,6 @@ export interface EndpointDetailsActivityLogUpdateIsInvalidDateRange {
};
}
export interface EndpointDetailsFlyoutTabChanged {
type: 'endpointDetailsFlyoutTabChanged';
payload: { flyoutView: EndpointIndexUIQueryParams['show'] };
}
export type EndpointAction =
| ServerReturnedEndpointList
| ServerFailedToReturnEndpointList
@ -185,7 +180,6 @@ export type EndpointAction =
| ServerFailedToReturnEndpointDetails
| EndpointDetailsActivityLogUpdatePaging
| EndpointDetailsActivityLogUpdateIsInvalidDateRange
| EndpointDetailsFlyoutTabChanged
| EndpointDetailsActivityLogChanged
| ServerReturnedEndpointPolicyResponse
| ServerFailedToReturnEndpointPolicyResponse

View file

@ -19,7 +19,6 @@ export const initialEndpointPageState = (): Immutable<EndpointState> => {
loading: false,
error: undefined,
endpointDetails: {
flyoutView: undefined,
activityLog: {
paging: {
disabled: false,

View file

@ -42,7 +42,6 @@ describe('EndpointList store concerns', () => {
loading: false,
error: undefined,
endpointDetails: {
flyoutView: undefined,
activityLog: {
paging: {
disabled: false,

View file

@ -44,7 +44,6 @@ import {
} from '../../../../common/lib/endpoint_isolation/mocks';
import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator';
import { endpointPageHttpMock } from '../mocks';
import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
jest.mock('../../policy/store/services/ingest', () => ({
sendGetAgentConfigList: () => Promise.resolve({ items: [] }),
@ -221,20 +220,12 @@ describe('endpoint list middleware', () => {
describe('handle ActivityLog State Change actions', () => {
const endpointList = getEndpointListApiResponse();
const search = getEndpointDetailsPath({
name: 'endpointDetails',
name: 'endpointActivityLog',
selected_endpoint: endpointList.hosts[0].metadata.agent.id,
});
const dispatchUserChangedUrl = () => {
dispatchUserChangedUrlToEndpointList({ search: `?${search.split('?').pop()}` });
};
const dispatchFlyoutViewChange = () => {
dispatch({
type: 'endpointDetailsFlyoutTabChanged',
payload: {
flyoutView: EndpointDetailsTabsTypes.activityLog,
},
});
};
const fleetActionGenerator = new FleetActionGenerator('seed');
const actionData = fleetActionGenerator.generate({
@ -274,7 +265,6 @@ describe('endpoint list middleware', () => {
it('should set ActivityLog state to loading', async () => {
dispatchUserChangedUrl();
dispatchFlyoutViewChange();
const loadingDispatched = waitForAction('endpointDetailsActivityLogChanged', {
validate(action) {

View file

@ -34,8 +34,8 @@ import {
getActivityLogDataPaging,
getLastLoadedActivityLogData,
detailsData,
getEndpointDetailsFlyoutView,
getIsEndpointPackageInfoUninitialized,
getIsOnEndpointDetailsActivityLog,
} from './selectors';
import { AgentIdsPendingActions, EndpointState, PolicyIds } from '../types';
import {
@ -63,7 +63,6 @@ import { AppAction } from '../../../../common/store/actions';
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
import { EndpointPackageInfoStateChanged } from './action';
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
import { getIsInvalidDateRange } from '../utils';
type EndpointPageStore = ImmutableMiddlewareAPI<EndpointState, AppAction>;
@ -369,7 +368,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
if (
action.type === 'userChangedUrl' &&
hasSelectedEndpoint(getState()) === true &&
getEndpointDetailsFlyoutView(getState()) === EndpointDetailsTabsTypes.activityLog
getIsOnEndpointDetailsActivityLog(getState())
) {
// call the activity log api
dispatch({

View file

@ -14,6 +14,7 @@ import {
isOnEndpointPage,
hasSelectedEndpoint,
uiQueryParams,
getIsOnEndpointDetailsActivityLog,
getCurrentIsolationRequestState,
} from './selectors';
import { EndpointState } from '../types';
@ -190,14 +191,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
},
},
};
} else if (action.type === 'endpointDetailsFlyoutTabChanged') {
return {
...state,
endpointDetails: {
...state.endpointDetails!,
flyoutView: action.payload.flyoutView,
},
};
} else if (action.type === 'endpointDetailsActivityLogChanged') {
return handleEndpointDetailsActivityLogChanged(state, action);
} else if (action.type === 'endpointPendingActionsStateChanged') {
@ -289,6 +282,19 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
const wasPreviouslyOnListPage = isOnEndpointPage(state) && !hasSelectedEndpoint(state);
const isCurrentlyOnDetailsPage = isOnEndpointPage(newState) && hasSelectedEndpoint(newState);
const wasPreviouslyOnDetailsPage = isOnEndpointPage(state) && hasSelectedEndpoint(state);
const wasPreviouslyOnActivityLogPage =
isOnEndpointPage(state) &&
hasSelectedEndpoint(state) &&
getIsOnEndpointDetailsActivityLog(state);
const isCurrentlyOnActivityLogPage =
isOnEndpointPage(newState) &&
hasSelectedEndpoint(newState) &&
getIsOnEndpointDetailsActivityLog(newState);
const isNotLoadingDetails =
isCurrentlyOnActivityLogPage ||
(wasPreviouslyOnActivityLogPage &&
uiQueryParams(state).selected_endpoint === uiQueryParams(newState).selected_endpoint);
const stateUpdates: Partial<EndpointState> = {
location: action.payload,
@ -343,7 +349,7 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
activityLog,
hostDetails: {
...state.endpointDetails.hostDetails,
detailsLoading: true,
detailsLoading: !isNotLoadingDetails,
detailsError: undefined,
},
},

View file

@ -38,6 +38,7 @@ import {
import { ServerApiError } from '../../../../common/types';
import { isEndpointHostIsolated } from '../../../../common/utils/validators';
import { EndpointHostIsolationStatusProps } from '../../../../common/components/endpoint/host_isolation';
import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
export const listData = (state: Immutable<EndpointState>) => state.hosts;
@ -362,9 +363,11 @@ export const getIsolationRequestError: (
}
});
export const getEndpointDetailsFlyoutView = (
export const getIsOnEndpointDetailsActivityLog: (
state: Immutable<EndpointState>
): EndpointIndexUIQueryParams['show'] => state.endpointDetails.flyoutView;
) => boolean = createSelector(uiQueryParams, (searchParams) => {
return searchParams.show === EndpointDetailsTabsTypes.activityLog;
});
export const getActivityLogDataPaging = (
state: Immutable<EndpointState>

View file

@ -36,7 +36,6 @@ export interface EndpointState {
/** api error from retrieving host list */
error?: ServerApiError;
endpointDetails: {
flyoutView: EndpointIndexUIQueryParams['show'];
activityLog: {
paging: {
disabled?: boolean;

View file

@ -6,16 +6,18 @@
*/
import { useDispatch } from 'react-redux';
import React, { memo, useCallback, useMemo, useState } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { EuiTab, EuiTabs, EuiFlyoutBody, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui';
import { EndpointIndexUIQueryParams } from '../../../types';
import { EndpointAction } from '../../../store/action';
import { useEndpointSelector } from '../../hooks';
import { getActivityLogDataPaging } from '../../../store/selectors';
import { EndpointDetailsFlyoutHeader } from './flyout_header';
import { useNavigateByRouterEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { useAppUrl } from '../../../../../../common/lib/kibana';
export enum EndpointDetailsTabsTypes {
overview = 'overview',
overview = 'details',
activityLog = 'activity_log',
}
@ -27,34 +29,52 @@ interface EndpointDetailsTabs {
id: string;
name: string;
content: JSX.Element;
route: string;
}
const EndpointDetailsTab = memo(
({
tab,
isSelected,
handleTabClick,
}: {
tab: EndpointDetailsTabs;
isSelected: boolean;
handleTabClick: () => void;
}) => {
const { getAppUrl } = useAppUrl();
const onClick = useNavigateByRouterEventHandler(tab.route, handleTabClick);
return (
<EuiTab
href={getAppUrl({ path: tab.route })}
onClick={onClick}
isSelected={isSelected}
key={tab.id}
data-test-subj={tab.id}
>
{tab.name}
</EuiTab>
);
}
);
EndpointDetailsTab.displayName = 'EndpointDetailsTab';
export const EndpointDetailsFlyoutTabs = memo(
({
hostname,
show,
tabs,
}: {
hostname?: string;
hostname: string;
show: EndpointIndexUIQueryParams['show'];
tabs: EndpointDetailsTabs[];
}) => {
const dispatch = useDispatch<(action: EndpointAction) => void>();
const { pageSize } = useEndpointSelector(getActivityLogDataPaging);
const [selectedTabId, setSelectedTabId] = useState<EndpointDetailsTabsId>(() => {
return show === 'details'
? EndpointDetailsTabsTypes.overview
: EndpointDetailsTabsTypes.activityLog;
});
const handleTabClick = useCallback(
(tab: EuiTabbedContentTab) => {
dispatch({
type: 'endpointDetailsFlyoutTabChanged',
payload: {
flyoutView: tab.id as EndpointIndexUIQueryParams['show'],
},
});
if (tab.id === EndpointDetailsTabsTypes.activityLog) {
dispatch({
type: 'endpointDetailsActivityLogUpdatePaging',
@ -67,25 +87,18 @@ export const EndpointDetailsFlyoutTabs = memo(
},
});
}
return setSelectedTabId(tab.id as EndpointDetailsTabsId);
},
[dispatch, pageSize, setSelectedTabId]
[dispatch, pageSize]
);
const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId), [
tabs,
selectedTabId,
]);
const selectedTab = useMemo(() => tabs.find((tab) => tab.id === show), [tabs, show]);
const renderTabs = tabs.map((tab) => (
<EuiTab
onClick={() => handleTabClick(tab)}
isSelected={tab.id === selectedTabId}
key={tab.id}
data-test-subj={tab.id}
>
{tab.name}
</EuiTab>
<EndpointDetailsTab
tab={tab}
handleTabClick={() => handleTabClick(tab)}
isSelected={tab.id === selectedTab?.id}
/>
));
return (

View file

@ -120,16 +120,21 @@ export default {
export const Tabs = () => (
<EndpointDetailsFlyoutTabs
show="details"
hostname="endpoint-name-01"
tabs={[
{
id: 'overview',
name: 'Overview',
content: <>{'Endpoint Details'}</>,
route:
'/administration/endpoints?page_index=0&page_size=10&selected_endpoint=endpoint-id-00001010&show=details',
},
{
id: 'activity_log',
name: 'Activity Log',
content: ActivityLogMarkup(),
route:
'/administration/endpoints?page_index=0&page_size=10&selected_endpoint=endpoint-id-00001010&show=activity_log',
},
]}
/>

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { useDispatch } from 'react-redux';
import React, { useCallback, useEffect, useMemo, memo } from 'react';
import {
EuiFlyout,
@ -51,14 +50,11 @@ import { PreferenceFormattedDateFromPrimitive } from '../../../../../common/comp
import { EndpointIsolationFlyoutPanel } from './components/endpoint_isolate_flyout_panel';
import { BackToEndpointDetailsFlyoutSubHeader } from './components/back_to_endpoint_details_flyout_subheader';
import { FlyoutBodyNoTopPadding } from './components/flyout_body_no_top_padding';
import { getEndpointListPath } from '../../../../common/routing';
import { getEndpointListPath, getEndpointDetailsPath } from '../../../../common/routing';
import { ActionsMenu } from './components/actions_menu';
import { EndpointIndexUIQueryParams } from '../../types';
import { EndpointAction } from '../../store/action';
import { EndpointDetailsFlyoutHeader } from './components/flyout_header';
export const EndpointDetailsFlyout = memo(() => {
const dispatch = useDispatch<(action: EndpointAction) => void>();
const history = useHistory();
const toasts = useToasts();
const queryParams = useEndpointSelector(uiQueryParams);
@ -75,18 +71,6 @@ export const EndpointDetailsFlyout = memo(() => {
const hostStatus = useEndpointSelector(hostStatusInfo);
const show = useEndpointSelector(showView);
const setFlyoutView = useCallback(
(flyoutView: EndpointIndexUIQueryParams['show']) => {
dispatch({
type: 'endpointDetailsFlyoutTabChanged',
payload: {
flyoutView,
},
});
},
[dispatch]
);
const ContentLoadingMarkup = useMemo(
() => (
<>
@ -98,23 +82,40 @@ export const EndpointDetailsFlyout = memo(() => {
[]
);
const tabs = [
{
id: EndpointDetailsTabsTypes.overview,
name: i18.OVERVIEW,
content:
hostDetails === undefined ? (
ContentLoadingMarkup
) : (
<EndpointDetails details={hostDetails} policyInfo={policyInfo} hostStatus={hostStatus} />
),
},
{
id: EndpointDetailsTabsTypes.activityLog,
name: i18.ACTIVITY_LOG.tabTitle,
content: <EndpointActivityLog activityLog={activityLog} />,
},
];
const getTabs = useCallback(
(id: string) => [
{
id: EndpointDetailsTabsTypes.overview,
name: i18.OVERVIEW,
route: getEndpointDetailsPath({
...queryParams,
name: 'endpointDetails',
selected_endpoint: id,
}),
content:
hostDetails === undefined ? (
ContentLoadingMarkup
) : (
<EndpointDetails
details={hostDetails}
policyInfo={policyInfo}
hostStatus={hostStatus}
/>
),
},
{
id: EndpointDetailsTabsTypes.activityLog,
name: i18.ACTIVITY_LOG.tabTitle,
route: getEndpointDetailsPath({
...queryParams,
name: 'endpointActivityLog',
selected_endpoint: id,
}),
content: <EndpointActivityLog activityLog={activityLog} />,
},
],
[ContentLoadingMarkup, hostDetails, policyInfo, hostStatus, activityLog, queryParams]
);
const showFlyoutFooter =
show === 'details' || show === 'policy_response' || show === 'activity_log';
@ -127,11 +128,9 @@ export const EndpointDetailsFlyout = memo(() => {
...urlSearchParams,
})
);
setFlyoutView(undefined);
}, [setFlyoutView, history, queryParamsWithoutSelectedEndpoint]);
}, [history, queryParamsWithoutSelectedEndpoint]);
useEffect(() => {
setFlyoutView(show);
if (hostDetailsError !== undefined) {
toasts.addDanger({
title: i18n.translate('xpack.securitySolution.endpoint.details.errorTitle', {
@ -142,10 +141,7 @@ export const EndpointDetailsFlyout = memo(() => {
}),
});
}
return () => {
setFlyoutView(undefined);
};
}, [hostDetailsError, setFlyoutView, show, toasts]);
}, [hostDetailsError, show, toasts]);
return (
<EuiFlyout
@ -167,9 +163,9 @@ export const EndpointDetailsFlyout = memo(() => {
<>
{(show === 'details' || show === 'activity_log') && (
<EndpointDetailsFlyoutTabs
hostname={hostDetails?.host?.hostname}
hostname={hostDetails.host.hostname}
show={show}
tabs={tabs}
tabs={getTabs(hostDetails.agent.id)}
/>
)}

View file

@ -997,7 +997,7 @@ describe('when on the endpoint list page', () => {
const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton');
expect(subHeaderBackLink.textContent).toBe('Endpoint Details');
expect(subHeaderBackLink.getAttribute('href')).toEqual(
`${APP_PATH}${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1`
`${APP_PATH}${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=details`
);
});
@ -1009,7 +1009,7 @@ describe('when on the endpoint list page', () => {
});
const changedUrlAction = await userChangedUrlChecker;
expect(changedUrlAction.payload.search).toEqual(
'?page_index=0&page_size=10&selected_endpoint=1'
'?page_index=0&page_size=10&selected_endpoint=1&show=details'
);
});
@ -1082,7 +1082,7 @@ describe('when on the endpoint list page', () => {
expect((await changeUrlAction).payload).toMatchObject({
pathname: `${MANAGEMENT_PATH}/endpoints`,
search: '?page_index=0&page_size=10&selected_endpoint=1',
search: '?page_index=0&page_size=10&selected_endpoint=1&show=details',
});
});
@ -1095,7 +1095,7 @@ describe('when on the endpoint list page', () => {
expect((await changeUrlAction).payload).toMatchObject({
pathname: `${MANAGEMENT_PATH}/endpoints`,
search: '?page_index=0&page_size=10&selected_endpoint=1',
search: '?page_index=0&page_size=10&selected_endpoint=1&show=details',
});
});
@ -1115,7 +1115,7 @@ describe('when on the endpoint list page', () => {
expect((await changeUrlAction).payload).toMatchObject({
pathname: `${MANAGEMENT_PATH}/endpoints`,
search: '?page_index=0&page_size=10&selected_endpoint=1',
search: '?page_index=0&page_size=10&selected_endpoint=1&show=details',
});
});