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

[7.15] [Security Solution][Endpoint] Fix activity log load empty state flicker (#110233) #110525

Merged
merged 12 commits into from
Sep 1, 2021
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 @@ -270,22 +270,15 @@ describe('endpoint list middleware', () => {

it('should set ActivityLog state to loading', async () => {
dispatchUserChangedUrl();
dispatchGetActivityLogLoading();

const loadingDispatched = waitForAction('endpointDetailsActivityLogChanged', {
validate(action) {
return isLoadingResourceState(action.payload);
},
});
dispatchGetActivityLogLoading();

const loadingDispatchedResponse = await loadingDispatched;
expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledWith({
path: expect.any(String),
query: {
page: 1,
page_size: 50,
},
});
expect(loadingDispatchedResponse.payload.type).toEqual('LoadingResourceState');
});

Expand Down Expand Up @@ -327,6 +320,25 @@ describe('endpoint list middleware', () => {
expect(failedAction.error).toBe(apiError);
});

it('should not call API again if it fails', async () => {
dispatchUserChangedUrl();

const apiError = new Error('oh oh');
const failedDispatched = waitForAction('endpointDetailsActivityLogChanged', {
validate(action) {
return isFailedResourceState(action.payload);
},
});

mockedApis.responseProvider.activityLogResponse.mockImplementation(() => {
throw apiError;
});

await failedDispatched;

expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledTimes(1);
});

it('should not fetch Activity Log with invalid date ranges', async () => {
dispatchUserChangedUrl();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ import {
getActivityLogData,
getActivityLogDataPaging,
getLastLoadedActivityLogData,
getActivityLogError,
detailsData,
getIsEndpointPackageInfoUninitialized,
getIsOnEndpointDetailsActivityLog,
getMetadataTransformStats,
isMetadataTransformStatsLoading,
getActivityLogIsUninitializedOrHasSubsequentAPIError,
} from './selectors';
import {
AgentIdsPendingActions,
Expand Down Expand Up @@ -124,14 +126,16 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
if (
action.type === 'userChangedUrl' &&
hasSelectedEndpoint(getState()) === true &&
getIsOnEndpointDetailsActivityLog(getState())
getIsOnEndpointDetailsActivityLog(getState()) &&
getActivityLogIsUninitializedOrHasSubsequentAPIError(getState())
) {
await endpointDetailsActivityLogChangedMiddleware({ store, coreStart });
}

// page activity log API
if (
action.type === 'endpointDetailsActivityLogUpdatePaging' &&
!getActivityLogError(getState()) &&
hasSelectedEndpoint(getState())
) {
await endpointDetailsActivityLogPagingMiddleware({ store, coreStart });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer<EndpointDetailsActivi
state,
action
) => {
const pagingOptions =
const updatedActivityLog =
action.payload.type === 'LoadedResourceState'
? {
...state.endpointDetails.activityLog,
Expand All @@ -53,7 +53,7 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer<EndpointDetailsActivi
endpointDetails: {
...state.endpointDetails!,
activityLog: {
...pagingOptions,
...updatedActivityLog,
logData: action.payload,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,12 @@ export const getLastLoadedActivityLogData: (
return getLastLoadedResourceState(activityLog)?.data;
});

export const getActivityLogUninitialized: (
state: Immutable<EndpointState>
) => boolean = createSelector(getActivityLogData, (activityLog) =>
isUninitialisedResourceState(activityLog)
);

export const getActivityLogRequestLoading: (
state: Immutable<EndpointState>
) => boolean = createSelector(getActivityLogData, (activityLog) =>
Expand Down Expand Up @@ -413,6 +419,19 @@ export const getActivityLogError: (
}
});

// returns a true if either lgo is uninitialised
// or if it has failed an api call after having fetched a non empty log list earlier
export const getActivityLogIsUninitializedOrHasSubsequentAPIError: (
state: Immutable<EndpointState>
) => boolean = createSelector(
getActivityLogUninitialized,
getLastLoadedActivityLogData,
getActivityLogError,
(isUninitialized, lastLoadedLogData, isAPIError) => {
return isUninitialized || (!isAPIError && !!lastLoadedLogData?.data.length);
}
);

export const getIsEndpointHostIsolated = createSelector(detailsData, (details) => {
return (details && isEndpointHostIsolated(details)) || false;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
* 2.0.
*/

import { useDispatch } from 'react-redux';
import React, { memo, useCallback, useMemo } from 'react';
import { EuiTab, EuiTabs, EuiFlyoutBody, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui';
import React, { memo, useMemo } from 'react';
import { EuiTab, EuiTabs, EuiFlyoutBody, 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';
Expand All @@ -33,17 +30,9 @@ interface EndpointDetailsTabs {
}

const EndpointDetailsTab = memo(
({
tab,
isSelected,
handleTabClick,
}: {
tab: EndpointDetailsTabs;
isSelected: boolean;
handleTabClick: () => void;
}) => {
({ tab, isSelected }: { tab: EndpointDetailsTabs; isSelected: boolean }) => {
const { getAppUrl } = useAppUrl();
const onClick = useNavigateByRouterEventHandler(tab.route, handleTabClick);
const onClick = useNavigateByRouterEventHandler(tab.route);
return (
<EuiTab
href={getAppUrl({ path: tab.route })}
Expand All @@ -70,35 +59,10 @@ export const EndpointDetailsFlyoutTabs = memo(
show: EndpointIndexUIQueryParams['show'];
tabs: EndpointDetailsTabs[];
}) => {
const dispatch = useDispatch<(action: EndpointAction) => void>();
const { pageSize } = useEndpointSelector(getActivityLogDataPaging);

const handleTabClick = useCallback(
(tab: EuiTabbedContentTab) => {
if (tab.id === EndpointDetailsTabsTypes.activityLog) {
dispatch({
type: 'endpointDetailsActivityLogUpdatePaging',
payload: {
disabled: false,
page: 1,
pageSize,
startDate: undefined,
endDate: undefined,
},
});
}
},
[dispatch, pageSize]
);

const selectedTab = useMemo(() => tabs.find((tab) => tab.id === show), [tabs, show]);

const renderTabs = tabs.map((tab) => (
<EndpointDetailsTab
tab={tab}
handleTabClick={() => handleTabClick(tab)}
isSelected={tab.id === selectedTab?.id}
/>
<EndpointDetailsTab tab={tab} isSelected={tab.id === selectedTab?.id} />
));

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
getActivityLogRequestLoaded,
getLastLoadedActivityLogData,
getActivityLogRequestLoading,
getActivityLogUninitialized,
} from '../../store/selectors';

const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ isShorter: boolean }>`
Expand All @@ -42,6 +43,7 @@ const LoadMoreTrigger = styled.div`

export const EndpointActivityLog = memo(
({ activityLog }: { activityLog: AsyncResourceState<Immutable<ActivityLog>> }) => {
const activityLogUninitialized = useEndpointSelector(getActivityLogUninitialized);
const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading);
const activityLogLoaded = useEndpointSelector(getActivityLogRequestLoaded);
const activityLastLogData = useEndpointSelector(getLastLoadedActivityLogData);
Expand Down Expand Up @@ -96,7 +98,9 @@ export const EndpointActivityLog = memo(
return (
<>
<StyledEuiFlexGroup direction="column" responsive={false} isShorter={isShorter}>
{showEmptyState ? (
{(activityLogLoading && !activityLastLogData?.data.length) || activityLogUninitialized ? (
<EuiLoadingContent lines={3} />
) : showEmptyState ? (
<EuiFlexItem>
<EuiEmptyPrompt
iconType="editorUnorderedList"
Expand Down