mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Endpoint] Can delete policies (#68567)
This commit is contained in:
parent
1b85b40220
commit
ccf8def829
13 changed files with 497 additions and 56 deletions
|
@ -2,11 +2,11 @@
|
|||
|
||||
exports[`PageView component should display body header custom element 1`] = `
|
||||
.c0.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 70px 0 24px;
|
||||
}
|
||||
|
||||
.c0.endpoint--isListView .endpoint-header {
|
||||
padding: 24px;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -97,11 +97,11 @@ exports[`PageView component should display body header custom element 1`] = `
|
|||
|
||||
exports[`PageView component should display body header wrapped in EuiTitle 1`] = `
|
||||
.c0.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 70px 0 24px;
|
||||
}
|
||||
|
||||
.c0.endpoint--isListView .endpoint-header {
|
||||
padding: 24px;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -195,11 +195,11 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
|
|||
|
||||
exports[`PageView component should display header left and right 1`] = `
|
||||
.c0.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 70px 0 24px;
|
||||
}
|
||||
|
||||
.c0.endpoint--isListView .endpoint-header {
|
||||
padding: 24px;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -308,11 +308,11 @@ exports[`PageView component should display header left and right 1`] = `
|
|||
|
||||
exports[`PageView component should display only body if not header props used 1`] = `
|
||||
.c0.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 70px 0 24px;
|
||||
}
|
||||
|
||||
.c0.endpoint--isListView .endpoint-header {
|
||||
padding: 24px;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -380,11 +380,11 @@ exports[`PageView component should display only body if not header props used 1`
|
|||
|
||||
exports[`PageView component should display only header left 1`] = `
|
||||
.c0.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 70px 0 24px;
|
||||
}
|
||||
|
||||
.c0.endpoint--isListView .endpoint-header {
|
||||
padding: 24px;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -482,11 +482,11 @@ exports[`PageView component should display only header left 1`] = `
|
|||
|
||||
exports[`PageView component should display only header right but include an empty left side 1`] = `
|
||||
.c0.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 70px 0 24px;
|
||||
}
|
||||
|
||||
.c0.endpoint--isListView .endpoint-header {
|
||||
padding: 24px;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -581,11 +581,11 @@ exports[`PageView component should display only header right but include an empt
|
|||
|
||||
exports[`PageView component should pass through EuiPage props 1`] = `
|
||||
.c0.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 70px 0 24px;
|
||||
}
|
||||
|
||||
.c0.endpoint--isListView .endpoint-header {
|
||||
padding: 24px;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -670,11 +670,11 @@ exports[`PageView component should pass through EuiPage props 1`] = `
|
|||
|
||||
exports[`PageView component should use custom element for header left and not wrap in EuiTitle 1`] = `
|
||||
.c0.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 70px 0 24px;
|
||||
}
|
||||
|
||||
.c0.endpoint--isListView .endpoint-header {
|
||||
padding: 24px;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,13 +21,14 @@ import {
|
|||
import React, { memo, MouseEventHandler, ReactNode, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
|
||||
import { gutterTimeline } from '../../lib/helpers';
|
||||
|
||||
const StyledEuiPage = styled(EuiPage)`
|
||||
&.endpoint--isListView {
|
||||
padding: 0;
|
||||
padding: 0 ${gutterTimeline} 0 ${(props) => props.theme.eui.euiSizeL};
|
||||
|
||||
.endpoint-header {
|
||||
padding: ${(props) => props.theme.eui.euiSizeL};
|
||||
padding: ${(props) => props.theme.eui.euiSizeL} 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.endpoint-page-content {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { PolicyData } from '../../../../../../common/endpoint/types';
|
||||
import { ServerApiError } from '../../../../../common/types';
|
||||
import { GetAgentStatusResponse } from '../../../../../../../ingest_manager/common/types/rest_spec';
|
||||
|
||||
interface ServerReturnedPolicyListData {
|
||||
type: 'serverReturnedPolicyListData';
|
||||
|
@ -22,4 +23,42 @@ interface ServerFailedToReturnPolicyListData {
|
|||
payload: ServerApiError;
|
||||
}
|
||||
|
||||
export type PolicyListAction = ServerReturnedPolicyListData | ServerFailedToReturnPolicyListData;
|
||||
interface UserClickedPolicyListDeleteButton {
|
||||
type: 'userClickedPolicyListDeleteButton';
|
||||
payload: { policyId: string };
|
||||
}
|
||||
|
||||
interface UserOpenedPolicyListDeleteModal {
|
||||
type: 'userOpenedPolicyListDeleteModal';
|
||||
payload: { agentConfigId: string };
|
||||
}
|
||||
|
||||
interface ServerDeletedPolicyFailure {
|
||||
type: 'serverDeletedPolicyFailure';
|
||||
payload: ServerApiError;
|
||||
}
|
||||
|
||||
interface ServerDeletedPolicy {
|
||||
type: 'serverDeletedPolicy';
|
||||
payload: { id: string; success: boolean };
|
||||
}
|
||||
|
||||
interface ServerReturnedPolicyAgentsSummaryForDeleteFailure {
|
||||
type: 'serverReturnedPolicyAgentsSummaryForDeleteFailure';
|
||||
payload: ServerApiError;
|
||||
}
|
||||
|
||||
interface ServerReturnedPolicyAgentsSummaryForDelete {
|
||||
type: 'serverReturnedPolicyAgentsSummaryForDelete';
|
||||
payload: { agentStatusSummary: GetAgentStatusResponse['results'] };
|
||||
}
|
||||
|
||||
export type PolicyListAction =
|
||||
| ServerReturnedPolicyListData
|
||||
| ServerFailedToReturnPolicyListData
|
||||
| UserClickedPolicyListDeleteButton
|
||||
| ServerDeletedPolicyFailure
|
||||
| ServerDeletedPolicy
|
||||
| UserOpenedPolicyListDeleteModal
|
||||
| ServerReturnedPolicyAgentsSummaryForDeleteFailure
|
||||
| ServerReturnedPolicyAgentsSummaryForDelete;
|
||||
|
|
|
@ -13,7 +13,12 @@ import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../ingest_manage
|
|||
import { policyListReducer } from './reducer';
|
||||
import { policyListMiddlewareFactory } from './middleware';
|
||||
|
||||
import { isOnPolicyListPage, selectIsLoading, urlSearchParams } from './selectors';
|
||||
import {
|
||||
isOnPolicyListPage,
|
||||
selectIsLoading,
|
||||
urlSearchParams,
|
||||
selectIsDeleting,
|
||||
} from './selectors';
|
||||
import { DepsStartMock, depsStartMock } from '../../../../../common/mock/endpoint';
|
||||
import { setPolicyListApiMockImplementation } from './test_mock_utils';
|
||||
import { INGEST_API_DATASOURCES } from './services/ingest';
|
||||
|
@ -85,6 +90,33 @@ describe('policy list store concerns', () => {
|
|||
expect(selectIsLoading(store.getState())).toBe(false);
|
||||
});
|
||||
|
||||
it('it sets `isDeleting` when `userClickedPolicyListDeleteButton`', async () => {
|
||||
expect(selectIsDeleting(store.getState())).toBe(false);
|
||||
store.dispatch({
|
||||
type: 'userClickedPolicyListDeleteButton',
|
||||
payload: {
|
||||
policyId: '123',
|
||||
},
|
||||
});
|
||||
expect(selectIsDeleting(store.getState())).toBe(true);
|
||||
await waitForAction('serverDeletedPolicy');
|
||||
expect(selectIsDeleting(store.getState())).toBe(false);
|
||||
});
|
||||
|
||||
it('it sets refreshes policy data when `serverDeletedPolicy`', async () => {
|
||||
expect(selectIsLoading(store.getState())).toBe(false);
|
||||
store.dispatch({
|
||||
type: 'serverDeletedPolicy',
|
||||
payload: {
|
||||
policyId: '',
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
expect(selectIsLoading(store.getState())).toBe(true);
|
||||
await waitForAction('serverReturnedPolicyListData');
|
||||
expect(selectIsLoading(store.getState())).toBe(false);
|
||||
});
|
||||
|
||||
it('it resets state on `userChangedUrl` and pathname is NOT `/policy`', async () => {
|
||||
store.dispatch({
|
||||
type: 'userChangedUrl',
|
||||
|
@ -108,9 +140,18 @@ describe('policy list store concerns', () => {
|
|||
location: undefined,
|
||||
policyItems: [],
|
||||
isLoading: false,
|
||||
isDeleting: false,
|
||||
deleteStatus: undefined,
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
agentStatusSummary: {
|
||||
error: 0,
|
||||
events: 0,
|
||||
offline: 0,
|
||||
online: 0,
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
it('uses default pagination params when not included in url', async () => {
|
||||
|
|
|
@ -5,10 +5,19 @@
|
|||
*/
|
||||
|
||||
import { GetPolicyListResponse, PolicyListState } from '../../types';
|
||||
import { sendGetEndpointSpecificDatasources } from './services/ingest';
|
||||
import {
|
||||
sendGetEndpointSpecificDatasources,
|
||||
sendDeleteDatasource,
|
||||
sendGetFleetAgentStatusForConfig,
|
||||
} from './services/ingest';
|
||||
import { isOnPolicyListPage, urlSearchParams } from './selectors';
|
||||
import { ImmutableMiddlewareFactory } from '../../../../../common/store';
|
||||
import { initialPolicyListState } from './reducer';
|
||||
import {
|
||||
DeleteDatasourcesResponse,
|
||||
DeleteDatasourcesRequest,
|
||||
GetAgentStatusResponse,
|
||||
} from '../../../../../../../ingest_manager/common';
|
||||
|
||||
export const policyListMiddlewareFactory: ImmutableMiddlewareFactory<PolicyListState> = (
|
||||
coreStart
|
||||
|
@ -19,7 +28,10 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory<PolicyListS
|
|||
next(action);
|
||||
|
||||
const state = getState();
|
||||
if (action.type === 'userChangedUrl' && isOnPolicyListPage(state)) {
|
||||
if (
|
||||
(action.type === 'userChangedUrl' && isOnPolicyListPage(state)) ||
|
||||
action.type === 'serverDeletedPolicy'
|
||||
) {
|
||||
const { page_index: pageIndex, page_size: pageSize } = urlSearchParams(state);
|
||||
let response: GetPolicyListResponse;
|
||||
|
||||
|
@ -47,6 +59,45 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory<PolicyListS
|
|||
total: response ? response.total : initialPolicyListState().total,
|
||||
},
|
||||
});
|
||||
} else if (action.type === 'userClickedPolicyListDeleteButton') {
|
||||
const { policyId } = action.payload;
|
||||
const datasourceIds: DeleteDatasourcesRequest['body']['datasourceIds'] = [policyId];
|
||||
let apiResponse: DeleteDatasourcesResponse;
|
||||
try {
|
||||
apiResponse = await sendDeleteDatasource(http, { body: { datasourceIds } });
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: 'serverDeletedPolicyFailure',
|
||||
payload: err.body ?? err,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'serverDeletedPolicy',
|
||||
payload: {
|
||||
id: apiResponse ? apiResponse[0].id : '',
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
} else if (action.type === 'userOpenedPolicyListDeleteModal') {
|
||||
const { agentConfigId } = action.payload;
|
||||
let apiResponse: GetAgentStatusResponse;
|
||||
try {
|
||||
apiResponse = await sendGetFleetAgentStatusForConfig(http, agentConfigId);
|
||||
} catch (err) {
|
||||
dispatch({
|
||||
type: 'serverReturnedPolicyAgentsSummaryForDeleteFailure',
|
||||
payload: err.body ?? err,
|
||||
});
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
type: 'serverReturnedPolicyAgentsSummaryForDelete',
|
||||
payload: {
|
||||
agentStatusSummary: apiResponse.results,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,11 +17,20 @@ import { PolicyListState } from '../../types';
|
|||
export const initialPolicyListState: () => Immutable<PolicyListState> = () => ({
|
||||
policyItems: [],
|
||||
isLoading: false,
|
||||
isDeleting: false,
|
||||
deleteStatus: undefined,
|
||||
apiError: undefined,
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
location: undefined,
|
||||
agentStatusSummary: {
|
||||
error: 0,
|
||||
events: 0,
|
||||
offline: 0,
|
||||
online: 0,
|
||||
total: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export const policyListReducer: ImmutableReducer<PolicyListState, AppAction> = (
|
||||
|
@ -33,6 +42,7 @@ export const policyListReducer: ImmutableReducer<PolicyListState, AppAction> = (
|
|||
...state,
|
||||
...action.payload,
|
||||
isLoading: false,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -41,6 +51,47 @@ export const policyListReducer: ImmutableReducer<PolicyListState, AppAction> = (
|
|||
...state,
|
||||
apiError: action.payload,
|
||||
isLoading: false,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'serverDeletedPolicyFailure') {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
isLoading: false,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'serverDeletedPolicy') {
|
||||
return {
|
||||
...state,
|
||||
deleteStatus: action.payload.success,
|
||||
isLoading: true,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'userClickedPolicyListDeleteButton') {
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
isDeleting: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'serverReturnedPolicyAgentsSummaryForDelete') {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'serverReturnedPolicyAgentsSummaryForDeleteFailure') {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -60,6 +111,7 @@ export const policyListReducer: ImmutableReducer<PolicyListState, AppAction> = (
|
|||
...newState,
|
||||
apiError: undefined,
|
||||
isLoading: true,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
return newState;
|
||||
|
|
|
@ -25,6 +25,13 @@ export const selectIsLoading = (state: Immutable<PolicyListState>) => state.isLo
|
|||
|
||||
export const selectApiError = (state: Immutable<PolicyListState>) => state.apiError;
|
||||
|
||||
export const selectIsDeleting = (state: Immutable<PolicyListState>) => state.isDeleting;
|
||||
|
||||
export const selectDeleteStatus = (state: Immutable<PolicyListState>) => state.deleteStatus;
|
||||
|
||||
export const selectAgentStatusSummary = (state: Immutable<PolicyListState>) =>
|
||||
state.agentStatusSummary;
|
||||
|
||||
export const isOnPolicyListPage = (state: Immutable<PolicyListState>) => {
|
||||
return (
|
||||
matchPath(state.location?.pathname ?? '', {
|
||||
|
|
|
@ -8,6 +8,8 @@ import { HttpFetchOptions, HttpStart } from 'kibana/public';
|
|||
import {
|
||||
GetDatasourcesRequest,
|
||||
GetAgentStatusResponse,
|
||||
DeleteDatasourcesResponse,
|
||||
DeleteDatasourcesRequest,
|
||||
DATASOURCE_SAVED_OBJECT_TYPE,
|
||||
} from '../../../../../../../../ingest_manager/common';
|
||||
import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types';
|
||||
|
@ -17,6 +19,7 @@ const INGEST_API_ROOT = `/api/ingest_manager`;
|
|||
export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`;
|
||||
const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`;
|
||||
const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`;
|
||||
const INGEST_API_DELETE_DATASOURCE = `${INGEST_API_DATASOURCES}/delete`;
|
||||
|
||||
/**
|
||||
* Retrieves a list of endpoint specific datasources (those created with a `package.name` of
|
||||
|
@ -53,6 +56,23 @@ export const sendGetDatasource = (
|
|||
return http.get<GetPolicyResponse>(`${INGEST_API_DATASOURCES}/${datasourceId}`, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a single datasource based on ID from ingest
|
||||
* @param http
|
||||
* @param datasourceId
|
||||
* @param options
|
||||
*/
|
||||
export const sendDeleteDatasource = (
|
||||
http: HttpStart,
|
||||
body: DeleteDatasourcesRequest,
|
||||
options?: HttpFetchOptions
|
||||
) => {
|
||||
return http.post<DeleteDatasourcesResponse>(INGEST_API_DELETE_DATASOURCE, {
|
||||
...options,
|
||||
body: JSON.stringify(body.body),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates a datasources
|
||||
*
|
||||
|
|
|
@ -37,6 +37,12 @@ export interface PolicyListState {
|
|||
isLoading: boolean;
|
||||
/** current location information */
|
||||
location?: Immutable<AppLocation>;
|
||||
/** policy is being deleted */
|
||||
isDeleting: boolean;
|
||||
/** Deletion status */
|
||||
deleteStatus?: boolean;
|
||||
/** A summary of stats for the agents associated with a given Fleet Agent Configuration */
|
||||
agentStatusSummary?: GetAgentStatusResponse['results'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,12 +17,17 @@ import {
|
|||
EuiContextMenuItem,
|
||||
EuiButtonIcon,
|
||||
EuiContextMenuPanel,
|
||||
EuiOverlayMask,
|
||||
EuiConfirmModal,
|
||||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import styled from 'styled-components';
|
||||
import { CreateStructuredSelector } from '../../../../common/store';
|
||||
import * as selectors from '../store/policy_list/selectors';
|
||||
import { usePolicyListSelector } from './policy_hooks';
|
||||
|
@ -52,6 +57,10 @@ const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({
|
|||
whiteSpace: 'nowrap',
|
||||
});
|
||||
|
||||
const DangerEuiContextMenuItem = styled(EuiContextMenuItem)`
|
||||
color: ${(props) => props.theme.eui.textColors.danger};
|
||||
`;
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['items'] }>(
|
||||
({ items }) => {
|
||||
|
@ -63,8 +72,10 @@ export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['ite
|
|||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
data-test-subj="policyActions"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
data-test-subj="policyActionsButton"
|
||||
iconType="boxesHorizontal"
|
||||
onClick={handleToggleMenu}
|
||||
aria-label={i18n.translate('xpack.securitySolution.endpoint.policyList.actionMenu', {
|
||||
|
@ -75,7 +86,7 @@ export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['ite
|
|||
isOpen={isOpen}
|
||||
closePopover={handleCloseMenu}
|
||||
>
|
||||
<EuiContextMenuPanel items={items} />
|
||||
<EuiContextMenuPanel items={items} data-test-subj="policyActionsMenu" />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
@ -106,6 +117,9 @@ export const PolicyList = React.memo(() => {
|
|||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const [showDelete, setShowDelete] = useState<boolean>(false);
|
||||
const [policyIdToDelete, setPolicyIdToDelete] = useState<string>('');
|
||||
|
||||
const dispatch = useDispatch<(action: PolicyListAction) => void>();
|
||||
const {
|
||||
selectPolicyItems: policyItems,
|
||||
|
@ -114,6 +128,9 @@ export const PolicyList = React.memo(() => {
|
|||
selectTotal: totalItemCount,
|
||||
selectIsLoading: loading,
|
||||
selectApiError: apiError,
|
||||
selectIsDeleting: isDeleting,
|
||||
selectDeleteStatus: deleteStatus,
|
||||
selectAgentStatusSummary: agentStatusSummary,
|
||||
} = usePolicyListSelector(selector);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -126,6 +143,38 @@ export const PolicyList = React.memo(() => {
|
|||
}
|
||||
}, [apiError, dispatch, notifications.toasts]);
|
||||
|
||||
// Handle showing update statuses
|
||||
useEffect(() => {
|
||||
if (deleteStatus !== undefined) {
|
||||
if (deleteStatus === true) {
|
||||
setPolicyIdToDelete('');
|
||||
setShowDelete(false);
|
||||
notifications.toasts.success({
|
||||
toastLifeTimeMs: 10000,
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteSuccessToast', {
|
||||
defaultMessage: 'Success!',
|
||||
}),
|
||||
body: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.deleteSuccessToastDetails"
|
||||
defaultMessage="Policy has been deleted."
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
notifications.toasts.danger({
|
||||
toastLifeTimeMs: 10000,
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteFailedToast', {
|
||||
defaultMessage: 'Failed!',
|
||||
}),
|
||||
body: i18n.translate('xpack.securitySolution.endpoint.policyList.deleteFailedToastBody', {
|
||||
defaultMessage: 'Failed to delete policy',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [notifications.toasts, deleteStatus]);
|
||||
|
||||
const paginationSetup = useMemo(() => {
|
||||
return {
|
||||
pageIndex,
|
||||
|
@ -143,6 +192,36 @@ export const PolicyList = React.memo(() => {
|
|||
[history, location.pathname]
|
||||
);
|
||||
|
||||
const handleDeleteOnClick = useCallback(
|
||||
({ policyId, agentConfigId }: { policyId: string; agentConfigId: string }) => {
|
||||
dispatch({
|
||||
type: 'userOpenedPolicyListDeleteModal',
|
||||
payload: {
|
||||
agentConfigId,
|
||||
},
|
||||
});
|
||||
setPolicyIdToDelete(policyId);
|
||||
setShowDelete(true);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleDeleteConfirmation = useCallback(
|
||||
({ policyId }: { policyId: string }) => {
|
||||
dispatch({
|
||||
type: 'userClickedPolicyListDeleteButton',
|
||||
payload: {
|
||||
policyId,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleDeleteCancel = useCallback(() => {
|
||||
setShowDelete(false);
|
||||
}, []);
|
||||
|
||||
const columns: Array<EuiTableFieldDataColumnType<Immutable<PolicyData>>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
|
@ -248,6 +327,19 @@ export const PolicyList = React.memo(() => {
|
|||
/>
|
||||
</LinkToApp>
|
||||
</EuiContextMenuItem>,
|
||||
<DangerEuiContextMenuItem
|
||||
data-test-subj="policyDeleteButton"
|
||||
icon="trash"
|
||||
key="policyDeletAction"
|
||||
onClick={() => {
|
||||
handleDeleteOnClick({ agentConfigId: item.config_id, policyId: item.id });
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.policyDeleteAction"
|
||||
defaultMessage="Delete Policy"
|
||||
/>
|
||||
</DangerEuiContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
@ -256,38 +348,122 @@ export const PolicyList = React.memo(() => {
|
|||
],
|
||||
},
|
||||
],
|
||||
[services.application]
|
||||
[services.application, handleDeleteOnClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<ManagementPageView
|
||||
viewType="list"
|
||||
data-test-subj="policyListPage"
|
||||
headerLeft={i18n.translate('xpack.securitySolution.endpoint.policyList.viewTitle', {
|
||||
defaultMessage: 'Policies',
|
||||
})}
|
||||
bodyHeader={
|
||||
<EuiText color="subdued" data-test-subj="policyTotalCount">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.viewTitleTotalCount"
|
||||
defaultMessage="{totalItemCount, plural, one {# Policy} other {# Policies}}"
|
||||
values={{ totalItemCount }}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiBasicTable
|
||||
items={useMemo(() => [...policyItems], [policyItems])}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
pagination={paginationSetup}
|
||||
onChange={handleTableChange}
|
||||
data-test-subj="policyTable"
|
||||
hasActions={false}
|
||||
/>
|
||||
<SpyRoute />
|
||||
</ManagementPageView>
|
||||
<>
|
||||
{showDelete && (
|
||||
<ConfirmDelete
|
||||
hostCount={agentStatusSummary ? agentStatusSummary.total : 0}
|
||||
onCancel={handleDeleteCancel}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={() => {
|
||||
handleDeleteConfirmation({ policyId: policyIdToDelete });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ManagementPageView
|
||||
viewType="list"
|
||||
data-test-subj="policyListPage"
|
||||
headerLeft={i18n.translate('xpack.securitySolution.endpoint.policyList.viewTitle', {
|
||||
defaultMessage: 'Policies',
|
||||
})}
|
||||
bodyHeader={
|
||||
<EuiText color="subdued" data-test-subj="policyTotalCount">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.viewTitleTotalCount"
|
||||
defaultMessage="{totalItemCount, plural, one {# Policy} other {# Policies}}"
|
||||
values={{ totalItemCount }}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiBasicTable
|
||||
items={useMemo(() => [...policyItems], [policyItems])}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
pagination={paginationSetup}
|
||||
onChange={handleTableChange}
|
||||
data-test-subj="policyTable"
|
||||
hasActions={false}
|
||||
/>
|
||||
<SpyRoute />
|
||||
</ManagementPageView>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
PolicyList.displayName = 'PolicyList';
|
||||
|
||||
const ConfirmDelete = React.memo<{
|
||||
hostCount: number;
|
||||
isDeleting: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}>(({ hostCount, isDeleting, onCancel, onConfirm }) => {
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
data-test-subj="policyListDeleteModal"
|
||||
title={i18n.translate('xpack.securitySolution.endpoint.policyList.deleteConfirm.title', {
|
||||
defaultMessage: 'Delete policy and deploy changes',
|
||||
})}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
buttonColor="danger"
|
||||
confirmButtonText={
|
||||
isDeleting ? (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.deleteConfirm.deletingButton"
|
||||
defaultMessage="Deleting..."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.deleteConfirm.confirmDeleteButton"
|
||||
defaultMessage="Delete Policy"
|
||||
/>
|
||||
)
|
||||
}
|
||||
confirmButtonDisabled={isDeleting}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policyList.deleteConfirm.cancelButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{hostCount > 0 && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
data-test-subj="policyListWarningCallout"
|
||||
color="danger"
|
||||
title={i18n.translate(
|
||||
'xpack.securitySolution.endpoint.policyList.deleteConfirm.warningTitle',
|
||||
{
|
||||
defaultMessage:
|
||||
'This action will delete Endpoint Security from {hostCount, plural, one {# host} other {# hosts}}',
|
||||
values: { hostCount },
|
||||
}
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.deleteConfirm.warningMessage"
|
||||
defaultMessage="Deleting this Policy will remove Endpoint Security from these hosts"
|
||||
/>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="xl" />
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpoint.policyList.deleteConfirm.message"
|
||||
defaultMessage="This action cannot be undone. Are you sure you wish to continue?"
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
});
|
||||
|
||||
ConfirmDelete.displayName = 'ConfirmDelete';
|
||||
|
|
|
@ -81,12 +81,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
expect(relativeDate).to.match(RELATIVE_DATE_FORMAT);
|
||||
});
|
||||
});
|
||||
it('should show policy name as link', async () => {
|
||||
const policyNameLink = await testSubjects.find('policyNameLink');
|
||||
expect(await policyNameLink.getTagName()).to.equal('a');
|
||||
expect(await policyNameLink.getAttribute('href')).to.match(
|
||||
new RegExp(`\/management\/policy\/${policyInfo.datasource.id}$`)
|
||||
|
||||
it('should show agent config action as a link', async () => {
|
||||
await (await pageObjects.policy.findFirstActionsButton()).click();
|
||||
const agentConfigLink = await testSubjects.find('agentConfigLink');
|
||||
expect(await agentConfigLink.getAttribute('href')).to.match(
|
||||
new RegExp(`\/ingestManager#\/configs\/${policyInfo.agentConfig.id}$`)
|
||||
);
|
||||
// Close action menu
|
||||
await (await pageObjects.policy.findFirstActionsButton()).click();
|
||||
});
|
||||
|
||||
it('should delete a policy', async () => {
|
||||
await pageObjects.policy.launchAndFindDeleteModal();
|
||||
await testSubjects.existOrFail('policyListDeleteModal');
|
||||
await pageObjects.common.clickConfirmOnModal();
|
||||
await pageObjects.endpoint.waitForTableToNotHaveData('policyTable');
|
||||
const policyTotal = await testSubjects.getVisibleText('policyTotalCount');
|
||||
expect(policyTotal).to.equal('0 Policies');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,6 +33,16 @@ export function EndpointPageProvider({ getService, getPageObjects }: FtrProvider
|
|||
});
|
||||
},
|
||||
|
||||
async waitForTableToNotHaveData(dataTestSubj: string) {
|
||||
await retry.waitForWithTimeout('table to not have data', 2000, async () => {
|
||||
const tableData = await pageObjects.endpointPageUtils.tableData(dataTestSubj);
|
||||
if (tableData[1][0] === 'No items found') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
async waitForVisibleTextToChange(dataTestSubj: string, currentText: string) {
|
||||
await retry.waitForWithTimeout('visible text to change', 2000, async () => {
|
||||
const detailFlyoutTitle = await testSubjects.getVisibleText(dataTestSubj);
|
||||
|
|
|
@ -19,6 +19,32 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr
|
|||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds and returns the Policy Details Page Save button
|
||||
*/
|
||||
async findFirstActionsButton() {
|
||||
await this.ensureIsOnPolicyPage();
|
||||
return (await testSubjects.findAll('policyActionsButton'))[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds and returns the Policy Details Page Save button
|
||||
*/
|
||||
async launchAndFindDeleteModal() {
|
||||
const actionsButton = await this.findFirstActionsButton();
|
||||
await actionsButton.click();
|
||||
const deleteAction = await testSubjects.find('policyDeleteButton');
|
||||
await deleteAction.click();
|
||||
return await testSubjects.find('policyListDeleteModal');
|
||||
},
|
||||
|
||||
/**
|
||||
* ensures that the Policy Page is the currently display view
|
||||
*/
|
||||
async ensureIsOnPolicyPage() {
|
||||
await testSubjects.existOrFail('policyTable');
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigates to the Endpoint Policy Details page
|
||||
*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue