[Endpoint] Can delete policies (#68567)

This commit is contained in:
Kevin Logan 2020-06-12 11:28:36 -04:00 committed by GitHub
parent 1b85b40220
commit ccf8def829
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 497 additions and 56 deletions

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;

View file

@ -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 () => {

View file

@ -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,
},
});
}
};
};

View file

@ -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;

View file

@ -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 ?? '', {

View file

@ -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
*

View file

@ -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'];
}
/**

View file

@ -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';

View file

@ -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');
});
});
});

View file

@ -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);

View file

@ -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
*