[Security Solution][Endpoint][Response Actions] Sync action status correctly when API response is slow (#136644)

* sync action details refresh times

fixes elastic/kibana/issues/136098

* mutate local actionId instead

fixes elastic/kibana/issues/136098

* Update API responses

review changes

* Correctly call API and update store

Ensure that we call the action API and also that we update the store only when the console is open

* fix types

* add tests for the fix

fixes elastic/kibana/issues/136098

* fix incorrect imports ++ failing tests

* Fix isolate/release tests

Co-authored-by: Paul Tavares <paul.tavares@elastic.co>
This commit is contained in:
Ashokaditya 2022-07-25 23:27:31 +02:00 committed by GitHub
parent 546f2b158b
commit 3dc26b6a75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 304 additions and 204 deletions

View file

@ -7,7 +7,7 @@
import type {
HostIsolationRequestBody,
HostIsolationResponse,
ResponseActionApiResponse,
} from '../../../../common/endpoint/types';
import { KibanaServices } from '../kibana';
import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/endpoint/constants';
@ -15,8 +15,8 @@ import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE } from '../../../../common/end
/** Isolates a Host running either elastic endpoint or fleet agent */
export const isolateHost = async (
params: HostIsolationRequestBody
): Promise<HostIsolationResponse> => {
return KibanaServices.get().http.post<HostIsolationResponse>(ISOLATE_HOST_ROUTE, {
): Promise<ResponseActionApiResponse> => {
return KibanaServices.get().http.post<ResponseActionApiResponse>(ISOLATE_HOST_ROUTE, {
body: JSON.stringify(params),
});
};
@ -24,8 +24,8 @@ export const isolateHost = async (
/** Un-isolates a Host running either elastic endpoint or fleet agent */
export const unIsolateHost = async (
params: HostIsolationRequestBody
): Promise<HostIsolationResponse> => {
return KibanaServices.get().http.post<HostIsolationResponse>(UNISOLATE_HOST_ROUTE, {
): Promise<ResponseActionApiResponse> => {
return KibanaServices.get().http.post<ResponseActionApiResponse>(UNISOLATE_HOST_ROUTE, {
body: JSON.stringify(params),
});
};

View file

@ -7,7 +7,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getCasesFromAlertsUrl } from '@kbn/cases-plugin/common';
import type { HostIsolationResponse, HostInfo } from '../../../../../common/endpoint/types';
import type { ResponseActionApiResponse, HostInfo } from '../../../../../common/endpoint/types';
import {
DETECTION_ENGINE_QUERY_SIGNALS_URL,
DETECTION_ENGINE_SIGNALS_STATUS_URL,
@ -149,7 +149,7 @@ export const createHostIsolation = async ({
endpointId: string;
comment?: string;
caseIds?: string[];
}): Promise<HostIsolationResponse> =>
}): Promise<ResponseActionApiResponse> =>
isolateHost({
endpoint_ids: [endpointId],
comment,
@ -173,7 +173,7 @@ export const createHostUnIsolation = async ({
endpointId: string;
comment?: string;
caseIds?: string[];
}): Promise<HostIsolationResponse> =>
}): Promise<ResponseActionApiResponse> =>
unIsolateHost({
endpoint_ids: [endpointId],
comment,

View file

@ -11,7 +11,8 @@ import type { ArtifactListPageProps } from './artifact_list_page';
import { act, fireEvent, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { ArtifactListPageRenderingSetup } from './mocks';
import { getArtifactListPageRenderingSetup, getDeferred } from './mocks';
import { getArtifactListPageRenderingSetup } from './mocks';
import { getDeferred } from '../mocks';
jest.mock('../../../common/components/user_privileges');

View file

@ -8,9 +8,10 @@
import type { AppContextTestRender } from '../../../../common/mock/endpoint';
import type { trustedAppsAllHttpMocks } from '../../../mocks';
import type { ArtifactListPageRenderingSetup } from '../mocks';
import { getArtifactListPageRenderingSetup, getDeferred } from '../mocks';
import { getArtifactListPageRenderingSetup } from '../mocks';
import { act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getDeferred } from '../../mocks';
// FLAKY: https://github.com/elastic/kibana/issues/135794
describe.skip('When displaying the Delete artfifact modal in the Artifact List Page', () => {

View file

@ -9,7 +9,7 @@ import type { ArtifactListPageProps } from '../artifact_list_page';
import { act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { getFormComponentMock } from '../mocks';
import { getArtifactListPageRenderingSetup, getDeferred } from '../mocks';
import { getArtifactListPageRenderingSetup } from '../mocks';
import { ExceptionsListItemGenerator } from '../../../../../common/endpoint/data_generators/exceptions_list_item_generator';
import type { HttpFetchOptionsWithPath } from '@kbn/core/public';
import { BY_POLICY_ARTIFACT_TAG_PREFIX } from '../../../../../common/endpoint/service/artifacts';
@ -19,6 +19,7 @@ import type { trustedAppsAllHttpMocks } from '../../../mocks';
import { useUserPrivileges as _useUserPrivileges } from '../../../../common/components/user_privileges';
import { entriesToConditionEntries } from '../../../../common/utils/exception_list_items/mappers';
import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { getDeferred } from '../../mocks';
jest.mock('../../../../common/components/user_privileges');
const useUserPrivileges = _useUserPrivileges as jest.Mock;

View file

@ -51,25 +51,6 @@ export const getFormComponentMock = (): {
};
};
interface DeferredInterface<T = void> {
promise: Promise<T>;
resolve: (data: T) => void;
reject: (e: Error) => void;
}
export const getDeferred = function <T = void>(): DeferredInterface<T> {
let resolve: DeferredInterface<T>['resolve'];
let reject: DeferredInterface<T>['reject'];
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
// @ts-ignore
return { promise, resolve, reject };
};
export const getFirstCard = async (
renderResult: ReturnType<AppContextTestRender['render']>,
{

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export const ACTION_DETAILS_REFRESH_INTERVAL = 3000;

View file

@ -0,0 +1,100 @@
/*
* 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, useRef } from 'react';
import { useIsMounted } from '../../hooks/use_is_mounted';
import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details';
import { ACTION_DETAILS_REFRESH_INTERVAL } from './constants';
import type { ActionRequestState, ActionRequestComponentProps } from './types';
import type { useSendIsolateEndpointRequest } from '../../hooks/endpoint/use_send_isolate_endpoint_request';
import type { useSendReleaseEndpointRequest } from '../../hooks/endpoint/use_send_release_endpoint_request';
export const useUpdateActionState = ({
actionRequestApi,
actionRequest,
command,
endpointId,
setStatus,
setStore,
isPending,
}: Pick<ActionRequestComponentProps, 'command' | 'setStatus' | 'setStore'> & {
actionRequestApi: ReturnType<
typeof useSendIsolateEndpointRequest | typeof useSendReleaseEndpointRequest
>;
actionRequest?: ActionRequestState;
endpointId?: string;
isPending: boolean;
}) => {
const isMounted = useIsMounted();
const actionRequestSent = Boolean(actionRequest?.requestSent);
const { data: actionDetails } = useGetActionDetails(actionRequest?.actionId ?? '-', {
enabled: Boolean(actionRequest?.actionId) && isPending,
refetchInterval: isPending ? ACTION_DETAILS_REFRESH_INTERVAL : false,
});
// keep a reference to track the console's mounted state
// in order to update the store and cause a re-render on action request API response
const latestIsMounted = useRef(false);
latestIsMounted.current = isMounted;
// Create action request
useEffect(() => {
if (!actionRequestSent && endpointId && isMounted) {
const request: ActionRequestState = {
requestSent: true,
actionId: undefined,
};
actionRequestApi
.mutateAsync({
endpoint_ids: [endpointId],
comment: command.args.args?.comment?.[0],
})
.then((response) => {
request.actionId = response.data.id;
if (latestIsMounted.current) {
setStore((prevState) => {
return { ...prevState, actionRequest: { ...request } };
});
}
});
setStore((prevState) => {
return { ...prevState, actionRequest: request };
});
}
}, [
actionRequestApi,
actionRequestSent,
command.args.args?.comment,
endpointId,
isMounted,
setStore,
]);
useEffect(() => {
// update the console's mounted state ref
latestIsMounted.current = isMounted;
// set to false when unmounted/console is hidden
return () => {
latestIsMounted.current = false;
};
}, [isMounted]);
useEffect(() => {
if (actionDetails?.data.isCompleted) {
setStatus('success');
setStore((prevState) => {
return {
...prevState,
completedActionDetails: actionDetails.data,
};
});
}
}, [actionDetails?.data, actionDetails?.data.isCompleted, setStatus, setStore]);
};

View file

@ -16,6 +16,7 @@ import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_a
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
import { enterConsoleCommand } from '../console/mocks';
import { waitFor } from '@testing-library/react';
import { getDeferred } from '../mocks';
describe('When using isolate action from response actions console', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
@ -119,6 +120,30 @@ describe('When using isolate action from response actions console', () => {
});
});
it('should create action request and store id even if console is closed prior to request api response', async () => {
const deferrable = getDeferred();
apiMocks.responseProvider.isolateHost.mockDelay.mockReturnValue(deferrable.promise);
await render();
// enter command
enterConsoleCommand(renderResult, 'isolate');
// hide console
await consoleManagerMockAccess.hideOpenedConsole();
// Release API response
deferrable.resolve();
await waitFor(() => {
expect(apiMocks.responseProvider.isolateHost).toHaveBeenCalledTimes(1);
});
// open console
await consoleManagerMockAccess.openRunningConsole();
// status should be updating
await waitFor(() => {
expect(apiMocks.responseProvider.actionDetails.mock.calls.length).toBeGreaterThan(0);
});
});
describe('and when console is closed (not terminated) and then reopened', () => {
beforeEach(() => {
const _render = render;

View file

@ -5,89 +5,47 @@
* 2.0.
*/
import React, { memo, useEffect } from 'react';
import type { ActionDetails } from '../../../../common/endpoint/types';
import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details';
import type { EndpointCommandDefinitionMeta } from './types';
import React, { memo } from 'react';
import type { ActionRequestComponentProps } from './types';
import { useSendIsolateEndpointRequest } from '../../hooks/endpoint/use_send_isolate_endpoint_request';
import type { CommandExecutionComponentProps } from '../console/types';
import { ActionError } from './action_error';
import { useUpdateActionState } from './hooks';
export const IsolateActionResult = memo<
CommandExecutionComponentProps<
{ comment?: string },
{
actionId?: string;
actionRequestSent?: boolean;
completedActionDetails?: ActionDetails;
},
EndpointCommandDefinitionMeta
>
>(({ command, setStore, store, status, setStatus, ResultComponent }) => {
const endpointId = command.commandDefinition?.meta?.endpointId;
const { actionId, completedActionDetails } = store;
const isPending = status === 'pending';
const actionRequestSent = Boolean(store.actionRequestSent);
export const IsolateActionResult = memo<ActionRequestComponentProps>(
({ command, setStore, store, status, setStatus, ResultComponent }) => {
const endpointId = command.commandDefinition?.meta?.endpointId;
const { completedActionDetails, actionRequest } = store;
const isPending = status === 'pending';
const isolateHostApi = useSendIsolateEndpointRequest();
const isolateHostApi = useSendIsolateEndpointRequest();
useUpdateActionState({
actionRequestApi: isolateHostApi,
actionRequest,
command,
endpointId,
setStatus,
setStore,
isPending,
});
const { data: actionDetails } = useGetActionDetails(actionId ?? '-', {
enabled: Boolean(actionId) && isPending,
refetchInterval: isPending ? 3000 : false,
});
// Send Isolate request if not yet done
useEffect(() => {
if (!actionRequestSent && endpointId) {
isolateHostApi.mutate({
endpoint_ids: [endpointId],
comment: command.args.args?.comment?.[0],
});
setStore((prevState) => {
return { ...prevState, actionRequestSent: true };
});
// Show nothing if still pending
if (isPending) {
return <ResultComponent showAs="pending" />;
}
}, [actionRequestSent, command.args.args?.comment, endpointId, isolateHostApi, setStore]);
// If isolate request was created, store the action id if necessary
useEffect(() => {
if (isolateHostApi.isSuccess && actionId !== isolateHostApi.data.action) {
setStore((prevState) => {
return { ...prevState, actionId: isolateHostApi.data.action };
});
// Show errors
if (completedActionDetails?.errors) {
return (
<ActionError
dataTestSubj={'isolateErrorCallout'}
errors={completedActionDetails?.errors}
ResultComponent={ResultComponent}
/>
);
}
}, [actionId, isolateHostApi?.data?.action, isolateHostApi.isSuccess, setStore]);
useEffect(() => {
if (actionDetails?.data.isCompleted) {
setStatus('success');
setStore((prevState) => {
return {
...prevState,
completedActionDetails: actionDetails.data,
};
});
}
}, [actionDetails?.data, setStatus, setStore]);
// Show nothing if still pending
if (isPending) {
return <ResultComponent showAs="pending" />;
// Show Success
return <ResultComponent showAs="success" data-test-subj="isolateSuccessCallout" />;
}
// Show errors
if (completedActionDetails?.errors) {
return (
<ActionError
dataTestSubj={'isolateErrorCallout'}
errors={completedActionDetails?.errors}
ResultComponent={ResultComponent}
/>
);
}
// Show Success
return <ResultComponent showAs="success" data-test-subj="isolateSuccessCallout" />;
});
);
IsolateActionResult.displayName = 'IsolateActionResult';

View file

@ -15,6 +15,7 @@ import { useSendKillProcessRequest } from '../../hooks/endpoint/use_send_kill_pr
import type { CommandExecutionComponentProps } from '../console/types';
import { parsedPidOrEntityIdParameter } from '../console/service/parsed_command_input';
import { ActionError } from './action_error';
import { ACTION_DETAILS_REFRESH_INTERVAL } from './constants';
export const KillProcessActionResult = memo<
CommandExecutionComponentProps<
@ -38,7 +39,7 @@ export const KillProcessActionResult = memo<
const { data: actionDetails } = useGetActionDetails(actionId ?? '-', {
enabled: Boolean(actionId) && isPending,
refetchInterval: isPending ? 3000 : false,
refetchInterval: isPending ? ACTION_DETAILS_REFRESH_INTERVAL : false,
});
// Send Kill request if not yet done

View file

@ -16,6 +16,7 @@ import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_a
import { enterConsoleCommand } from '../console/mocks';
import { waitFor } from '@testing-library/react';
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
import { getDeferred } from '../mocks';
describe('When using the release action from response actions console', () => {
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
@ -120,6 +121,30 @@ describe('When using the release action from response actions console', () => {
});
});
it('should create action request and store id even if console is closed prior to request api response', async () => {
const deferrable = getDeferred();
apiMocks.responseProvider.releaseHost.mockDelay.mockReturnValue(deferrable.promise);
await render();
// enter command
enterConsoleCommand(renderResult, 'release');
// hide console
await consoleManagerMockAccess.hideOpenedConsole();
// Release API response
deferrable.resolve();
await waitFor(() => {
expect(apiMocks.responseProvider.releaseHost).toHaveBeenCalledTimes(1);
});
// open console
await consoleManagerMockAccess.openRunningConsole();
// status should be updating
await waitFor(() => {
expect(apiMocks.responseProvider.actionDetails.mock.calls.length).toBeGreaterThan(0);
});
});
describe('and when console is closed (not terminated) and then reopened', () => {
beforeEach(() => {
const _render = render;

View file

@ -5,89 +5,48 @@
* 2.0.
*/
import React, { memo, useEffect } from 'react';
import type { ActionDetails } from '../../../../common/endpoint/types';
import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details';
import type { EndpointCommandDefinitionMeta } from './types';
import React, { memo } from 'react';
import type { ActionRequestComponentProps } from './types';
import { useSendReleaseEndpointRequest } from '../../hooks/endpoint/use_send_release_endpoint_request';
import type { CommandExecutionComponentProps } from '../console/types';
import { ActionError } from './action_error';
import { useUpdateActionState } from './hooks';
export const ReleaseActionResult = memo<
CommandExecutionComponentProps<
{ comment?: string },
{
actionId?: string;
actionRequestSent?: boolean;
completedActionDetails?: ActionDetails;
},
EndpointCommandDefinitionMeta
>
>(({ command, setStore, store, status, setStatus, ResultComponent }) => {
const endpointId = command.commandDefinition?.meta?.endpointId;
const { actionId, completedActionDetails } = store;
const isPending = status === 'pending';
const actionRequestSent = Boolean(store.actionRequestSent);
export const ReleaseActionResult = memo<ActionRequestComponentProps>(
({ command, setStore, store, status, setStatus, ResultComponent }) => {
const endpointId = command.commandDefinition?.meta?.endpointId;
const { completedActionDetails, actionRequest } = store;
const isPending = status === 'pending';
const releaseHostApi = useSendReleaseEndpointRequest();
const releaseHostApi = useSendReleaseEndpointRequest();
const { data: actionDetails } = useGetActionDetails(actionId ?? '-', {
enabled: Boolean(actionId) && isPending,
refetchInterval: isPending ? 3000 : false,
});
useUpdateActionState({
actionRequestApi: releaseHostApi,
actionRequest,
command,
endpointId,
setStatus,
setStore,
isPending,
});
// Send Release request if not yet done
useEffect(() => {
if (!actionRequestSent && endpointId) {
releaseHostApi.mutate({
endpoint_ids: [endpointId],
comment: command.args.args?.comment?.[0],
});
setStore((prevState) => {
return { ...prevState, actionRequestSent: true };
});
// Show nothing if still pending
if (isPending) {
return <ResultComponent showAs="pending" />;
}
}, [actionRequestSent, command.args.args?.comment, endpointId, releaseHostApi, setStore]);
// If release request was created, store the action id if necessary
useEffect(() => {
if (releaseHostApi.isSuccess && actionId !== releaseHostApi.data.action) {
setStore((prevState) => {
return { ...prevState, actionId: releaseHostApi.data.action };
});
// Show errors
if (completedActionDetails?.errors) {
return (
<ActionError
dataTestSubj={'releaseErrorCallout'}
errors={completedActionDetails?.errors}
ResultComponent={ResultComponent}
/>
);
}
}, [actionId, releaseHostApi?.data?.action, releaseHostApi.isSuccess, setStore]);
useEffect(() => {
if (actionDetails?.data.isCompleted) {
setStatus('success');
setStore((prevState) => {
return {
...prevState,
completedActionDetails: actionDetails.data,
};
});
}
}, [actionDetails?.data, setStatus, setStore]);
// Show nothing if still pending
if (isPending) {
return <ResultComponent showAs="pending" />;
// Show Success
return <ResultComponent data-test-subj="releaseSuccessCallout" />;
}
// Show errors
if (completedActionDetails?.errors) {
return (
<ActionError
dataTestSubj={'releaseErrorCallout'}
errors={completedActionDetails?.errors}
ResultComponent={ResultComponent}
/>
);
}
// Show Success
return <ResultComponent data-test-subj="releaseSuccessCallout" />;
});
);
ReleaseActionResult.displayName = 'ReleaseActionResult';

View file

@ -15,6 +15,7 @@ import { useSendSuspendProcessRequest } from '../../hooks/endpoint/use_send_susp
import type { CommandExecutionComponentProps } from '../console/types';
import { parsedPidOrEntityIdParameter } from '../console/service/parsed_command_input';
import { ActionError } from './action_error';
import { ACTION_DETAILS_REFRESH_INTERVAL } from './constants';
export const SuspendProcessActionResult = memo<
CommandExecutionComponentProps<
@ -38,7 +39,7 @@ export const SuspendProcessActionResult = memo<
const { data: actionDetails } = useGetActionDetails(actionId ?? '-', {
enabled: Boolean(actionId) && isPending,
refetchInterval: isPending ? 3000 : false,
refetchInterval: isPending ? ACTION_DETAILS_REFRESH_INTERVAL : false,
});
// Send Suspend request if not yet done

View file

@ -6,7 +6,8 @@
*/
import type { ManagedConsoleExtensionComponentProps } from '../console';
import type { HostMetadata } from '../../../../common/endpoint/types';
import type { ActionDetails, HostMetadata } from '../../../../common/endpoint/types';
import type { CommandExecutionComponentProps } from '../console/types';
export interface EndpointCommandDefinitionMeta {
endpointId: string;
@ -15,3 +16,17 @@ export interface EndpointCommandDefinitionMeta {
export type EndpointResponderExtensionComponentProps = ManagedConsoleExtensionComponentProps<{
endpoint: HostMetadata;
}>;
export interface ActionRequestState {
requestSent: boolean;
actionId?: string;
}
export type ActionRequestComponentProps = CommandExecutionComponentProps<
{ comment?: string },
{
actionRequest?: ActionRequestState;
completedActionDetails?: ActionDetails;
},
EndpointCommandDefinitionMeta
>;

View file

@ -0,0 +1,24 @@
/*
* 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.
*/
interface DeferredInterface<T = void> {
promise: Promise<T>;
resolve: (data: T) => void;
reject: (e: Error) => void;
}
export const getDeferred = function <T = void>(): DeferredInterface<T> {
let resolve: DeferredInterface<T>['resolve'];
let reject: DeferredInterface<T>['reject'];
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
// @ts-ignore
return { promise, resolve, reject };
};

View file

@ -11,7 +11,7 @@ import type { IHttpFetchError } from '@kbn/core-http-browser';
import { isolateHost } from '../../../common/lib/endpoint_isolation';
import type {
HostIsolationRequestBody,
HostIsolationResponse,
ResponseActionApiResponse,
} from '../../../../common/endpoint/types';
/**
@ -20,12 +20,12 @@ import type {
*/
export const useSendIsolateEndpointRequest = (
customOptions?: UseMutationOptions<
HostIsolationResponse,
ResponseActionApiResponse,
IHttpFetchError,
HostIsolationRequestBody
>
): UseMutationResult<HostIsolationResponse, IHttpFetchError, HostIsolationRequestBody> => {
return useMutation<HostIsolationResponse, IHttpFetchError, HostIsolationRequestBody>(
): UseMutationResult<ResponseActionApiResponse, IHttpFetchError, HostIsolationRequestBody> => {
return useMutation<ResponseActionApiResponse, IHttpFetchError, HostIsolationRequestBody>(
(isolateData: HostIsolationRequestBody) => {
return isolateHost(isolateData);
},

View file

@ -10,7 +10,7 @@ import { useMutation } from 'react-query';
import type { IHttpFetchError } from '@kbn/core-http-browser';
import type {
HostIsolationRequestBody,
HostIsolationResponse,
ResponseActionApiResponse,
} from '../../../../common/endpoint/types';
import { unIsolateHost } from '../../../common/lib/endpoint_isolation';
@ -20,12 +20,12 @@ import { unIsolateHost } from '../../../common/lib/endpoint_isolation';
*/
export const useSendReleaseEndpointRequest = (
customOptions?: UseMutationOptions<
HostIsolationResponse,
ResponseActionApiResponse,
IHttpFetchError,
HostIsolationRequestBody
>
): UseMutationResult<HostIsolationResponse, IHttpFetchError, HostIsolationRequestBody> => {
return useMutation<HostIsolationResponse, IHttpFetchError, HostIsolationRequestBody>(
): UseMutationResult<ResponseActionApiResponse, IHttpFetchError, HostIsolationRequestBody> => {
return useMutation<ResponseActionApiResponse, IHttpFetchError, HostIsolationRequestBody>(
(releaseData: HostIsolationRequestBody) => {
return unIsolateHost(releaseData);
},

View file

@ -22,16 +22,16 @@ import { httpHandlerMockFactory } from '../../common/mock/endpoint/http_handler_
import type {
ActionDetailsApiResponse,
ActionListApiResponse,
HostIsolationResponse,
ResponseActionApiResponse,
PendingActionsResponse,
ProcessesEntry,
ActionDetails,
} from '../../../common/endpoint/types';
export type ResponseActionsHttpMocksInterface = ResponseProvidersInterface<{
isolateHost: () => HostIsolationResponse;
isolateHost: () => ResponseActionApiResponse;
releaseHost: () => HostIsolationResponse;
releaseHost: () => ResponseActionApiResponse;
killProcess: () => ActionDetailsApiResponse;
@ -51,16 +51,16 @@ export const responseActionsHttpMocks = httpHandlerMockFactory<ResponseActionsHt
id: 'isolateHost',
path: ISOLATE_HOST_ROUTE,
method: 'post',
handler: (): HostIsolationResponse => {
return { action: '1-2-3' };
handler: (): ResponseActionApiResponse => {
return { action: '1-2-3', data: { id: '1-2-3' } as ResponseActionApiResponse['data'] };
},
},
{
id: 'releaseHost',
path: UNISOLATE_HOST_ROUTE,
method: 'post',
handler: (): HostIsolationResponse => {
return { action: '3-2-1' };
handler: (): ResponseActionApiResponse => {
return { action: '3-2-1', data: { id: '3-2-1' } as ResponseActionApiResponse['data'] };
},
},
{

View file

@ -22,7 +22,7 @@ import type {
GetHostPolicyResponse,
HostInfo,
HostIsolationRequestBody,
HostIsolationResponse,
ResponseActionApiResponse,
HostResultList,
Immutable,
ImmutableObject,
@ -264,7 +264,7 @@ const handleIsolateEndpointHost = async (
try {
// Cast needed below due to the value of payload being `Immutable<>`
let response: HostIsolationResponse;
let response: ResponseActionApiResponse;
if (action.payload.type === 'unisolate') {
response = await unIsolateHost(action.payload.data as HostIsolationRequestBody);
@ -274,12 +274,12 @@ const handleIsolateEndpointHost = async (
dispatch({
type: 'endpointIsolationRequestStateChange',
payload: createLoadedResourceState<HostIsolationResponse>(response),
payload: createLoadedResourceState<ResponseActionApiResponse>(response),
});
} catch (error) {
dispatch({
type: 'endpointIsolationRequestStateChange',
payload: createFailedResourceState<HostIsolationResponse>(error.body ?? error),
payload: createFailedResourceState<ResponseActionApiResponse>(error.body ?? error),
});
}
};

View file

@ -239,7 +239,7 @@ export const searchBarQuery: (state: Immutable<EndpointState>) => Query = create
export const getCurrentIsolationRequestState = (
state: Immutable<EndpointState>
): EndpointState['isolationRequestState'] => {
return state.isolationRequestState;
return state.isolationRequestState as EndpointState['isolationRequestState'];
};
export const getIsIsolationRequestPending: (state: Immutable<EndpointState>) => boolean =

View file

@ -15,7 +15,7 @@ import type {
AppLocation,
PolicyData,
HostStatus,
HostIsolationResponse,
ResponseActionApiResponse,
EndpointPendingActions,
} from '../../../../common/endpoint/types';
import type { ServerApiError } from '../../../common/types';
@ -88,7 +88,7 @@ export interface EndpointState {
/** The status of the host, which is mapped to the Elastic Agent status in Fleet */
hostStatus?: HostStatus;
/** Host isolation request state for a single endpoint */
isolationRequestState: AsyncResourceState<HostIsolationResponse>;
isolationRequestState: AsyncResourceState<ResponseActionApiResponse>;
/**
* Holds a map of `agentId` to `EndpointPendingActions` that is used by both the list and details view
* Getting pending endpoint actions is "supplemental" data, so there is no need to show other Async