mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution][Endpoint] Remove redux from endpoint list page etc. (#161253)
## Summary Removes the following from `management` redux store - `endpointDetails` and child keys (`hostInfo`, `hostInfoError` and `isHostInfoLoading`) - `policyResponse`, `policyResponseLoading` and `policyResponseError` - `hostStatus` - rearranged/refactored a huge chunk of endpoint list component to make it more manageable for later when we remove endpoint list data out of redux - endpoint list nav link component:0e281f1191
- back to policy link component:9e3c771eb7
- extracted table columns out of the component:3728ca604e
- rearranged variables:09bb01a1d2
- transform callout component:808fa81373
### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
bf148fb35f
commit
ddd58d9bfd
21 changed files with 892 additions and 1214 deletions
|
@ -8,7 +8,6 @@
|
|||
import type { Action } from 'redux';
|
||||
import type { DataViewBase } from '@kbn/es-query';
|
||||
import type {
|
||||
HostInfo,
|
||||
GetHostPolicyResponse,
|
||||
HostIsolationRequestBody,
|
||||
ISOLATION_ACTIONS,
|
||||
|
@ -28,15 +27,6 @@ export interface ServerFailedToReturnEndpointList {
|
|||
payload: ServerApiError;
|
||||
}
|
||||
|
||||
export interface ServerReturnedEndpointDetails {
|
||||
type: 'serverReturnedEndpointDetails';
|
||||
payload: HostInfo;
|
||||
}
|
||||
|
||||
export interface ServerFailedToReturnEndpointDetails {
|
||||
type: 'serverFailedToReturnEndpointDetails';
|
||||
payload: ServerApiError;
|
||||
}
|
||||
export interface ServerReturnedEndpointPolicyResponse {
|
||||
type: 'serverReturnedEndpointPolicyResponse';
|
||||
payload: GetHostPolicyResponse;
|
||||
|
@ -150,13 +140,6 @@ export type EndpointPendingActionsStateChanged = Action<'endpointPendingActionsS
|
|||
payload: EndpointState['endpointPendingActions'];
|
||||
};
|
||||
|
||||
export interface EndpointDetailsLoad {
|
||||
type: 'endpointDetailsLoad';
|
||||
payload: {
|
||||
endpointId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type LoadMetadataTransformStats = Action<'loadMetadataTransformStats'>;
|
||||
|
||||
export type MetadataTransformStatsChanged = Action<'metadataTransformStatsChanged'> & {
|
||||
|
@ -166,11 +149,6 @@ export type MetadataTransformStatsChanged = Action<'metadataTransformStatsChange
|
|||
export type EndpointAction =
|
||||
| ServerReturnedEndpointList
|
||||
| ServerFailedToReturnEndpointList
|
||||
| ServerReturnedEndpointDetails
|
||||
| ServerFailedToReturnEndpointDetails
|
||||
| EndpointDetailsLoad
|
||||
| ServerReturnedEndpointPolicyResponse
|
||||
| ServerFailedToReturnEndpointPolicyResponse
|
||||
| ServerReturnedPoliciesForOnboarding
|
||||
| ServerFailedToReturnPoliciesForOnboarding
|
||||
| UserSelectedEndpointPolicy
|
||||
|
|
|
@ -18,14 +18,6 @@ export const initialEndpointPageState = (): Immutable<EndpointState> => {
|
|||
total: 0,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
endpointDetails: {
|
||||
hostInfo: undefined,
|
||||
hostInfoError: undefined,
|
||||
isHostInfoLoading: false,
|
||||
},
|
||||
policyResponse: undefined,
|
||||
policyResponseLoading: false,
|
||||
policyResponseError: undefined,
|
||||
location: undefined,
|
||||
policyItems: [],
|
||||
selectedPolicyId: undefined,
|
||||
|
@ -42,8 +34,6 @@ export const initialEndpointPageState = (): Immutable<EndpointState> => {
|
|||
agentsWithEndpointsTotalError: undefined,
|
||||
endpointsTotal: 0,
|
||||
endpointsTotalError: undefined,
|
||||
policyVersionInfo: undefined,
|
||||
hostStatus: undefined,
|
||||
isolationRequestState: createUninitialisedResourceState(),
|
||||
endpointPendingActions: createLoadedResourceState(new Map()),
|
||||
metadataTransformStats: createUninitialisedResourceState(),
|
||||
|
|
|
@ -43,14 +43,6 @@ describe('EndpointList store concerns', () => {
|
|||
total: 0,
|
||||
loading: false,
|
||||
error: undefined,
|
||||
endpointDetails: {
|
||||
hostInfo: undefined,
|
||||
hostInfoError: undefined,
|
||||
isHostInfoLoading: false,
|
||||
},
|
||||
policyResponse: undefined,
|
||||
policyResponseLoading: false,
|
||||
policyResponseError: undefined,
|
||||
location: undefined,
|
||||
policyItems: [],
|
||||
selectedPolicyId: undefined,
|
||||
|
@ -70,7 +62,6 @@ describe('EndpointList store concerns', () => {
|
|||
agentsWithEndpointsTotalError: undefined,
|
||||
endpointsTotalError: undefined,
|
||||
queryStrategyVersion: undefined,
|
||||
policyVersionInfo: undefined,
|
||||
isolationRequestState: {
|
||||
type: 'UninitialisedResourceState',
|
||||
},
|
||||
|
|
|
@ -17,8 +17,8 @@ import { depsStartMock } from '../../../../common/mock/endpoint';
|
|||
import type { MiddlewareActionSpyHelper } from '../../../../common/store/test_utils';
|
||||
import { createSpyMiddleware } from '../../../../common/store/test_utils';
|
||||
import type {
|
||||
Immutable,
|
||||
HostIsolationResponse,
|
||||
Immutable,
|
||||
ISOLATION_ACTIONS,
|
||||
MetadataListResponse,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
|
@ -28,8 +28,7 @@ import { listData } from './selectors';
|
|||
import type { EndpointState, TransformStats } from '../types';
|
||||
import { endpointListReducer } from './reducer';
|
||||
import { endpointMiddlewareFactory } from './middleware';
|
||||
import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/routing';
|
||||
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
|
||||
import { getEndpointListPath } from '../../../common/routing';
|
||||
import type { FailedResourceState, LoadedResourceState } from '../../../state';
|
||||
import {
|
||||
isFailedResourceState,
|
||||
|
@ -43,10 +42,7 @@ import {
|
|||
hostIsolationResponseMock,
|
||||
} from '../../../../common/lib/endpoint_isolation/mocks';
|
||||
import { endpointPageHttpMock, failedTransformStateMock } from '../mocks';
|
||||
import {
|
||||
HOST_METADATA_GET_ROUTE,
|
||||
HOST_METADATA_LIST_ROUTE,
|
||||
} from '../../../../../common/endpoint/constants';
|
||||
import { HOST_METADATA_LIST_ROUTE } from '../../../../../common/endpoint/constants';
|
||||
|
||||
jest.mock('../../../services/policies/ingest', () => ({
|
||||
sendGetAgentConfigList: () => Promise.resolve({ items: [] }),
|
||||
|
@ -343,66 +339,4 @@ describe('endpoint list middleware', () => {
|
|||
expect(failedAction.error).toBe(apiError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loads selected endpoint details', () => {
|
||||
beforeEach(() => {
|
||||
endpointPageHttpMock(fakeHttpServices);
|
||||
});
|
||||
|
||||
const endpointList = getEndpointListApiResponse();
|
||||
const agentId = endpointList.data[0].metadata.agent.id;
|
||||
const search = getEndpointDetailsPath({
|
||||
name: 'endpointDetails',
|
||||
selected_endpoint: agentId,
|
||||
});
|
||||
const dispatchUserChangedUrl = () => {
|
||||
dispatchUserChangedUrlToEndpointList({ search: `?${search.split('?').pop()}` });
|
||||
};
|
||||
|
||||
it('triggers the endpoint details related actions when the url is changed', async () => {
|
||||
dispatchUserChangedUrl();
|
||||
|
||||
// Note: these are left intentionally in sequence
|
||||
// to test specific race conditions that currently exist in the middleware
|
||||
await waitForAction('serverCancelledPolicyItemsLoading');
|
||||
|
||||
// loads the endpoints list
|
||||
await waitForAction('serverReturnedEndpointList');
|
||||
|
||||
// loads the specific endpoint details
|
||||
await waitForAction('serverReturnedEndpointDetails');
|
||||
|
||||
// loads the specific endpoint pending actions
|
||||
await waitForAction('endpointPendingActionsStateChanged');
|
||||
|
||||
expect(fakeHttpServices.get).toHaveBeenCalledWith(
|
||||
resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }),
|
||||
{ version: '2023-10-31' }
|
||||
);
|
||||
});
|
||||
|
||||
it('handles the endpointDetailsLoad action', async () => {
|
||||
const endpointId = agentId;
|
||||
dispatch({
|
||||
type: 'endpointDetailsLoad',
|
||||
payload: {
|
||||
endpointId,
|
||||
},
|
||||
});
|
||||
|
||||
// note: this action does not load the endpoints list
|
||||
|
||||
// loads the specific endpoint details
|
||||
await waitForAction('serverReturnedEndpointDetails');
|
||||
await waitForAction('serverReturnedEndpointNonExistingPolicies');
|
||||
|
||||
// loads the specific endpoint pending actions
|
||||
await waitForAction('endpointPendingActionsStateChanged');
|
||||
|
||||
expect(fakeHttpServices.get).toHaveBeenCalledWith(
|
||||
resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: endpointId }),
|
||||
{ version: '2023-10-31' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,17 +15,13 @@ import type {
|
|||
IndexFieldsStrategyResponse,
|
||||
} from '@kbn/timelines-plugin/common';
|
||||
import {
|
||||
BASE_POLICY_RESPONSE_ROUTE,
|
||||
ENDPOINT_FIELDS_SEARCH_STRATEGY,
|
||||
HOST_METADATA_GET_ROUTE,
|
||||
HOST_METADATA_LIST_ROUTE,
|
||||
METADATA_TRANSFORMS_STATUS_ROUTE,
|
||||
METADATA_UNITED_INDEX,
|
||||
metadataCurrentIndexPattern,
|
||||
} from '../../../../../common/endpoint/constants';
|
||||
import type {
|
||||
GetHostPolicyResponse,
|
||||
HostInfo,
|
||||
HostIsolationRequestBody,
|
||||
HostResultList,
|
||||
Immutable,
|
||||
|
@ -37,7 +33,6 @@ import { isolateHost, unIsolateHost } from '../../../../common/lib/endpoint_isol
|
|||
import { fetchPendingActionsByAgentId } from '../../../../common/lib/endpoint_pending_actions';
|
||||
import type { ImmutableMiddlewareAPI, ImmutableMiddlewareFactory } from '../../../../common/store';
|
||||
import type { AppAction } from '../../../../common/store/actions';
|
||||
import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables';
|
||||
import { sendGetEndpointSpecificPackagePolicies } from '../../../services/policies/policies';
|
||||
import {
|
||||
asStaleResourceState,
|
||||
|
@ -61,12 +56,10 @@ import type { EndpointPackageInfoStateChanged } from './action';
|
|||
import {
|
||||
endpointPackageInfo,
|
||||
endpointPackageVersion,
|
||||
fullDetailsHostInfo,
|
||||
getCurrentIsolationRequestState,
|
||||
getIsEndpointPackageInfoUninitialized,
|
||||
getIsIsolationRequestPending,
|
||||
getMetadataTransformStats,
|
||||
hasSelectedEndpoint,
|
||||
isMetadataTransformStatsLoading,
|
||||
isOnEndpointPage,
|
||||
listData,
|
||||
|
@ -126,20 +119,9 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory<EndpointState
|
|||
// Endpoint list
|
||||
if (
|
||||
(action.type === 'userChangedUrl' || action.type === 'appRequestedEndpointList') &&
|
||||
isOnEndpointPage(getState()) &&
|
||||
!hasSelectedEndpoint(getState())
|
||||
isOnEndpointPage(getState())
|
||||
) {
|
||||
await endpointDetailsListMiddleware({ coreStart, store, fetchIndexPatterns });
|
||||
}
|
||||
|
||||
// Endpoint Details
|
||||
if (action.type === 'userChangedUrl' && hasSelectedEndpoint(getState())) {
|
||||
const { selected_endpoint: selectedEndpoint } = uiQueryParams(getState());
|
||||
await endpointDetailsMiddleware({ store, coreStart, selectedEndpoint });
|
||||
}
|
||||
|
||||
if (action.type === 'endpointDetailsLoad') {
|
||||
await loadEndpointDetails({ store, coreStart, selectedEndpoint: action.payload.endpointId });
|
||||
await endpointListMiddleware({ coreStart, store, fetchIndexPatterns });
|
||||
}
|
||||
|
||||
// Isolate Host
|
||||
|
@ -320,7 +302,7 @@ async function getEndpointPackageInfo(
|
|||
}
|
||||
|
||||
/**
|
||||
* retrieves the Endpoint pending actions for all of the existing endpoints being displayed on the list
|
||||
* retrieves the Endpoint pending actions for all the existing endpoints being displayed on the list
|
||||
* or the details tab.
|
||||
*
|
||||
* @param store
|
||||
|
@ -330,15 +312,9 @@ const loadEndpointsPendingActions = async ({
|
|||
dispatch,
|
||||
}: EndpointPageStore): Promise<void> => {
|
||||
const state = getState();
|
||||
const detailsEndpoint = fullDetailsHostInfo(state);
|
||||
const listEndpoints = listData(state);
|
||||
const agentsIds = new Set<string>();
|
||||
|
||||
// get all agent ids for the endpoints in the list
|
||||
if (detailsEndpoint) {
|
||||
agentsIds.add(detailsEndpoint.metadata.elastic.agent.id);
|
||||
}
|
||||
|
||||
for (const endpointInfo of listEndpoints) {
|
||||
agentsIds.add(endpointInfo.metadata.elastic.agent.id);
|
||||
}
|
||||
|
@ -366,7 +342,7 @@ const loadEndpointsPendingActions = async ({
|
|||
}
|
||||
};
|
||||
|
||||
async function endpointDetailsListMiddleware({
|
||||
async function endpointListMiddleware({
|
||||
store,
|
||||
coreStart,
|
||||
fetchIndexPatterns,
|
||||
|
@ -407,7 +383,7 @@ async function endpointDetailsListMiddleware({
|
|||
});
|
||||
}
|
||||
|
||||
// get index pattern and fields for search bar
|
||||
// get an index pattern and fields for search bar
|
||||
if (patterns(getState()).length === 0) {
|
||||
try {
|
||||
const indexPatterns = await fetchIndexPatterns(getState());
|
||||
|
@ -445,7 +421,7 @@ async function endpointDetailsListMiddleware({
|
|||
const policyDataResponse: GetPolicyListResponse =
|
||||
await sendGetEndpointSpecificPackagePolicies(http, {
|
||||
query: {
|
||||
perPage: 50, // Since this is an oboarding flow, we'll cap at 50 policies.
|
||||
perPage: 50, // Since this is an onboarding flow, we'll cap at 50 policies.
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
|
@ -474,130 +450,6 @@ async function endpointDetailsListMiddleware({
|
|||
}
|
||||
}
|
||||
|
||||
async function loadEndpointDetails({
|
||||
selectedEndpoint,
|
||||
store,
|
||||
coreStart,
|
||||
}: {
|
||||
store: ImmutableMiddlewareAPI<EndpointState, AppAction>;
|
||||
coreStart: CoreStart;
|
||||
selectedEndpoint: string;
|
||||
}) {
|
||||
const { getState, dispatch } = store;
|
||||
// call the endpoint details api
|
||||
try {
|
||||
const response = await coreStart.http.get<HostInfo>(
|
||||
resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: selectedEndpoint as string }),
|
||||
{ version: '2023-10-31' }
|
||||
);
|
||||
dispatch({
|
||||
type: 'serverReturnedEndpointDetails',
|
||||
payload: response,
|
||||
});
|
||||
|
||||
try {
|
||||
const ingestPolicies = await getAgentAndPoliciesForEndpointsList(
|
||||
coreStart.http,
|
||||
[response],
|
||||
nonExistingPolicies(getState())
|
||||
);
|
||||
if (ingestPolicies !== undefined) {
|
||||
dispatch({
|
||||
type: 'serverReturnedEndpointNonExistingPolicies',
|
||||
payload: ingestPolicies.packagePolicy,
|
||||
});
|
||||
}
|
||||
if (ingestPolicies?.agentPolicy !== undefined) {
|
||||
dispatch({
|
||||
type: 'serverReturnedEndpointAgentPolicies',
|
||||
payload: ingestPolicies.agentPolicy,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO should handle the error instead of logging it to the browser
|
||||
// Also this is an anti-pattern we shouldn't use
|
||||
// Ignore Errors, since this should not hinder the user's ability to use the UI
|
||||
logError(error);
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: 'serverFailedToReturnEndpointDetails',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
|
||||
loadEndpointsPendingActions(store);
|
||||
|
||||
// call the policy response api
|
||||
try {
|
||||
const policyResponse = await coreStart.http.get<GetHostPolicyResponse>(
|
||||
BASE_POLICY_RESPONSE_ROUTE,
|
||||
{
|
||||
version: '2023-10-31',
|
||||
query: { agentId: selectedEndpoint },
|
||||
}
|
||||
);
|
||||
dispatch({
|
||||
type: 'serverReturnedEndpointPolicyResponse',
|
||||
payload: policyResponse,
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: 'serverFailedToReturnEndpointPolicyResponse',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function endpointDetailsMiddleware({
|
||||
coreStart,
|
||||
selectedEndpoint,
|
||||
store,
|
||||
}: {
|
||||
coreStart: CoreStart;
|
||||
selectedEndpoint?: string;
|
||||
store: ImmutableMiddlewareAPI<EndpointState, AppAction>;
|
||||
}) {
|
||||
const { getState, dispatch } = store;
|
||||
dispatch({
|
||||
type: 'serverCancelledPolicyItemsLoading',
|
||||
});
|
||||
|
||||
// If user navigated directly to a endpoint details page, load the endpoint list
|
||||
if (listData(getState()).length === 0) {
|
||||
const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(getState());
|
||||
try {
|
||||
const response = await coreStart.http.get<MetadataListResponse>(HOST_METADATA_LIST_ROUTE, {
|
||||
version: '2023-10-31',
|
||||
query: {
|
||||
page: pageIndex,
|
||||
pageSize,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: 'serverReturnedEndpointList',
|
||||
payload: response,
|
||||
});
|
||||
|
||||
dispatchIngestPolicies({ http: coreStart.http, hosts: response.data, store });
|
||||
} catch (error) {
|
||||
dispatch({
|
||||
type: 'serverFailedToReturnEndpointList',
|
||||
payload: error,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'serverCancelledEndpointListLoading',
|
||||
});
|
||||
}
|
||||
if (typeof selectedEndpoint === 'undefined') {
|
||||
return;
|
||||
}
|
||||
await loadEndpointDetails({ store, coreStart, selectedEndpoint });
|
||||
}
|
||||
|
||||
export async function handleLoadMetadataTransformStats(http: HttpStart, store: EndpointPageStore) {
|
||||
const { getState, dispatch } = store;
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import type {
|
|||
} from './action';
|
||||
import {
|
||||
getCurrentIsolationRequestState,
|
||||
getIsOnEndpointDetailsActivityLog,
|
||||
hasSelectedEndpoint,
|
||||
isOnEndpointPage,
|
||||
uiQueryParams,
|
||||
|
@ -108,27 +107,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
...state,
|
||||
patternsError: action.payload,
|
||||
};
|
||||
} else if (action.type === 'serverReturnedEndpointDetails') {
|
||||
return {
|
||||
...state,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostInfo: action.payload,
|
||||
hostInfoError: undefined,
|
||||
isHostInfoLoading: false,
|
||||
},
|
||||
policyVersionInfo: action.payload.policy_info,
|
||||
hostStatus: action.payload.host_status,
|
||||
};
|
||||
} else if (action.type === 'serverFailedToReturnEndpointDetails') {
|
||||
return {
|
||||
...state,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostInfoError: action.payload,
|
||||
isHostInfoLoading: false,
|
||||
},
|
||||
};
|
||||
} else if (action.type === 'endpointPendingActionsStateChanged') {
|
||||
return handleEndpointPendingActionsStateChanged(state, action);
|
||||
} else if (action.type === 'serverReturnedPoliciesForOnboarding') {
|
||||
|
@ -143,24 +121,10 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
error: action.payload,
|
||||
policyItemsLoading: false,
|
||||
};
|
||||
} else if (action.type === 'serverReturnedEndpointPolicyResponse') {
|
||||
return {
|
||||
...state,
|
||||
policyResponse: action.payload.policy_response,
|
||||
policyResponseLoading: false,
|
||||
policyResponseError: undefined,
|
||||
};
|
||||
} else if (action.type === 'serverFailedToReturnEndpointPolicyResponse') {
|
||||
return {
|
||||
...state,
|
||||
policyResponseError: action.payload,
|
||||
policyResponseLoading: false,
|
||||
};
|
||||
} else if (action.type === 'userSelectedEndpointPolicy') {
|
||||
return {
|
||||
...state,
|
||||
selectedPolicyId: action.payload.selectedPolicyId,
|
||||
policyResponseLoading: false,
|
||||
};
|
||||
} else if (action.type === 'serverCancelledEndpointListLoading') {
|
||||
return {
|
||||
|
@ -218,25 +182,10 @@ 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,
|
||||
error: undefined,
|
||||
policyResponseError: undefined,
|
||||
};
|
||||
|
||||
// Reset `isolationRequestState` if needed
|
||||
|
@ -253,10 +202,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
return {
|
||||
...state,
|
||||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostInfoError: undefined,
|
||||
},
|
||||
loading: true,
|
||||
policyItemsLoading: true,
|
||||
};
|
||||
|
@ -267,26 +212,15 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
return {
|
||||
...state,
|
||||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostInfoError: undefined,
|
||||
isHostInfoLoading: !isNotLoadingDetails,
|
||||
},
|
||||
detailsLoading: true,
|
||||
policyResponseLoading: true,
|
||||
};
|
||||
} else {
|
||||
// if the previous page was not endpoint list or endpoint details, load both list and details
|
||||
return {
|
||||
...state,
|
||||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostInfoError: undefined,
|
||||
isHostInfoLoading: true,
|
||||
},
|
||||
loading: true,
|
||||
policyResponseLoading: true,
|
||||
|
||||
policyItemsLoading: true,
|
||||
};
|
||||
}
|
||||
|
@ -295,10 +229,6 @@ export const endpointListReducer: StateReducer = (state = initialEndpointPageSta
|
|||
return {
|
||||
...state,
|
||||
...stateUpdates,
|
||||
endpointDetails: {
|
||||
...state.endpointDetails,
|
||||
hostInfoError: undefined,
|
||||
},
|
||||
endpointsExist: true,
|
||||
};
|
||||
} else if (action.type === 'metadataTransformStatsChanged') {
|
||||
|
|
|
@ -12,7 +12,6 @@ import { matchPath } from 'react-router-dom';
|
|||
import { decode } from '@kbn/rison';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import type { EndpointPendingActions, Immutable } from '../../../../../common/endpoint/types';
|
||||
import { HostStatus } from '../../../../../common/endpoint/types';
|
||||
import type { EndpointIndexUIQueryParams, EndpointState } from '../types';
|
||||
import { extractListPaginationParams } from '../../../common/routing';
|
||||
import {
|
||||
|
@ -28,7 +27,6 @@ import {
|
|||
} from '../../../state';
|
||||
|
||||
import type { ServerApiError } from '../../../../common/types';
|
||||
import { isEndpointHostIsolated } from '../../../../common/utils/validators';
|
||||
import { EndpointDetailsTabsTypes } from '../view/details/components/endpoint_details_tabs';
|
||||
|
||||
export const listData = (state: Immutable<EndpointState>) => state.hosts;
|
||||
|
@ -43,18 +41,6 @@ export const listLoading = (state: Immutable<EndpointState>): boolean => state.l
|
|||
|
||||
export const listError = (state: Immutable<EndpointState>) => state.error;
|
||||
|
||||
export const fullDetailsHostInfo = (
|
||||
state: Immutable<EndpointState>
|
||||
): EndpointState['endpointDetails']['hostInfo'] => state.endpointDetails.hostInfo;
|
||||
|
||||
export const isHostInfoLoading = (
|
||||
state: Immutable<EndpointState>
|
||||
): EndpointState['endpointDetails']['isHostInfoLoading'] => state.endpointDetails.isHostInfoLoading;
|
||||
|
||||
export const hostInfoError = (
|
||||
state: Immutable<EndpointState>
|
||||
): EndpointState['endpointDetails']['hostInfoError'] => state.endpointDetails.hostInfoError;
|
||||
|
||||
export const policyItems = (state: Immutable<EndpointState>) => state.policyItems;
|
||||
|
||||
export const policyItemsLoading = (state: Immutable<EndpointState>) => state.policyItemsLoading;
|
||||
|
@ -68,14 +54,12 @@ export const isAutoRefreshEnabled = (state: Immutable<EndpointState>) => state.i
|
|||
|
||||
export const autoRefreshInterval = (state: Immutable<EndpointState>) => state.autoRefreshInterval;
|
||||
|
||||
export const policyVersionInfo = (state: Immutable<EndpointState>) => state.policyVersionInfo;
|
||||
|
||||
export const endpointPackageVersion = createSelector(endpointPackageInfo, (info) =>
|
||||
isLoadedResourceState(info) ? info.data.version : undefined
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the index patterns for the SearchBar to use for autosuggest
|
||||
* Returns the index patterns for the SearchBar to use for auto-suggest
|
||||
*/
|
||||
export const patterns = (state: Immutable<EndpointState>) => state.patterns;
|
||||
|
||||
|
@ -160,26 +144,6 @@ export const showView: (state: EndpointState) => EndpointIndexUIQueryParams['sho
|
|||
return searchParams.show ?? 'details';
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the Host Status which is connected the fleet agent
|
||||
*/
|
||||
export const hostStatusInfo: (state: Immutable<EndpointState>) => HostStatus = createSelector(
|
||||
(state: Immutable<EndpointState>) => state.hostStatus,
|
||||
(hostStatus) => {
|
||||
return hostStatus ? hostStatus : HostStatus.UNHEALTHY;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the Policy Response overall status
|
||||
*/
|
||||
export const policyResponseStatus: (state: Immutable<EndpointState>) => string = createSelector(
|
||||
(state: Immutable<EndpointState>) => state.policyResponse,
|
||||
(policyResponse) => {
|
||||
return (policyResponse && policyResponse?.Endpoint?.policy?.applied?.status) || '';
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* returns the list of known non-existing polices that may have been in the Endpoint API response.
|
||||
* @param state
|
||||
|
@ -255,10 +219,6 @@ export const getIsOnEndpointDetailsActivityLog: (state: Immutable<EndpointState>
|
|||
return searchParams.show === EndpointDetailsTabsTypes.activityLog;
|
||||
});
|
||||
|
||||
export const getIsEndpointHostIsolated = createSelector(fullDetailsHostInfo, (details) => {
|
||||
return (details && isEndpointHostIsolated(details.metadata)) || false;
|
||||
});
|
||||
|
||||
export const getEndpointPendingActionsState = (
|
||||
state: Immutable<EndpointState>
|
||||
): Immutable<EndpointState['endpointPendingActions']> => {
|
||||
|
|
|
@ -11,8 +11,6 @@ import type {
|
|||
AppLocation,
|
||||
EndpointPendingActions,
|
||||
HostInfo,
|
||||
HostPolicyResponse,
|
||||
HostStatus,
|
||||
Immutable,
|
||||
PolicyData,
|
||||
ResponseActionApiResponse,
|
||||
|
@ -34,19 +32,6 @@ export interface EndpointState {
|
|||
loading: boolean;
|
||||
/** api error from retrieving host list */
|
||||
error?: ServerApiError;
|
||||
endpointDetails: {
|
||||
// Adding `hostInfo` to store full API response in order to support the
|
||||
// refactoring effort with AgentStatus component
|
||||
hostInfo?: HostInfo;
|
||||
hostInfoError?: ServerApiError;
|
||||
isHostInfoLoading: boolean;
|
||||
};
|
||||
/** Holds the Policy Response for the Host currently being displayed in the details */
|
||||
policyResponse?: HostPolicyResponse;
|
||||
/** policyResponse is being retrieved */
|
||||
policyResponseLoading: boolean;
|
||||
/** api error from retrieving the policy response */
|
||||
policyResponseError?: ServerApiError;
|
||||
/** current location info */
|
||||
location?: Immutable<AppLocation>;
|
||||
/** policies */
|
||||
|
@ -57,7 +42,7 @@ export interface EndpointState {
|
|||
selectedPolicyId?: string;
|
||||
/** Endpoint package info */
|
||||
endpointPackageInfo: AsyncResourceState<GetInfoResponse['item']>;
|
||||
/** Tracks the list of policies IDs used in Host metadata that may no longer exist */
|
||||
/** Tracks the list of policy IDs used in Host metadata that may no longer exist */
|
||||
nonExistingPolicies: PolicyIds['packagePolicy'];
|
||||
/** List of Package Policy Ids mapped to an associated Fleet Parent Agent Policy Id*/
|
||||
agentPolicies: PolicyIds['agentPolicy'];
|
||||
|
@ -67,7 +52,7 @@ export interface EndpointState {
|
|||
patterns: DataViewBase[];
|
||||
/** api error from retrieving index patters for query bar */
|
||||
patternsError?: ServerApiError;
|
||||
/** Is auto-refresh enabled */
|
||||
/** Is auto-refresh enabled? */
|
||||
isAutoRefreshEnabled: boolean;
|
||||
/** The current auto refresh interval for data in ms */
|
||||
autoRefreshInterval: number;
|
||||
|
@ -79,10 +64,6 @@ export interface EndpointState {
|
|||
endpointsTotal: number;
|
||||
/** api error for total, actual Endpoints */
|
||||
endpointsTotalError?: ServerApiError;
|
||||
/** The policy IDs and revision number of the corresponding agent, and endpoint. May be more recent than what's running */
|
||||
policyVersionInfo?: HostInfo['policy_info'];
|
||||
/** 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<ResponseActionApiResponse>;
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 React, { memo, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAppUrl } from '../../../../../common/lib/kibana';
|
||||
import type { BackToExternalAppButtonProps } from '../../../../components/back_to_external_app_button/back_to_external_app_button';
|
||||
import { BackToExternalAppButton } from '../../../../components/back_to_external_app_button/back_to_external_app_button';
|
||||
import { getPoliciesPath } from '../../../../common/routing';
|
||||
import { APP_UI_ID } from '../../../../../../common';
|
||||
import type { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types';
|
||||
|
||||
export const BackToPolicyListButton = memo<{ backLink?: PolicyDetailsRouteState['backLink'] }>(
|
||||
({ backLink }) => {
|
||||
const { getAppUrl } = useAppUrl();
|
||||
|
||||
const backLinkOptions = useMemo<BackToExternalAppButtonProps>(() => {
|
||||
if (backLink) {
|
||||
const { navigateTo, label, href } = backLink;
|
||||
return {
|
||||
onBackButtonNavigateTo: navigateTo,
|
||||
backButtonLabel: label,
|
||||
backButtonUrl: href,
|
||||
};
|
||||
}
|
||||
|
||||
// the default back button is to the policy list
|
||||
const policyListPath = getPoliciesPath();
|
||||
|
||||
return {
|
||||
backButtonLabel: i18n.translate('xpack.securitySolution.endpoint.list.backToPolicyButton', {
|
||||
defaultMessage: 'Back to policy list',
|
||||
}),
|
||||
backButtonUrl: getAppUrl({ path: policyListPath }),
|
||||
onBackButtonNavigateTo: [
|
||||
APP_UI_ID,
|
||||
{
|
||||
path: policyListPath,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [getAppUrl, backLink]);
|
||||
|
||||
if (!backLink) {
|
||||
return null;
|
||||
}
|
||||
return <BackToExternalAppButton {...backLinkOptions} data-test-subj="endpointListBackLink" />;
|
||||
}
|
||||
);
|
||||
|
||||
BackToPolicyListButton.displayName = 'BackToPolicyListButton';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 React, { memo } from 'react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
|
||||
|
||||
export const EndpointListNavLink = memo<{
|
||||
name: string;
|
||||
href: string;
|
||||
route: string;
|
||||
dataTestSubj: string;
|
||||
}>(({ name, href, route, dataTestSubj }) => {
|
||||
const clickHandler = useNavigateByRouterEventHandler(route);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink
|
||||
data-test-subj={dataTestSubj}
|
||||
className="eui-displayInline eui-textTruncate"
|
||||
href={href}
|
||||
onClick={clickHandler}
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
);
|
||||
});
|
||||
EndpointListNavLink.displayName = 'EndpointListNavLink';
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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 React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiLink, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import type { ImmutableArray } from '../../../../../../common/endpoint/types';
|
||||
import type { TransformStats } from '../../types';
|
||||
import { WARNING_TRANSFORM_STATES } from '../../../../../../common/constants';
|
||||
import { metadataTransformPrefix } from '../../../../../../common/endpoint/constants';
|
||||
import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
|
||||
import { CallOut } from '../../../../../common/components/callouts';
|
||||
import type { EndpointAction } from '../../store/action';
|
||||
|
||||
const TRANSFORM_URL = '/data/transform';
|
||||
|
||||
interface TransformFailedCalloutProps {
|
||||
hasNoPolicyData: boolean;
|
||||
metadataTransformStats: ImmutableArray<TransformStats>;
|
||||
}
|
||||
|
||||
export const TransformFailedCallout = memo<TransformFailedCalloutProps>(
|
||||
({ hasNoPolicyData, metadataTransformStats }) => {
|
||||
const [showTransformFailedCallout, setShowTransformFailedCallout] = useState(false);
|
||||
const [shouldCheckTransforms, setShouldCheckTransforms] = useState(true);
|
||||
const { services } = useKibana();
|
||||
const dispatch = useDispatch<(a: EndpointAction) => void>();
|
||||
|
||||
useEffect(() => {
|
||||
// if no endpoint policy, skip transform check
|
||||
if (!shouldCheckTransforms || hasNoPolicyData) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'loadMetadataTransformStats' });
|
||||
setShouldCheckTransforms(false);
|
||||
}, [dispatch, hasNoPolicyData, shouldCheckTransforms]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasFailure = metadataTransformStats.some((transform) =>
|
||||
WARNING_TRANSFORM_STATES.has(transform?.state)
|
||||
);
|
||||
setShowTransformFailedCallout(hasFailure);
|
||||
}, [metadataTransformStats]);
|
||||
|
||||
const closeTransformFailedCallout = useCallback(() => {
|
||||
setShowTransformFailedCallout(false);
|
||||
}, []);
|
||||
|
||||
const failingTransformIds: string = useMemo(
|
||||
() =>
|
||||
metadataTransformStats
|
||||
.reduce<string[]>((acc, currentValue) => {
|
||||
if (WARNING_TRANSFORM_STATES.has(currentValue.state)) {
|
||||
acc.push(currentValue.id);
|
||||
}
|
||||
return acc;
|
||||
}, [])
|
||||
.join(', '),
|
||||
[metadataTransformStats]
|
||||
);
|
||||
|
||||
const calloutDescription = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.transformFailed.message"
|
||||
defaultMessage="A required transform, {transformId}, is currently failing. Most of the time this can be fixed by {transformsPage}. For additional help, please visit the {docsPage}"
|
||||
values={{
|
||||
transformId: failingTransformIds || metadataTransformPrefix,
|
||||
transformsPage: (
|
||||
<LinkToApp
|
||||
data-test-subj="failed-transform-restart-link"
|
||||
appId="management"
|
||||
appPath={TRANSFORM_URL}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.transformFailed.restartLink"
|
||||
defaultMessage="restarting the transform"
|
||||
/>
|
||||
</LinkToApp>
|
||||
),
|
||||
docsPage: (
|
||||
<EuiLink
|
||||
data-test-subj="failed-transform-docs-link"
|
||||
href={services.docLinks.links.endpoints.troubleshooting}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.transformFailed.docsLink"
|
||||
defaultMessage="troubleshooting documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
),
|
||||
[failingTransformIds, services.docLinks.links.endpoints.troubleshooting]
|
||||
);
|
||||
|
||||
if (!showTransformFailedCallout) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CallOut
|
||||
message={{
|
||||
id: 'endpoints-list-transform-failed',
|
||||
type: 'warning',
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.list.transformFailed.title', {
|
||||
defaultMessage: 'Required transform failed',
|
||||
}),
|
||||
description: calloutDescription,
|
||||
}}
|
||||
dismissButtonText={i18n.translate(
|
||||
'xpack.securitySolution.endpoint.list.transformFailed.dismiss',
|
||||
{
|
||||
defaultMessage: 'Dismiss',
|
||||
}
|
||||
)}
|
||||
onDismiss={closeTransformFailedCallout}
|
||||
showDismissButton={true}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TransformFailedCallout.displayName = 'TransformFailedCallout';
|
|
@ -12,11 +12,12 @@ import { ActionsMenu } from './actions_menu';
|
|||
import React from 'react';
|
||||
import { act } from '@testing-library/react';
|
||||
import { endpointPageHttpMock } from '../../../mocks';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
import { licenseService } from '../../../../../../common/hooks/use_license';
|
||||
import { useUserPrivileges } from '../../../../../../common/components/user_privileges';
|
||||
import { initialUserPrivilegesState } from '../../../../../../common/components/user_privileges/user_privileges_context';
|
||||
import { getUserPrivilegesMockDefaultValue } from '../../../../../../common/components/user_privileges/__mocks__';
|
||||
import type { HostInfo } from '../../../../../../../common/endpoint/types';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../../../../common/lib/kibana/kibana_react', () => {
|
||||
const originalModule = jest.requireActual('../../../../../../common/lib/kibana/kibana_react');
|
||||
|
@ -39,12 +40,13 @@ jest.mock('../../../../../../common/components/user_privileges');
|
|||
describe('When using the Endpoint Details Actions Menu', () => {
|
||||
let render: () => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
let coreStart: AppContextTestRender['coreStart'];
|
||||
let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction'];
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
let httpMocks: ReturnType<typeof endpointPageHttpMock>;
|
||||
let middlewareSpy: AppContextTestRender['middlewareSpy'];
|
||||
let endpointHost: HostInfo;
|
||||
|
||||
const setEndpointMetadataResponse = (isolation: boolean = false) => {
|
||||
const endpointHost = httpMocks.responseProvider.metadataDetails();
|
||||
endpointHost = httpMocks.responseProvider.metadataDetails();
|
||||
// Safe to mutate this mocked data
|
||||
// @ts-expect-error TS2540
|
||||
endpointHost.metadata.Endpoint.state.isolation = isolation;
|
||||
|
@ -60,7 +62,8 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
|
||||
(useKibana as jest.Mock).mockReturnValue({ services: mockedContext.startServices });
|
||||
coreStart = mockedContext.coreStart;
|
||||
waitForAction = mockedContext.middlewareSpy.waitForAction;
|
||||
middlewareSpy = mockedContext.middlewareSpy;
|
||||
|
||||
httpMocks = endpointPageHttpMock(mockedContext.coreStart.http);
|
||||
|
||||
(useUserPrivileges as jest.Mock).mockReturnValue(getUserPrivilegesMockDefaultValue());
|
||||
|
@ -72,15 +75,10 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
});
|
||||
|
||||
render = async () => {
|
||||
renderResult = mockedContext.render(<ActionsMenu />);
|
||||
|
||||
await act(async () => {
|
||||
await waitForAction('serverReturnedEndpointDetails');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(renderResult.getByTestId('endpointDetailsActionsButton'));
|
||||
});
|
||||
renderResult = mockedContext.render(<ActionsMenu hostMetadata={endpointHost?.metadata} />);
|
||||
const endpointDetailsActionsButton = renderResult.getByTestId('endpointDetailsActionsButton');
|
||||
endpointDetailsActionsButton.style.pointerEvents = 'all';
|
||||
userEvent.click(endpointDetailsActionsButton);
|
||||
|
||||
return renderResult;
|
||||
};
|
||||
|
@ -119,10 +117,14 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
'should navigate via kibana `navigateToApp()` when %s is clicked',
|
||||
async (_, dataTestSubj) => {
|
||||
await render();
|
||||
act(() => {
|
||||
fireEvent.click(renderResult.getByTestId(dataTestSubj));
|
||||
await act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointAgentPolicies');
|
||||
});
|
||||
|
||||
const takeActionMenuItem = renderResult.getByTestId(dataTestSubj);
|
||||
takeActionMenuItem.style.pointerEvents = 'all';
|
||||
userEvent.click(takeActionMenuItem);
|
||||
|
||||
expect(coreStart.application.navigateToApp).toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
@ -132,16 +134,16 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
beforeEach(() => setEndpointMetadataResponse(true));
|
||||
|
||||
describe('and user has unisolate privilege', () => {
|
||||
it('should display Unisolate action', async () => {
|
||||
it('should display `Release` action', async () => {
|
||||
await render();
|
||||
expect(renderResult.getByTestId('unIsolateLink')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should navigate via router when unisolate is clicked', async () => {
|
||||
it('should navigate via router when `Release` is clicked', async () => {
|
||||
await render();
|
||||
act(() => {
|
||||
fireEvent.click(renderResult.getByTestId('unIsolateLink'));
|
||||
});
|
||||
const isolateButton = renderResult.getByTestId('unIsolateLink');
|
||||
isolateButton.style.pointerEvents = 'all';
|
||||
userEvent.click(isolateButton);
|
||||
|
||||
expect(coreStart.application.navigateToApp).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -159,7 +161,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not display unisolate action', async () => {
|
||||
it('should not display `Release` action', async () => {
|
||||
await render();
|
||||
expect(renderResult.queryByTestId('unIsolateLink')).toBeNull();
|
||||
});
|
||||
|
@ -173,7 +175,7 @@ describe('When using the Endpoint Details Actions Menu', () => {
|
|||
|
||||
afterEach(() => licenseServiceMock.isPlatinumPlus.mockReturnValue(true));
|
||||
|
||||
it('should still show `unisolate` action for endpoints that are currently isolated', async () => {
|
||||
it('should still show `Release` action for endpoints that are currently isolated', async () => {
|
||||
setEndpointMetadataResponse(true);
|
||||
await render();
|
||||
expect(renderResult.queryByTestId('isolateLink')).toBeNull();
|
||||
|
|
|
@ -5,16 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { EuiButton, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useEndpointActionItems, useEndpointSelector } from '../../hooks';
|
||||
import { fullDetailsHostInfo } from '../../../store/selectors';
|
||||
import type { HostMetadata } from '../../../../../../../common/endpoint/types';
|
||||
import { useEndpointActionItems } from '../../hooks';
|
||||
import { ContextMenuItemNavByRouter } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router';
|
||||
|
||||
export const ActionsMenu = React.memo<{}>(() => {
|
||||
const endpointDetails = useEndpointSelector(fullDetailsHostInfo);
|
||||
const menuOptions = useEndpointActionItems(endpointDetails?.metadata);
|
||||
export const ActionsMenu = memo<{ hostMetadata: HostMetadata }>(({ hostMetadata }) => {
|
||||
const menuOptions = useEndpointActionItems(hostMetadata);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const closePopoverHandler = useCallback(() => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { EuiTab, EuiTabs, EuiFlyoutBody } from '@elastic/eui';
|
||||
import { EuiFlyoutBody, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import type { EndpointIndexUIQueryParams } from '../../../types';
|
||||
|
||||
import { EndpointDetailsFlyoutHeader } from './flyout_header';
|
||||
|
@ -45,16 +45,15 @@ const EndpointDetailsTab = memo(
|
|||
|
||||
EndpointDetailsTab.displayName = 'EndpointDetailsTab';
|
||||
|
||||
export const EndpointDetailsFlyoutTabs = memo(
|
||||
({
|
||||
hostname,
|
||||
show,
|
||||
tabs,
|
||||
}: {
|
||||
hostname: string;
|
||||
show: EndpointIndexUIQueryParams['show'];
|
||||
tabs: EndpointDetailsTabs[];
|
||||
}) => {
|
||||
interface EndpointDetailsTabsProps {
|
||||
hostname: string;
|
||||
isHostInfoLoading: boolean;
|
||||
show: EndpointIndexUIQueryParams['show'];
|
||||
tabs: EndpointDetailsTabs[];
|
||||
}
|
||||
|
||||
export const EndpointDetailsFlyoutTabs = memo<EndpointDetailsTabsProps>(
|
||||
({ hostname, isHostInfoLoading, show, tabs }) => {
|
||||
const selectedTab = useMemo(() => tabs.find((tab) => tab.id === show), [tabs, show]);
|
||||
|
||||
const renderTabs = tabs.map((tab) => (
|
||||
|
@ -63,7 +62,11 @@ export const EndpointDetailsFlyoutTabs = memo(
|
|||
|
||||
return (
|
||||
<>
|
||||
<EndpointDetailsFlyoutHeader hostname={hostname} hasBorder>
|
||||
<EndpointDetailsFlyoutHeader
|
||||
hostname={hostname}
|
||||
isHostInfoLoading={isHostInfoLoading}
|
||||
hasBorder
|
||||
>
|
||||
<EuiTabs bottomBorder={false} style={{ marginBottom: '-25px' }}>
|
||||
{renderTabs}
|
||||
</EuiTabs>
|
||||
|
|
|
@ -9,24 +9,24 @@ import React, { memo, useCallback, useState } from 'react';
|
|||
import { useHistory } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { EuiForm, EuiFlyoutBody } from '@elastic/eui';
|
||||
import { EuiFlyoutBody, EuiForm } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEndpointHostIsolated } from '../../../../../../common/utils/validators';
|
||||
import type { HostMetadata } from '../../../../../../../common/endpoint/types';
|
||||
import type { EndpointIsolatedFormProps } from '../../../../../../common/components/endpoint/host_isolation';
|
||||
import {
|
||||
ActionCompletionReturnButton,
|
||||
EndpointIsolateForm,
|
||||
EndpointIsolateSuccess,
|
||||
EndpointUnisolateForm,
|
||||
ActionCompletionReturnButton,
|
||||
} from '../../../../../../common/components/endpoint/host_isolation';
|
||||
import { getEndpointDetailsPath } from '../../../../../common/routing';
|
||||
import { useEndpointSelector } from '../../hooks';
|
||||
import {
|
||||
getIsolationRequestError,
|
||||
getIsIsolationRequestPending,
|
||||
getIsolationRequestError,
|
||||
getWasIsolationRequestSuccessful,
|
||||
uiQueryParams,
|
||||
getIsEndpointHostIsolated,
|
||||
} from '../../../store/selectors';
|
||||
import type { AppAction } from '../../../../../../common/store/actions';
|
||||
|
||||
|
@ -40,7 +40,7 @@ export const EndpointIsolationFlyoutPanel = memo<{
|
|||
const dispatch = useDispatch<Dispatch<AppAction>>();
|
||||
|
||||
const { show, ...queryParams } = useEndpointSelector(uiQueryParams);
|
||||
const isCurrentlyIsolated = useEndpointSelector(getIsEndpointHostIsolated);
|
||||
const isCurrentlyIsolated = isEndpointHostIsolated(hostMeta);
|
||||
const isPending = useEndpointSelector(getIsIsolationRequestPending);
|
||||
const wasSuccessful = useEndpointSelector(getWasIsolationRequestSuccessful);
|
||||
const isolateError = useEndpointSelector(getIsolationRequestError);
|
||||
|
|
|
@ -7,29 +7,23 @@
|
|||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiFlyoutHeader, EuiSkeletonText, EuiTitle, EuiToolTip } from '@elastic/eui';
|
||||
import { useEndpointSelector } from '../../hooks';
|
||||
import { isHostInfoLoading } from '../../../store/selectors';
|
||||
import { BackToEndpointDetailsFlyoutSubHeader } from './back_to_endpoint_details_flyout_subheader';
|
||||
|
||||
export const EndpointDetailsFlyoutHeader = memo(
|
||||
({
|
||||
endpointId,
|
||||
hasBorder = false,
|
||||
hostname,
|
||||
children,
|
||||
}: {
|
||||
endpointId?: string;
|
||||
hasBorder?: boolean;
|
||||
hostname?: string;
|
||||
children?: React.ReactNode | React.ReactNode[];
|
||||
}) => {
|
||||
const hostDetailsLoading = useEndpointSelector(isHostInfoLoading);
|
||||
interface EndpointDetailsFlyoutHeaderProps {
|
||||
children?: React.ReactNode | React.ReactNode[];
|
||||
endpointId?: string;
|
||||
hasBorder?: boolean;
|
||||
hostname?: string;
|
||||
isHostInfoLoading: boolean;
|
||||
}
|
||||
|
||||
export const EndpointDetailsFlyoutHeader = memo<EndpointDetailsFlyoutHeaderProps>(
|
||||
({ children, endpointId, hasBorder = false, hostname, isHostInfoLoading }) => {
|
||||
return (
|
||||
<EuiFlyoutHeader hasBorder={hasBorder}>
|
||||
{endpointId && <BackToEndpointDetailsFlyoutSubHeader endpointId={endpointId} />}
|
||||
|
||||
{hostDetailsLoading ? (
|
||||
{isHostInfoLoading ? (
|
||||
<EuiSkeletonText lines={1} />
|
||||
) : (
|
||||
<EuiToolTip content={hostname} anchorClassName="eui-textTruncate">
|
||||
|
|
|
@ -7,19 +7,14 @@
|
|||
import { EuiFlyoutBody, EuiFlyoutFooter, EuiSkeletonText, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useGetEndpointDetails } from '../../../../hooks';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
import { ResponseActionsLog } from '../../../../components/endpoint_response_actions_list/response_actions_log';
|
||||
import { PolicyResponseWrapper } from '../../../../components/policy_response';
|
||||
import type { HostMetadata } from '../../../../../../common/endpoint/types';
|
||||
import { useToasts } from '../../../../../common/lib/kibana';
|
||||
import { getEndpointDetailsPath } from '../../../../common/routing';
|
||||
import {
|
||||
fullDetailsHostInfo,
|
||||
hostInfoError,
|
||||
policyVersionInfo,
|
||||
showView,
|
||||
uiQueryParams,
|
||||
} from '../../store/selectors';
|
||||
import { showView, uiQueryParams } from '../../store/selectors';
|
||||
import { useEndpointSelector } from '../hooks';
|
||||
import * as i18 from '../translations';
|
||||
import { ActionsMenu } from './components/actions_menu';
|
||||
|
@ -36,14 +31,16 @@ export const EndpointDetails = memo(() => {
|
|||
const toasts = useToasts();
|
||||
const queryParams = useEndpointSelector(uiQueryParams);
|
||||
|
||||
const hostInfo = useEndpointSelector(fullDetailsHostInfo);
|
||||
const hostDetailsError = useEndpointSelector(hostInfoError);
|
||||
const {
|
||||
data: hostInfo,
|
||||
error: hostInfoError,
|
||||
isFetching: isHostInfoLoading,
|
||||
} = useGetEndpointDetails(queryParams.selected_endpoint ?? '');
|
||||
|
||||
const policyInfo = useEndpointSelector(policyVersionInfo);
|
||||
const show = useEndpointSelector(showView);
|
||||
const { canAccessEndpointActionsLogManagement } = useUserPrivileges().endpointPrivileges;
|
||||
|
||||
const ContentLoadingMarkup = useMemo(
|
||||
const contentLoadingMarkup = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<EuiSkeletonText lines={3} />
|
||||
|
@ -67,9 +64,9 @@ export const EndpointDetails = memo(() => {
|
|||
}),
|
||||
content:
|
||||
hostInfo === undefined ? (
|
||||
ContentLoadingMarkup
|
||||
contentLoadingMarkup
|
||||
) : (
|
||||
<EndpointDetailsContent hostInfo={hostInfo} policyInfo={policyInfo} />
|
||||
<EndpointDetailsContent hostInfo={hostInfo} policyInfo={hostInfo.policy_info} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
@ -90,14 +87,14 @@ export const EndpointDetails = memo(() => {
|
|||
}
|
||||
return tabs;
|
||||
},
|
||||
[canAccessEndpointActionsLogManagement, ContentLoadingMarkup, hostInfo, policyInfo, queryParams]
|
||||
[canAccessEndpointActionsLogManagement, contentLoadingMarkup, hostInfo, queryParams]
|
||||
);
|
||||
|
||||
const showFlyoutFooter =
|
||||
show === 'details' || show === 'policy_response' || show === 'activity_log';
|
||||
|
||||
useEffect(() => {
|
||||
if (hostDetailsError !== undefined) {
|
||||
if (hostInfoError !== null) {
|
||||
toasts.addDanger({
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.details.errorTitle', {
|
||||
defaultMessage: 'Could not find host',
|
||||
|
@ -107,15 +104,16 @@ export const EndpointDetails = memo(() => {
|
|||
}),
|
||||
});
|
||||
}
|
||||
}, [hostDetailsError, show, toasts]);
|
||||
}, [hostInfoError, show, toasts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{(show === 'policy_response' || show === 'isolate' || show === 'unisolate') && (
|
||||
<EndpointDetailsFlyoutHeader
|
||||
hasBorder
|
||||
endpointId={hostInfo?.metadata?.agent.id}
|
||||
hasBorder
|
||||
hostname={hostInfo?.metadata?.host?.hostname}
|
||||
isHostInfoLoading={isHostInfoLoading}
|
||||
/>
|
||||
)}
|
||||
{hostInfo === undefined ? (
|
||||
|
@ -127,6 +125,7 @@ export const EndpointDetails = memo(() => {
|
|||
{(show === 'details' || show === 'activity_log') && (
|
||||
<EndpointDetailsFlyoutTabs
|
||||
hostname={hostInfo.metadata.host.hostname}
|
||||
isHostInfoLoading={isHostInfoLoading}
|
||||
// show overview tab if forcing response actions history
|
||||
// tab via URL without permission
|
||||
show={!canAccessEndpointActionsLogManagement ? 'details' : show}
|
||||
|
@ -142,7 +141,7 @@ export const EndpointDetails = memo(() => {
|
|||
|
||||
{showFlyoutFooter && (
|
||||
<EuiFlyoutFooter className="eui-textRight" data-test-subj="endpointDetailsFlyoutFooter">
|
||||
<ActionsMenu />
|
||||
<ActionsMenu hostMetadata={hostInfo.metadata} />
|
||||
</EuiFlyoutFooter>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -24,7 +24,6 @@ import { useEndpointSelector } from '../hooks';
|
|||
import {
|
||||
getEndpointPendingActionsCallback,
|
||||
nonExistingPolicies,
|
||||
policyResponseStatus,
|
||||
uiQueryParams,
|
||||
} from '../../store/selectors';
|
||||
import { POLICY_STATUS_TO_BADGE_COLOR } from '../host_constants';
|
||||
|
@ -64,9 +63,10 @@ interface EndpointDetailsContentProps {
|
|||
export const EndpointDetailsContent = memo<EndpointDetailsContentProps>(
|
||||
({ hostInfo, policyInfo }) => {
|
||||
const queryParams = useEndpointSelector(uiQueryParams);
|
||||
const policyStatus = useEndpointSelector(
|
||||
policyResponseStatus
|
||||
) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR;
|
||||
const policyStatus = useMemo(
|
||||
() => hostInfo.metadata.Endpoint.policy.applied.status,
|
||||
[hostInfo]
|
||||
);
|
||||
const getHostPendingActions = useEndpointSelector(getEndpointPendingActionsCallback);
|
||||
const missingPolicies = useEndpointSelector(nonExistingPolicies);
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ interface Options {
|
|||
/**
|
||||
* Returns a list (array) of actions for an individual endpoint
|
||||
* @param endpointMetadata
|
||||
* @param options
|
||||
*/
|
||||
export const useEndpointActionItems = (
|
||||
endpointMetadata: MaybeImmutable<HostMetadata> | undefined,
|
||||
|
@ -45,218 +46,214 @@ export const useEndpointActionItems = (
|
|||
} = useUserPrivileges().endpointPrivileges;
|
||||
|
||||
return useMemo<ContextMenuItemNavByRouterProps[]>(() => {
|
||||
if (endpointMetadata) {
|
||||
const isIsolated = isEndpointHostIsolated(endpointMetadata);
|
||||
const endpointId = endpointMetadata.agent.id;
|
||||
const endpointPolicyId = endpointMetadata.Endpoint.policy.applied.id;
|
||||
const endpointHostName = endpointMetadata.host.hostname;
|
||||
const fleetAgentId = endpointMetadata.elastic.agent.id;
|
||||
const isolationSupported = isIsolationSupported({
|
||||
osName: endpointMetadata.host.os.name,
|
||||
version: endpointMetadata.agent.version,
|
||||
capabilities: endpointMetadata.Endpoint.capabilities,
|
||||
});
|
||||
const {
|
||||
show,
|
||||
selected_endpoint: _selectedEndpoint,
|
||||
...currentUrlParams
|
||||
} = allCurrentUrlParams;
|
||||
const endpointActionsPath = getEndpointDetailsPath({
|
||||
name: 'endpointActivityLog',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: endpointId,
|
||||
});
|
||||
const endpointIsolatePath = getEndpointDetailsPath({
|
||||
name: 'endpointIsolate',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: endpointId,
|
||||
});
|
||||
const endpointUnIsolatePath = getEndpointDetailsPath({
|
||||
name: 'endpointUnIsolate',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: endpointId,
|
||||
});
|
||||
if (!endpointMetadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isolationActions = [];
|
||||
const isIsolated = isEndpointHostIsolated(endpointMetadata);
|
||||
const endpointId = endpointMetadata.agent.id;
|
||||
const endpointPolicyId = endpointMetadata.Endpoint.policy.applied.id;
|
||||
const endpointHostName = endpointMetadata.host.hostname;
|
||||
const fleetAgentId = endpointMetadata.elastic.agent.id;
|
||||
const isolationSupported = isIsolationSupported({
|
||||
osName: endpointMetadata.host.os.name,
|
||||
version: endpointMetadata.agent.version,
|
||||
capabilities: endpointMetadata.Endpoint.capabilities,
|
||||
});
|
||||
const { show, selected_endpoint: _selectedEndpoint, ...currentUrlParams } = allCurrentUrlParams;
|
||||
const endpointActionsPath = getEndpointDetailsPath({
|
||||
name: 'endpointActivityLog',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: endpointId,
|
||||
});
|
||||
const endpointIsolatePath = getEndpointDetailsPath({
|
||||
name: 'endpointIsolate',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: endpointId,
|
||||
});
|
||||
const endpointUnIsolatePath = getEndpointDetailsPath({
|
||||
name: 'endpointUnIsolate',
|
||||
...currentUrlParams,
|
||||
selected_endpoint: endpointId,
|
||||
});
|
||||
|
||||
if (isIsolated && canUnIsolateHost) {
|
||||
// Un-isolate is available to users regardless of license level if they have unisolate permissions
|
||||
isolationActions.push({
|
||||
'data-test-subj': 'unIsolateLink',
|
||||
icon: 'lockOpen',
|
||||
key: 'unIsolateHost',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: {
|
||||
path: endpointUnIsolatePath,
|
||||
},
|
||||
href: getAppUrl({ path: endpointUnIsolatePath }),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.unIsolateHost"
|
||||
defaultMessage="Release host"
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else if (isolationSupported && canIsolateHost) {
|
||||
// For Platinum++ licenses, users also have ability to isolate
|
||||
isolationActions.push({
|
||||
'data-test-subj': 'isolateLink',
|
||||
icon: 'lock',
|
||||
key: 'isolateHost',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: {
|
||||
path: endpointIsolatePath,
|
||||
},
|
||||
href: getAppUrl({ path: endpointIsolatePath }),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.isolateHost"
|
||||
defaultMessage="Isolate host"
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
const isolationActions = [];
|
||||
|
||||
return [
|
||||
...isolationActions,
|
||||
...(canAccessResponseConsole
|
||||
? [
|
||||
{
|
||||
'data-test-subj': 'console',
|
||||
icon: 'console',
|
||||
key: 'consoleLink',
|
||||
onClick: (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
showEndpointResponseActionsConsole(endpointMetadata);
|
||||
},
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.console"
|
||||
defaultMessage="Respond"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(options?.isEndpointList && canAccessEndpointActionsLogManagement
|
||||
? [
|
||||
{
|
||||
'data-test-subj': 'actionsLink',
|
||||
icon: 'logoSecurity',
|
||||
key: 'actionsLogLink',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: { path: endpointActionsPath },
|
||||
href: getAppUrl({ path: endpointActionsPath }),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.responseActionsHistory"
|
||||
defaultMessage="View response actions history"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
'data-test-subj': 'hostLink',
|
||||
icon: 'logoSecurity',
|
||||
key: 'hostDetailsLink',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: { path: `/hosts/${endpointHostName}` },
|
||||
href: getAppUrl({ path: `/hosts/${endpointHostName}` }),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.hostDetails"
|
||||
defaultMessage="View host details"
|
||||
/>
|
||||
),
|
||||
if (isIsolated && canUnIsolateHost) {
|
||||
// Un-isolate is available to users regardless of license level if they have unisolate permissions
|
||||
isolationActions.push({
|
||||
'data-test-subj': 'unIsolateLink',
|
||||
icon: 'lockOpen',
|
||||
key: 'unIsolateHost',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: {
|
||||
path: endpointUnIsolatePath,
|
||||
},
|
||||
...(canAccessFleet
|
||||
? [
|
||||
{
|
||||
icon: 'gear',
|
||||
key: 'agentConfigLink',
|
||||
'data-test-subj': 'agentPolicyLink',
|
||||
navigateAppId: 'fleet',
|
||||
navigateOptions: {
|
||||
path: `${
|
||||
pagePathGetters.policy_details({
|
||||
policyId: fleetAgentPolicies[endpointPolicyId],
|
||||
})[1]
|
||||
}`,
|
||||
},
|
||||
href: `${getAppUrl({ appId: 'fleet' })}${
|
||||
href: getAppUrl({ path: endpointUnIsolatePath }),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.unIsolateHost"
|
||||
defaultMessage="Release host"
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else if (isolationSupported && canIsolateHost) {
|
||||
// For Platinum++ licenses, users also have ability to isolate
|
||||
isolationActions.push({
|
||||
'data-test-subj': 'isolateLink',
|
||||
icon: 'lock',
|
||||
key: 'isolateHost',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: {
|
||||
path: endpointIsolatePath,
|
||||
},
|
||||
href: getAppUrl({ path: endpointIsolatePath }),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.isolateHost"
|
||||
defaultMessage="Isolate host"
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
...isolationActions,
|
||||
...(canAccessResponseConsole
|
||||
? [
|
||||
{
|
||||
'data-test-subj': 'console',
|
||||
icon: 'console',
|
||||
key: 'consoleLink',
|
||||
onClick: (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
showEndpointResponseActionsConsole(endpointMetadata);
|
||||
},
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.console"
|
||||
defaultMessage="Respond"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(options?.isEndpointList && canAccessEndpointActionsLogManagement
|
||||
? [
|
||||
{
|
||||
'data-test-subj': 'actionsLink',
|
||||
icon: 'logoSecurity',
|
||||
key: 'actionsLogLink',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: { path: endpointActionsPath },
|
||||
href: getAppUrl({ path: endpointActionsPath }),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.responseActionsHistory"
|
||||
defaultMessage="View response actions history"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
'data-test-subj': 'hostLink',
|
||||
icon: 'logoSecurity',
|
||||
key: 'hostDetailsLink',
|
||||
navigateAppId: APP_UI_ID,
|
||||
navigateOptions: { path: `/hosts/${endpointHostName}` },
|
||||
href: getAppUrl({ path: `/hosts/${endpointHostName}` }),
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.hostDetails"
|
||||
defaultMessage="View host details"
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(canAccessFleet
|
||||
? [
|
||||
{
|
||||
icon: 'gear',
|
||||
key: 'agentConfigLink',
|
||||
'data-test-subj': 'agentPolicyLink',
|
||||
navigateAppId: 'fleet',
|
||||
navigateOptions: {
|
||||
path: `${
|
||||
pagePathGetters.policy_details({
|
||||
policyId: fleetAgentPolicies[endpointPolicyId],
|
||||
})[1]
|
||||
}`,
|
||||
disabled: fleetAgentPolicies[endpointPolicyId] === undefined,
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.agentPolicy"
|
||||
defaultMessage="View agent policy"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'gear',
|
||||
key: 'agentDetailsLink',
|
||||
'data-test-subj': 'agentDetailsLink',
|
||||
navigateAppId: 'fleet',
|
||||
navigateOptions: {
|
||||
path: `${
|
||||
pagePathGetters.agent_details({
|
||||
agentId: fleetAgentId,
|
||||
})[1]
|
||||
}`,
|
||||
},
|
||||
href: `${getAppUrl({ appId: 'fleet' })}${
|
||||
href: `${getAppUrl({ appId: 'fleet' })}${
|
||||
pagePathGetters.policy_details({
|
||||
policyId: fleetAgentPolicies[endpointPolicyId],
|
||||
})[1]
|
||||
}`,
|
||||
disabled: fleetAgentPolicies[endpointPolicyId] === undefined,
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.agentPolicy"
|
||||
defaultMessage="View agent policy"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'gear',
|
||||
key: 'agentDetailsLink',
|
||||
'data-test-subj': 'agentDetailsLink',
|
||||
navigateAppId: 'fleet',
|
||||
navigateOptions: {
|
||||
path: `${
|
||||
pagePathGetters.agent_details({
|
||||
agentId: fleetAgentId,
|
||||
})[1]
|
||||
}`,
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.agentDetails"
|
||||
defaultMessage="View agent details"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'gear',
|
||||
key: 'agentPolicyReassignLink',
|
||||
'data-test-subj': 'agentPolicyReassignLink',
|
||||
navigateAppId: 'fleet',
|
||||
navigateOptions: {
|
||||
path: `${
|
||||
pagePathGetters.agent_details({
|
||||
agentId: fleetAgentId,
|
||||
})[1]
|
||||
}?openReassignFlyout=true`,
|
||||
state: {
|
||||
onDoneNavigateTo: [
|
||||
APP_UI_ID,
|
||||
{ path: getEndpointListPath({ name: 'endpointList' }) },
|
||||
],
|
||||
},
|
||||
},
|
||||
href: `${getAppUrl({ appId: 'fleet' })}${
|
||||
href: `${getAppUrl({ appId: 'fleet' })}${
|
||||
pagePathGetters.agent_details({
|
||||
agentId: fleetAgentId,
|
||||
})[1]
|
||||
}`,
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.agentDetails"
|
||||
defaultMessage="View agent details"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
icon: 'gear',
|
||||
key: 'agentPolicyReassignLink',
|
||||
'data-test-subj': 'agentPolicyReassignLink',
|
||||
navigateAppId: 'fleet',
|
||||
navigateOptions: {
|
||||
path: `${
|
||||
pagePathGetters.agent_details({
|
||||
agentId: fleetAgentId,
|
||||
})[1]
|
||||
}?openReassignFlyout=true`,
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.agentPolicyReassign"
|
||||
defaultMessage="Reassign agent policy"
|
||||
/>
|
||||
),
|
||||
state: {
|
||||
onDoneNavigateTo: [
|
||||
APP_UI_ID,
|
||||
{ path: getEndpointListPath({ name: 'endpointList' }) },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
href: `${getAppUrl({ appId: 'fleet' })}${
|
||||
pagePathGetters.agent_details({
|
||||
agentId: fleetAgentId,
|
||||
})[1]
|
||||
}?openReassignFlyout=true`,
|
||||
children: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.actions.agentPolicyReassign"
|
||||
defaultMessage="Reassign agent policy"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}, [
|
||||
allCurrentUrlParams,
|
||||
canAccessResponseConsole,
|
||||
|
|
|
@ -19,11 +19,7 @@ import {
|
|||
} from '../store/mock_endpoint_result_list';
|
||||
import type { AppContextTestRender } from '../../../../common/mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../../common/mock/endpoint';
|
||||
import type {
|
||||
HostInfo,
|
||||
HostPolicyResponse,
|
||||
HostPolicyResponseAppliedAction,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import type { HostInfo, HostPolicyResponse } from '../../../../../common/endpoint/types';
|
||||
import { HostPolicyResponseActionStatus, HostStatus } from '../../../../../common/endpoint/types';
|
||||
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
|
||||
import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
|
||||
|
@ -59,6 +55,7 @@ import {
|
|||
import { getUserPrivilegesMockDefaultValue } from '../../../../common/components/user_privileges/__mocks__';
|
||||
import { ENDPOINT_CAPABILITIES } from '../../../../../common/endpoint/service/response_actions/constants';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../../../../common/components/user_privileges/endpoint/mocks';
|
||||
import { useGetEndpointDetails } from '../../../hooks/endpoint/use_get_endpoint_details';
|
||||
|
||||
const mockUserPrivileges = useUserPrivileges as jest.Mock;
|
||||
// not sure why this can't be imported from '../../../../common/mock/formatted_relative';
|
||||
|
@ -138,6 +135,8 @@ const timepickerRanges = [
|
|||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../../common/hooks/use_license');
|
||||
jest.mock('../../../hooks/endpoint/use_get_endpoint_details');
|
||||
const mockUseGetEndpointDetails = useGetEndpointDetails as jest.Mock;
|
||||
|
||||
describe('when on the endpoint list page', () => {
|
||||
const docGenerator = new EndpointDocGenerator();
|
||||
|
@ -407,8 +406,29 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
describe('when the user clicks the first hostname in the table', () => {
|
||||
const endpointDetails: HostInfo = mockEndpointDetailsApiResult();
|
||||
let renderResult: reactTestingLibrary.RenderResult;
|
||||
beforeEach(async () => {
|
||||
mockUseGetEndpointDetails.mockReturnValue({
|
||||
data: {
|
||||
...endpointDetails,
|
||||
host_status: endpointDetails.host_status,
|
||||
metadata: {
|
||||
...endpointDetails.metadata,
|
||||
Endpoint: {
|
||||
...endpointDetails.metadata.Endpoint,
|
||||
state: {
|
||||
...endpointDetails.metadata.Endpoint.state,
|
||||
isolation: false,
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
...endpointDetails.metadata.agent,
|
||||
id: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
renderResult = render();
|
||||
await reactTestingLibrary.act(async () => {
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointList');
|
||||
|
@ -487,13 +507,13 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
describe('when there is a selected host in the url', () => {
|
||||
let hostInfo: HostInfo;
|
||||
let renderAndWaitForData: () => Promise<ReturnType<AppContextTestRender['render']>>;
|
||||
const endpointDetails: HostInfo = mockEndpointDetailsApiResult();
|
||||
const mockEndpointListApi = (mockedPolicyResponse?: HostPolicyResponse) => {
|
||||
const {
|
||||
host_status: hostStatus,
|
||||
last_checkin: lastCheckin,
|
||||
metadata: { agent, Endpoint, ...details },
|
||||
} = mockEndpointDetailsApiResult();
|
||||
} = endpointDetails;
|
||||
|
||||
hostInfo = {
|
||||
host_status: hostStatus,
|
||||
|
@ -524,83 +544,35 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const createPolicyResponse = (
|
||||
overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success
|
||||
): HostPolicyResponse => {
|
||||
const policyResponse = docGenerator.generatePolicyResponse();
|
||||
const malwareResponseConfigurations =
|
||||
policyResponse.Endpoint.policy.applied.response.configurations.malware;
|
||||
policyResponse.Endpoint.policy.applied.status = overallStatus;
|
||||
malwareResponseConfigurations.status = overallStatus;
|
||||
let downloadModelAction = policyResponse.Endpoint.policy.applied.actions.find(
|
||||
(action) => action.name === 'download_model'
|
||||
);
|
||||
|
||||
if (!downloadModelAction) {
|
||||
downloadModelAction = {
|
||||
name: 'download_model',
|
||||
message: 'Failed to apply a portion of the configuration (kernel)',
|
||||
status: overallStatus,
|
||||
};
|
||||
policyResponse.Endpoint.policy.applied.actions.push(downloadModelAction);
|
||||
} else {
|
||||
// Else, make sure the status of the generated action matches what was passed in
|
||||
downloadModelAction.status = overallStatus;
|
||||
}
|
||||
|
||||
if (
|
||||
overallStatus === HostPolicyResponseActionStatus.failure ||
|
||||
overallStatus === HostPolicyResponseActionStatus.warning
|
||||
) {
|
||||
downloadModelAction.message = 'no action taken';
|
||||
}
|
||||
|
||||
// Make sure that at least one configuration has the above action, else
|
||||
// we get into an out-of-sync condition
|
||||
if (
|
||||
malwareResponseConfigurations.concerned_actions.indexOf(downloadModelAction.name) === -1
|
||||
) {
|
||||
malwareResponseConfigurations.concerned_actions.push(downloadModelAction.name);
|
||||
}
|
||||
|
||||
// Add an unknown Action Name - to ensure we handle the format of it on the UI
|
||||
const unknownAction: HostPolicyResponseAppliedAction = {
|
||||
status: HostPolicyResponseActionStatus.success,
|
||||
message: 'test message',
|
||||
name: 'a_new_unknown_action',
|
||||
};
|
||||
policyResponse.Endpoint.policy.applied.actions.push(unknownAction);
|
||||
malwareResponseConfigurations.concerned_actions.push(unknownAction.name);
|
||||
|
||||
return policyResponse;
|
||||
};
|
||||
|
||||
const dispatchServerReturnedEndpointPolicyResponse = (
|
||||
overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success
|
||||
) => {
|
||||
reactTestingLibrary.act(() => {
|
||||
store.dispatch({
|
||||
type: 'serverReturnedEndpointPolicyResponse',
|
||||
payload: {
|
||||
policy_response: createPolicyResponse(overallStatus),
|
||||
const getMockUseEndpointDetails = (policyStatus?: HostPolicyResponseActionStatus) => {
|
||||
return mockUseGetEndpointDetails.mockReturnValue({
|
||||
data: {
|
||||
...hostInfo,
|
||||
metadata: {
|
||||
...hostInfo.metadata,
|
||||
Endpoint: {
|
||||
...hostInfo.metadata.Endpoint,
|
||||
policy: {
|
||||
...hostInfo.metadata.Endpoint.policy,
|
||||
applied: {
|
||||
...hostInfo.metadata.Endpoint.policy.applied,
|
||||
status: policyStatus,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockEndpointListApi();
|
||||
getMockUseEndpointDetails();
|
||||
mockUserPrivileges.mockReturnValue(getUserPrivilegesMockDefaultValue());
|
||||
|
||||
reactTestingLibrary.act(() => {
|
||||
history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1`);
|
||||
});
|
||||
|
||||
renderAndWaitForData = async () => {
|
||||
const renderResult = render();
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointDetails');
|
||||
return renderResult;
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -609,13 +581,13 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should show the flyout and footer', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = render();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyout')).not.toBeNull();
|
||||
expect(renderResult.getByTestId('endpointDetailsFlyoutFooter')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should display policy name value as a link', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = render();
|
||||
const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue');
|
||||
expect(policyDetailsLink).not.toBeNull();
|
||||
expect(policyDetailsLink.getAttribute('href')).toEqual(
|
||||
|
@ -624,7 +596,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should display policy revision number', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = render();
|
||||
const policyDetailsRevElement = await renderResult.findByTestId('policyDetailsRevNo');
|
||||
expect(policyDetailsRevElement).not.toBeNull();
|
||||
expect(policyDetailsRevElement.textContent).toEqual(
|
||||
|
@ -633,7 +605,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should update the URL when policy name link is clicked', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = render();
|
||||
const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue');
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -646,7 +618,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should update the URL when policy status link is clicked', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = render();
|
||||
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -659,38 +631,39 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should display Success overall policy status', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
getMockUseEndpointDetails(HostPolicyResponseActionStatus.success);
|
||||
const renderResult = render();
|
||||
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
|
||||
expect(renderResult.getByTestId('policyStatusValue-success')).toBeTruthy();
|
||||
expect(policyStatusBadge.textContent).toEqual('Success');
|
||||
});
|
||||
|
||||
it('should display Warning overall policy status', async () => {
|
||||
mockEndpointListApi(createPolicyResponse(HostPolicyResponseActionStatus.warning));
|
||||
const renderResult = await renderAndWaitForData();
|
||||
getMockUseEndpointDetails(HostPolicyResponseActionStatus.warning);
|
||||
const renderResult = render();
|
||||
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
|
||||
expect(policyStatusBadge.textContent).toEqual('Warning');
|
||||
expect(renderResult.getByTestId('policyStatusValue-warning')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display Failed overall policy status', async () => {
|
||||
mockEndpointListApi(createPolicyResponse(HostPolicyResponseActionStatus.failure));
|
||||
const renderResult = await renderAndWaitForData();
|
||||
getMockUseEndpointDetails(HostPolicyResponseActionStatus.failure);
|
||||
const renderResult = render();
|
||||
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
|
||||
expect(policyStatusBadge.textContent).toEqual('Failed');
|
||||
expect(renderResult.getByTestId('policyStatusValue-failure')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display Unknown overall policy status', async () => {
|
||||
mockEndpointListApi(createPolicyResponse('' as HostPolicyResponseActionStatus));
|
||||
const renderResult = await renderAndWaitForData();
|
||||
getMockUseEndpointDetails('' as HostPolicyResponseActionStatus);
|
||||
const renderResult = render();
|
||||
const policyStatusBadge = await renderResult.findByTestId('policyStatusValue');
|
||||
expect(policyStatusBadge.textContent).toEqual('Unknown');
|
||||
expect(renderResult.getByTestId('policyStatusValue-')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show the Take Action button', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = render();
|
||||
expect(renderResult.getByTestId('endpointDetailsActionsButton')).not.toBeNull();
|
||||
});
|
||||
|
||||
|
@ -711,7 +684,7 @@ describe('when on the endpoint list page', () => {
|
|||
|
||||
describe('when `canReadActionsLogManagement` is TRUE', () => {
|
||||
it('should start with the activity log tab as unselected', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = await render();
|
||||
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
|
||||
const activityLogTab = renderResult.getByTestId(
|
||||
'endpoint-details-flyout-tab-activity_log'
|
||||
|
@ -724,7 +697,7 @@ describe('when on the endpoint list page', () => {
|
|||
});
|
||||
|
||||
it('should show the activity log content when selected', async () => {
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = await render();
|
||||
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
|
||||
const activityLogTab = renderResult.getByTestId(
|
||||
'endpoint-details-flyout-tab-activity_log'
|
||||
|
@ -749,7 +722,7 @@ describe('when on the endpoint list page', () => {
|
|||
canAccessFleet: true,
|
||||
},
|
||||
});
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = await render();
|
||||
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
|
||||
const activityLogTab = renderResult.queryByTestId(
|
||||
'endpoint-details-flyout-tab-activity_log'
|
||||
|
@ -774,7 +747,7 @@ describe('when on the endpoint list page', () => {
|
|||
history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=activity_log`);
|
||||
});
|
||||
|
||||
const renderResult = await renderAndWaitForData();
|
||||
const renderResult = await render();
|
||||
const detailsTab = renderResult.getByTestId('endpoint-details-flyout-tab-details');
|
||||
const activityLogTab = renderResult.queryByTestId(
|
||||
'endpoint-details-flyout-tab-activity_log'
|
||||
|
@ -796,17 +769,13 @@ describe('when on the endpoint list page', () => {
|
|||
}
|
||||
throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`);
|
||||
});
|
||||
renderResult = await renderAndWaitForData();
|
||||
renderResult = await render();
|
||||
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
reactTestingLibrary.act(() => {
|
||||
reactTestingLibrary.fireEvent.click(policyStatusLink);
|
||||
});
|
||||
await userChangedUrlChecker;
|
||||
await middlewareSpy.waitForAction('serverReturnedEndpointPolicyResponse');
|
||||
reactTestingLibrary.act(() => {
|
||||
dispatchServerReturnedEndpointPolicyResponse();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(reactTestingLibrary.cleanup);
|
||||
|
@ -876,7 +845,7 @@ describe('when on the endpoint list page', () => {
|
|||
reactTestingLibrary.act(() => {
|
||||
history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=isolate`);
|
||||
});
|
||||
renderResult = await renderAndWaitForData();
|
||||
renderResult = render();
|
||||
// Need to reset `http.post` and adjust it so that the mock for http host
|
||||
// isolation api does not output error noise to the console
|
||||
coreStart.http.post.mockReset();
|
||||
|
@ -1254,6 +1223,7 @@ describe('when on the endpoint list page', () => {
|
|||
expect(banner).toHaveTextContent(transforms[1].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endpoint list onboarding screens with RBAC', () => {
|
||||
beforeEach(() => {
|
||||
setEndpointListApiMockImplementation(coreStart.http, {
|
||||
|
@ -1314,6 +1284,7 @@ describe('when on the endpoint list page', () => {
|
|||
expect(startButton).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('endpoint list take action with RBAC controls', () => {
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
|
||||
|
|
|
@ -5,20 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, memo, useEffect, useState } from 'react';
|
||||
import React, { type CSSProperties, useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { EuiBasicTableColumn, EuiSelectableProps } from '@elastic/eui';
|
||||
import {
|
||||
EuiHorizontalRule,
|
||||
EuiBasicTable,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiHealth,
|
||||
EuiToolTip,
|
||||
EuiSuperDatePicker,
|
||||
EuiSpacer,
|
||||
type EuiBasicTableColumn,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHealth,
|
||||
EuiHorizontalRule,
|
||||
type EuiSelectableProps,
|
||||
EuiSpacer,
|
||||
EuiSuperDatePicker,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -26,85 +26,285 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { createStructuredSelector } from 'reselect';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import type {
|
||||
CreatePackagePolicyRouteState,
|
||||
AgentPolicyDetailsDeployAgentAction,
|
||||
CreatePackagePolicyRouteState,
|
||||
} from '@kbn/fleet-plugin/public';
|
||||
import { TransformFailedCallout } from './components/transform_failed_callout';
|
||||
import type { EndpointIndexUIQueryParams } from '../types';
|
||||
import { EndpointListNavLink } from './components/endpoint_list_nav_link';
|
||||
import { EndpointAgentStatus } from '../../../../common/components/endpoint/endpoint_agent_status';
|
||||
import { EndpointDetailsFlyout } from './details';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { getEndpointPendingActionsCallback } from '../store/selectors';
|
||||
import { useEndpointSelector } from './hooks';
|
||||
import { isPolicyOutOfDate } from '../utils';
|
||||
import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
|
||||
import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
|
||||
import type { CreateStructuredSelector } from '../../../../common/store';
|
||||
import type {
|
||||
Immutable,
|
||||
HostInfo,
|
||||
Immutable,
|
||||
PolicyDetailsRouteState,
|
||||
} from '../../../../../common/endpoint/types';
|
||||
import { DEFAULT_POLL_INTERVAL, MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants';
|
||||
import { PolicyEmptyState, HostsEmptyState } from '../../../components/management_empty_state';
|
||||
import { HostsEmptyState, PolicyEmptyState } from '../../../components/management_empty_state';
|
||||
import { FormattedDate } from '../../../../common/components/formatted_date';
|
||||
import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
|
||||
import { EndpointPolicyLink } from '../../../components/endpoint_policy_link';
|
||||
import { SecurityPageName } from '../../../../app/types';
|
||||
import {
|
||||
getEndpointListPath,
|
||||
getEndpointDetailsPath,
|
||||
getPoliciesPath,
|
||||
} from '../../../common/routing';
|
||||
import { getEndpointDetailsPath, getEndpointListPath } from '../../../common/routing';
|
||||
import { useFormatUrl } from '../../../../common/components/link_to';
|
||||
import { useAppUrl } from '../../../../common/lib/kibana/hooks';
|
||||
import type { EndpointAction } from '../store/action';
|
||||
import { OutOfDate } from './components/out_of_date';
|
||||
import { AdminSearchBar } from './components/search_bar';
|
||||
import { AdministrationListPage } from '../../../components/administration_list_page';
|
||||
import { LinkToApp } from '../../../../common/components/endpoint/link_to_app';
|
||||
import { TableRowActions } from './components/table_row_actions';
|
||||
import { CallOut } from '../../../../common/components/callouts';
|
||||
import { metadataTransformPrefix } from '../../../../../common/endpoint/constants';
|
||||
import { WARNING_TRANSFORM_STATES, APP_UI_ID } from '../../../../../common/constants';
|
||||
import type { BackToExternalAppButtonProps } from '../../../components/back_to_external_app_button/back_to_external_app_button';
|
||||
import { BackToExternalAppButton } from '../../../components/back_to_external_app_button/back_to_external_app_button';
|
||||
import { APP_UI_ID } from '../../../../../common/constants';
|
||||
import { ManagementEmptyStateWrapper } from '../../../components/management_empty_state_wrapper';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { getEndpointPendingActionsCallback } from '../store/selectors';
|
||||
import { BackToPolicyListButton } from './components/back_to_policy_list_button';
|
||||
|
||||
const MAX_PAGINATED_ITEM = 9999;
|
||||
const TRANSFORM_URL = '/data/transform';
|
||||
|
||||
const StyledDatePicker = styled.div`
|
||||
.euiFormControlLayout--group {
|
||||
background-color: rgba(0, 119, 204, 0.2);
|
||||
}
|
||||
`;
|
||||
const EndpointListNavLink = memo<{
|
||||
name: string;
|
||||
href: string;
|
||||
route: string;
|
||||
dataTestSubj: string;
|
||||
}>(({ name, href, route, dataTestSubj }) => {
|
||||
const clickHandler = useNavigateByRouterEventHandler(route);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink
|
||||
data-test-subj={dataTestSubj}
|
||||
className="eui-displayInline eui-textTruncate"
|
||||
href={href}
|
||||
onClick={clickHandler}
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
);
|
||||
});
|
||||
EndpointListNavLink.displayName = 'EndpointListNavLink';
|
||||
interface GetEndpointListColumnsProps {
|
||||
canReadPolicyManagement: boolean;
|
||||
backToEndpointList: PolicyDetailsRouteState['backLink'];
|
||||
getHostPendingActions: ReturnType<typeof getEndpointPendingActionsCallback>;
|
||||
queryParams: Immutable<EndpointIndexUIQueryParams>;
|
||||
search: string;
|
||||
getAppUrl: ReturnType<typeof useAppUrl>['getAppUrl'];
|
||||
}
|
||||
|
||||
const getEndpointListColumns = ({
|
||||
canReadPolicyManagement,
|
||||
backToEndpointList,
|
||||
getHostPendingActions,
|
||||
queryParams,
|
||||
search,
|
||||
getAppUrl,
|
||||
}: GetEndpointListColumnsProps): Array<EuiBasicTableColumn<Immutable<HostInfo>>> => {
|
||||
const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpoint.list.lastActive', {
|
||||
defaultMessage: 'Last active',
|
||||
});
|
||||
const padLeft: CSSProperties = { paddingLeft: '6px' };
|
||||
|
||||
return [
|
||||
{
|
||||
field: 'metadata',
|
||||
width: '15%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.hostname', {
|
||||
defaultMessage: 'Endpoint',
|
||||
}),
|
||||
render: ({ host: { hostname }, agent: { id } }: HostInfo['metadata']) => {
|
||||
const toRoutePath = getEndpointDetailsPath(
|
||||
{
|
||||
...queryParams,
|
||||
name: 'endpointDetails',
|
||||
selected_endpoint: id,
|
||||
},
|
||||
search
|
||||
);
|
||||
const toRouteUrl = getAppUrl({ path: toRoutePath });
|
||||
return (
|
||||
<EuiToolTip content={hostname} anchorClassName="eui-textTruncate">
|
||||
<EndpointListNavLink
|
||||
name={hostname}
|
||||
href={toRouteUrl}
|
||||
route={toRoutePath}
|
||||
dataTestSubj="hostnameCellLink"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'host_status',
|
||||
width: '14%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.hostStatus', {
|
||||
defaultMessage: 'Agent status',
|
||||
}),
|
||||
render: (hostStatus: HostInfo['host_status'], endpointInfo) => {
|
||||
return (
|
||||
<EndpointAgentStatus
|
||||
endpointHostInfo={endpointInfo}
|
||||
pendingActions={getHostPendingActions(endpointInfo.metadata.agent.id)}
|
||||
data-test-subj="rowHostStatus"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.Endpoint.policy.applied',
|
||||
width: '15%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.policy', {
|
||||
defaultMessage: 'Policy',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
|
||||
return (
|
||||
<>
|
||||
<EuiToolTip content={policy.name} anchorClassName="eui-textTruncate">
|
||||
{canReadPolicyManagement ? (
|
||||
<EndpointPolicyLink
|
||||
policyId={policy.id}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="policyNameCellLink"
|
||||
backLink={backToEndpointList}
|
||||
>
|
||||
{policy.name}
|
||||
</EndpointPolicyLink>
|
||||
) : (
|
||||
<>{policy.name}</>
|
||||
)}
|
||||
</EuiToolTip>
|
||||
{policy.endpoint_policy_version && (
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
style={{ whiteSpace: 'nowrap', ...padLeft }}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="policyListRevNo"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.policy.revisionNumber"
|
||||
defaultMessage="rev. {revNumber}"
|
||||
values={{ revNumber: policy.endpoint_policy_version }}
|
||||
/>
|
||||
</EuiText>
|
||||
)}
|
||||
{isPolicyOutOfDate(policy, item.policy_info) && (
|
||||
<OutOfDate style={padLeft} data-test-subj="rowPolicyOutOfDate" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.Endpoint.policy.applied',
|
||||
width: '9%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.policyStatus', {
|
||||
defaultMessage: 'Policy status',
|
||||
}),
|
||||
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
|
||||
const toRoutePath = getEndpointDetailsPath({
|
||||
name: 'endpointPolicyResponse',
|
||||
...queryParams,
|
||||
selected_endpoint: item.metadata.agent.id,
|
||||
});
|
||||
const toRouteUrl = getAppUrl({ path: toRoutePath });
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={POLICY_STATUS_TO_TEXT[policy.status]}
|
||||
anchorClassName="eui-textTruncate"
|
||||
>
|
||||
<EuiHealth
|
||||
color={POLICY_STATUS_TO_HEALTH_COLOR[policy.status]}
|
||||
className="eui-textTruncate eui-fullWidth"
|
||||
data-test-subj="rowPolicyStatus"
|
||||
>
|
||||
<EndpointListNavLink
|
||||
name={POLICY_STATUS_TO_TEXT[policy.status]}
|
||||
href={toRouteUrl}
|
||||
route={toRoutePath}
|
||||
dataTestSubj="policyStatusCellLink"
|
||||
/>
|
||||
</EuiHealth>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.host.os.name',
|
||||
width: '9%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.os', {
|
||||
defaultMessage: 'OS',
|
||||
}),
|
||||
render: (os: string) => {
|
||||
return (
|
||||
<EuiToolTip content={os} anchorClassName="eui-textTruncate">
|
||||
<EuiText size="s" className="eui-textTruncate eui-fullWidth">
|
||||
<p className="eui-displayInline eui-TextTruncate">{os}</p>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.host.ip',
|
||||
width: '12%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.ip', {
|
||||
defaultMessage: 'IP address',
|
||||
}),
|
||||
render: (ip: string[]) => {
|
||||
return (
|
||||
<EuiToolTip content={ip.toString().replace(',', ', ')} anchorClassName="eui-textTruncate">
|
||||
<EuiText size="s" className="eui-textTruncate eui-fullWidth">
|
||||
<p className="eui-displayInline eui-textTruncate">
|
||||
{ip.toString().replace(',', ', ')}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.agent.version',
|
||||
width: '9%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.endpointVersion', {
|
||||
defaultMessage: 'Version',
|
||||
}),
|
||||
render: (version: string) => {
|
||||
return (
|
||||
<EuiToolTip content={version} anchorClassName="eui-textTruncate">
|
||||
<EuiText size="s" className="eui-textTruncate eui-fullWidth">
|
||||
<p className="eui-displayInline eui-TextTruncate">{version}</p>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.@timestamp',
|
||||
name: lastActiveColumnName,
|
||||
width: '9%',
|
||||
render(dateValue: HostInfo['metadata']['@timestamp']) {
|
||||
return (
|
||||
<FormattedDate
|
||||
fieldName={lastActiveColumnName}
|
||||
value={dateValue}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: '',
|
||||
width: '8%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
render: (item: HostInfo) => {
|
||||
return <TableRowActions endpointMetadata={item.metadata} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// FIXME: this needs refactoring - we are pulling in all selectors from endpoint, which includes many more than what the list uses
|
||||
const selector = (createStructuredSelector as CreateStructuredSelector)(selectors);
|
||||
|
||||
export const EndpointList = () => {
|
||||
const history = useHistory();
|
||||
const { services } = useKibana();
|
||||
const {
|
||||
listData,
|
||||
pageIndex,
|
||||
|
@ -133,65 +333,30 @@ export const EndpointList = () => {
|
|||
} = useUserPrivileges().endpointPrivileges;
|
||||
const { search } = useFormatUrl(SecurityPageName.administration);
|
||||
const { search: searchParams } = useLocation();
|
||||
const { state: routeState = {} } = useLocation<PolicyDetailsRouteState>();
|
||||
const { getAppUrl } = useAppUrl();
|
||||
const dispatch = useDispatch<(a: EndpointAction) => void>();
|
||||
// cap ability to page at 10k records. (max_result_window)
|
||||
const maxPageCount = totalItemCount > MAX_PAGINATED_ITEM ? MAX_PAGINATED_ITEM : totalItemCount;
|
||||
const [showTransformFailedCallout, setShowTransformFailedCallout] = useState(false);
|
||||
const [shouldCheckTransforms, setShouldCheckTransforms] = useState(true);
|
||||
|
||||
const { state: routeState = {} } = useLocation<PolicyDetailsRouteState>();
|
||||
const hasPolicyData = useMemo(() => policyItems && policyItems.length > 0, [policyItems]);
|
||||
const hasListData = useMemo(() => listData && listData.length > 0, [listData]);
|
||||
|
||||
const backLinkOptions = useMemo<BackToExternalAppButtonProps>(() => {
|
||||
if (routeState?.backLink) {
|
||||
return {
|
||||
onBackButtonNavigateTo: routeState.backLink.navigateTo,
|
||||
backButtonLabel: routeState.backLink.label,
|
||||
backButtonUrl: routeState.backLink.href,
|
||||
};
|
||||
}
|
||||
const refreshStyle = useMemo(() => {
|
||||
return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 };
|
||||
}, [endpointsExist]);
|
||||
|
||||
// default back button is to the policy list
|
||||
const policyListPath = getPoliciesPath();
|
||||
const refreshIsPaused = useMemo(() => {
|
||||
return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled;
|
||||
}, [endpointsExist, hasSelectedEndpoint, isAutoRefreshEnabled]);
|
||||
|
||||
return {
|
||||
backButtonLabel: i18n.translate('xpack.securitySolution.endpoint.list.backToPolicyButton', {
|
||||
defaultMessage: 'Back to policy list',
|
||||
}),
|
||||
backButtonUrl: getAppUrl({ path: policyListPath }),
|
||||
onBackButtonNavigateTo: [
|
||||
APP_UI_ID,
|
||||
{
|
||||
path: policyListPath,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [getAppUrl, routeState?.backLink]);
|
||||
const refreshInterval = useMemo(() => {
|
||||
return !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval;
|
||||
}, [endpointsExist, autoRefreshInterval]);
|
||||
|
||||
const backToPolicyList = (
|
||||
<BackToExternalAppButton {...backLinkOptions} data-test-subj="endpointListBackLink" />
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// if no endpoint policy, skip transform check
|
||||
if (!shouldCheckTransforms || !policyItems || !policyItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'loadMetadataTransformStats' });
|
||||
setShouldCheckTransforms(false);
|
||||
}, [policyItems, shouldCheckTransforms, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasFailure = metadataTransformStats.some((transform) =>
|
||||
WARNING_TRANSFORM_STATES.has(transform?.state)
|
||||
);
|
||||
setShowTransformFailedCallout(hasFailure);
|
||||
}, [metadataTransformStats]);
|
||||
|
||||
const closeTransformFailedCallout = useCallback(() => {
|
||||
setShowTransformFailedCallout(false);
|
||||
}, []);
|
||||
const shouldShowKQLBar = useMemo(() => {
|
||||
return endpointsExist && !patternsError;
|
||||
}, [endpointsExist, patternsError]);
|
||||
|
||||
const paginationSetup = useMemo(() => {
|
||||
return {
|
||||
|
@ -279,9 +444,8 @@ export const EndpointList = () => {
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const NOOP = useCallback(() => {}, []);
|
||||
|
||||
const PAD_LEFT: React.CSSProperties = useMemo(() => ({ paddingLeft: '6px' }), []);
|
||||
// Used for an auto-refresh super date picker version without any date/time selection
|
||||
const onTimeChange = useCallback(() => {}, []);
|
||||
|
||||
const handleDeployEndpointsClick =
|
||||
useNavigateToAppEventHandler<AgentPolicyDetailsDeployAgentAction>('fleet', {
|
||||
|
@ -291,16 +455,6 @@ export const EndpointList = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const selectionOptions = useMemo<EuiSelectableProps['options']>(() => {
|
||||
return policyItems.map((item) => {
|
||||
return {
|
||||
key: item.policy_id,
|
||||
label: item.name,
|
||||
checked: selectedPolicyId === item.policy_id ? 'on' : undefined,
|
||||
};
|
||||
});
|
||||
}, [policyItems, selectedPolicyId]);
|
||||
|
||||
const handleSelectableOnChange = useCallback<(o: EuiSelectableProps['options']) => void>(
|
||||
(changedOptions) => {
|
||||
return changedOptions.some((option) => {
|
||||
|
@ -326,227 +480,25 @@ export const EndpointList = () => {
|
|||
};
|
||||
}, []);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<Immutable<HostInfo>>> = useMemo(() => {
|
||||
const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpoint.list.lastActive', {
|
||||
defaultMessage: 'Last active',
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
field: 'metadata',
|
||||
width: '15%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.hostname', {
|
||||
defaultMessage: 'Endpoint',
|
||||
}),
|
||||
render: ({ host: { hostname }, agent: { id } }: HostInfo['metadata']) => {
|
||||
const toRoutePath = getEndpointDetailsPath(
|
||||
{
|
||||
...queryParams,
|
||||
name: 'endpointDetails',
|
||||
selected_endpoint: id,
|
||||
},
|
||||
search
|
||||
);
|
||||
const toRouteUrl = getAppUrl({ path: toRoutePath });
|
||||
return (
|
||||
<EuiToolTip content={hostname} anchorClassName="eui-textTruncate">
|
||||
<EndpointListNavLink
|
||||
name={hostname}
|
||||
href={toRouteUrl}
|
||||
route={toRoutePath}
|
||||
dataTestSubj="hostnameCellLink"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'host_status',
|
||||
width: '14%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.hostStatus', {
|
||||
defaultMessage: 'Agent status',
|
||||
}),
|
||||
render: (hostStatus: HostInfo['host_status'], endpointInfo) => {
|
||||
return (
|
||||
<EndpointAgentStatus
|
||||
endpointHostInfo={endpointInfo}
|
||||
pendingActions={getHostPendingActions(endpointInfo.metadata.agent.id)}
|
||||
data-test-subj="rowHostStatus"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.Endpoint.policy.applied',
|
||||
width: '15%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.policy', {
|
||||
defaultMessage: 'Policy',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
|
||||
return (
|
||||
<>
|
||||
<EuiToolTip content={policy.name} anchorClassName="eui-textTruncate">
|
||||
{canReadPolicyManagement ? (
|
||||
<EndpointPolicyLink
|
||||
policyId={policy.id}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="policyNameCellLink"
|
||||
backLink={backToEndpointList}
|
||||
>
|
||||
{policy.name}
|
||||
</EndpointPolicyLink>
|
||||
) : (
|
||||
<>{policy.name}</>
|
||||
)}
|
||||
</EuiToolTip>
|
||||
{policy.endpoint_policy_version && (
|
||||
<EuiText
|
||||
color="subdued"
|
||||
size="xs"
|
||||
style={{ whiteSpace: 'nowrap', ...PAD_LEFT }}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="policyListRevNo"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.policy.revisionNumber"
|
||||
defaultMessage="rev. {revNumber}"
|
||||
values={{ revNumber: policy.endpoint_policy_version }}
|
||||
/>
|
||||
</EuiText>
|
||||
)}
|
||||
{isPolicyOutOfDate(policy, item.policy_info) && (
|
||||
<OutOfDate style={PAD_LEFT} data-test-subj="rowPolicyOutOfDate" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.Endpoint.policy.applied',
|
||||
width: '9%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.policyStatus', {
|
||||
defaultMessage: 'Policy status',
|
||||
}),
|
||||
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
|
||||
const toRoutePath = getEndpointDetailsPath({
|
||||
name: 'endpointPolicyResponse',
|
||||
...queryParams,
|
||||
selected_endpoint: item.metadata.agent.id,
|
||||
});
|
||||
const toRouteUrl = getAppUrl({ path: toRoutePath });
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={POLICY_STATUS_TO_TEXT[policy.status]}
|
||||
anchorClassName="eui-textTruncate"
|
||||
>
|
||||
<EuiHealth
|
||||
color={POLICY_STATUS_TO_HEALTH_COLOR[policy.status]}
|
||||
className="eui-textTruncate eui-fullWidth"
|
||||
data-test-subj="rowPolicyStatus"
|
||||
>
|
||||
<EndpointListNavLink
|
||||
name={POLICY_STATUS_TO_TEXT[policy.status]}
|
||||
href={toRouteUrl}
|
||||
route={toRoutePath}
|
||||
dataTestSubj="policyStatusCellLink"
|
||||
/>
|
||||
</EuiHealth>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.host.os.name',
|
||||
width: '9%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.os', {
|
||||
defaultMessage: 'OS',
|
||||
}),
|
||||
render: (os: string) => {
|
||||
return (
|
||||
<EuiToolTip content={os} anchorClassName="eui-textTruncate">
|
||||
<EuiText size="s" className="eui-textTruncate eui-fullWidth">
|
||||
<p className="eui-displayInline eui-TextTruncate">{os}</p>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.host.ip',
|
||||
width: '12%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.ip', {
|
||||
defaultMessage: 'IP address',
|
||||
}),
|
||||
render: (ip: string[]) => {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={ip.toString().replace(',', ', ')}
|
||||
anchorClassName="eui-textTruncate"
|
||||
>
|
||||
<EuiText size="s" className="eui-textTruncate eui-fullWidth">
|
||||
<p className="eui-displayInline eui-textTruncate">
|
||||
{ip.toString().replace(',', ', ')}
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.agent.version',
|
||||
width: '9%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.endpointVersion', {
|
||||
defaultMessage: 'Version',
|
||||
}),
|
||||
render: (version: string) => {
|
||||
return (
|
||||
<EuiToolTip content={version} anchorClassName="eui-textTruncate">
|
||||
<EuiText size="s" className="eui-textTruncate eui-fullWidth">
|
||||
<p className="eui-displayInline eui-TextTruncate">{version}</p>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'metadata.@timestamp',
|
||||
name: lastActiveColumnName,
|
||||
width: '9%',
|
||||
render(dateValue: HostInfo['metadata']['@timestamp']) {
|
||||
return (
|
||||
<FormattedDate
|
||||
fieldName={lastActiveColumnName}
|
||||
value={dateValue}
|
||||
className="eui-textTruncate"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: '',
|
||||
width: '8%',
|
||||
name: i18n.translate('xpack.securitySolution.endpoint.list.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions: [
|
||||
{
|
||||
render: (item: HostInfo) => {
|
||||
return <TableRowActions endpointMetadata={item.metadata} />;
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}, [
|
||||
queryParams,
|
||||
search,
|
||||
getAppUrl,
|
||||
getHostPendingActions,
|
||||
canReadPolicyManagement,
|
||||
backToEndpointList,
|
||||
PAD_LEFT,
|
||||
]);
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getEndpointListColumns({
|
||||
canReadPolicyManagement,
|
||||
backToEndpointList,
|
||||
getAppUrl,
|
||||
getHostPendingActions,
|
||||
queryParams,
|
||||
search,
|
||||
}),
|
||||
[
|
||||
backToEndpointList,
|
||||
canReadPolicyManagement,
|
||||
getAppUrl,
|
||||
getHostPendingActions,
|
||||
queryParams,
|
||||
search,
|
||||
]
|
||||
);
|
||||
|
||||
const renderTableOrEmptyState = useMemo(() => {
|
||||
if (endpointsExist) {
|
||||
|
@ -568,12 +520,19 @@ export const EndpointList = () => {
|
|||
<PolicyEmptyState loading={endpointPrivilegesLoading} />
|
||||
</ManagementEmptyStateWrapper>
|
||||
);
|
||||
} else if (!policyItemsLoading && policyItems && policyItems.length > 0) {
|
||||
} else if (!policyItemsLoading && hasPolicyData) {
|
||||
const selectionOptions: EuiSelectableProps['options'] = policyItems.map((item) => {
|
||||
return {
|
||||
key: item.policy_id,
|
||||
label: item.name,
|
||||
checked: selectedPolicyId === item.policy_id ? 'on' : undefined,
|
||||
};
|
||||
});
|
||||
return (
|
||||
<HostsEmptyState
|
||||
loading={loading}
|
||||
onActionClick={handleDeployEndpointsClick}
|
||||
actionDisabled={selectedPolicyId === undefined}
|
||||
actionDisabled={!selectedPolicyId}
|
||||
handleSelectableOnChange={handleSelectableOnChange}
|
||||
selectionOptions={selectionOptions}
|
||||
/>
|
||||
|
@ -586,118 +545,26 @@ export const EndpointList = () => {
|
|||
);
|
||||
}
|
||||
}, [
|
||||
endpointsExist,
|
||||
policyItemsLoading,
|
||||
policyItems,
|
||||
listData,
|
||||
columns,
|
||||
listError?.message,
|
||||
paginationSetup,
|
||||
onTableChange,
|
||||
loading,
|
||||
setTableRowProps,
|
||||
handleDeployEndpointsClick,
|
||||
selectedPolicyId,
|
||||
handleSelectableOnChange,
|
||||
selectionOptions,
|
||||
handleCreatePolicyClick,
|
||||
canAccessFleet,
|
||||
canReadEndpointList,
|
||||
columns,
|
||||
endpointsExist,
|
||||
endpointPrivilegesLoading,
|
||||
handleCreatePolicyClick,
|
||||
handleDeployEndpointsClick,
|
||||
handleSelectableOnChange,
|
||||
hasPolicyData,
|
||||
listData,
|
||||
listError?.message,
|
||||
loading,
|
||||
onTableChange,
|
||||
paginationSetup,
|
||||
policyItemsLoading,
|
||||
policyItems,
|
||||
selectedPolicyId,
|
||||
setTableRowProps,
|
||||
]);
|
||||
|
||||
const hasListData = listData && listData.length > 0;
|
||||
|
||||
const refreshStyle = useMemo(() => {
|
||||
return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 };
|
||||
}, [endpointsExist]);
|
||||
|
||||
const refreshIsPaused = useMemo(() => {
|
||||
return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled;
|
||||
}, [endpointsExist, hasSelectedEndpoint, isAutoRefreshEnabled]);
|
||||
|
||||
const refreshInterval = useMemo(() => {
|
||||
return !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval;
|
||||
}, [endpointsExist, autoRefreshInterval]);
|
||||
|
||||
const shouldShowKQLBar = useMemo(() => {
|
||||
return endpointsExist && !patternsError;
|
||||
}, [endpointsExist, patternsError]);
|
||||
|
||||
const transformFailedCalloutDescription = useMemo(() => {
|
||||
const failingTransformIds = metadataTransformStats
|
||||
.filter((transformStat) => WARNING_TRANSFORM_STATES.has(transformStat.state))
|
||||
.map((transformStat) => transformStat.id)
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.transformFailed.message"
|
||||
defaultMessage="A required transform, {transformId}, is currently failing. Most of the time this can be fixed by {transformsPage}. For additional help, please visit the {docsPage}"
|
||||
values={{
|
||||
transformId: failingTransformIds || metadataTransformPrefix,
|
||||
transformsPage: (
|
||||
<LinkToApp
|
||||
data-test-subj="failed-transform-restart-link"
|
||||
appId="management"
|
||||
appPath={TRANSFORM_URL}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.transformFailed.restartLink"
|
||||
defaultMessage="restarting the transform"
|
||||
/>
|
||||
</LinkToApp>
|
||||
),
|
||||
docsPage: (
|
||||
<EuiLink
|
||||
data-test-subj="failed-transform-docs-link"
|
||||
href={services.docLinks.links.endpoints.troubleshooting}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.list.transformFailed.docsLink"
|
||||
defaultMessage="troubleshooting documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}, [metadataTransformStats, services.docLinks.links.endpoints.troubleshooting]);
|
||||
|
||||
const transformFailedCallout = useMemo(() => {
|
||||
if (!showTransformFailedCallout) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CallOut
|
||||
message={{
|
||||
id: 'endpoints-list-transform-failed',
|
||||
type: 'warning',
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.list.transformFailed.title', {
|
||||
defaultMessage: 'Required transform failed',
|
||||
}),
|
||||
description: transformFailedCalloutDescription,
|
||||
}}
|
||||
dismissButtonText={i18n.translate(
|
||||
'xpack.securitySolution.endpoint.list.transformFailed.dismiss',
|
||||
{
|
||||
defaultMessage: 'Dismiss',
|
||||
}
|
||||
)}
|
||||
onDismiss={closeTransformFailedCallout}
|
||||
showDismissButton={true}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
}, [showTransformFailedCallout, closeTransformFailedCallout, transformFailedCalloutDescription]);
|
||||
|
||||
return (
|
||||
<AdministrationListPage
|
||||
data-test-subj="endpointPage"
|
||||
|
@ -714,11 +581,14 @@ export const EndpointList = () => {
|
|||
defaultMessage="Hosts running Elastic Defend"
|
||||
/>
|
||||
}
|
||||
headerBackComponent={routeState.backLink && backToPolicyList}
|
||||
headerBackComponent={<BackToPolicyListButton backLink={routeState.backLink} />}
|
||||
>
|
||||
{hasSelectedEndpoint && <EndpointDetailsFlyout />}
|
||||
<>
|
||||
{transformFailedCallout}
|
||||
<TransformFailedCallout
|
||||
metadataTransformStats={metadataTransformStats}
|
||||
hasNoPolicyData={!hasPolicyData}
|
||||
/>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{shouldShowKQLBar && (
|
||||
<EuiFlexItem>
|
||||
|
@ -729,7 +599,7 @@ export const EndpointList = () => {
|
|||
<StyledDatePicker>
|
||||
<EuiSuperDatePicker
|
||||
className="endpointListDatePicker"
|
||||
onTimeChange={NOOP}
|
||||
onTimeChange={onTimeChange}
|
||||
isDisabled={hasSelectedEndpoint}
|
||||
onRefresh={onRefresh}
|
||||
isPaused={refreshIsPaused}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue