diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/constant.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/constant.ts
index 372b5bc6f6529..1fb6aa8686b7e 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/constant.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/constant.ts
@@ -4,7 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { getRepository } from '../../../test/fixtures';
+import { getRepository, getPolicy } from '../../../test/fixtures';
+
export const REPOSITORY_NAME = 'my-test-repository';
export const REPOSITORY_EDIT = getRepository({ name: REPOSITORY_NAME });
+
+export const POLICY_NAME = 'my-test-policy';
+
+export const POLICY_EDIT = getPolicy({ name: POLICY_NAME });
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
index dd9d51a9990cc..d9f2c1b510a14 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts
@@ -72,6 +72,37 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
);
};
+ const setLoadIndicesResponse = (response: HttpResponse = {}) => {
+ const defaultResponse = { indices: [] };
+
+ server.respondWith(
+ 'GET',
+ `${API_BASE_PATH}policies/indices`,
+ response
+ ? mockResponse(defaultResponse, response)
+ : [200, { 'Content-Type': 'application/json' }, '']
+ );
+ };
+
+ const setAddPolicyResponse = (response?: HttpResponse, error?: any) => {
+ const status = error ? error.status || 400 : 200;
+ const body = error ? JSON.stringify(error.body) : JSON.stringify(response);
+
+ server.respondWith('PUT', `${API_BASE_PATH}policies`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ body,
+ ]);
+ };
+
+ const setGetPolicyResponse = (response?: HttpResponse) => {
+ server.respondWith('GET', `${API_BASE_PATH}policy/:name`, [
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(response),
+ ]);
+ };
+
return {
setLoadRepositoriesResponse,
setLoadRepositoryTypesResponse,
@@ -79,6 +110,9 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
setSaveRepositoryResponse,
setLoadSnapshotsResponse,
setGetSnapshotResponse,
+ setLoadIndicesResponse,
+ setAddPolicyResponse,
+ setGetPolicyResponse,
};
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts
index d8bb3d4c25e10..e6fea41d86928 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/index.ts
@@ -7,6 +7,8 @@
import { setup as homeSetup } from './home.helpers';
import { setup as repositoryAddSetup } from './repository_add.helpers';
import { setup as repositoryEditSetup } from './repository_edit.helpers';
+import { setup as policyAddSetup } from './policy_add.helpers';
+import { setup as policyEditSetup } from './policy_edit.helpers';
export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../../test_utils';
@@ -16,4 +18,6 @@ export const pageHelpers = {
home: { setup: homeSetup },
repositoryAdd: { setup: repositoryAddSetup },
repositoryEdit: { setup: repositoryEditSetup },
+ policyAdd: { setup: policyAddSetup },
+ policyEdit: { setup: policyEditSetup },
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_add.helpers.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_add.helpers.ts
new file mode 100644
index 0000000000000..ff59bd83dc1e8
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_add.helpers.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
+import { PolicyAdd } from '../../../public/app/sections/policy_add';
+import { WithProviders } from './providers';
+import { formSetup, PolicyFormTestSubjects } from './policy_form.helpers';
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: ['/add_policy'],
+ componentRoutePath: '/add_policy',
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(
+ WithProviders(PolicyAdd),
+ testBedConfig
+);
+
+export const setup = formSetup.bind(null, initTestBed);
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_edit.helpers.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_edit.helpers.ts
new file mode 100644
index 0000000000000..b2c0e4242a3fd
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_edit.helpers.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { registerTestBed, TestBedConfig } from '../../../../../../test_utils';
+import { PolicyEdit } from '../../../public/app/sections/policy_edit';
+import { WithProviders } from './providers';
+import { POLICY_NAME } from './constant';
+import { formSetup, PolicyFormTestSubjects } from './policy_form.helpers';
+
+const testBedConfig: TestBedConfig = {
+ memoryRouter: {
+ initialEntries: [`/edit_policy/${POLICY_NAME}`],
+ componentRoutePath: '/edit_policy/:name',
+ },
+ doMountAsync: true,
+};
+
+const initTestBed = registerTestBed(
+ WithProviders(PolicyEdit),
+ testBedConfig
+);
+
+export const setup = formSetup.bind(null, initTestBed);
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts
new file mode 100644
index 0000000000000..302af7a1ec7f0
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { TestBed, SetupFunc } from '../../../../../../test_utils';
+
+export interface PolicyFormTestBed extends TestBed {
+ actions: {
+ clickNextButton: () => void;
+ clickSubmitButton: () => void;
+ };
+}
+
+export const formSetup = async (
+ initTestBed: SetupFunc
+): Promise => {
+ const testBed = await initTestBed();
+
+ // User actions
+ const clickNextButton = () => {
+ testBed.find('nextButton').simulate('click');
+ };
+
+ const clickSubmitButton = () => {
+ testBed.find('submitButton').simulate('click');
+ };
+
+ return {
+ ...testBed,
+ actions: {
+ clickNextButton,
+ clickSubmitButton,
+ },
+ };
+};
+
+export type PolicyFormTestSubjects =
+ | 'advancedCronInput'
+ | 'allIndicesToggle'
+ | 'backButton'
+ | 'deselectIndicesLink'
+ | 'expireAfterValueInput'
+ | 'expireAfterUnitSelect'
+ | 'ignoreUnavailableIndicesToggle'
+ | 'nameInput'
+ | 'maxCountInput'
+ | 'minCountInput'
+ | 'nextButton'
+ | 'pageTitle'
+ | 'savePolicyApiError'
+ | 'selectIndicesLink'
+ | 'showAdvancedCronLink'
+ | 'snapshotNameInput'
+ | 'submitButton';
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/providers.tsx b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/providers.tsx
index 5257c030518ba..187d2da0d7a3d 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/providers.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/providers.tsx
@@ -10,7 +10,15 @@ import { setAppDependencies } from '../../../public/app/index';
const { core, plugins } = createShim();
const appDependencies = {
- core,
+ core: {
+ ...core,
+ chrome: {
+ ...core.chrome,
+ // mock getInjected() to return true
+ // this is used so the policy tab renders (slmUiEnabled config)
+ getInjected: () => true,
+ },
+ },
plugins,
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.ts
index dcfcdb1031dd5..e914f06d8e16f 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.ts
@@ -9,11 +9,15 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr';
import { i18n } from '@kbn/i18n';
+import { docTitle } from 'ui/doc_title/doc_title';
import { httpService } from '../../../public/app/services/http';
-import { breadcrumbService } from '../../../public/app/services/navigation';
+import { breadcrumbService, docTitleService } from '../../../public/app/services/navigation';
import { textService } from '../../../public/app/services/text';
import { chrome } from '../../../public/test/mocks';
import { init as initHttpRequests } from './http_requests';
+import { uiMetricService } from '../../../public/app/services/ui_metric';
+import { documentationLinksService } from '../../../public/app/services/documentation';
+import { createUiStatsReporter } from '../../../../../../../src/legacy/core_plugins/ui_metric/public';
export const setupEnvironment = () => {
httpService.init(axios.create({ adapter: axiosXhrAdapter }), {
@@ -21,6 +25,9 @@ export const setupEnvironment = () => {
});
breadcrumbService.init(chrome, {});
textService.init(i18n);
+ uiMetricService.init(createUiStatsReporter);
+ documentationLinksService.init('', '');
+ docTitleService.init(docTitle.change);
const { server, httpRequestsMockHelpers } = initHttpRequests();
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
index 1cbafab69da7c..7f4860e74bafe 100644
--- a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/home.test.ts
@@ -17,6 +17,7 @@ import {
} from './helpers';
import { HomeTestBed } from './helpers/home.helpers';
import { REPOSITORY_NAME } from './helpers/constant';
+import moment from 'moment-timezone';
const { setup } = pageHelpers.home;
@@ -51,7 +52,7 @@ describe.skip('', () => {
test('should set the correct app title', () => {
const { exists, find } = testBed;
expect(exists('appTitle')).toBe(true);
- expect(find('appTitle').text()).toEqual('Snapshot Repositories');
+ expect(find('appTitle').text()).toEqual('Snapshot and Restore');
});
test('should display a loading while fetching the repositories', () => {
@@ -63,7 +64,7 @@ describe.skip('', () => {
test('should have a link to the documentation', () => {
const { exists, find } = testBed;
expect(exists('documentationLink')).toBe(true);
- expect(find('documentationLink').text()).toBe('Snapshot docs');
+ expect(find('documentationLink').text()).toBe('Snapshot and Restore docs');
});
describe('tabs', () => {
@@ -77,14 +78,19 @@ describe.skip('', () => {
});
});
- test('should have 2 tabs', () => {
+ test('should have 4 tabs', () => {
const { find } = testBed;
- expect(find('tab').length).toBe(2);
- expect(find('tab').map(t => t.text())).toEqual(['Snapshots', 'Repositories']);
+ expect(find('tab').length).toBe(4);
+ expect(find('tab').map(t => t.text())).toEqual([
+ 'Snapshots',
+ 'Repositories',
+ 'Policies',
+ 'Restore Status',
+ ]);
});
- test('should navigate to snapshot list tab', () => {
+ test('should navigate to snapshot list tab', async () => {
const { exists, actions } = testBed;
expect(exists('repositoryList')).toBe(true);
@@ -92,6 +98,12 @@ describe.skip('', () => {
actions.selectTab('snapshots');
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ await nextTick();
+ testBed.component.update();
+ });
+
expect(exists('repositoryList')).toBe(false);
expect(exists('snapshotList')).toBe(true);
});
@@ -264,6 +276,11 @@ describe.skip('', () => {
expect(exists('repositoryDetail')).toBe(false);
await actions.clickRepositoryAt(0);
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ await nextTick();
+ testBed.component.update();
+ });
expect(exists('repositoryDetail')).toBe(true);
});
@@ -454,14 +471,19 @@ describe.skip('', () => {
const { tableCellsValues } = table.getMetaData('snapshotTable');
tableCellsValues.forEach((row, i) => {
const snapshot = snapshots[i];
+ const startTime = moment(new Date(snapshot.startTimeInMillis));
+ const timezone = moment.tz.guess();
+
expect(row).toEqual([
+ '', // Checkbox
snapshot.snapshot, // Snapshot
REPOSITORY_NAME, // Repository
- 'foo', // TODO: fix this with FormattedDateTime value
- `${Math.ceil(snapshot.durationInMillis / 1000).toString()}s`, // Duration
snapshot.indices.length.toString(), // Indices
snapshot.shards.total.toString(), // Shards
snapshot.shards.failed.toString(), // Failed shards
+ startTime.tz(timezone).format('MMMM D, YYYY h:mm A z'), // Start time
+ `${Math.ceil(snapshot.durationInMillis / 1000).toString()}s`, // Duration
+ '',
]);
});
});
@@ -590,22 +612,38 @@ describe.skip('', () => {
describe('summary tab', () => {
test('should set the correct summary values', () => {
+ const {
+ version,
+ versionId,
+ uuid,
+ indices,
+ endTimeInMillis,
+ startTimeInMillis,
+ } = snapshot1;
+
const { find } = testBed;
+ const startTime = moment(new Date(startTimeInMillis));
+ const endTime = moment(new Date(endTimeInMillis));
+ const timezone = moment.tz.guess();
expect(find('snapshotDetail.version.value').text()).toBe(
- `${snapshot1.version} / ${snapshot1.versionId}`
+ `${version} / ${versionId}`
);
- expect(find('snapshotDetail.uuid.value').text()).toBe(snapshot1.uuid);
+ expect(find('snapshotDetail.uuid.value').text()).toBe(uuid);
expect(find('snapshotDetail.state.value').text()).toBe('Snapshot complete');
expect(find('snapshotDetail.includeGlobalState.value').text()).toBe('Yes');
expect(find('snapshotDetail.indices.title').text()).toBe(
- `Indices (${snapshot1.indices.length})`
+ `Indices (${indices.length})`
+ );
+ expect(find('snapshotDetail.indices.value').text()).toContain(
+ indices.splice(0, 10).join('')
+ );
+ expect(find('snapshotDetail.startTime.value').text()).toBe(
+ startTime.tz(timezone).format('MMMM D, YYYY h:mm A z')
);
- expect(find('snapshotDetail.indices.value').text()).toBe(
- snapshot1.indices.join('')
+ expect(find('snapshotDetail.endTime.value').text()).toBe(
+ endTime.tz(timezone).format('MMMM D, YYYY h:mm A z')
);
- expect(find('snapshotDetail.startTime.value').text()).toBe('foo'); // TODO: fix this with FormattedDateTime value
- expect(find('snapshotDetail.endTime.value').text()).toBe('foo'); // TODO: fix this with FormattedDateTime value
});
test('should indicate the different snapshot states', async () => {
@@ -647,7 +685,7 @@ describe.skip('', () => {
[SNAPSHOT_STATE.INCOMPATIBLE]: 'Incompatible version ',
};
- // Call sequencially each state and verify that the message is ok
+ // Call sequentially each state and verify that the message is ok
return Object.entries(mapStateToMessage).reduce((promise, [state, message]) => {
return promise.then(async () => expectMessageForSnapshotState(state, message));
}, Promise.resolve());
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts
new file mode 100644
index 0000000000000..19feb85e4f04e
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts
@@ -0,0 +1,215 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+
+import * as fixtures from '../../test/fixtures';
+
+import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers';
+import { PolicyFormTestBed } from './helpers/policy_form.helpers';
+import { DEFAULT_POLICY_SCHEDULE } from '../../public/app/constants';
+
+const { setup } = pageHelpers.policyAdd;
+
+jest.mock('ui/i18n', () => {
+ const I18nContext = ({ children }: any) => children;
+ return { I18nContext };
+});
+
+const POLICY_NAME = 'my_policy';
+const SNAPSHOT_NAME = 'my_snapshot';
+const MIN_COUNT = '5';
+const MAX_COUNT = '10';
+const EXPIRE_AFTER_VALUE = '30';
+const repository = fixtures.getRepository({ name: `a${getRandomString()}`, type: 'fs' });
+
+// We need to skip the tests until react 16.9.0 is released
+// which supports asynchronous code inside act()
+describe.skip('', () => {
+ let testBed: PolicyFormTestBed;
+
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ describe('on component mount', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories: [repository] });
+ httpRequestsMockHelpers.setLoadIndicesResponse({ indices: ['my_index'] });
+
+ testBed = await setup();
+ await nextTick();
+ testBed.component.update();
+ });
+
+ test('should set the correct page title', () => {
+ const { exists, find } = testBed;
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual('Create policy');
+ });
+
+ test('should not let the user go to the next step if required fields are missing', () => {
+ const { find } = testBed;
+
+ expect(find('nextButton').props().disabled).toBe(true);
+ });
+
+ describe('form validation', () => {
+ describe('logistics (step 1)', () => {
+ test('should require a policy name', async () => {
+ const { form, find } = testBed;
+
+ form.setInputValue('nameInput', '');
+ find('nameInput').simulate('blur');
+
+ expect(form.getErrorsMessages()).toEqual(['Policy name is required.']);
+ });
+
+ test('should require a snapshot name', () => {
+ const { form, find } = testBed;
+
+ form.setInputValue('snapshotNameInput', '');
+ find('snapshotNameInput').simulate('blur');
+
+ expect(form.getErrorsMessages()).toEqual(['Snapshot name is required.']);
+ });
+
+ it('should require a schedule', () => {
+ const { form, find } = testBed;
+
+ find('showAdvancedCronLink').simulate('click');
+ form.setInputValue('advancedCronInput', '');
+ find('advancedCronInput').simulate('blur');
+
+ expect(form.getErrorsMessages()).toEqual(['Schedule is required.']);
+ });
+ });
+
+ describe('snapshot settings (step 2)', () => {
+ beforeEach(() => {
+ const { form, actions } = testBed;
+ // Complete step 1
+ form.setInputValue('nameInput', POLICY_NAME);
+ form.setInputValue('snapshotNameInput', SNAPSHOT_NAME);
+ actions.clickNextButton();
+ });
+
+ test('should require at least one index', async () => {
+ const { find, form, component } = testBed;
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ // Toggle "All indices" switch
+ form.toggleEuiSwitch('allIndicesToggle', false);
+ await nextTick();
+ component.update();
+ });
+
+ // Deselect all indices from list
+ find('deselectIndicesLink').simulate('click');
+
+ expect(form.getErrorsMessages()).toEqual(['You must select at least one index.']);
+ });
+ });
+
+ describe('retention (step 3)', () => {
+ beforeEach(() => {
+ const { form, actions } = testBed;
+ // Complete step 1
+ form.setInputValue('nameInput', POLICY_NAME);
+ form.setInputValue('snapshotNameInput', SNAPSHOT_NAME);
+ actions.clickNextButton();
+
+ // Complete step 2
+ actions.clickNextButton();
+ });
+
+ test('should not allow the minimum count be greater than the maximum count', () => {
+ const { find, form } = testBed;
+
+ form.setInputValue('minCountInput', MAX_COUNT + 1);
+ find('minCountInput').simulate('blur');
+
+ form.setInputValue('maxCountInput', MAX_COUNT);
+ find('maxCountInput').simulate('blur');
+
+ expect(form.getErrorsMessages()).toEqual(['Min count cannot be greater than max count.']);
+ });
+ });
+ });
+
+ describe('form payload & api errors', () => {
+ beforeEach(async () => {
+ const { actions, form } = testBed;
+
+ // Complete step 1
+ form.setInputValue('nameInput', POLICY_NAME);
+ form.setInputValue('snapshotNameInput', SNAPSHOT_NAME);
+ actions.clickNextButton();
+
+ // Complete step 2
+ actions.clickNextButton();
+
+ // Complete step 3
+ form.setInputValue('expireAfterValueInput', EXPIRE_AFTER_VALUE);
+ form.setInputValue('minCountInput', MIN_COUNT);
+ form.setInputValue('maxCountInput', MAX_COUNT);
+ actions.clickNextButton();
+ });
+
+ it('should send the correct payload', async () => {
+ const { actions } = testBed;
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ const expected = {
+ config: {},
+ name: POLICY_NAME,
+ repository: repository.name,
+ retention: {
+ expireAfterUnit: 'd', // default
+ expireAfterValue: Number(EXPIRE_AFTER_VALUE),
+ maxCount: Number(MAX_COUNT),
+ minCount: Number(MIN_COUNT),
+ },
+ schedule: DEFAULT_POLICY_SCHEDULE,
+ snapshotName: SNAPSHOT_NAME,
+ };
+
+ expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
+ });
+
+ it('should surface the API errors from the put HTTP request', async () => {
+ const { component, actions, find, exists } = testBed;
+
+ const error = {
+ status: 409,
+ error: 'Conflict',
+ message: `There is already a policy with name '${POLICY_NAME}'`,
+ };
+
+ httpRequestsMockHelpers.setAddPolicyResponse(undefined, { body: error });
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ component.update();
+ });
+
+ expect(exists('savePolicyApiError')).toBe(true);
+ expect(find('savePolicyApiError').text()).toContain(error.message);
+ });
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts
new file mode 100644
index 0000000000000..efcb338e6d268
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { act } from 'react-dom/test-utils';
+
+import { setupEnvironment, pageHelpers, nextTick } from './helpers';
+import { PolicyForm } from '../../public/app/components/policy_form';
+import { PolicyFormTestBed } from './helpers/policy_form.helpers';
+import { POLICY_EDIT } from './helpers/constant';
+
+const { setup } = pageHelpers.policyEdit;
+const { setup: setupPolicyAdd } = pageHelpers.policyAdd;
+
+const EXPIRE_AFTER_VALUE = '5';
+const EXPIRE_AFTER_UNIT = 'm';
+
+jest.mock('ui/i18n', () => {
+ const I18nContext = ({ children }: any) => children;
+ return { I18nContext };
+});
+
+// We need to skip the tests until react 16.9.0 is released
+// which supports asynchronous code inside act()
+describe.skip('', () => {
+ let testBed: PolicyFormTestBed;
+ let testBedPolicyAdd: PolicyFormTestBed;
+ const { server, httpRequestsMockHelpers } = setupEnvironment();
+
+ afterAll(() => {
+ server.restore();
+ });
+
+ describe('on component mount', () => {
+ beforeEach(async () => {
+ httpRequestsMockHelpers.setGetPolicyResponse({ policy: POLICY_EDIT });
+ httpRequestsMockHelpers.setLoadIndicesResponse({ indices: ['my_index'] });
+ httpRequestsMockHelpers.setLoadRepositoriesResponse({
+ repositories: [{ name: POLICY_EDIT.repository }],
+ });
+
+ testBed = await setup();
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ await nextTick();
+ testBed.component.update();
+ });
+ });
+
+ test('should set the correct page title', () => {
+ const { exists, find } = testBed;
+ expect(exists('pageTitle')).toBe(true);
+ expect(find('pageTitle').text()).toEqual('Edit policy');
+ });
+
+ /**
+ * As the "edit" policy component uses the same form underneath that
+ * the "create" policy, we won't test it again but simply make sure that
+ * the same form component is indeed shared between the 2 app sections.
+ */
+ test('should use the same Form component as the "" section', async () => {
+ testBedPolicyAdd = await setupPolicyAdd();
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ await nextTick();
+ testBedPolicyAdd.component.update();
+ });
+
+ const formEdit = testBed.component.find(PolicyForm);
+ const formAdd = testBedPolicyAdd.component.find(PolicyForm);
+
+ expect(formEdit.length).toBe(1);
+ expect(formAdd.length).toBe(1);
+ });
+
+ test('should disable the policy name field', () => {
+ const { find } = testBed;
+
+ const nameInput = find('nameInput');
+ expect(nameInput.props().disabled).toEqual(true);
+ });
+
+ describe('form payload', () => {
+ beforeEach(async () => {
+ const { form, actions } = testBed;
+
+ const { snapshotName } = POLICY_EDIT;
+
+ // Complete step 1
+ form.setInputValue('snapshotNameInput', `${snapshotName}-edited`);
+ actions.clickNextButton();
+
+ // Complete step 2
+ // console.log(testBed.component.debug());
+ form.toggleEuiSwitch('ignoreUnavailableIndicesToggle');
+ actions.clickNextButton();
+
+ // Complete step 3
+ form.setInputValue('expireAfterValueInput', EXPIRE_AFTER_VALUE);
+ form.setInputValue('expireAfterUnitSelect', EXPIRE_AFTER_UNIT);
+ actions.clickNextButton();
+ });
+
+ it('should send the correct payload with changed values', async () => {
+ const { actions } = testBed;
+
+ // @ts-ignore (remove when react 16.9.0 is released)
+ await act(async () => {
+ actions.clickSubmitButton();
+ await nextTick();
+ });
+
+ const latestRequest = server.requests[server.requests.length - 1];
+
+ const expected = {
+ ...POLICY_EDIT,
+ ...{
+ config: {
+ ignoreUnavailable: true,
+ },
+ retention: {
+ expireAfterValue: Number(EXPIRE_AFTER_VALUE),
+ expireAfterUnit: EXPIRE_AFTER_UNIT,
+ },
+ snapshotName: `${POLICY_EDIT.snapshotName}-edited`,
+ },
+ };
+ expect(JSON.parse(latestRequest.requestBody)).toEqual(expected);
+ });
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/constants.ts b/x-pack/legacy/plugins/snapshot_restore/common/constants.ts
index a881bf3081c5e..f04a5d6dc6e75 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/constants.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/constants.ts
@@ -54,3 +54,10 @@ export const APP_REQUIRED_CLUSTER_PRIVILEGES = [
];
export const APP_RESTORE_INDEX_PRIVILEGES = ['monitor'];
export const APP_SLM_CLUSTER_PRIVILEGES = ['manage_slm'];
+
+export const TIME_UNITS: { [key: string]: 'd' | 'h' | 'm' | 's' } = {
+ DAY: 'd',
+ HOUR: 'h',
+ MINUTE: 'm',
+ SECOND: 's',
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/index.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/index.ts
index bede2689bb855..579dae0265939 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/lib/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/index.ts
@@ -12,5 +12,7 @@ export {
deserializeSnapshotDetails,
deserializeSnapshotConfig,
serializeSnapshotConfig,
+ deserializeSnapshotRetention,
+ serializeSnapshotRetention,
} from './snapshot_serialization';
export { deserializePolicy, serializePolicy } from './policy_serialization';
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts
index 86adde4db7f99..9ce9367bc0e0e 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { deserializePolicy } from './policy_serialization';
+import { deserializePolicy, serializePolicy } from './policy_serialization';
describe('repository_serialization', () => {
describe('deserializePolicy()', () => {
@@ -25,6 +25,11 @@ describe('repository_serialization', () => {
foo: 'bar',
},
},
+ retention: {
+ expire_after: '14d',
+ max_count: 30,
+ min_count: 4,
+ },
},
next_execution: '2019-07-11T01:30:00.000Z',
next_execution_millis: 1562722200000,
@@ -45,6 +50,12 @@ describe('repository_serialization', () => {
foo: 'bar',
},
},
+ retention: {
+ expireAfterValue: 14,
+ expireAfterUnit: 'd',
+ maxCount: 30,
+ minCount: 4,
+ },
nextExecution: '2019-07-11T01:30:00.000Z',
nextExecutionMillis: 1562722200000,
});
@@ -112,4 +123,48 @@ describe('repository_serialization', () => {
});
});
});
+
+ describe('serializePolicy()', () => {
+ it('should serialize a slm policy', () => {
+ expect(
+ serializePolicy({
+ name: 'my-snapshot-policy',
+ snapshotName: 'my-backups-snapshots',
+ schedule: '0 30 1 * * ?',
+ repository: 'my-backups',
+ config: {
+ indices: ['kibana-*'],
+ includeGlobalState: false,
+ ignoreUnavailable: false,
+ metadata: {
+ foo: 'bar',
+ },
+ },
+ retention: {
+ expireAfterValue: 14,
+ expireAfterUnit: 'd',
+ maxCount: 30,
+ minCount: 4,
+ },
+ })
+ ).toEqual({
+ name: 'my-backups-snapshots',
+ schedule: '0 30 1 * * ?',
+ repository: 'my-backups',
+ config: {
+ indices: ['kibana-*'],
+ include_global_state: false,
+ ignore_unavailable: false,
+ metadata: {
+ foo: 'bar',
+ },
+ },
+ retention: {
+ expire_after: '14d',
+ max_count: 30,
+ min_count: 4,
+ },
+ });
+ });
+ });
});
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts
index dc52765670540..4652ac4bc5cc4 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts
@@ -4,19 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { SlmPolicy, SlmPolicyEs, SlmPolicyPayload } from '../types';
-import { deserializeSnapshotConfig, serializeSnapshotConfig } from './';
+import {
+ deserializeSnapshotConfig,
+ serializeSnapshotConfig,
+ deserializeSnapshotRetention,
+ serializeSnapshotRetention,
+} from './';
export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolicy => {
const {
version,
modified_date: modifiedDate,
modified_date_millis: modifiedDateMillis,
- policy: { name: snapshotName, schedule, repository, config },
+ policy: { name: snapshotName, schedule, repository, config, retention },
next_execution: nextExecution,
next_execution_millis: nextExecutionMillis,
last_failure: lastFailure,
last_success: lastSuccess,
in_progress: inProgress,
+ stats,
} = esPolicy;
const policy: SlmPolicy = {
@@ -35,6 +41,10 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic
policy.config = deserializeSnapshotConfig(config);
}
+ if (retention) {
+ policy.retention = deserializeSnapshotRetention(retention);
+ }
+
if (lastFailure) {
const {
snapshot_name: failureSnapshotName,
@@ -82,11 +92,27 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic
};
}
+ if (stats) {
+ const {
+ snapshots_taken: snapshotsTaken,
+ snapshots_failed: snapshotsFailed,
+ snapshots_deleted: snapshotsDeleted,
+ snapshot_deletion_failures: snapshotDeletionFailures,
+ } = stats;
+
+ policy.stats = {
+ snapshotsTaken,
+ snapshotsFailed,
+ snapshotsDeleted,
+ snapshotDeletionFailures,
+ };
+ }
+
return policy;
};
export const serializePolicy = (policy: SlmPolicyPayload): SlmPolicyEs['policy'] => {
- const { snapshotName: name, schedule, repository, config } = policy;
+ const { snapshotName: name, schedule, repository, config, retention } = policy;
const policyEs: SlmPolicyEs['policy'] = {
name,
schedule,
@@ -97,5 +123,13 @@ export const serializePolicy = (policy: SlmPolicyPayload): SlmPolicyEs['policy']
policyEs.config = serializeSnapshotConfig(config);
}
+ if (retention) {
+ const serializedRetention = serializeSnapshotRetention(retention);
+
+ if (serializedRetention) {
+ policyEs.retention = serializeSnapshotRetention(retention);
+ }
+ }
+
return policyEs;
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts
index b1f6d2005a2e3..50fdef4175787 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts
@@ -6,7 +6,16 @@
import { sortBy } from 'lodash';
-import { SnapshotDetails, SnapshotDetailsEs, SnapshotConfig, SnapshotConfigEs } from '../types';
+import {
+ SnapshotDetails,
+ SnapshotDetailsEs,
+ SnapshotConfig,
+ SnapshotConfigEs,
+ SnapshotRetention,
+ SnapshotRetentionEs,
+} from '../types';
+
+import { deserializeTime, serializeTime } from './time_serialization';
export function deserializeSnapshotDetails(
repository: string,
@@ -128,3 +137,68 @@ export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): Snapsho
return config;
}, {});
}
+
+export function deserializeSnapshotRetention(
+ snapshotRetentionEs: SnapshotRetentionEs
+): SnapshotRetention {
+ const {
+ expire_after: expireAfter,
+ max_count: maxCount,
+ min_count: minCount,
+ } = snapshotRetentionEs;
+
+ let expireAfterValue;
+ let expireAfterUnit;
+
+ if (expireAfter) {
+ const { timeValue, timeUnit } = deserializeTime(expireAfter);
+
+ if (timeValue && timeUnit) {
+ expireAfterValue = timeValue;
+ expireAfterUnit = timeUnit;
+ }
+ }
+
+ const snapshotRetention: SnapshotRetention = {
+ expireAfterValue,
+ expireAfterUnit,
+ maxCount,
+ minCount,
+ };
+
+ return Object.entries(snapshotRetention).reduce((retention: any, [key, value]) => {
+ if (value !== undefined) {
+ retention[key] = value;
+ }
+ return retention;
+ }, {});
+}
+
+export function serializeSnapshotRetention(
+ snapshotRetention: SnapshotRetention
+): SnapshotRetentionEs | undefined {
+ const { expireAfterValue, expireAfterUnit, minCount, maxCount } = snapshotRetention;
+
+ const snapshotRetentionEs: SnapshotRetentionEs = {
+ expire_after:
+ expireAfterValue && expireAfterUnit
+ ? serializeTime(expireAfterValue, expireAfterUnit)
+ : undefined,
+ min_count: !minCount ? undefined : minCount,
+ max_count: !maxCount ? undefined : maxCount,
+ };
+
+ const flattenedSnapshotRetentionEs = Object.entries(snapshotRetentionEs).reduce(
+ (retention: any, [key, value]) => {
+ if (value !== undefined) {
+ retention[key] = value;
+ }
+ return retention;
+ },
+ {}
+ );
+
+ return Object.entries(flattenedSnapshotRetentionEs).length
+ ? flattenedSnapshotRetentionEs
+ : undefined;
+}
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/time_serialization.test.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/time_serialization.test.ts
new file mode 100644
index 0000000000000..f661c0c585852
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/time_serialization.test.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { deserializeTime, serializeTime } from './time_serialization';
+import { TIME_UNITS } from '../constants';
+
+describe('time_serialization', () => {
+ describe('deserializeTime()', () => {
+ it('should deserialize valid ES time', () => {
+ Object.values(TIME_UNITS).forEach(unit => {
+ expect(deserializeTime(`15${unit}`)).toEqual({
+ timeValue: 15,
+ timeUnit: unit,
+ });
+ });
+ });
+ it('should return an empty object if time unit is invalid', () => {
+ expect(deserializeTime('15foobar')).toEqual({});
+ expect(deserializeTime('15minutes')).toEqual({});
+ });
+ });
+ describe('serializeTime()', () => {
+ it('should serialize ES time', () => {
+ expect(serializeTime(15, 'd')).toEqual('15d');
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/time_serialization.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/time_serialization.ts
new file mode 100644
index 0000000000000..5f65ec861e81b
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/time_serialization.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 { TIME_UNITS } from '../constants';
+
+export const deserializeTime = (time: string) => {
+ const timeUnits = Object.values(TIME_UNITS);
+
+ const timeUnit = timeUnits.find(unit => {
+ const unitIndex = time.indexOf(unit);
+ return unitIndex !== -1 && unitIndex === time.length - 1;
+ });
+
+ if (timeUnit) {
+ const timeValue = Number(time.replace(timeUnit, ''));
+
+ if (!isNaN(timeValue)) {
+ return {
+ timeValue,
+ timeUnit,
+ };
+ }
+ }
+
+ return {};
+};
+
+export const serializeTime = (timeValue: number, timeUnit: string) => {
+ return `${timeValue}${timeUnit}`; // e.g., '15d'
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts b/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts
index 888cad13d213b..ed67b1eb77063 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts
@@ -4,14 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SnapshotConfig, SnapshotConfigEs } from './snapshot';
-
+import {
+ SnapshotConfig,
+ SnapshotConfigEs,
+ SnapshotRetention,
+ SnapshotRetentionEs,
+} from './snapshot';
export interface SlmPolicyPayload {
name: string;
snapshotName: string;
schedule: string;
repository: string;
config?: SnapshotConfig;
+ retention?: SnapshotRetention;
}
export interface SlmPolicy extends SlmPolicyPayload {
@@ -34,6 +39,12 @@ export interface SlmPolicy extends SlmPolicyPayload {
inProgress?: {
snapshotName: string;
};
+ stats?: {
+ snapshotsTaken: number;
+ snapshotsFailed: number;
+ snapshotsDeleted: number;
+ snapshotDeletionFailures: number;
+ };
}
export interface SlmPolicyEs {
@@ -45,6 +56,7 @@ export interface SlmPolicyEs {
schedule: string;
repository: string;
config?: SnapshotConfigEs;
+ retention?: SnapshotRetentionEs;
};
next_execution: string;
next_execution_millis: number;
@@ -66,4 +78,10 @@ export interface SlmPolicyEs {
start_time: string;
start_time_millis: number;
};
+ stats?: {
+ snapshots_taken: number;
+ snapshots_failed: number;
+ snapshots_deleted: number;
+ snapshot_deletion_failures: number;
+ };
}
diff --git a/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts
index dd561bd50d352..46713c937fd3f 100644
--- a/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts
@@ -79,3 +79,16 @@ interface SnapshotDetailsShardsStatusEs {
failed: number;
successful: number;
}
+
+export interface SnapshotRetention {
+ expireAfterValue?: number | '';
+ expireAfterUnit?: string;
+ maxCount?: number | '';
+ minCount?: number | '';
+}
+
+export interface SnapshotRetentionEs {
+ expire_after?: string;
+ max_count?: number;
+ min_count?: number;
+}
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 a367e529cf63b..a1f3d3554a266 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
@@ -16,4 +16,8 @@ export { SnapshotDeleteProvider } from './snapshot_delete_provider';
export { RestoreSnapshotForm } from './restore_snapshot_form';
export { PolicyExecuteProvider } from './policy_execute_provider';
export { PolicyDeleteProvider } from './policy_delete_provider';
+export {
+ UpdateRetentionModalProvider,
+ UpdateRetentionSetting,
+} from './update_retention_modal_provider';
export { PolicyForm } from './policy_form';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/navigation.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/navigation.tsx
index ba9877a9e9f41..6bb376b9298ed 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/navigation.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/navigation.tsx
@@ -41,14 +41,23 @@ export const PolicyNavigation: React.FunctionComponent = ({
onClick: () => updateCurrentStep(2),
},
{
- title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepReviewName', {
- defaultMessage: 'Review',
+ title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepRetentionName', {
+ defaultMessage: 'Snapshot retention',
}),
- isComplete: maxCompletedStep >= 2,
+ isComplete: maxCompletedStep >= 3,
isSelected: currentStep === 3,
disabled: maxCompletedStep < 2,
onClick: () => updateCurrentStep(3),
},
+ {
+ title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepReviewName', {
+ defaultMessage: 'Review',
+ }),
+ isComplete: maxCompletedStep >= 3,
+ isSelected: currentStep === 4,
+ disabled: maxCompletedStep < 3,
+ onClick: () => updateCurrentStep(4),
+ },
];
return ;
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx
index 6c631ab8e6c69..7e55cee63a0ac 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx
@@ -13,9 +13,15 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { SlmPolicyPayload } from '../../../../common/types';
+import { TIME_UNITS } from '../../../../common/constants';
import { PolicyValidation, validatePolicy } from '../../services/validation';
import { useAppDependencies } from '../../index';
-import { PolicyStepLogistics, PolicyStepSettings, PolicyStepReview } from './steps';
+import {
+ PolicyStepLogistics,
+ PolicyStepSettings,
+ PolicyStepRetention,
+ PolicyStepReview,
+} from './steps';
import { PolicyNavigation } from './navigation';
interface Props {
@@ -53,7 +59,8 @@ export const PolicyForm: React.FunctionComponent = ({
const stepMap: { [key: number]: any } = {
1: PolicyStepLogistics,
2: PolicyStepSettings,
- 3: PolicyStepReview,
+ 3: PolicyStepRetention,
+ 4: PolicyStepReview,
};
const CurrentStepForm = stepMap[currentStep];
@@ -63,6 +70,11 @@ export const PolicyForm: React.FunctionComponent = ({
config: {
...(originalPolicy.config || {}),
},
+ retention: {
+ ...(originalPolicy.retention || {
+ expireAfterUnit: TIME_UNITS.DAY,
+ }),
+ },
});
// Policy validation state
@@ -161,7 +173,9 @@ export const PolicyForm: React.FunctionComponent = ({
fill
iconType="arrowRight"
onClick={() => onNext()}
+ iconSide="right"
disabled={!validation.isValid}
+ data-test-subj="nextButton"
>
= ({
iconType="check"
onClick={() => savePolicy()}
isLoading={isSaving}
+ data-test-subj="submitButton"
>
{isSaving ? (
= ({
}}
onBlur={() => setTouched({ ...touched, schedule: true })}
placeholder={DEFAULT_POLICY_SCHEDULE}
- data-test-subj="snapshotNameInput"
+ data-test-subj="advancedCronInput"
/>
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx
new file mode 100644
index 0000000000000..b32d579650134
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_retention.tsx
@@ -0,0 +1,238 @@
+/*
+ * 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, useState } from 'react';
+
+import {
+ EuiDescribedFormGroup,
+ EuiTitle,
+ EuiFormRow,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiFieldNumber,
+ EuiSelect,
+} from '@elastic/eui';
+
+import { SlmPolicyPayload } from '../../../../../common/types';
+import { TIME_UNITS } from '../../../../../common/constants';
+import { documentationLinksService } from '../../../services/documentation';
+import { useAppDependencies } from '../../../index';
+import { StepProps } from './';
+import { textService } from '../../../services/text';
+
+const getExpirationTimeOptions = (unitSize = '0') =>
+ Object.entries(TIME_UNITS).map(([_key, value]) => ({
+ text: textService.getTimeUnitLabel(value, unitSize),
+ value,
+ }));
+
+export const PolicyStepRetention: React.FunctionComponent = ({
+ policy,
+ updatePolicy,
+ errors,
+}) => {
+ const {
+ core: { i18n },
+ } = useAppDependencies();
+ const { FormattedMessage } = i18n;
+
+ const { retention = {} } = policy;
+
+ const updatePolicyRetention = (updatedFields: Partial): void => {
+ const newRetention = { ...retention, ...updatedFields };
+ updatePolicy({
+ retention: newRetention,
+ });
+ };
+
+ // State for touched inputs
+ const [touched, setTouched] = useState({
+ expireAfterValue: false,
+ minCount: false,
+ maxCount: false,
+ });
+
+ const renderExpireAfterField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="expirationDescription"
+ fullWidth
+ >
+
+ }
+ describedByIds={['expirationDescription']}
+ isInvalid={touched.expireAfterValue && Boolean(errors.expireAfterValue)}
+ error={errors.expireAfter}
+ fullWidth
+ >
+
+
+ setTouched({ ...touched, expireAfterValue: true })}
+ onChange={e => {
+ const value = e.target.value;
+ updatePolicyRetention({
+ expireAfterValue: value !== '' ? Number(value) : value,
+ });
+ }}
+ data-test-subj="expireAfterValueInput"
+ />
+
+
+ {
+ updatePolicyRetention({
+ expireAfterUnit: e.target.value,
+ });
+ }}
+ data-test-subj="expireAfterUnitSelect"
+ />
+
+
+
+
+ );
+
+ const renderCountFields = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ idAria="countDescription"
+ fullWidth
+ >
+
+
+
+ }
+ describedByIds={['countDescription']}
+ isInvalid={touched.minCount && Boolean(errors.minCount)}
+ error={errors.minCount}
+ fullWidth
+ >
+ setTouched({ ...touched, minCount: true })}
+ onChange={e => {
+ const value = e.target.value;
+ updatePolicyRetention({
+ minCount: value !== '' ? Number(value) : value,
+ });
+ }}
+ data-test-subj="minCountInput"
+ />
+
+
+
+
+ }
+ describedByIds={['countDescription']}
+ error={errors.maxCount}
+ fullWidth
+ >
+ setTouched({ ...touched, maxCount: true })}
+ onChange={e => {
+ const value = e.target.value;
+ updatePolicyRetention({
+ maxCount: value !== '' ? Number(value) : value,
+ });
+ }}
+ data-test-subj="maxCountInput"
+ />
+
+
+
+
+ );
+
+ return (
+
+ {/* Step title and doc link */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {renderExpireAfterField()}
+ {renderCountFields()}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_review.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_review.tsx
index 2599aa4b19bb1..b2f9a4231e853 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_review.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_review.tsx
@@ -13,11 +13,11 @@ import {
EuiDescriptionListDescription,
EuiSpacer,
EuiTabbedContent,
+ EuiText,
EuiTitle,
EuiLink,
EuiIcon,
EuiToolTip,
- EuiText,
} from '@elastic/eui';
import { serializePolicy } from '../../../../../common/lib';
import { useAppDependencies } from '../../../index';
@@ -31,7 +31,7 @@ export const PolicyStepReview: React.FunctionComponent = ({
core: { i18n },
} = useAppDependencies();
const { FormattedMessage } = i18n;
- const { name, snapshotName, schedule, repository, config } = policy;
+ const { name, snapshotName, schedule, repository, config, retention } = policy;
const { indices, includeGlobalState, ignoreUnavailable, partial } = config || {
indices: undefined,
includeGlobalState: undefined,
@@ -48,8 +48,27 @@ export const PolicyStepReview: React.FunctionComponent = ({
const hiddenIndicesCount =
displayIndices && displayIndices.length > 10 ? displayIndices.length - 10 : 0;
+ const serializedPolicy = serializePolicy(policy);
+ const { retention: serializedRetention } = serializedPolicy;
+
+ const EditStepTooltip = ({ step }: { step: number }) => (
+
+ }
+ >
+ updateCurrentStep(step)}>
+
+
+
+ );
+
const renderSummaryTab = () => (
+ {/* Logistics summary */}
@@ -57,18 +76,7 @@ export const PolicyStepReview: React.FunctionComponent = ({
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.sectionLogisticsTitle"
defaultMessage="Logistics"
/>{' '}
-
- }
- >
- updateCurrentStep(1)}>
-
-
-
+
@@ -125,24 +133,15 @@ export const PolicyStepReview: React.FunctionComponent = ({
+
+ {/* Snapshot settings summary */}
{' '}
-
- }
- >
- updateCurrentStep(2)}>
-
-
-
+
@@ -279,12 +278,69 @@ export const PolicyStepReview: React.FunctionComponent = ({
+
+ {/* Retention summary */}
+ {serializedRetention ? (
+
+
+
+
+ {' '}
+
+
+
+
+
+
+ {retention!.expireAfterValue && (
+
+
+
+
+
+ {retention!.expireAfterValue}
+ {retention!.expireAfterUnit}
+
+
+ )}
+ {retention!.minCount && (
+
+
+
+
+ {retention!.minCount}
+
+ )}
+ {retention!.maxCount && (
+
+
+
+
+ {retention!.maxCount}
+
+ )}
+
+
+ ) : null}
);
const renderRequestTab = () => {
const endpoint = `PUT _slm/policy/${name}`;
- const json = JSON.stringify(serializePolicy(policy), null, 2);
+ const json = JSON.stringify(serializedPolicy, null, 2);
+
return (
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx
index 642440a8c5e91..6f1b2ed2cef4d 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx
@@ -109,6 +109,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({
/>
}
checked={isAllIndices}
+ data-test-subj="allIndicesToggle"
onChange={e => {
const isChecked = e.target.checked;
setIsAllIndices(isChecked);
@@ -162,6 +163,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({
{
setSelectIndicesMode('list');
updatePolicyConfig({ indices: indicesSelection });
@@ -186,6 +188,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({
selectOrDeselectAllLink:
config.indices && config.indices.length > 0 ? (
{
// TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
indicesOptions.forEach((option: Option) => {
@@ -313,6 +316,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({
fullWidth
>
= ({
>
}
@@ -397,6 +402,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({
fullWidth
>
React.ReactElement;
+}
+
+export type UpdateRetentionSetting = (
+ retentionSchedule?: string,
+ onSuccess?: OnSuccessCallback
+) => void;
+
+type OnSuccessCallback = () => void;
+
+export const UpdateRetentionModalProvider: React.FunctionComponent = ({ children }) => {
+ const {
+ core: {
+ i18n,
+ notification: { toastNotifications },
+ },
+ } = useAppDependencies();
+ const { FormattedMessage } = i18n;
+
+ const [retentionSchedule, setRetentionSchedule] = useState(DEFAULT_RETENTION_SCHEDULE);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const [saveError, setSaveError] = useState(null);
+ const [isAdvancedCronVisible, setIsAdvancedCronVisible] = useState(false);
+
+ const onSuccessCallback = useRef(null);
+
+ const [simpleCron, setSimpleCron] = useState<{
+ expression: string;
+ frequency: string;
+ }>({
+ expression: DEFAULT_RETENTION_SCHEDULE,
+ frequency: DEFAULT_RETENTION_FREQUENCY,
+ });
+
+ const [fieldToPreferredValueMap, setFieldToPreferredValueMap] = useState({});
+
+ const [isInvalid, setIsInvalid] = useState(false);
+
+ const updateRetentionPrompt: UpdateRetentionSetting = (
+ originalRetentionSchedule,
+ onSuccess = () => undefined
+ ) => {
+ setIsModalOpen(true);
+
+ setIsAdvancedCronVisible(
+ Boolean(originalRetentionSchedule && originalRetentionSchedule !== DEFAULT_RETENTION_SCHEDULE)
+ );
+
+ if (originalRetentionSchedule) {
+ setIsEditing(true);
+ setRetentionSchedule(originalRetentionSchedule);
+ }
+
+ onSuccessCallback.current = onSuccess;
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ };
+
+ const updateRetentionSetting = async () => {
+ if (!retentionSchedule) {
+ setIsInvalid(true);
+ return;
+ }
+
+ setIsSaving(true);
+ setSaveError(null);
+
+ const { error } = await updateRetentionSchedule(retentionSchedule);
+
+ setIsSaving(false);
+
+ if (error) {
+ setSaveError(error);
+ } else {
+ closeModal();
+
+ toastNotifications.addSuccess(
+ i18n.translate(
+ 'xpack.snapshotRestore.policyForm.stepRetention.policyUpdateRetentionSuccessMessage',
+ {
+ defaultMessage: 'Updated retention schedule',
+ }
+ )
+ );
+
+ if (onSuccessCallback.current) {
+ onSuccessCallback.current();
+ }
+ }
+ };
+
+ const renderModal = () => {
+ if (!isModalOpen) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {isEditing ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {saveError && (
+
+
+ }
+ color="danger"
+ iconType="alert"
+ >
+ {saveError.data && saveError.data.message ? (
+ {saveError.data.message}
+ ) : null}
+
+
+
+ )}
+ {isAdvancedCronVisible ? (
+
+
+ }
+ isInvalid={isInvalid}
+ error={i18n.translate(
+ 'xpack.snapshotRestore.policyForm.stepRetention.policyUpdateRetentionScheduleFieldErrorMessage',
+ {
+ defaultMessage: 'Retention schedule is required.',
+ }
+ )}
+ helpText={
+
+
+
+ ),
+ }}
+ />
+ }
+ fullWidth
+ >
+ setRetentionSchedule(e.target.value)}
+ />
+
+
+
+
+
+ {
+ setIsAdvancedCronVisible(false);
+ setRetentionSchedule(simpleCron.expression);
+ }}
+ data-test-subj="showBasicCronLink"
+ >
+
+
+
+
+ ) : (
+
+ {
+ setSimpleCron({
+ expression,
+ frequency,
+ });
+ setFieldToPreferredValueMap(newFieldToPreferredValueMap);
+ setRetentionSchedule(expression);
+ }}
+ />
+
+
+
+
+ {
+ setIsAdvancedCronVisible(true);
+ }}
+ data-test-subj="showAdvancedCronLink"
+ >
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+ {children(updateRetentionPrompt)}
+ {renderModal()}
+
+ );
+};
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 d95c243aeed62..56da4d8a50972 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
@@ -91,6 +91,9 @@ export const REMOVE_INDEX_SETTINGS_SUGGESTIONS: string[] = INDEX_SETTING_SUGGEST
export const DEFAULT_POLICY_SCHEDULE = '0 30 1 * * ?';
export const DEFAULT_POLICY_FREQUENCY = DAY;
+export const DEFAULT_RETENTION_SCHEDULE = '0 30 1 * * ?';
+export const DEFAULT_RETENTION_FREQUENCY = DAY;
+
// UI Metric constants
export const UIM_APP_NAME = 'snapshot_restore';
export const UIM_REPOSITORY_LIST_LOAD = 'repository_list_load';
@@ -119,3 +122,4 @@ export const UIM_POLICY_DELETE = 'policy_delete';
export const UIM_POLICY_DELETE_MANY = 'policy_delete_many';
export const UIM_POLICY_CREATE = 'policy_create';
export const UIM_POLICY_UPDATE = 'policy_update';
+export const UIM_POLICY_RETENTION_SETTINGS_UPDATE = 'policy_retention_settings_update';
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
index ea29d6492cb4b..68dc9fb164c70 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
@@ -3,7 +3,7 @@
* 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, { useState, useEffect } from 'react';
+import React, { useState, useEffect, Fragment } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@@ -14,6 +14,10 @@ import {
EuiDescriptionListDescription,
EuiIcon,
EuiText,
+ EuiPanel,
+ EuiStat,
+ EuiSpacer,
+ EuiHorizontalRule,
} from '@elastic/eui';
import { SlmPolicy } from '../../../../../../../common/types';
@@ -40,6 +44,8 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => {
schedule,
nextExecutionMillis,
config,
+ stats,
+ retention,
} = policy;
const { includeGlobalState, ignoreUnavailable, indices, partial } = config || {
includeGlobalState: undefined,
@@ -123,176 +129,306 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => {
}, []);
return (
-
-
-
-
-
-
-
-
- {version}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {snapshotName}
-
-
-
-
-
-
-
-
-
- {repository}
-
-
-
-
-
-
-
-
-
-
-
- {schedule}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isShowingFullIndicesList ? fullIndicesList : shortIndicesList}
-
-
-
-
-
-
-
-
-
- {ignoreUnavailable ? (
+
+ {/** Stats panel */}
+ {stats && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/** General description list */}
+
+
+
+
+
+
+
+
+
+
+
- ) : (
+
+
+
+ {version}
+
+
+
+
+
- )}
-
-
-
-
-
-
-
-
-
-
-
- {partial ? (
+
+
+
+
+
+
+
+
+
+
+
- ) : (
+
+
+
+ {snapshotName}
+
+
+
+
+
- )}
-
-
-
-
-
-
-
-
-
- {includeGlobalState === false ? (
+
+
+
+ {repository}
+
+
+
+
+
+
+
+
+
+
+
+ {schedule}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- ) : (
+
+
+
+ {isShowingFullIndicesList ? fullIndicesList : shortIndicesList}
+
+
+
+
+
+
+
+
+
+ {ignoreUnavailable ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {partial ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {includeGlobalState === false ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {retention && (
+
+
+
+
+ {/** Retention description list */}
+
+
+
+
+
+
+
+
+ {retention.expireAfterValue && (
+
+
+
+
+
+ {retention.expireAfterValue}
+ {retention.expireAfterUnit}
+
+
+ )}
+ {retention.minCount && (
+
+
+
+
+ {retention.minCount}
+
+ )}
+ {retention.maxCount && (
+
+
+
+
+ {retention.maxCount}
+
)}
-
-
-
-
+
+
+ )}
+
);
};
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 a4664ea414526..6dec1e04e2515 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
@@ -13,13 +13,14 @@ import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants';
import { SectionError, SectionLoading } from '../../../components';
import { BASE_PATH, UIM_POLICY_LIST_LOAD } from '../../../constants';
import { useAppDependencies } from '../../../index';
-import { useLoadPolicies } from '../../../services/http';
+import { useLoadPolicies, useLoadRetentionSettings } from '../../../services/http';
import { uiMetricService } from '../../../services/ui_metric';
import { linkToAddPolicy, linkToPolicy } from '../../../services/navigation';
import { WithPrivileges, NotAuthorizedSection } from '../../../lib/authorization';
import { PolicyDetails } from './policy_details';
import { PolicyTable } from './policy_table';
+import { PolicyRetentionSchedule } from './policy_retention_schedule';
interface MatchParams {
policyName?: SlmPolicy['name'];
@@ -46,6 +47,14 @@ export const PolicyList: React.FunctionComponent {
return linkToPolicy(newPolicyName);
};
@@ -137,6 +146,8 @@ export const PolicyList: React.FunctionComponent policy.schedule);
const hasDuplicateSchedules = policySchedules.length > new Set(policySchedules).size;
+ const hasRetention = Boolean(policies.find((policy: SlmPolicy) => policy.retention));
+
content = (
{hasDuplicateSchedules ? (
@@ -159,6 +170,16 @@ export const PolicyList: React.FunctionComponent
) : null}
+
+ {hasRetention ? (
+
+ ) : null}
+
void;
+ isLoading: boolean;
+ error: any;
+}
+
+export const PolicyRetentionSchedule: React.FunctionComponent = ({
+ retentionSettings,
+ onRetentionScheduleUpdated,
+ isLoading,
+ error,
+}) => {
+ const {
+ core: { i18n },
+ } = useAppDependencies();
+
+ const { FormattedMessage } = i18n;
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ }
+ color="danger"
+ iconType="alert"
+ >
+ {error.data && error.data.message ? {error.data.message}
: null}
+
+
+
+
+
+
+ );
+ }
+
+ if (retentionSettings && retentionSettings.retentionSchedule) {
+ const { retentionSchedule } = retentionSettings;
+
+ return (
+
+
+
+
+
+
+ {retentionSchedule} }}
+ />
+
+
+
+
+
+ {(updateRetentionPrompt: UpdateRetentionSetting) => {
+ return (
+
+ }
+ >
+
+ updateRetentionPrompt(retentionSchedule, onRetentionScheduleUpdated)
+ }
+ aria-label={i18n.translate(
+ 'xpack.snapshotRestore.policyRetentionSchedulePanel.retentionScheduleEditLinkAriaLabel',
+ {
+ defaultMessage: 'Edit retention schedule',
+ }
+ )}
+ />
+
+ );
+ }}
+
+
+
+
+
+
+ );
+ } else {
+ return (
+
+
+ }
+ color="warning"
+ iconType="alert"
+ >
+
+
+
+
+ {(updateRetentionPrompt: UpdateRetentionSetting) => {
+ return (
+ updateRetentionPrompt(undefined, onRetentionScheduleUpdated)}
+ >
+
+
+ );
+ }}
+
+
+
+
+ );
+ }
+};
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 cb2ced1411c47..8ce5f46daf284 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
@@ -146,6 +146,24 @@ export const PolicyTable: React.FunctionComponent = ({
truncateText: true,
sortable: true,
},
+ {
+ field: 'retention',
+ name: i18n.translate('xpack.snapshotRestore.policyList.table.retentionColumnTitle', {
+ defaultMessage: 'Retention',
+ }),
+ render: (retention: SlmPolicy['retention']) =>
+ retention ? (
+
+ ) : null,
+ },
{
field: 'nextExecutionMillis',
name: i18n.translate('xpack.snapshotRestore.policyList.table.nextExecutionColumnTitle', {
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx
index 1af3cfb4d133e..ea2b8b9904d8f 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/tabs/tab_failures.tsx
@@ -70,8 +70,8 @@ export const TabFailures: React.SFC = ({ indexFailures, snapshotState })
-
- {status}: {reason}
+
+ {`${status}: ${reason}`}
{failuresCount < failures.length - 1 ? : undefined}
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx
index 3f186dad142bb..0547e811d4617 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx
@@ -8,12 +8,13 @@ import { RouteComponentProps } from 'react-router-dom';
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
import { SlmPolicyPayload } from '../../../../common/types';
+import { TIME_UNITS } from '../../../../common/constants';
import { PolicyForm, SectionError, SectionLoading } from '../../components';
import { useAppDependencies } from '../../index';
import { BASE_PATH, DEFAULT_POLICY_SCHEDULE } from '../../constants';
import { breadcrumbService, docTitleService } from '../../services/navigation';
-import { addPolicy, useLoadIndicies } from '../../services/http';
+import { addPolicy, useLoadIndices } from '../../services/http';
export const PolicyAdd: React.FunctionComponent = ({
history,
@@ -33,7 +34,7 @@ export const PolicyAdd: React.FunctionComponent = ({
data: { indices } = {
indices: [],
},
- } = useLoadIndicies();
+ } = useLoadIndices();
// Set breadcrumb and page title
useEffect(() => {
@@ -64,6 +65,12 @@ export const PolicyAdd: React.FunctionComponent = ({
schedule: DEFAULT_POLICY_SCHEDULE,
repository: '',
config: {},
+ retention: {
+ expireAfterValue: '',
+ expireAfterUnit: TIME_UNITS.DAY,
+ maxCount: '',
+ minCount: '',
+ },
};
const renderSaveError = () => {
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx
index 4ada745062c6f..eabb2d71754ea 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx
@@ -3,17 +3,18 @@
* 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, { useEffect, useState, Fragment } from 'react';
+import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
import { SlmPolicyPayload } from '../../../../common/types';
+import { TIME_UNITS } from '../../../../common/constants';
import { SectionError, SectionLoading, PolicyForm } from '../../components';
import { BASE_PATH } from '../../constants';
import { useAppDependencies } from '../../index';
import { breadcrumbService, docTitleService } from '../../services/navigation';
-import { editPolicy, useLoadPolicy, useLoadIndicies } from '../../services/http';
+import { editPolicy, useLoadPolicy, useLoadIndices } from '../../services/http';
interface MatchParams {
name: string;
@@ -44,6 +45,12 @@ export const PolicyEdit: React.FunctionComponent
-
-
+
);
};
@@ -195,7 +200,7 @@ export const PolicyEdit: React.FunctionComponent
-
+
{
});
};
-export const useLoadIndicies = () => {
+export const useLoadIndices = () => {
return useRequest({
path: httpService.addBasePath(`${API_BASE_PATH}policies/indices`),
method: 'get',
@@ -86,3 +87,24 @@ export const editPolicy = async (editedPolicy: SlmPolicyPayload) => {
trackUiMetric(UIM_POLICY_UPDATE);
return result;
};
+
+export const useLoadRetentionSettings = () => {
+ return useRequest({
+ path: httpService.addBasePath(`${API_BASE_PATH}policies/retention_settings`),
+ method: 'get',
+ });
+};
+
+export const updateRetentionSchedule = (retentionSchedule: string) => {
+ const result = sendRequest({
+ path: httpService.addBasePath(`${API_BASE_PATH}policies/retention_settings`),
+ method: 'put',
+ body: {
+ retentionSchedule,
+ },
+ });
+
+ const { trackUiMetric } = uiMetricService;
+ trackUiMetric(UIM_POLICY_RETENTION_SETTINGS_UPDATE);
+ return result;
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/text/text.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/text/text.ts
index ec92250373a05..e3b5b0115d687 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/text/text.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/text/text.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { REPOSITORY_TYPES } from '../../../../common/constants';
+import { REPOSITORY_TYPES, TIME_UNITS } from '../../../../common/constants';
class TextService {
public breadcrumbs: { [key: string]: string } = {};
@@ -112,6 +112,31 @@ class TextService {
},
});
}
+
+ public getTimeUnitLabel(timeUnit: 'd' | 'h' | 'm' | 's', timeValue: string) {
+ switch (timeUnit) {
+ case TIME_UNITS.SECOND:
+ return this.i18n.translate('xpack.snapshotRestore.policyForm.timeUnits.secondLabel', {
+ defaultMessage: '{timeValue, plural, one {second} other {seconds}}',
+ values: { timeValue },
+ });
+ case TIME_UNITS.MINUTE:
+ return this.i18n.translate('xpack.snapshotRestore.policyForm.timeUnits.minuteLabel', {
+ defaultMessage: '{timeValue, plural, one {minute} other {minutes}}',
+ values: { timeValue },
+ });
+ case TIME_UNITS.HOUR:
+ return this.i18n.translate('xpack.snapshotRestore.policyForm.timeUnits.hourLabel', {
+ defaultMessage: '{timeValue, plural, one {hour} other {hours}}',
+ values: { timeValue },
+ });
+ case TIME_UNITS.DAY:
+ return this.i18n.translate('xpack.snapshotRestore.policyForm.timeUnits.dayLabel', {
+ defaultMessage: '{timeValue, plural, one {day} other {days}}',
+ values: { timeValue },
+ });
+ }
+ }
}
export const textService = new TextService();
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts
index 53c62da97bdac..8a60740b1610c 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts
@@ -18,7 +18,7 @@ const isStringEmpty = (str: string | null): boolean => {
export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
const i18n = textService.i18n;
- const { name, snapshotName, schedule, repository, config } = policy;
+ const { name, snapshotName, schedule, repository, config, retention } = policy;
const validation: PolicyValidation = {
isValid: true,
@@ -28,12 +28,13 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
schedule: [],
repository: [],
indices: [],
+ minCount: [],
},
};
if (isStringEmpty(name)) {
validation.errors.name.push(
- i18n.translate('xpack.snapshotRestore.policyValidation.nameRequiredError', {
+ i18n.translate('xpack.snapshotRestore.policyValidation.nameRequiredErroMessage', {
defaultMessage: 'Policy name is required.',
})
);
@@ -41,7 +42,7 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
if (isStringEmpty(snapshotName)) {
validation.errors.snapshotName.push(
- i18n.translate('xpack.snapshotRestore.policyValidation.snapshotNameRequiredError', {
+ i18n.translate('xpack.snapshotRestore.policyValidation.snapshotNameRequiredErrorMessage', {
defaultMessage: 'Snapshot name is required.',
})
);
@@ -49,7 +50,7 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
if (isStringEmpty(schedule)) {
validation.errors.schedule.push(
- i18n.translate('xpack.snapshotRestore.policyValidation.scheduleRequiredError', {
+ i18n.translate('xpack.snapshotRestore.policyValidation.scheduleRequiredErrorMessage', {
defaultMessage: 'Schedule is required.',
})
);
@@ -57,7 +58,7 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
if (isStringEmpty(repository)) {
validation.errors.repository.push(
- i18n.translate('xpack.snapshotRestore.policyValidation.repositoryRequiredError', {
+ i18n.translate('xpack.snapshotRestore.policyValidation.repositoryRequiredErrorMessage', {
defaultMessage: 'Repository is required.',
})
);
@@ -65,7 +66,7 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
if (config && typeof config.indices === 'string' && config.indices.trim().length === 0) {
validation.errors.indices.push(
- i18n.translate('xpack.snapshotRestore.policyValidation.indexPatternRequiredError', {
+ i18n.translate('xpack.snapshotRestore.policyValidation.indexPatternRequiredErrorMessage', {
defaultMessage: 'At least one index pattern is required.',
})
);
@@ -73,12 +74,24 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
if (config && Array.isArray(config.indices) && config.indices.length === 0) {
validation.errors.indices.push(
- i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredError', {
+ i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredErrorMessage', {
defaultMessage: 'You must select at least one index.',
})
);
}
+ if (
+ retention &&
+ retention.minCount &&
+ retention.maxCount &&
+ retention.minCount > retention.maxCount
+ ) {
+ validation.errors.minCount.push(
+ i18n.translate('xpack.snapshotRestore.policyValidation.invalidMinCountErrorMessage', {
+ defaultMessage: 'Min count cannot be greater than max count.',
+ })
+ );
+ }
// Remove fields with no errors
validation.errors = Object.entries(validation.errors)
.filter(([key, value]) => value.length > 0)
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts b/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts
index 10c7a86d640e6..77db8dd993c2e 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts
@@ -39,11 +39,7 @@ export class Plugin {
textService.init(i18n);
breadcrumbService.init(chrome, management.constants.BREADCRUMB);
uiMetricService.init(uiMetric.createUiStatsReporter);
- documentationLinksService.init(
- documentation.esDocBasePath,
- documentation.esPluginDocBasePath,
- documentation.esStackOverviewDocBasePath
- );
+ documentationLinksService.init(documentation.esDocBasePath, documentation.esPluginDocBasePath);
docTitleService.init(docTitle.change);
const unmountReactApp = (): void => {
diff --git a/x-pack/legacy/plugins/snapshot_restore/public/shim.ts b/x-pack/legacy/plugins/snapshot_restore/public/shim.ts
index 02574890afffd..595edbfd1cea4 100644
--- a/x-pack/legacy/plugins/snapshot_restore/public/shim.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/public/shim.ts
@@ -53,7 +53,6 @@ export interface Core extends AppCore {
documentation: {
esDocBasePath: string;
esPluginDocBasePath: string;
- esStackOverviewDocBasePath: string;
};
docTitle: {
change: typeof docTitle.change;
@@ -113,7 +112,6 @@ export function createShim(): { core: Core; plugins: Plugins } {
documentation: {
esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`,
esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`,
- esStackOverviewDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-overview/${DOC_LINK_VERSION}/`,
},
docTitle: {
change: docTitle.change,
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 52e6449559bcc..c0016a4f643cd 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
@@ -12,9 +12,10 @@ import {
createHandler,
updateHandler,
getIndicesHandler,
+ updateRetentionSettingsHandler,
} from './policy';
-describe('[Snapshot and Restore API Routes] Restore', () => {
+describe('[Snapshot and Restore API Routes] Policy', () => {
const mockRequest = {} as Request;
const mockResponseToolkit = {} as ResponseToolkit;
const mockEsPolicy = {
@@ -25,6 +26,11 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
schedule: '0 30 1 * * ?',
repository: 'my-backups',
config: {},
+ retention: {
+ expire_after: '15d',
+ min_count: 5,
+ max_count: 10,
+ },
},
next_execution_millis: 1562722200000,
};
@@ -35,6 +41,12 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
schedule: '0 30 1 * * ?',
repository: 'my-backups',
config: {},
+ retention: {
+ expireAfterValue: 15,
+ expireAfterUnit: 'd',
+ minCount: 5,
+ maxCount: 10,
+ },
nextExecutionMillis: 1562722200000,
};
@@ -323,4 +335,29 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
).rejects.toThrow();
});
});
+
+ describe('updateRetentionSettingsHandler()', () => {
+ const retentionSettings = {
+ retentionSchedule: '0 30 1 * * ?',
+ };
+ const mockCreateRequest = ({
+ payload: retentionSettings,
+ } as unknown) as Request;
+
+ it('should return successful ES response', async () => {
+ const mockEsResponse = { acknowledged: true };
+ const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
+ const expectedResponse = { ...mockEsResponse };
+ await expect(
+ updateRetentionSettingsHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
+ ).resolves.toEqual(expectedResponse);
+ });
+
+ it('should throw if ES error', async () => {
+ const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
+ await expect(
+ updateRetentionSettingsHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
+ ).rejects.toThrow();
+ });
+ });
});
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 ed16a44bccdc6..ef9e48190a5b7 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
@@ -10,8 +10,12 @@ import {
} from '../../../../../server/lib/create_router/error_wrappers';
import { SlmPolicyEs, SlmPolicy, SlmPolicyPayload } from '../../../common/types';
import { deserializePolicy, serializePolicy } from '../../../common/lib';
+import { Plugins } from '../../../shim';
-export function registerPolicyRoutes(router: Router) {
+let callWithInternalUser: any;
+
+export function registerPolicyRoutes(router: Router, plugins: Plugins) {
+ callWithInternalUser = plugins.elasticsearch.getCluster('data').callWithInternalUser;
router.get('policies', getAllHandler);
router.get('policy/{name}', getOneHandler);
router.post('policy/{name}/run', executeHandler);
@@ -19,10 +23,12 @@ export function registerPolicyRoutes(router: Router) {
router.put('policies', createHandler);
router.put('policies/{name}', updateHandler);
router.get('policies/indices', getIndicesHandler);
+ router.get('policies/retention_settings', getRetentionSettingsHandler);
+ router.put('policies/retention_settings', updateRetentionSettingsHandler);
}
export const getAllHandler: RouterRouteHandler = async (
- req,
+ _req,
callWithRequest
): Promise<{
policies: SlmPolicy[];
@@ -144,7 +150,7 @@ export const updateHandler: RouterRouteHandler = async (req, callWithRequest) =>
};
export const getIndicesHandler: RouterRouteHandler = async (
- req,
+ _req,
callWithRequest
): Promise<{
indices: string[];
@@ -161,3 +167,38 @@ export const getIndicesHandler: RouterRouteHandler = async (
indices: indices.map(({ index }) => index).sort(),
};
};
+
+export const getRetentionSettingsHandler: RouterRouteHandler = async (): Promise<
+ | {
+ [key: string]: string;
+ }
+ | undefined
+> => {
+ const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', {
+ filterPath: '**.slm.retention*',
+ includeDefaults: true,
+ });
+ const { slm: retentionSettings = undefined } = {
+ ...defaults,
+ ...persistent,
+ ...transient,
+ };
+
+ const { retention_schedule: retentionSchedule } = retentionSettings;
+
+ return { retentionSchedule };
+};
+
+export const updateRetentionSettingsHandler: RouterRouteHandler = async (req, callWithRequest) => {
+ const { retentionSchedule } = req.payload as { retentionSchedule: string };
+
+ return await callWithRequest('cluster.putSettings', {
+ body: {
+ persistent: {
+ slm: {
+ retention_schedule: retentionSchedule,
+ },
+ },
+ },
+ });
+};
diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/register_routes.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/register_routes.ts
index 5a76f1c268138..11a6cad86640e 100644
--- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/register_routes.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/register_routes.ts
@@ -20,6 +20,6 @@ export const registerRoutes = (router: Router, plugins: Plugins): void => {
registerRestoreRoutes(router);
if (isSlmEnabled) {
- registerPolicyRoutes(router);
+ registerPolicyRoutes(router, plugins);
}
};
diff --git a/x-pack/legacy/plugins/snapshot_restore/test/fixtures/index.ts b/x-pack/legacy/plugins/snapshot_restore/test/fixtures/index.ts
index 1e744a96d81cf..f3f2f0faa744d 100644
--- a/x-pack/legacy/plugins/snapshot_restore/test/fixtures/index.ts
+++ b/x-pack/legacy/plugins/snapshot_restore/test/fixtures/index.ts
@@ -6,3 +6,4 @@
export * from './repository';
export * from './snapshot';
+export * from './policy';
diff --git a/x-pack/legacy/plugins/snapshot_restore/test/fixtures/policy.ts b/x-pack/legacy/plugins/snapshot_restore/test/fixtures/policy.ts
new file mode 100644
index 0000000000000..3dc5f78c42457
--- /dev/null
+++ b/x-pack/legacy/plugins/snapshot_restore/test/fixtures/policy.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { getRandomString, getRandomNumber } from '../../../../../test_utils';
+import { SlmPolicy } from '../../common/types';
+import { DEFAULT_POLICY_SCHEDULE } from '../../public/app/constants';
+
+const dateNow = new Date();
+const randomModifiedDateMillis = new Date().setDate(dateNow.getDate() - 1);
+const randomExecutionDateMillis = new Date().setDate(dateNow.getDate() + 1);
+
+const DEFAULT_STATS: SlmPolicy['stats'] = {
+ snapshotsTaken: 0,
+ snapshotsFailed: 0,
+ snapshotsDeleted: 0,
+ snapshotDeletionFailures: 0,
+};
+
+export const getPolicy = ({
+ name = `policy-${getRandomString()}`,
+ config = {},
+ modifiedDate = new Date(randomModifiedDateMillis).toString(),
+ modifiedDateMillis = randomModifiedDateMillis,
+ nextExecution = new Date(randomExecutionDateMillis).toString(),
+ nextExecutionMillis = randomExecutionDateMillis,
+ repository = `repo-${getRandomString()}`,
+ retention = {},
+ schedule = DEFAULT_POLICY_SCHEDULE,
+ snapshotName = `snapshot-${getRandomString()}`,
+ stats = DEFAULT_STATS,
+ version = getRandomNumber(),
+}: Partial = {}): SlmPolicy => ({
+ name,
+ config,
+ modifiedDate,
+ modifiedDateMillis,
+ nextExecution,
+ nextExecutionMillis,
+ repository,
+ retention,
+ schedule,
+ snapshotName,
+ stats,
+ version,
+});
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index af96f6eeb7bad..22f861a132063 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -10801,7 +10801,6 @@
"xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "インデックスを選択",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "利用不可能なプライマリシャードのインデックスのスナップショットを許可します。これが設定されていない場合、スナップショット全体がエラーになります。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "部分インデックスを許可",
- "xpack.snapshotRestore.policyForm.stepSettings.partialLabel": "部分インデックスを許可",
"xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "グローバルステータスを含める",
"xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "すべて選択",
"xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}がバックアップされます。{selectOrDeselectAllLink}",
@@ -10833,12 +10832,6 @@
"xpack.snapshotRestore.policyList.table.snapshotNameColumnTitle": "スナップショット名",
"xpack.snapshotRestore.policyScheduleWarningDescription": "一度に 1 つのスナップショットしか撮影できません。スナップショットのエラーを避けるために、ポリシーを編集または削除してください。",
"xpack.snapshotRestore.policyScheduleWarningTitle": "2 つ以上のポリシーに同じスケジュールが設定されています",
- "xpack.snapshotRestore.policyValidation.indexPatternRequiredError": "インデックスパターンが最低 1 つ必要です。",
- "xpack.snapshotRestore.policyValidation.indicesRequiredError": "1 つ以上のインデックスを選択する必要があります。",
- "xpack.snapshotRestore.policyValidation.nameRequiredError": "ポリシー名が必要です。",
- "xpack.snapshotRestore.policyValidation.repositoryRequiredError": "レポジトリが必要です。",
- "xpack.snapshotRestore.policyValidation.scheduleRequiredError": "スケジュールが必要です。",
- "xpack.snapshotRestore.policyValidation.snapshotNameRequiredError": "スナップショット名が必要です。",
"xpack.snapshotRestore.repositories.breadcrumbTitle": "レポジトリ",
"xpack.snapshotRestore.repositoryList.table.typeFilterLabel": "タイプ",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "インデックスパターン",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 7aa1095d227ce..68e13c2a436a9 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -10803,7 +10803,6 @@
"xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "选择索引",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "允许具有不可用主分片的索引的快照。否则,整个快照将失败。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "允许部分索引",
- "xpack.snapshotRestore.policyForm.stepSettings.partialLabel": "允许部分索引",
"xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "包括全局状态",
"xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "全选",
"xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "将备份 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}",
@@ -10835,12 +10834,6 @@
"xpack.snapshotRestore.policyList.table.snapshotNameColumnTitle": "快照名称",
"xpack.snapshotRestore.policyScheduleWarningDescription": "一次仅可以拍取一个快照。要避免快照失败,请编辑或删除策略。",
"xpack.snapshotRestore.policyScheduleWarningTitle": "两个或更多策略有相同的计划",
- "xpack.snapshotRestore.policyValidation.indexPatternRequiredError": "至少需要一个索引模式。",
- "xpack.snapshotRestore.policyValidation.indicesRequiredError": "必须至少选择一个索引。",
- "xpack.snapshotRestore.policyValidation.nameRequiredError": "策略名称必填。",
- "xpack.snapshotRestore.policyValidation.repositoryRequiredError": "存储库必填。",
- "xpack.snapshotRestore.policyValidation.scheduleRequiredError": "计划必填。",
- "xpack.snapshotRestore.policyValidation.snapshotNameRequiredError": "快照名称必填。",
"xpack.snapshotRestore.repositories.breadcrumbTitle": "存储库",
"xpack.snapshotRestore.repositoryList.table.typeFilterLabel": "类型",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "索引模式",
diff --git a/x-pack/test_utils/testbed/types.ts b/x-pack/test_utils/testbed/types.ts
index 31140c21cb530..b9ced88f3774c 100644
--- a/x-pack/test_utils/testbed/types.ts
+++ b/x-pack/test_utils/testbed/types.ts
@@ -85,7 +85,7 @@ export interface TestBed {
*
* @param switchTestSubject The test subject of the EuiSwitch (can be a nested path. e.g. "myForm.mySwitch").
*/
- toggleEuiSwitch: (switchTestSubject: T) => void;
+ toggleEuiSwitch: (switchTestSubject: T, isChecked?: boolean) => void;
/**
* The EUI ComboBox is a special input as it needs the ENTER key to be pressed
* in order to register the value set. This helpers automatically does that.