mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[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:
parent
546f2b158b
commit
3dc26b6a75
22 changed files with 304 additions and 204 deletions
|
@ -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),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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']>,
|
||||
{
|
||||
|
|
|
@ -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;
|
|
@ -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]);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
>;
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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'] };
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue