diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/components/index.ts index 355805babf25c..a017299a78914 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/index.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/index.ts @@ -14,3 +14,5 @@ export { SectionError } from './section_error'; export { SectionLoading } from './section_loading'; export { SnapshotDeleteProvider } from './snapshot_delete_provider'; export { RestoreSnapshotForm } from './restore_snapshot_form'; +export { PolicyExecuteProvider } from './policy_execute_provider'; +export { PolicyDeleteProvider } from './policy_delete_provider'; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_delete_provider.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_delete_provider.tsx new file mode 100644 index 0000000000000..b9265f96273d8 --- /dev/null +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_delete_provider.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useRef, useState } from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { useAppDependencies } from '../index'; +import { deletePolicies } from '../services/http'; + +interface Props { + children: (deletePolicy: DeletePolicy) => React.ReactElement; +} + +export type DeletePolicy = (names: string[], onSuccess?: OnSuccessCallback) => void; + +type OnSuccessCallback = (policiesDeleted: string[]) => void; + +export const PolicyDeleteProvider: React.FunctionComponent = ({ children }) => { + const { + core: { + i18n, + notification: { toastNotifications }, + }, + } = useAppDependencies(); + const { FormattedMessage } = i18n; + const [policyNames, setPolicyNames] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const onSuccessCallback = useRef(null); + + const deletePolicyPrompt: DeletePolicy = (names, onSuccess = () => undefined) => { + if (!names || !names.length) { + throw new Error('No policy names specified for deletion'); + } + setIsModalOpen(true); + setPolicyNames(names); + onSuccessCallback.current = onSuccess; + }; + + const closeModal = () => { + setIsModalOpen(false); + setPolicyNames([]); + }; + + const deletePolicy = () => { + const policiesToDelete = [...policyNames]; + deletePolicies(policiesToDelete).then(({ data, error }) => { + const { itemsDeleted, errors } = data || { itemsDeleted: undefined, errors: undefined }; + + // Surface success notifications + if (itemsDeleted && itemsDeleted.length) { + const hasMultipleSuccesses = itemsDeleted.length > 1; + const successMessage = hasMultipleSuccesses + ? i18n.translate('xpack.snapshotRestore.deletePolicy.successMultipleNotificationTitle', { + defaultMessage: 'Deleted {count} policies', + values: { count: itemsDeleted.length }, + }) + : i18n.translate('xpack.snapshotRestore.deletePolicy.successSingleNotificationTitle', { + defaultMessage: "Deleted policy '{name}'", + values: { name: itemsDeleted[0] }, + }); + toastNotifications.addSuccess(successMessage); + if (onSuccessCallback.current) { + onSuccessCallback.current([...itemsDeleted]); + } + } + + // Surface error notifications + // `error` is generic server error + // `data.errors` are specific errors with removing particular policy(ies) + if (error || (errors && errors.length)) { + const hasMultipleErrors = + (errors && errors.length > 1) || (error && policiesToDelete.length > 1); + const errorMessage = hasMultipleErrors + ? i18n.translate('xpack.snapshotRestore.deletePolicy.errorMultipleNotificationTitle', { + defaultMessage: 'Error deleting {count} policies', + values: { + count: (errors && errors.length) || policiesToDelete.length, + }, + }) + : i18n.translate('xpack.snapshotRestore.deletePolicy.errorSingleNotificationTitle', { + defaultMessage: "Error deleting policy '{name}'", + values: { name: (errors && errors[0].name) || policiesToDelete[0] }, + }); + toastNotifications.addDanger(errorMessage); + } + }); + closeModal(); + }; + + const renderModal = () => { + if (!isModalOpen) { + return null; + } + + const isSingle = policyNames.length === 1; + + return ( + + + ) : ( + + ) + } + onCancel={closeModal} + onConfirm={deletePolicy} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + data-test-subj="srdeletePolicyConfirmationModal" + > + {!isSingle ? ( + +

+ +

+
    + {policyNames.map(name => ( +
  • {name}
  • + ))} +
+
+ ) : null} +
+
+ ); + }; + + return ( + + {children(deletePolicyPrompt)} + {renderModal()} + + ); +}; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_execute_provider.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_execute_provider.tsx new file mode 100644 index 0000000000000..3df081e9c9dba --- /dev/null +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_execute_provider.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useRef, useState } from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { useAppDependencies } from '../index'; +import { executePolicy as executePolicyRequest } from '../services/http'; + +interface Props { + children: (executePolicy: ExecutePolicy) => React.ReactElement; +} + +export type ExecutePolicy = (name: string, onSuccess?: OnSuccessCallback) => void; + +type OnSuccessCallback = () => void; + +export const PolicyExecuteProvider: React.FunctionComponent = ({ children }) => { + const { + core: { + i18n, + notification: { toastNotifications }, + }, + } = useAppDependencies(); + const { FormattedMessage } = i18n; + const [policyName, setPolicyName] = useState(''); + const [isModalOpen, setIsModalOpen] = useState(false); + const onSuccessCallback = useRef(null); + + const executePolicyPrompt: ExecutePolicy = (name, onSuccess = () => undefined) => { + if (!name || !name.length) { + throw new Error('No policy name specified for execution'); + } + setIsModalOpen(true); + setPolicyName(name); + onSuccessCallback.current = onSuccess; + }; + + const closeModal = () => { + setIsModalOpen(false); + setPolicyName(''); + }; + const executePolicy = () => { + executePolicyRequest(policyName).then(({ data, error }) => { + const { snapshotName } = data || { snapshotName: undefined }; + + // Surface success notification + if (snapshotName) { + const successMessage = i18n.translate( + 'xpack.snapshotRestore.executePolicy.successNotificationTitle', + { + defaultMessage: "Policy '{name}' is running", + values: { name: policyName }, + } + ); + toastNotifications.addSuccess(successMessage); + if (onSuccessCallback.current) { + onSuccessCallback.current(); + } + } + + // Surface error notifications + if (error) { + const errorMessage = i18n.translate( + 'xpack.snapshotRestore.executePolicy.errorNotificationTitle', + { + defaultMessage: "Error running policy '{name}'", + values: { name: policyName }, + } + ); + toastNotifications.addDanger(errorMessage); + } + }); + closeModal(); + }; + + const renderModal = () => { + if (!isModalOpen) { + return null; + } + + return ( + + + } + onCancel={closeModal} + onConfirm={executePolicy} + cancelButtonText={ + + } + confirmButtonText={ + + } + data-test-subj="srExecutePolicyConfirmationModal" + > +

+ +

+
+
+ ); + }; + + return ( + + {children(executePolicyPrompt)} + {renderModal()} + + ); +}; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx index 049a7b3baf5bc..509eeb0201825 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/repository_delete_provider.tsx @@ -49,7 +49,9 @@ export const RepositoryDeleteProvider: React.FunctionComponent = ({ child const deleteRepository = () => { const repositoriesToDelete = [...repositoryNames]; - deleteRepositories(repositoriesToDelete).then(({ data: { itemsDeleted, errors }, error }) => { + deleteRepositories(repositoriesToDelete).then(({ data, error }) => { + const { itemsDeleted, errors } = data || { itemsDeleted: undefined, errors: undefined }; + // Surface success notifications if (itemsDeleted && itemsDeleted.length) { const hasMultipleSuccesses = itemsDeleted.length > 1; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/snapshot_delete_provider.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/snapshot_delete_provider.tsx index 84a92dd01c18a..4c3d84a285b99 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/snapshot_delete_provider.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/snapshot_delete_provider.tsx @@ -61,7 +61,9 @@ export const SnapshotDeleteProvider: React.FunctionComponent = ({ childre const deleteSnapshot = () => { const snapshotsToDelete = [...snapshotIds]; setIsDeleting(true); - deleteSnapshots(snapshotsToDelete).then(({ data: { itemsDeleted, errors }, error }) => { + deleteSnapshots(snapshotsToDelete).then(({ data, error }) => { + const { itemsDeleted, errors } = data || { itemsDeleted: undefined, errors: undefined }; + // Wait until request is done to close modal; deleting snapshots take longer due to their sequential nature closeModal(); setIsDeleting(false); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts index f7a7ec6abebea..61722bada4d13 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/constants/index.ts @@ -109,3 +109,6 @@ export const UIM_POLICY_LIST_LOAD = 'policy_list_load'; export const UIM_POLICY_SHOW_DETAILS_CLICK = 'policy_show_details_click'; export const UIM_POLICY_DETAIL_PANEL_SUMMARY_TAB = 'policy_detail_panel_summary_tab'; export const UIM_POLICY_DETAIL_PANEL_HISTORY_TAB = 'policy_detail_panel_last_success_tab'; +export const UIM_POLICY_EXECUTE = 'policy_execute'; +export const UIM_POLICY_DELETE = 'policy_delete'; +export const UIM_POLICY_DELETE_MANY = 'policy_delete_many'; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/policy_details.tsx index ef8f8cf5e1774..ab658373283c8 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/policy_details.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/policy_details.tsx @@ -15,6 +15,7 @@ import { EuiTitle, EuiTabs, EuiTab, + EuiButton, } from '@elastic/eui'; import { SlmPolicy } from '../../../../../../common/types'; @@ -26,12 +27,19 @@ import { import { useLoadPolicy } from '../../../../services/http'; import { uiMetricService } from '../../../../services/ui_metric'; -import { SectionError, SectionLoading } from '../../../../components'; +import { + SectionError, + SectionLoading, + PolicyExecuteProvider, + PolicyDeleteProvider, +} from '../../../../components'; import { TabSummary, TabHistory } from './tabs'; interface Props { policyName: SlmPolicy['name']; onClose: () => void; + onPolicyDeleted: (policiesDeleted: Array) => void; + onPolicyExecuted: () => void; } const TAB_SUMMARY = 'summary'; @@ -42,14 +50,19 @@ const tabToUiMetricMap: { [key: string]: string } = { [TAB_HISTORY]: UIM_POLICY_DETAIL_PANEL_HISTORY_TAB, }; -export const PolicyDetails: React.FunctionComponent = ({ policyName, onClose }) => { +export const PolicyDetails: React.FunctionComponent = ({ + policyName, + onClose, + onPolicyDeleted, + onPolicyExecuted, +}) => { const { core: { i18n }, } = useAppDependencies(); const { FormattedMessage } = i18n; const { trackUiMetric } = uiMetricService; - const { error, data: policyDetails } = useLoadPolicy(policyName); + const { error, data: policyDetails, sendRequest: reload } = useLoadPolicy(policyName); const [activeTab, setActiveTab] = useState(TAB_SUMMARY); // Reset tab when we look at a different policy @@ -170,6 +183,55 @@ export const PolicyDetails: React.FunctionComponent = ({ policyName, onCl /> + + {policyDetails ? ( + + + + + {deletePolicyPrompt => { + return ( + deletePolicyPrompt([policyName], onPolicyDeleted)} + > + + + ); + }} + + + + + {executePolicyPrompt => { + return ( + + executePolicyPrompt(policyName, () => { + onPolicyExecuted(); + reload(); + }) + } + fill + color="primary" + data-test-subj="srPolicyDetailsExecuteActionButton" + > + + + ); + }} + + + + + ) : null} ); }; @@ -180,7 +242,7 @@ export const PolicyDetails: React.FunctionComponent = ({ policyName, onCl data-test-subj="policyDetail" aria-labelledby="srPolicyDetailsFlyoutTitle" size="m" - maxWidth={400} + maxWidth={550} > diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx index c0b17b1d488df..d94f3b0310387 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_list.tsx @@ -53,6 +53,19 @@ export const PolicyList: React.FunctionComponent): void => { + if (policyName && policiesDeleted.includes(policyName)) { + closePolicyDetails(); + } + if (policiesDeleted.length) { + reload(); + } + }; + + const onPolicyExecuted = () => { + reload(); + }; + // Track component loaded const { trackUiMetric } = uiMetricService; useEffect(() => { @@ -113,13 +126,22 @@ export const PolicyList: React.FunctionComponent ); } return (
- {policyName ? : null} + {policyName ? ( + + ) : null} {content}
); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx index a097f7930d124..7b944afe97d1b 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx @@ -4,31 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiLink } from '@elastic/eui'; +import React, { useState } from 'react'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiInMemoryTable, + EuiLink, + EuiToolTip, + EuiButtonIcon, +} from '@elastic/eui'; import { SlmPolicy } from '../../../../../../common/types'; import { UIM_POLICY_SHOW_DETAILS_CLICK } from '../../../../constants'; import { useAppDependencies } from '../../../../index'; -import { FormattedDateTime } from '../../../../components'; +import { + FormattedDateTime, + PolicyExecuteProvider, + PolicyDeleteProvider, +} from '../../../../components'; import { uiMetricService } from '../../../../services/ui_metric'; interface Props { policies: SlmPolicy[]; reload: () => Promise; openPolicyDetailsUrl: (name: SlmPolicy['name']) => string; + onPolicyDeleted: (policiesDeleted: Array) => void; + onPolicyExecuted: () => void; } export const PolicyTable: React.FunctionComponent = ({ policies, reload, openPolicyDetailsUrl, + onPolicyDeleted, + onPolicyExecuted, }) => { const { core: { i18n }, } = useAppDependencies(); const { FormattedMessage } = i18n; const { trackUiMetric } = uiMetricService; + const [selectedItems, setSelectedItems] = useState([]); const columns = [ { @@ -85,6 +102,76 @@ export const PolicyTable: React.FunctionComponent = ({ ), }, + { + name: i18n.translate('xpack.snapshotRestore.policyList.table.actionsColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + render: ({ name }: SlmPolicy) => { + return ( + + {executePolicyPrompt => { + const label = i18n.translate( + 'xpack.snapshotRestore.policyList.table.actionExecuteTooltip', + { defaultMessage: 'Run policy' } + ); + return ( + + executePolicyPrompt(name, onPolicyExecuted)} + /> + + ); + }} + + ); + }, + }, + { + render: ({ name }: SlmPolicy) => { + return ( + + {deletePolicyPrompt => { + const label = i18n.translate( + 'xpack.snapshotRestore.policyList.table.actionDeleteTooltip', + { defaultMessage: 'Delete' } + ); + return ( + + deletePolicyPrompt([name], onPolicyDeleted)} + /> + + ); + }} + + ); + }, + }, + ], + width: '100px', + }, ]; const sorting = { @@ -99,7 +186,41 @@ export const PolicyTable: React.FunctionComponent = ({ pageSizeOptions: [10, 20, 50], }; + const selection = { + onSelectionChange: (newSelectedItems: SlmPolicy[]) => setSelectedItems(newSelectedItems), + }; + const search = { + toolsLeft: selectedItems.length ? ( + + {( + deletePolicyPrompt: ( + names: Array, + onSuccess?: (policiesDeleted: Array) => void + ) => void + ) => { + return ( + + deletePolicyPrompt(selectedItems.map(({ name }) => name), onPolicyDeleted) + } + color="danger" + data-test-subj="srPolicyListBulkDeleteActionButton" + > + + + ); + }} + + ) : ( + undefined + ), toolsRight: ( @@ -151,6 +272,7 @@ export const PolicyTable: React.FunctionComponent = ({ columns={columns} search={search} sorting={sorting} + selection={selection} pagination={pagination} isSelectable={true} rowProps={() => ({ diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx index fa2d8673332ec..35eaf331b4f0f 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_details/repository_details.tsx @@ -390,7 +390,7 @@ export const RepositoryDetails: React.FunctionComponent = ({ data-test-subj="repositoryDetail" aria-labelledby="srRepositoryDetailsFlyoutTitle" size="m" - maxWidth={400} + maxWidth={550} > diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx index c202e210e3c77..e3534e3c89459 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx @@ -248,7 +248,7 @@ export const SnapshotDetails: React.FunctionComponent = ({ data-test-subj="snapshotDetail" aria-labelledby="srSnapshotDetailsFlyoutTitle" size="m" - maxWidth={400} + maxWidth={550} > diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/policy_requests.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/policy_requests.ts index d7e2e42bef0dd..6a2c9c685a01f 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/policy_requests.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/http/policy_requests.ts @@ -5,8 +5,10 @@ */ import { API_BASE_PATH } from '../../../../common/constants'; import { SlmPolicy } from '../../../../common/types'; +import { UIM_POLICY_EXECUTE, UIM_POLICY_DELETE, UIM_POLICY_DELETE_MANY } from '../../constants'; +import { uiMetricService } from '../ui_metric'; import { httpService } from './http'; -import { useRequest } from './use_request'; +import { useRequest, sendRequest } from './use_request'; export const useLoadPolicies = () => { return useRequest({ @@ -21,3 +23,27 @@ export const useLoadPolicy = (name: SlmPolicy['name']) => { method: 'get', }); }; + +export const executePolicy = async (name: SlmPolicy['name']) => { + const result = sendRequest({ + path: httpService.addBasePath(`${API_BASE_PATH}policy/${encodeURIComponent(name)}/run`), + method: 'post', + }); + + const { trackUiMetric } = uiMetricService; + trackUiMetric(UIM_POLICY_EXECUTE); + return result; +}; + +export const deletePolicies = async (names: Array) => { + const result = sendRequest({ + path: httpService.addBasePath( + `${API_BASE_PATH}policies/${names.map(name => encodeURIComponent(name)).join(',')}` + ), + method: 'delete', + }); + + const { trackUiMetric } = uiMetricService; + trackUiMetric(names.length > 1 ? UIM_POLICY_DELETE_MANY : UIM_POLICY_DELETE); + return result; +}; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts b/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts index 75a6215b68142..c37cc51f67eb0 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/client/elasticsearch_slm.ts @@ -32,4 +32,32 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) ], method: 'GET', }); + + slm.deletePolicy = ca({ + urls: [ + { + fmt: '/_slm/policy/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'DELETE', + }); + + slm.executePolicy = ca({ + urls: [ + { + fmt: '/_slm/policy/<%=name%>/_execute', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'PUT', + }); }; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts index 9c27613279d39..f2335d4f78dd9 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { Request, ResponseToolkit } from 'hapi'; -import { getAllHandler, getOneHandler } from './policy'; +import { getAllHandler, getOneHandler, executeHandler, deleteHandler } from './policy'; describe('[Snapshot and Restore API Routes] Restore', () => { const mockRequest = {} as Request; @@ -116,4 +116,97 @@ describe('[Snapshot and Restore API Routes] Restore', () => { ).rejects.toThrow(); }); }); + + describe('executeHandler()', () => { + const name = 'fooPolicy'; + const mockExecuteRequest = ({ + params: { + name, + }, + } as unknown) as Request; + + it('should return snapshot name from ES', async () => { + const mockEsResponse = { + snapshot_name: 'foo-policy-snapshot', + }; + const callWithRequest = jest.fn().mockResolvedValueOnce(mockEsResponse); + const expectedResponse = { + snapshotName: 'foo-policy-snapshot', + }; + await expect( + executeHandler(mockExecuteRequest, callWithRequest, mockResponseToolkit) + ).resolves.toEqual(expectedResponse); + }); + + it('should throw if ES error', async () => { + const callWithRequest = jest.fn().mockRejectedValueOnce(new Error()); + await expect( + executeHandler(mockExecuteRequest, callWithRequest, mockResponseToolkit) + ).rejects.toThrow(); + }); + }); + + describe('deleteHandler()', () => { + const names = ['fooPolicy', 'barPolicy']; + const mockCreateRequest = ({ + params: { + names: names.join(','), + }, + } as unknown) as Request; + + it('should return successful ES responses', async () => { + const mockEsResponse = { acknowledged: true }; + const callWithRequest = jest + .fn() + .mockResolvedValueOnce(mockEsResponse) + .mockResolvedValueOnce(mockEsResponse); + const expectedResponse = { itemsDeleted: names, errors: [] }; + await expect( + deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) + ).resolves.toEqual(expectedResponse); + }); + + it('should return error ES responses', async () => { + const mockEsError = new Error('Test error') as any; + mockEsError.response = '{}'; + mockEsError.statusCode = 500; + const callWithRequest = jest + .fn() + .mockRejectedValueOnce(mockEsError) + .mockRejectedValueOnce(mockEsError); + const expectedResponse = { + itemsDeleted: [], + errors: names.map(name => ({ + name, + error: mockEsError, + })), + }; + await expect( + deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) + ).resolves.toEqual(expectedResponse); + }); + + it('should return combination of ES successes and errors', async () => { + const mockEsError = new Error('Test error') as any; + mockEsError.response = '{}'; + mockEsError.statusCode = 500; + const mockEsResponse = { acknowledged: true }; + const callWithRequest = jest + .fn() + .mockRejectedValueOnce(mockEsError) + .mockResolvedValueOnce(mockEsResponse); + const expectedResponse = { + itemsDeleted: [names[1]], + errors: [ + { + name: names[0], + error: mockEsError, + }, + ], + }; + await expect( + deleteHandler(mockCreateRequest, callWithRequest, mockResponseToolkit) + ).resolves.toEqual(expectedResponse); + }); + }); }); diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts index 0a7130c6b4157..28b75b706bcad 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router'; -import { wrapCustomError } from '../../../../../server/lib/create_router/error_wrappers'; +import { + wrapCustomError, + wrapEsError, +} from '../../../../../server/lib/create_router/error_wrappers'; import { SlmPolicyEs, SlmPolicy } from '../../../common/types'; import { deserializePolicy } from '../../lib'; export function registerPolicyRoutes(router: Router) { router.get('policies', getAllHandler); router.get('policy/{name}', getOneHandler); + router.post('policy/{name}/run', executeHandler); + router.delete('policies/{names}', deleteHandler); } export const getAllHandler: RouterRouteHandler = async ( @@ -59,3 +64,35 @@ export const getOneHandler: RouterRouteHandler = async ( policy: deserializePolicy(name, policiesByName[name]), }; }; + +export const executeHandler: RouterRouteHandler = async (req, callWithRequest) => { + const { name } = req.params; + const { snapshot_name: snapshotName } = await callWithRequest('slm.executePolicy', { + name, + }); + return { snapshotName }; +}; + +export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) => { + const { names } = req.params; + const policyNames = names.split(','); + const response: { itemsDeleted: string[]; errors: any[] } = { + itemsDeleted: [], + errors: [], + }; + + await Promise.all( + policyNames.map(name => { + return callWithRequest('slm.deletePolicy', { name }) + .then(() => response.itemsDeleted.push(name)) + .catch(e => + response.errors.push({ + name, + error: wrapEsError(e), + }) + ); + }) + ); + + return response; +};