diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/constant.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/constant.ts index a04c8aed403d9..0a2979f66851c 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/constant.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/constant.ts @@ -15,4 +15,8 @@ export const POLICY_NAME = 'my-test-policy'; export const SNAPSHOT_NAME = 'my-test-snapshot'; -export const POLICY_EDIT = getPolicy({ name: POLICY_NAME, retention: { minCount: 1 } }); +export const POLICY_EDIT = getPolicy({ + name: POLICY_NAME, + retention: { minCount: 1 }, + config: { includeGlobalState: true, featureStates: ['kibana'] }, +}); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts index e75d0c3544b74..835566b19c4e3 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/http_requests.ts @@ -90,6 +90,11 @@ const registerHttpRequestMockHelpers = ( error?: ResponseError ) => mockResponse('GET', `${API_BASE_PATH}policies/indices`, response, error); + const setLoadPoliciesResponse = ( + response: HttpResponse = { indices: [] }, + error?: ResponseError + ) => mockResponse('GET', `${API_BASE_PATH}policies`, response, error); + const setAddPolicyResponse = (response?: HttpResponse, error?: ResponseError) => mockResponse('POST', `${API_BASE_PATH}policies`, response, error); @@ -119,6 +124,9 @@ const registerHttpRequestMockHelpers = ( error ); + const setLoadFeaturesResponse = (response: HttpResponse = [], error?: ResponseError) => + mockResponse('GET', `${API_BASE_PATH}policies/features`, response, error); + return { setLoadRepositoriesResponse, setLoadRepositoryTypesResponse, @@ -131,6 +139,8 @@ const registerHttpRequestMockHelpers = ( setGetPolicyResponse, setCleanupRepositoryResponse, setRestoreSnapshotResponse, + setLoadFeaturesResponse, + setLoadPoliciesResponse, }; }; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts index c776a22ef9868..d5e3e63bbdc49 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts @@ -40,6 +40,11 @@ export const formSetup = async ( export type PolicyFormTestSubjects = | 'advancedCronInput' | 'allIndicesToggle' + | 'globalStateToggle' + | 'featureStatesDropdown' + | 'toggleIncludeNone' + | 'noFeatureStatesCallout' + | 'featureStatesToggle' | 'backButton' | 'deselectIndicesLink' | 'allDataStreamsToggle' diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_list.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_list.helpers.ts new file mode 100644 index 0000000000000..b1a3631ac5ba8 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_list.helpers.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { + registerTestBed, + AsyncTestBedConfig, + TestBed, + findTestSubject, +} from '@kbn/test-jest-helpers'; +import { HttpSetup } from '@kbn/core/public'; +import { PolicyList } from '../../../public/application/sections/home/policy_list'; +import { WithAppDependencies } from './setup_environment'; + +const testBedConfig: AsyncTestBedConfig = { + memoryRouter: { + initialEntries: ['/policies'], + componentRoutePath: '/policies/:policyName?', + }, + doMountAsync: true, +}; + +const createActions = (testBed: TestBed) => { + const clickPolicyAt = async (index: number) => { + const { component, table, router } = testBed; + const { rows } = table.getMetaData('policyTable'); + const repositoryLink = findTestSubject(rows[index].reactWrapper, 'policyLink'); + + await act(async () => { + const { href } = repositoryLink.props(); + router.navigateTo(href!); + }); + + component.update(); + }; + + return { + clickPolicyAt, + }; +}; + +export type PoliciesListTestBed = TestBed & { + actions: ReturnType; +}; + +export const setupPoliciesListPage = async (httpSetup: HttpSetup) => { + const initTestBed = registerTestBed(WithAppDependencies(PolicyList, httpSetup), testBedConfig); + + const testBed = await initTestBed(); + + return { + ...testBed, + actions: createActions(testBed), + }; +}; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts index afc5e14140b01..988a65a912a6c 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/restore_snapshot.helpers.ts @@ -51,6 +51,14 @@ const setupActions = (testBed: TestBed) => { component.update(); }, + async toggleFeatureState() { + await act(async () => { + form.toggleEuiSwitch('includeFeatureStatesSwitch'); + }); + + component.update(); + }, + toggleIncludeAliases() { act(() => { form.toggleEuiSwitch('includeAliasesSwitch'); @@ -99,9 +107,13 @@ export type RestoreSnapshotFormTestSubject = | 'snapshotRestoreStepLogistics' | 'includeGlobalStateSwitch' | 'includeAliasesSwitch' + | 'featureStatesDropdown' + | 'includeFeatureStatesSwitch' + | 'toggleIncludeNone' | 'nextButton' | 'restoreButton' | 'systemIndicesInfoCallOut' + | 'noFeatureStatesCallout' | 'dataStreamWarningCallOut' | 'restoreSnapshotsForm.backButton' | 'restoreSnapshotsForm.nextButton' diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx index 0ba6ee619b16c..1124a179d245d 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/setup_environment.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { merge } from 'lodash'; import { LocationDescriptorObject } from 'history'; import { HttpSetup } from '@kbn/core/public'; @@ -16,16 +17,30 @@ import { breadcrumbService, docTitleService, } from '../../../public/application/services/navigation'; +import { + AuthorizationContext, + Authorization, + Privileges, + GlobalFlyout, +} from '../../../public/shared_imports'; import { AppContextProvider } from '../../../public/application/app_context'; import { textService } from '../../../public/application/services/text'; import { init as initHttpRequests } from './http_requests'; import { UiMetricService } from '../../../public/application/services'; +const { GlobalFlyoutProvider } = GlobalFlyout; const history = scopedHistoryMock.create(); history.createHref.mockImplementation((location: LocationDescriptorObject) => { return `${location.pathname}?${location.search}`; }); +const createAuthorizationContextValue = (privileges: Privileges) => { + return { + isLoading: false, + privileges: privileges ?? { hasAllPrivileges: false, missingPrivileges: {} }, + } as Authorization; +}; + export const services = { uiMetricService: new UiMetricService('snapshot_restore'), httpService, @@ -60,16 +75,24 @@ export const setupEnvironment = () => { this.terminate = () => {}; }; -export const WithAppDependencies = (Comp: any, httpSetup?: HttpSetup) => (props: any) => { - // We need to optionally setup the httpService since some cit helpers (such as snapshot_list.helpers) - // use jest mocks to stub the fetch hooks instead of mocking api responses. - if (httpSetup) { - httpService.setup(httpSetup); - } +export const WithAppDependencies = + (Comp: any, httpSetup?: HttpSetup, { privileges, ...overrides }: Record = {}) => + (props: any) => { + // We need to optionally setup the httpService since some cit helpers (such as snapshot_list.helpers) + // use jest mocks to stub the fetch hooks instead of mocking api responses. + if (httpSetup) { + httpService.setup(httpSetup); + } - return ( - - - - ); -}; + return ( + + + + + + + + ); + }; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts index 09a726a6b9abe..2b13553d4e604 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/home.test.ts @@ -497,10 +497,12 @@ describe('', () => { const snapshot1 = fixtures.getSnapshot({ repository: REPOSITORY_NAME, snapshot: `a${getRandomString()}`, + featureStates: ['kibana'], }); const snapshot2 = fixtures.getSnapshot({ repository: REPOSITORY_NAME, snapshot: `b${getRandomString()}`, + includeGlobalState: false, }); const snapshots = [snapshot1, snapshot2]; @@ -709,6 +711,30 @@ describe('', () => { expect(exists('snapshotDetail')).toBe(false); }); + test('should show feature states if include global state is enabled', async () => { + const { find } = testBed; + + // Assert against first snapshot shown in the table, which should have includeGlobalState and a featureState + expect(find('includeGlobalState.value').text()).toEqual('Yes'); + expect(find('snapshotFeatureStatesSummary.featureStatesList').text()).toEqual('kibana'); + + // Close the flyout + find('snapshotDetail.closeButton').simulate('click'); + + // Replace the get snapshot details api call with the payload of the second snapshot which we're about to click + httpRequestsMockHelpers.setGetSnapshotResponse( + snapshot2.repository, + snapshot2.snapshot, + snapshot2 + ); + + // Now we will assert against the second result of the table which shouldnt have includeGlobalState or a featureState + await testBed.actions.clickSnapshotAt(1); + + expect(find('includeGlobalState.value').text()).toEqual('No'); + expect(find('snapshotFeatureStatesSummary.value').text()).toEqual('No'); + }); + describe('tabs', () => { test('should have 2 tabs', () => { const { find } = testBed; @@ -738,7 +764,10 @@ describe('', () => { ); 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.includeGlobalState.value').text()).toEqual('Yes'); + expect( + find('snapshotDetail.snapshotFeatureStatesSummary.featureStatesList').text() + ).toEqual('kibana'); expect(find('snapshotDetail.indices.title').text()).toBe( `Indices (${indices.length})` ); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index 0950eda8fc630..1007e0d583b52 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -6,16 +6,18 @@ */ // import helpers first, this also sets up the mocks -import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; +import { setupEnvironment, pageHelpers, getRandomString } from './helpers'; import { ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; +import { HttpFetchOptionsWithPath } from '@kbn/core/public'; import * as fixtures from '../../test/fixtures'; import { API_BASE_PATH } from '../../common'; import { PolicyFormTestBed } from './helpers/policy_form.helpers'; import { DEFAULT_POLICY_SCHEDULE } from '../../public/application/constants'; +import { FEATURE_STATES_NONE_OPTION } from '../../common/constants'; const { setup } = pageHelpers.policyAdd; @@ -46,9 +48,11 @@ describe('', () => { indices: ['my_index'], dataStreams: ['my_data_stream', 'my_other_data_stream'], }); + httpRequestsMockHelpers.setLoadFeaturesResponse({ + features: [{ name: 'kibana' }, { name: 'tasks' }], + }); testBed = await setup(httpSetup); - await nextTick(); testBed.component.update(); }); @@ -137,9 +141,8 @@ describe('', () => { await act(async () => { // Toggle "All indices" switch form.toggleEuiSwitch('allIndicesToggle'); - await nextTick(); - component.update(); }); + component.update(); // Deselect all indices from list find('deselectIndicesLink').simulate('click'); @@ -155,7 +158,6 @@ describe('', () => { await act(async () => { // Toggle "All indices" switch form.toggleEuiSwitch('allIndicesToggle'); - await nextTick(); }); component.update(); @@ -210,6 +212,123 @@ describe('', () => { }); }); + describe('feature states', () => { + beforeEach(async () => { + const { actions, form, component } = testBed; + + // Complete step 1 + form.setInputValue('nameInput', POLICY_NAME); + form.setInputValue('snapshotNameInput', SNAPSHOT_NAME); + actions.clickNextButton(); + + component.update(); + }); + + test('Enabling include global state enables include feature state', async () => { + const { find, component, form } = testBed; + + // By default includeGlobalState is enabled, so we need to toogle twice + await act(async () => { + form.toggleEuiSwitch('globalStateToggle'); + form.toggleEuiSwitch('globalStateToggle'); + }); + component.update(); + + expect(find('featureStatesToggle').props().disabled).toBeUndefined(); + }); + + test('feature states dropdown is only shown when include feature states is enabled', async () => { + const { exists, component, form } = testBed; + + // By default the toggle is enabled + expect(exists('featureStatesDropdown')).toBe(true); + + await act(async () => { + form.toggleEuiSwitch('featureStatesToggle'); + }); + component.update(); + + expect(exists('featureStatesDropdown')).toBe(false); + }); + + test('include all features by default', async () => { + const { actions } = testBed; + + // Complete step 2 + actions.clickNextButton(); + // Complete step 3 + actions.clickNextButton(); + + await act(async () => { + actions.clickSubmitButton(); + }); + + const lastReq: HttpFetchOptionsWithPath[] = httpSetup.post.mock.calls.pop() || []; + const [requestUrl, requestBody] = lastReq; + const parsedReqBody = JSON.parse((requestBody as Record).body); + + expect(requestUrl).toBe(`${API_BASE_PATH}policies`); + expect(parsedReqBody.config).toEqual({ + includeGlobalState: true, + featureStates: [], + }); + }); + + test('include some features', async () => { + const { actions, form } = testBed; + + form.setComboBoxValue('featureStatesDropdown', 'kibana'); + + // Complete step 2 + actions.clickNextButton(); + // Complete step 3 + actions.clickNextButton(); + + await act(async () => { + actions.clickSubmitButton(); + }); + + const lastReq: HttpFetchOptionsWithPath[] = httpSetup.post.mock.calls.pop() || []; + const [requestUrl, requestBody] = lastReq; + const parsedReqBody = JSON.parse((requestBody as Record).body); + + expect(requestUrl).toBe(`${API_BASE_PATH}policies`); + expect(parsedReqBody.config).toEqual({ + includeGlobalState: true, + featureStates: ['kibana'], + }); + }); + + test('include no features', async () => { + const { actions, form, component } = testBed; + + // Disable all features + await act(async () => { + form.toggleEuiSwitch('featureStatesToggle'); + }); + component.update(); + + // Complete step 2 + actions.clickNextButton(); + // Complete step 3 + actions.clickNextButton(); + + await act(async () => { + actions.clickSubmitButton(); + }); + + const lastReq: HttpFetchOptionsWithPath[] = httpSetup.post.mock.calls.pop() || []; + const [requestUrl, requestBody] = lastReq; + const parsedReqBody = JSON.parse((requestBody as Record).body); + + expect(requestUrl).toBe(`${API_BASE_PATH}policies`); + expect(parsedReqBody.config).toEqual({ + includeGlobalState: true, + featureStates: [FEATURE_STATES_NONE_OPTION], + }); + }); + }); + describe('form payload & api errors', () => { beforeEach(async () => { const { actions, form } = testBed; @@ -234,7 +353,6 @@ describe('', () => { await act(async () => { actions.clickSubmitButton(); - await nextTick(); }); expect(httpSetup.post).toHaveBeenLastCalledWith( @@ -245,7 +363,7 @@ describe('', () => { snapshotName: SNAPSHOT_NAME, schedule: DEFAULT_POLICY_SCHEDULE, repository: repository.name, - config: {}, + config: { featureStates: [], includeGlobalState: true }, retention: { expireAfterValue: Number(EXPIRE_AFTER_VALUE), expireAfterUnit: 'd', // default @@ -271,9 +389,8 @@ describe('', () => { await act(async () => { actions.clickSubmitButton(); - await nextTick(); - component.update(); }); + component.update(); expect(exists('savePolicyApiError')).toBe(true); expect(find('savePolicyApiError').text()).toContain(error.message); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index 117d6f0e0e223..59e52d8cf6539 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -35,6 +35,9 @@ describe('', () => { httpRequestsMockHelpers.setLoadRepositoriesResponse({ repositories: [{ name: POLICY_EDIT.repository }], }); + httpRequestsMockHelpers.setLoadFeaturesResponse({ + features: [{ name: 'kibana' }, { name: 'tasks' }], + }); testBed = await setup(httpSetup); @@ -151,6 +154,8 @@ describe('', () => { schedule, repository, config: { + featureStates: ['kibana'], + includeGlobalState: true, ignoreUnavailable: true, }, retention: { @@ -182,7 +187,7 @@ describe('', () => { await nextTick(); }); - const { name, isManagedPolicy, schedule, repository, retention, config, snapshotName } = + const { name, isManagedPolicy, schedule, repository, retention, snapshotName } = POLICY_EDIT; expect(httpSetup.put).toHaveBeenLastCalledWith( @@ -193,7 +198,10 @@ describe('', () => { snapshotName, schedule, repository, - config, + config: { + featureStates: ['kibana'], + includeGlobalState: true, + }, retention: { ...retention, expireAfterUnit: TIME_UNITS.DAY, // default value diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_list.test.tsx b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_list.test.tsx new file mode 100644 index 0000000000000..77410febb9bea --- /dev/null +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_list.test.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setupEnvironment } from './helpers'; +import { getPolicy } from '../../test/fixtures'; +import { setupPoliciesListPage, PoliciesListTestBed } from './helpers/policy_list.helpers'; + +const POLICY_WITH_GLOBAL_STATE_AND_FEATURES = getPolicy({ + name: 'with_state', + retention: { minCount: 1 }, + config: { includeGlobalState: true, featureStates: ['kibana'] }, +}); +const POLICY_WITHOUT_GLOBAL_STATE = getPolicy({ + name: 'without_state', + retention: { minCount: 1 }, + config: { includeGlobalState: false }, +}); + +const POLICY_WITH_JUST_GLOBAL_STATE = getPolicy({ + name: 'without_state', + retention: { minCount: 1 }, + config: { includeGlobalState: true }, +}); + +describe('', () => { + let testBed: PoliciesListTestBed; + const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPoliciesResponse({ + policies: [ + POLICY_WITH_GLOBAL_STATE_AND_FEATURES, + POLICY_WITHOUT_GLOBAL_STATE, + POLICY_WITH_JUST_GLOBAL_STATE, + ], + }); + httpRequestsMockHelpers.setGetPolicyResponse(POLICY_WITH_GLOBAL_STATE_AND_FEATURES.name, { + policy: POLICY_WITH_GLOBAL_STATE_AND_FEATURES, + }); + + testBed = await setupPoliciesListPage(httpSetup); + + testBed.component.update(); + }); + + describe('details flyout', () => { + test('should show the detail flyout when clicking on a policy', async () => { + const { exists, actions } = testBed; + + expect(exists('policyDetail')).toBe(false); + + await actions.clickPolicyAt(0); + + expect(exists('policyDetail')).toBe(true); + }); + + test('should show feature states if include global state is enabled', async () => { + const { find, actions } = testBed; + + // Assert against first result shown in the table, which should have includeGlobalState enabled + await actions.clickPolicyAt(0); + + expect(find('includeGlobalState.value').text()).toEqual('Yes'); + expect(find('policyFeatureStatesSummary.featureStatesList').text()).toEqual('kibana'); + + // Close the flyout + find('srPolicyDetailsFlyoutCloseButton').simulate('click'); + + // Replace the get policy details api call with the payload of the second row which we're about to click + httpRequestsMockHelpers.setGetPolicyResponse(POLICY_WITHOUT_GLOBAL_STATE.name, { + policy: POLICY_WITHOUT_GLOBAL_STATE, + }); + + // Now we will assert against the second result of the table which shouldnt have includeGlobalState + await actions.clickPolicyAt(1); + + expect(find('includeGlobalState.value').text()).toEqual('No'); + expect(find('policyFeatureStatesSummary.value').text()).toEqual('No'); + + // Close the flyout + find('srPolicyDetailsFlyoutCloseButton').simulate('click'); + }); + + test('When it only has include globalState summary should also mention that it includes all features', async () => { + const { find, actions } = testBed; + + // Replace the get policy details api call with the payload of the second row which we're about to click + httpRequestsMockHelpers.setGetPolicyResponse(POLICY_WITH_JUST_GLOBAL_STATE.name, { + policy: POLICY_WITH_JUST_GLOBAL_STATE, + }); + + // Assert against third result shown in the table, which should have just includeGlobalState enabled + await actions.clickPolicyAt(2); + + expect(find('includeGlobalState.value').text()).toEqual('Yes'); + expect(find('policyFeatureStatesSummary.value').text()).toEqual('All features'); + }); + }); +}); diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts index 1bd9898f9f1b2..e25b550b74de1 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/restore_snapshot.test.ts @@ -10,6 +10,7 @@ import { API_BASE_PATH } from '../../common'; import { pageHelpers, setupEnvironment } from './helpers'; import { RestoreSnapshotTestBed } from './helpers/restore_snapshot.helpers'; import { REPOSITORY_NAME, SNAPSHOT_NAME } from './helpers/constant'; +import { FEATURE_STATES_NONE_OPTION } from '../../common/constants'; import * as fixtures from '../../test/fixtures'; const { @@ -85,26 +86,45 @@ describe('', () => { }); }); - describe('global state', () => { - beforeEach(async () => { + describe('feature states', () => { + test('when no feature states hide dropdown and show no features callout', async () => { httpRequestsMockHelpers.setGetSnapshotResponse( REPOSITORY_NAME, SNAPSHOT_NAME, - fixtures.getSnapshot() + fixtures.getSnapshot({ featureStates: [] }) ); + await act(async () => { testBed = await setup(httpSetup); }); - testBed.component.update(); + + const { exists, actions } = testBed; + + actions.toggleGlobalState(); + expect(exists('systemIndicesInfoCallOut')).toBe(false); + expect(exists('featureStatesDropdown')).toBe(false); + expect(exists('noFeatureStatesCallout')).toBe(true); }); - test('shows an info callout when include_global_state is enabled', () => { + test('shows an extra info callout when includeFeatureState is enabled and we have featureStates present in snapshot', async () => { + httpRequestsMockHelpers.setGetSnapshotResponse( + REPOSITORY_NAME, + SNAPSHOT_NAME, + fixtures.getSnapshot({ featureStates: ['kibana'] }) + ); + + await act(async () => { + testBed = await setup(httpSetup); + }); + + testBed.component.update(); + const { exists, actions } = testBed; expect(exists('systemIndicesInfoCallOut')).toBe(false); - actions.toggleGlobalState(); + await actions.toggleFeatureState(); expect(exists('systemIndicesInfoCallOut')).toBe(true); }); @@ -137,6 +157,7 @@ describe('', () => { `${API_BASE_PATH}restore/${REPOSITORY_NAME}/${SNAPSHOT_NAME}`, expect.objectContaining({ body: JSON.stringify({ + featureStates: [FEATURE_STATES_NONE_OPTION], includeAliases: false, }), }) diff --git a/x-pack/plugins/snapshot_restore/common/constants.ts b/x-pack/plugins/snapshot_restore/common/constants.ts index cd323d9bc4819..bcabda17343fb 100644 --- a/x-pack/plugins/snapshot_restore/common/constants.ts +++ b/x-pack/plugins/snapshot_restore/common/constants.ts @@ -65,3 +65,5 @@ export const TIME_UNITS: { [key: string]: 'd' | 'h' | 'm' | 's' } = { MINUTE: 'm', SECOND: 's', }; + +export const FEATURE_STATES_NONE_OPTION = 'none'; diff --git a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts index 3a78001c742ff..697086e4ad424 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.test.ts @@ -28,7 +28,6 @@ describe('restore_settings_serialization()', () => { }); it('should serialize partial restore settings with index pattern', () => { - expect(serializeRestoreSettings({})).toEqual({}); expect( serializeRestoreSettings({ indices: 'foo*,bar', @@ -42,6 +41,18 @@ describe('restore_settings_serialization()', () => { }); }); + it('should serialize feature_states', () => { + expect( + serializeRestoreSettings({ + indices: ['foo'], + featureStates: ['kibana', 'machinelearning'], + }) + ).toEqual({ + indices: ['foo'], + feature_states: ['kibana', 'machinelearning'], + }); + }); + it('should serialize full restore settings', () => { expect( serializeRestoreSettings({ diff --git a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts index c017bc721884c..d452d8828d0de 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/restore_settings_serialization.ts @@ -22,6 +22,7 @@ export function serializeRestoreSettings(restoreSettings: RestoreSettings): Rest renamePattern, renameReplacement, includeGlobalState, + featureStates, partial, indexSettings, ignoreIndexSettings, @@ -44,6 +45,7 @@ export function serializeRestoreSettings(restoreSettings: RestoreSettings): Rest rename_pattern: renamePattern, rename_replacement: renameReplacement, include_global_state: includeGlobalState, + feature_states: featureStates, partial, index_settings: parsedIndexSettings, ignore_index_settings: ignoreIndexSettings, diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts index 22e1e9329ca2c..4a83bac807a29 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts @@ -114,6 +114,7 @@ describe('Snapshot serialization and deserialization', () => { indices: ['index1', 'index2', 'index3'], dataStreams: [], includeGlobalState: false, + featureStates: ['kibana'], // Failures are grouped and sorted by index, and the failures themselves are sorted by shard. indexFailures: [ { diff --git a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts index e6e047cfa5d7d..70cef8e16bc46 100644 --- a/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts +++ b/x-pack/plugins/snapshot_restore/common/lib/snapshot_serialization.ts @@ -103,6 +103,7 @@ export function deserializeSnapshotDetails( indices: snapshotIndicesWithoutSystemIndices, dataStreams: [...dataStreams].sort(), includeGlobalState, + featureStates: featureStates.map((feature) => feature.feature_name), state, startTime, startTimeInMillis, @@ -129,6 +130,7 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S indices, ignore_unavailable: ignoreUnavailable, include_global_state: includeGlobalState, + feature_states: featureStates, partial, metadata, } = snapshotConfigEs; @@ -137,6 +139,7 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S indices, ignoreUnavailable, includeGlobalState, + featureStates, partial, metadata, }; @@ -150,7 +153,8 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S } export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): SnapshotConfigEs { - const { indices, ignoreUnavailable, includeGlobalState, partial, metadata } = snapshotConfig; + const { indices, ignoreUnavailable, includeGlobalState, featureStates, partial, metadata } = + snapshotConfig; const maybeIndicesArray = csvToArray(indices); @@ -158,6 +162,7 @@ export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): Snapsho indices: maybeIndicesArray, ignore_unavailable: ignoreUnavailable, include_global_state: includeGlobalState, + feature_states: featureStates, partial, metadata, }; diff --git a/x-pack/plugins/snapshot_restore/common/types/indices.ts b/x-pack/plugins/snapshot_restore/common/types/indices.ts index 5b0e0ef0c5fd3..5e344ae6296ca 100644 --- a/x-pack/plugins/snapshot_restore/common/types/indices.ts +++ b/x-pack/plugins/snapshot_restore/common/types/indices.ts @@ -9,3 +9,10 @@ export interface PolicyIndicesResponse { indices: string[]; dataStreams: string[]; } + +export interface PolicyFeaturesResponse { + features: Array<{ + name: string; + description: string; + }>; +} diff --git a/x-pack/plugins/snapshot_restore/common/types/restore.ts b/x-pack/plugins/snapshot_restore/common/types/restore.ts index 9e9b91de1859e..7caaf2e2d0e2b 100644 --- a/x-pack/plugins/snapshot_restore/common/types/restore.ts +++ b/x-pack/plugins/snapshot_restore/common/types/restore.ts @@ -10,6 +10,7 @@ export interface RestoreSettings { renamePattern?: string; renameReplacement?: string; includeGlobalState?: boolean; + featureStates?: string[]; partial?: boolean; indexSettings?: string; ignoreIndexSettings?: string[]; @@ -22,6 +23,7 @@ export interface RestoreSettingsEs { rename_pattern?: string; rename_replacement?: string; include_global_state?: boolean; + feature_states?: string[]; partial?: boolean; index_settings?: { [key: string]: any }; ignore_index_settings?: string[]; diff --git a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts index baddcac7f5094..18e88fe5dadf7 100644 --- a/x-pack/plugins/snapshot_restore/common/types/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/common/types/snapshot.ts @@ -9,6 +9,7 @@ export interface SnapshotConfig { indices?: string | string[]; ignoreUnavailable?: boolean; includeGlobalState?: boolean; + featureStates?: string[]; partial?: boolean; metadata?: { [key: string]: string; @@ -19,6 +20,7 @@ export interface SnapshotConfigEs { indices?: string | string[]; ignore_unavailable?: boolean; include_global_state?: boolean; + feature_states?: string[]; partial?: boolean; metadata?: { [key: string]: string; @@ -34,6 +36,7 @@ export interface SnapshotDetails { indices: string[]; dataStreams: string[]; includeGlobalState: boolean; + featureStates: string[]; state: string; /** e.g. '2019-04-05T21:56:40.438Z' */ startTime: string; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx index f96ef15be4852..bf89e5703df7d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_data_streams_list.tsx @@ -28,15 +28,13 @@ export const CollapsibleDataStreamsList: React.FunctionComponent = ({ dat ) : ( <> -
    - {items.map((dataStream) => ( -
  • - - {dataStream} - -
  • - ))} -
+ {items.map((dataStream) => ( +
+ + {dataStream} + +
+ ))}
{hiddenItemsCount ? ( <> diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_feature_states.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_feature_states.tsx new file mode 100644 index 0000000000000..2edb9bac873b5 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_feature_states.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiTitle, EuiLink, EuiIcon, EuiText, EuiSpacer } from '@elastic/eui'; + +import { FEATURE_STATES_NONE_OPTION } from '../../../../common/constants'; +import { useCollapsibleList } from './use_collapsible_list'; + +interface Props { + featureStates: string[] | undefined; +} + +export const CollapsibleFeatureStatesList: React.FunctionComponent = ({ featureStates }) => { + const { isShowingFullList, setIsShowingFullList, items, hiddenItemsCount } = useCollapsibleList({ + items: featureStates, + }); + + if (items === 'all' || items.length === 0) { + return ( + + ); + } + + if (items.find((option) => option === FEATURE_STATES_NONE_OPTION)) { + return ( + + ); + } + + return ( + <> + + {items.map((feature) => ( +
+ + {feature} + +
+ ))} +
+ {hiddenItemsCount ? ( + <> + + + isShowingFullList ? setIsShowingFullList(false) : setIsShowingFullList(true) + } + > + {isShowingFullList ? ( + + ) : ( + + )}{' '} + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx index 5f05e69c36eda..7362a6634565a 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/collapsible_indices_list.tsx @@ -27,15 +27,13 @@ export const CollapsibleIndicesList: React.FunctionComponent = ({ indices ) : ( <> -
    - {items.map((index) => ( -
  • - - {index} - -
  • - ))} -
+ {items.map((index) => ( +
+ + {index} + +
+ ))}
{hiddenItemsCount ? ( <> diff --git a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts index c479cf2e5ead6..939da5001974a 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/collapsible_lists/index.ts @@ -7,3 +7,4 @@ export { CollapsibleIndicesList } from './collapsible_indices_list'; export { CollapsibleDataStreamsList } from './collapsible_data_streams_list'; +export { CollapsibleFeatureStatesList } from './collapsible_feature_states'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/feature_states_form_field/feature_states_form_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/feature_states_form_field/feature_states_form_field.tsx new file mode 100644 index 0000000000000..d4dab26a8ee9d --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/feature_states_form_field/feature_states_form_field.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useMemo } from 'react'; +import { sortBy } from 'lodash'; + +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { useServices } from '../../app_context'; +import { SlmPolicyPayload, RestoreSettings } from '../../../../common/types'; + +export type FeaturesOption = EuiComboBoxOptionOption; + +interface Props { + featuresOptions: string[]; + selectedOptions: FeaturesOption[]; + onUpdateFormSettings: ( + arg: Partial & Partial + ) => void; + isLoadingFeatures?: boolean; +} + +export const FeatureStatesFormField: FunctionComponent = ({ + isLoadingFeatures = false, + featuresOptions, + selectedOptions, + onUpdateFormSettings, +}) => { + const { i18n } = useServices(); + + const optionsList = useMemo(() => { + if (!isLoadingFeatures) { + const featuresList = featuresOptions.map((feature) => ({ + label: feature, + })); + + return sortBy(featuresList, 'label'); + } + + return []; + }, [isLoadingFeatures, featuresOptions]); + + const onChange = (selected: FeaturesOption[]) => { + onUpdateFormSettings({ + featureStates: selected.map((option) => option.label), + }); + }; + + return ( + + + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/feature_states_form_field/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/feature_states_form_field/index.ts new file mode 100644 index 0000000000000..80ccd6cf9370a --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/feature_states_form_field/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { FeatureStatesFormField } from './feature_states_form_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/index.ts index 77f53a3533a16..bb1ede0dd78c0 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/index.ts @@ -16,9 +16,14 @@ 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 { CollapsibleIndicesList, CollapsibleDataStreamsList } from './collapsible_lists'; +export { + CollapsibleIndicesList, + CollapsibleDataStreamsList, + CollapsibleFeatureStatesList, +} from './collapsible_lists'; export type { UpdateRetentionSettings } from './retention_update_modal_provider'; export { RetentionSettingsUpdateModalProvider } from './retention_update_modal_provider'; export type { ExecuteRetention } from './retention_execute_modal_provider'; export { RetentionExecuteModalProvider } from './retention_execute_modal_provider'; export { PolicyForm } from './policy_form'; +export { FeatureStatesFormField } from './feature_states_form_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx index 8603c72918bed..140e38ae12738 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx @@ -67,6 +67,14 @@ export const PolicyForm: React.FunctionComponent = ({ const [policy, setPolicy] = useState({ ...originalPolicy, config: { + // When creating a new policy includesGlobalState is enabled by default and the API will also + // include all featureStates into the snapshot when this happens. We need to take this case into account + // when creating the local state for the form and also set featureStates to be an empty array, which + // for the API it means that it will include all featureStates. + featureStates: [], + // IncludeGlobalState is set as default by the api, so we want to replicate that behaviour in our + // form state so that it gets explicitly represented in the request. + includeGlobalState: true, ...(originalPolicy.config || {}), }, retention: { diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx index 1f744f05d3627..2859661435af4 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_review.tsx @@ -25,6 +25,7 @@ import { serializePolicy } from '../../../../../common/lib'; import { useServices } from '../../../app_context'; import { StepProps } from '.'; import { CollapsibleIndicesList } from '../../collapsible_lists'; +import { PolicyFeatureStatesSummary } from '../../summaries'; export const PolicyStepReview: React.FunctionComponent = ({ policy, @@ -32,9 +33,10 @@ export const PolicyStepReview: React.FunctionComponent = ({ }) => { const { i18n } = useServices(); const { name, snapshotName, schedule, repository, config, retention } = policy; - const { indices, includeGlobalState, ignoreUnavailable, partial } = config || { + const { indices, includeGlobalState, featureStates, ignoreUnavailable, partial } = config || { indices: undefined, includeGlobalState: undefined, + featureStates: [], ignoreUnavailable: undefined, partial: undefined, }; @@ -131,7 +133,7 @@ export const PolicyStepReview: React.FunctionComponent = ({ - + {/* Snapshot settings summary */} @@ -189,43 +191,51 @@ export const PolicyStepReview: React.FunctionComponent = ({ - {partial ? ( + {includeGlobalState === false ? ( ) : ( )} + + + + + - {includeGlobalState === false ? ( + {partial ? ( ) : ( )} diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_feature_states_field/include_feature_states_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_feature_states_field/include_feature_states_field.tsx new file mode 100644 index 0000000000000..b0f63675fa32a --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_feature_states_field/include_feature_states_field.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiSwitch, + EuiSwitchEvent, + EuiTitle, + EuiCallOut, + EuiSpacer, + EuiComboBoxOptionOption, +} from '@elastic/eui'; + +import { FEATURE_STATES_NONE_OPTION } from '../../../../../../../../common/constants'; +import { SlmPolicyPayload } from '../../../../../../../../common/types'; +import { PolicyValidation } from '../../../../../../services/validation'; +import { useLoadFeatures } from '../../../../../../services/http/policy_requests'; +import { FeatureStatesFormField } from '../../../../../feature_states_form_field'; + +interface Props { + policy: SlmPolicyPayload; + onUpdate: (arg: Partial) => void; + errors: PolicyValidation['errors']; +} + +export type FeaturesOption = EuiComboBoxOptionOption; + +export const IncludeFeatureStatesField: FunctionComponent = ({ policy, onUpdate }) => { + const { config = {} } = policy; + const { + error: errorLoadingFeatures, + isLoading: isLoadingFeatures, + data: featuresResponse, + } = useLoadFeatures(); + + const featuresOptions = useMemo(() => { + const features = featuresResponse?.features || []; + return features.map((feature) => feature.name); + }, [featuresResponse]); + + const selectedOptions = useMemo(() => { + return config?.featureStates?.map((feature) => ({ label: feature })) as FeaturesOption[]; + }, [config.featureStates]); + + const isFeatureStatesToggleEnabled = + config.featureStates !== undefined && + !config.featureStates.includes(FEATURE_STATES_NONE_OPTION); + + const onFeatureStatesToggleChange = (event: EuiSwitchEvent) => { + const { checked } = event.target; + + onUpdate({ + featureStates: checked ? [] : [FEATURE_STATES_NONE_OPTION], + }); + }; + + return ( + +

+ +

+
+ } + description={ + + } + fullWidth + > + + + } + checked={isFeatureStatesToggleEnabled} + onChange={onFeatureStatesToggleChange} + /> + + + {isFeatureStatesToggleEnabled && ( + <> + + {!errorLoadingFeatures ? ( + + ) : ( + + } + /> + )} + + )} + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_feature_states_field/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_feature_states_field/index.ts new file mode 100644 index 0000000000000..e1f30a383ce80 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_feature_states_field/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { IncludeFeatureStatesField } from './include_feature_states_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_global_state_field/include_global_state_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_global_state_field/include_global_state_field.tsx new file mode 100644 index 0000000000000..a30083f05dffd --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_global_state_field/include_global_state_field.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiSwitch, + EuiSwitchEvent, + EuiTitle, + EuiComboBoxOptionOption, +} from '@elastic/eui'; + +import { FEATURE_STATES_NONE_OPTION } from '../../../../../../../../common/constants'; +import { SlmPolicyPayload } from '../../../../../../../../common/types'; +import { PolicyValidation } from '../../../../../../services/validation'; + +interface Props { + policy: SlmPolicyPayload; + onUpdate: (arg: Partial) => void; + errors: PolicyValidation['errors']; +} + +export type FeaturesOption = EuiComboBoxOptionOption; + +export const IncludeGlobalStateField: FunctionComponent = ({ policy, onUpdate }) => { + const { config = {} } = policy; + + const onIncludeGlobalStateToggle = (event: EuiSwitchEvent) => { + const { checked } = event.target; + const hasFeatureStates = !config?.featureStates?.includes(FEATURE_STATES_NONE_OPTION); + + onUpdate({ + includeGlobalState: checked, + // if we ever include global state, we want to preselect featureStates for the users + // so that we include all features as well. + featureStates: checked && !hasFeatureStates ? [] : config.featureStates || [], + }); + }; + + return ( + +

+ +

+ + } + description={ + + } + fullWidth + > + + + } + checked={config.includeGlobalState === undefined || config.includeGlobalState} + onChange={onIncludeGlobalStateToggle} + /> + +
+ ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_global_state_field/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_global_state_field/index.ts new file mode 100644 index 0000000000000..a04183aadc01d --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/include_global_state_field/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { IncludeGlobalStateField } from './include_global_state_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts index 258233c1ed369..f9aba2eb537ed 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts @@ -6,3 +6,5 @@ */ export { IndicesAndDataStreamsField } from './indices_and_data_streams_field'; +export { IncludeGlobalStateField } from './include_global_state_field'; +export { IncludeFeatureStatesField } from './include_feature_states_field'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx index cf2b92cf9e9d5..6ffd1b1ec031b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx @@ -17,8 +17,8 @@ import { EuiLink, EuiPanel, EuiSelectable, - EuiSelectableOption, EuiSpacer, + EuiSelectableOption, EuiSwitch, EuiTitle, EuiToolTip, @@ -115,7 +115,7 @@ export const IndicesAndDataStreamsField: FunctionComponent = ({ label={ } checked={isAllIndices} diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx index 7b74ef05aa0fc..e576a6ee090ec 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx @@ -21,7 +21,11 @@ import { import { SlmPolicyPayload } from '../../../../../../common/types'; import { StepProps } from '..'; -import { IndicesAndDataStreamsField } from './fields'; +import { + IndicesAndDataStreamsField, + IncludeGlobalStateField, + IncludeFeatureStatesField, +} from './fields'; import { useCore } from '../../../../app_context'; export const PolicyStepSettings: React.FunctionComponent = ({ @@ -127,45 +131,6 @@ export const PolicyStepSettings: React.FunctionComponent = ({ ); - const renderIncludeGlobalStateField = () => ( - -

- -

- - } - description={ - - } - fullWidth - > - - - } - checked={config.includeGlobalState === undefined || config.includeGlobalState} - onChange={(e) => { - updatePolicyConfig({ - includeGlobalState: e.target.checked, - }); - }} - /> - -
- ); return (
{/* Step title and doc link */} @@ -209,7 +174,9 @@ export const PolicyStepSettings: React.FunctionComponent = ({ {renderIgnoreUnavailableField()} {renderPartialField()} - {renderIncludeGlobalStateField()} + + +
); }; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx index 5419e6a4625c9..f1eb562b22420 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/restore_snapshot_form.tsx @@ -15,6 +15,7 @@ import { EuiForm, EuiSpacer, } from '@elastic/eui'; +import { FEATURE_STATES_NONE_OPTION } from '../../../../common/constants'; import { SnapshotDetails, RestoreSettings } from '../../../../common/types'; import { RestoreValidation, validateRestore } from '../../services/validation'; import { @@ -50,7 +51,12 @@ export const RestoreSnapshotForm: React.FunctionComponent = ({ const CurrentStepForm = stepMap[currentStep]; // Restore details state - const [restoreSettings, setRestoreSettings] = useState({}); + const [restoreSettings, setRestoreSettings] = useState({ + // Since includeGlobalState always includes all featureStates when enabled, + // we wanna keep in the local state that no feature states will be restored + // by default. + featureStates: [FEATURE_STATES_NONE_OPTION], + }); // Restore validation state const [validation, setValidation] = useState({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx index fa6ee6c91abf1..e9fd9559deecf 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState } from 'react'; +import React, { Fragment, useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import semverGt from 'semver/functions/gt'; import { @@ -20,11 +20,15 @@ import { EuiSelectable, EuiSpacer, EuiSwitch, + EuiSwitchEvent, EuiTitle, + EuiCallOut, EuiComboBox, + EuiComboBoxOptionOption, } from '@elastic/eui'; import { EuiSelectableOption } from '@elastic/eui'; +import { FEATURE_STATES_NONE_OPTION } from '../../../../../../common/constants'; import { csvToArray, isDataStreamBackingIndex } from '../../../../../../common/lib'; import { RestoreSettings } from '../../../../../../common/types'; @@ -41,6 +45,10 @@ import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_li import { SystemIndicesOverwrittenCallOut } from './system_indices_overwritten_callout'; +import { FeatureStatesFormField } from '../../../feature_states_form_field'; + +export type FeaturesOption = EuiComboBoxOptionOption; + export const RestoreSnapshotStepLogistics: React.FunctionComponent = ({ snapshotDetails, restoreSettings, @@ -54,6 +62,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = dataStreams: snapshotDataStreams = [], includeGlobalState: snapshotIncludeGlobalState, version, + featureStates: snapshotIncludeFeatureStates, } = snapshotDetails; const snapshotIndices = unfilteredSnapshotIndices.filter( @@ -81,6 +90,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = renameReplacement, partial, includeGlobalState, + featureStates, includeAliases, } = restoreSettings; @@ -148,6 +158,21 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = renameReplacement: '', }); + const selectedFeatureStateOptions = useMemo(() => { + return featureStates?.map((feature) => ({ label: feature })) as FeaturesOption[]; + }, [featureStates]); + + const isFeatureStatesToggleEnabled = + featureStates !== undefined && !featureStates?.includes(FEATURE_STATES_NONE_OPTION); + + const onFeatureStatesToggleChange = (event: EuiSwitchEvent) => { + const { checked } = event.target; + + updateRestoreSettings({ + featureStates: checked ? [] : [FEATURE_STATES_NONE_OPTION], + }); + }; + return (
= label={ } checked={isAllIndicesAndDataStreams} @@ -568,32 +593,65 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } + description={ + + } + fullWidth + > + + ) + } + > + + } + checked={includeGlobalState === undefined ? false : includeGlobalState} + onChange={(e) => updateRestoreSettings({ includeGlobalState: e.target.checked })} + disabled={!snapshotIncludeGlobalState} + data-test-subj="includeGlobalStateSwitch" + /> + + + + {/* Include feature states */} + +

+ +

+ + } description={ <> - {i18n.translate( - 'xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink', - { defaultMessage: 'Learn more.' } - )} - - ), - }} + id="xpack.snapshotRestore.restoreForm.stepLogistics.includeFeatureStatesDescription" + defaultMessage="Restores the configuration, history, and other data stored in Elasticsearch by a feature such as Elasticsearch security." /> - {/* Only display callout if include_global_state is enabled and the snapshot was created by ES 7.12+ - * Note: Once we support features states in the UI, we will also need to add a check here for that - * See https://github.com/elastic/kibana/issues/95128 more details - */} - {includeGlobalState && semverGt(version, '7.12.0') && ( + {/* Only display callout if includeFeatureState is enabled and the snapshot was created by ES 7.12+ */} + {semverGt(version, '7.12.0') && isFeatureStatesToggleEnabled && ( <> - + )} @@ -604,9 +662,9 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = hasEmptyLabelSpace={true} fullWidth helpText={ - snapshotIncludeGlobalState ? null : ( + snapshotIncludeFeatureStates ? null : ( ) @@ -615,16 +673,42 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent = } - checked={includeGlobalState === undefined ? false : includeGlobalState} - onChange={(e) => updateRestoreSettings({ includeGlobalState: e.target.checked })} - disabled={!snapshotIncludeGlobalState} - data-test-subj="includeGlobalStateSwitch" + checked={isFeatureStatesToggleEnabled} + onChange={onFeatureStatesToggleChange} + disabled={snapshotIncludeFeatureStates?.length === 0} + data-test-subj="includeFeatureStatesSwitch" /> + + {isFeatureStatesToggleEnabled && ( + <> + + + + )} + {snapshotIncludeFeatureStates?.length === 0 && ( + <> + + + + )}
{/* Include aliases */} diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx index fac21de0bce22..da351b959ea10 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/system_indices_overwritten_callout.tsx @@ -9,7 +9,9 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiCallOut } from '@elastic/eui'; -export const SystemIndicesOverwrittenCallOut: FunctionComponent = () => { +export const SystemIndicesOverwrittenCallOut: FunctionComponent<{ + featureStates: string[] | undefined; +}> = ({ featureStates }) => { return ( { 'xpack.snapshotRestore.restoreForm.stepLogistics.systemIndicesCallOut.title', { defaultMessage: - 'When this snapshot is restored, system indices will be overwritten with data from the snapshot.', + 'When this snapshot is restored, system indices {featuresCount, plural, =0 {} other {from {features}}} will be overwritten with data from the snapshot.', + values: { + featuresCount: featureStates?.length || 0, + features: featureStates?.join(', '), + }, } )} iconType="pin" diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx index 6ee119a2da62e..2f650ee754259 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx @@ -26,7 +26,8 @@ import { serializeRestoreSettings } from '../../../../../common/lib'; import { EuiCodeEditor } from '../../../../shared_imports'; import { useServices } from '../../../app_context'; import { StepProps } from '.'; -import { CollapsibleIndicesList } from '../../collapsible_lists/collapsible_indices_list'; +import { CollapsibleIndicesList } from '../../collapsible_lists'; +import { PolicyFeatureStatesSummary } from '../../summaries'; export const RestoreSnapshotStepReview: React.FunctionComponent = ({ restoreSettings, @@ -39,6 +40,7 @@ export const RestoreSnapshotStepReview: React.FunctionComponent = ({ renameReplacement, partial, includeGlobalState, + featureStates, ignoreIndexSettings, } = restoreSettings; @@ -129,33 +131,8 @@ export const RestoreSnapshotStepReview: React.FunctionComponent = ({ ) : null} - {partial !== undefined || includeGlobalState !== undefined ? ( + {featureStates !== undefined || includeGlobalState !== undefined ? ( - {partial !== undefined ? ( - - - - - - - {partial ? ( - - ) : ( - - )} - - - - ) : null} {includeGlobalState !== undefined ? ( @@ -166,21 +143,52 @@ export const RestoreSnapshotStepReview: React.FunctionComponent = ({ /> - {includeGlobalState ? ( + {includeGlobalState === false ? ( ) : ( )} ) : null} + {featureStates !== undefined ? ( + + ) : null} + + ) : null} + + {partial !== undefined ? ( + + + + + + + + {partial ? ( + + ) : ( + + )} + + + ) : null} diff --git a/x-pack/plugins/snapshot_restore/public/application/components/summaries/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/summaries/index.ts new file mode 100644 index 0000000000000..41aa062d3cfbe --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/summaries/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './policies'; +export * from './snapshots'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/summaries/policies/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/summaries/policies/index.ts new file mode 100644 index 0000000000000..88ad4cf894975 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/summaries/policies/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { PolicyFeatureStatesSummary } from './policy_feature_states_summary'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/summaries/policies/policy_feature_states_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/components/summaries/policies/policy_feature_states_summary.tsx new file mode 100644 index 0000000000000..cc016fbb4bfd1 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/summaries/policies/policy_feature_states_summary.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFlexItem, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { SnapshotConfig } from '../../../../../common/types'; +import { FEATURE_STATES_NONE_OPTION } from '../../../../../common/constants'; +import { CollapsibleFeatureStatesList } from '../../collapsible_lists'; + +export const PolicyFeatureStatesSummary: React.FunctionComponent = ({ + includeGlobalState, + featureStates, +}) => { + const hasGlobalStateButNoFeatureStates = includeGlobalState && featureStates === undefined; + const hasNoFeatureStates = !featureStates || featureStates?.includes(FEATURE_STATES_NONE_OPTION); + const hasAllFeatureStates = hasGlobalStateButNoFeatureStates || featureStates?.length === 0; + + return ( + + + + + + + {!hasGlobalStateButNoFeatureStates && hasNoFeatureStates && ( + + )} + {hasAllFeatureStates && ( + + )} + {!hasNoFeatureStates && !hasAllFeatureStates && ( + + )} + + + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/summaries/snapshots/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/summaries/snapshots/index.ts new file mode 100644 index 0000000000000..b15170c455138 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/summaries/snapshots/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SnapshotFeatureStatesSummary } from './snapshot_feature_states_summary'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/summaries/snapshots/snapshot_feature_states_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/components/summaries/snapshots/snapshot_feature_states_summary.tsx new file mode 100644 index 0000000000000..57b624416c6a2 --- /dev/null +++ b/x-pack/plugins/snapshot_restore/public/application/components/summaries/snapshots/snapshot_feature_states_summary.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFlexItem, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { SnapshotConfig } from '../../../../../common/types'; +import { CollapsibleFeatureStatesList } from '../../collapsible_lists'; + +export const SnapshotFeatureStatesSummary: React.FunctionComponent = ({ + featureStates, +}) => { + // When a policy that includes featureStates: ['none'] is executed, the resulting + // snapshot wont include the `none` in the featureStates array but instead will + // return an empty array. + const hasNoFeatureStates = !featureStates || featureStates.length === 0; + + return ( + + + + + + + {hasNoFeatureStates ? ( + + ) : ( + + )} + + + + ); +}; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx index 8482eb4520460..18f7daf2d9dd2 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx @@ -27,6 +27,7 @@ import { SlmPolicy } from '../../../../../../../common/types'; import { useServices } from '../../../../../app_context'; import { FormattedDateTime, CollapsibleIndicesList } from '../../../../../components'; import { linkToSnapshots, linkToRepository } from '../../../../../services/navigation'; +import { PolicyFeatureStatesSummary } from '../../../../../components/summaries'; interface Props { policy: SlmPolicy; @@ -48,8 +49,9 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { retention, isManagedPolicy, } = policy; - const { includeGlobalState, ignoreUnavailable, indices, partial } = config || { + const { includeGlobalState, featureStates, ignoreUnavailable, indices, partial } = config || { includeGlobalState: undefined, + featureStates: [], ignoreUnavailable: undefined, indices: undefined, partial: undefined, @@ -247,7 +249,7 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { - + = ({ policy }) => { - + - {partial ? ( + {includeGlobalState === false ? ( ) : ( )} - + + + + + - {includeGlobalState === false ? ( + {partial ? ( ) : ( )} diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx index c633b6cf886d0..47313aaeea626 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx @@ -29,6 +29,7 @@ import { import { linkToPolicy } from '../../../../../services/navigation'; import { SnapshotState } from './snapshot_state'; import { useServices } from '../../../../../app_context'; +import { SnapshotFeatureStatesSummary } from '../../../../../components/summaries'; interface Props { snapshotDetails: SnapshotDetails; @@ -41,6 +42,7 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { // TODO: Add a tooltip explaining that: a false value means that the cluster global state // is not stored as part of the snapshot. includeGlobalState, + featureStates, dataStreams, indices, state, @@ -97,6 +99,32 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { + + + + + + + {state === SNAPSHOT_STATE.IN_PROGRESS ? ( + + ) : ( + + + + )} + + + + + = ({ snapshotDetails }) => { - {includeGlobalState ? ( + {includeGlobalState === false ? ( ) : ( )} + + @@ -190,30 +222,6 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => { - - - - - - - {state === SNAPSHOT_STATE.IN_PROGRESS ? ( - - ) : ( - - - - )} - - - {policyName ? ( diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/policy_requests.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/policy_requests.ts index fea00283f98c2..f4e8abc34e993 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/http/policy_requests.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/http/policy_requests.ts @@ -6,7 +6,12 @@ */ import { API_BASE_PATH } from '../../../../common/constants'; -import { SlmPolicy, SlmPolicyPayload, PolicyIndicesResponse } from '../../../../common/types'; +import { + SlmPolicy, + SlmPolicyPayload, + PolicyIndicesResponse, + PolicyFeaturesResponse, +} from '../../../../common/types'; import { UIM_POLICY_EXECUTE, UIM_POLICY_DELETE, @@ -48,6 +53,13 @@ export const useLoadIndices = () => { }); }; +export const useLoadFeatures = () => { + return useRequest({ + path: `${API_BASE_PATH}policies/features`, + method: 'get', + }); +}; + export const executePolicy = async (name: SlmPolicy['name']) => { const result = sendRequest({ path: `${API_BASE_PATH}policy/${encodeURIComponent(name)}/run`, diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts index dae759b818ced..749d5ed51b53b 100644 --- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts @@ -12,6 +12,9 @@ export type { SendRequestResponse, UseRequestResponse, UseRequestConfig, + Privileges, + MissingPrivileges, + Authorization, } from '@kbn/es-ui-shared-plugin/public'; export { @@ -26,6 +29,8 @@ export { useRequest, WithPrivileges, EuiCodeEditor, + AuthorizationContext, + GlobalFlyout, } from '@kbn/es-ui-shared-plugin/public'; export { APP_WRAPPER_CLASS } from '@kbn/core/public'; diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts index 7401f126b3594..3d4f0f5505b30 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -386,6 +386,31 @@ describe('[Snapshot and Restore API Routes] Policy', () => { await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); + + it('should not return system indices', async () => { + const mockEsResponse: ResolveIndexResponseFromES = { + indices: [ + { + name: 'fooIndex', + attributes: ['open'], + }, + { + name: 'kibana', + attributes: ['open', 'system'], + }, + ], + aliases: [], + data_streams: [], + }; + + resolveIndicesFn.mockResolvedValue(mockEsResponse); + + const expectedResponse = { + indices: ['fooIndex'], + dataStreams: [], + }; + await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); + }); }); describe('updateRetentionSettingsHandler()', () => { diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index 8068c64ec77b6..51bdf96361a24 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -210,6 +210,7 @@ export function registerPolicyRoutes({ const body: PolicyIndicesResponse = { dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(), indices: resolvedIndicesResponse.indices + .filter((index) => !index.attributes.includes('system')) .flatMap((index) => (index.data_stream ? [] : index.name)) .sort(), }; @@ -223,6 +224,22 @@ export function registerPolicyRoutes({ }) ); + // Get policy feature states + router.get( + { path: addBasePath('policies/features'), validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { client: clusterClient } = (await ctx.core).elasticsearch; + + try { + const response = await clusterClient.asCurrentUser.features.getFeatures(); + + return res.ok({ body: response }); + } catch (e) { + return handleEsError({ error: e, response: res }); + } + }) + ); + // Get retention settings router.get( { path: addBasePath('policies/retention_settings'), validate: false }, diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index ba4b888c58773..2855984bc98b0 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -18,6 +18,7 @@ const defaultSnapshot = { indices: [], dataStreams: [], includeGlobalState: undefined, + featureStates: [], state: undefined, startTime: undefined, startTimeInMillis: undefined, diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts index e93ee2b3d78ca..35437c2e39acd 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts @@ -15,6 +15,7 @@ const snapshotConfigSchema = schema.object({ indices: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), ignoreUnavailable: schema.maybe(schema.boolean()), includeGlobalState: schema.maybe(schema.boolean()), + featureStates: schema.maybe(schema.arrayOf(schema.string())), partial: schema.maybe(schema.boolean()), metadata: schema.maybe(schema.recordOf(schema.string(), schema.string())), }); @@ -197,6 +198,7 @@ export const restoreSettingsSchema = schema.object({ renamePattern: schema.maybe(schema.string()), renameReplacement: schema.maybe(schema.string()), includeGlobalState: schema.maybe(schema.boolean()), + featureStates: schema.maybe(schema.arrayOf(schema.string())), partial: schema.maybe(schema.boolean()), indexSettings: schema.maybe(schema.string()), ignoreIndexSettings: schema.maybe(schema.arrayOf(schema.string())), diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts index d5710bb39d4c9..37220ef9fe59b 100644 --- a/x-pack/plugins/snapshot_restore/server/types.ts +++ b/x-pack/plugins/snapshot_restore/server/types.ts @@ -41,7 +41,7 @@ export interface RouteDependencies { interface IndexAndAliasFromEs { name: string; // per https://github.com/elastic/elasticsearch/pull/57626 - attributes: Array<'open' | 'closed' | 'hidden' | 'frozen'>; + attributes: Array<'open' | 'closed' | 'hidden' | 'frozen' | 'system'>; data_stream?: string; } diff --git a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts index e7fad7f4a291f..7d260aad02998 100644 --- a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts +++ b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts @@ -13,6 +13,8 @@ export const getSnapshot = ({ uuid = getRandomString(), state = 'SUCCESS', indexFailures = [], + includeGlobalState = true, + featureStates = [], totalIndices = getRandomNumber(), totalDataStreams = getRandomNumber(), }: Partial<{ @@ -21,6 +23,8 @@ export const getSnapshot = ({ uuid: string; state: string; indexFailures: any[]; + featureStates: string[]; + includeGlobalState: boolean; totalIndices: number; totalDataStreams: number; }> = {}) => ({ @@ -31,7 +35,8 @@ export const getSnapshot = ({ version: '8.0.0', indices: new Array(totalIndices).fill('').map(getRandomString), dataStreams: new Array(totalDataStreams).fill('').map(getRandomString), - includeGlobalState: 1, + featureStates, + includeGlobalState, state, startTime: '2019-05-23T06:25:15.896Z', startTimeInMillis: 1558592715896, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 093c8828b7642..1e6e51d8084c2 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -27449,9 +27449,7 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesDescription": "Restaure les alias des index avec leurs index associés.", "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesLabel": "Restaurer les alias", "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesTitle": "Restaurer les alias", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "Restaure les modèles qui n'existent pas actuellement dans le cluster et remplace les modèles portant le même nom. Restaure également les paramètres persistants et tous les index système. {learnMoreLink}", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "Non disponible pour ce snapshot.", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink": "En savoir plus.", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "Restaurer l'état global", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "Restaurer l'état global", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "Modèles d'indexation", @@ -27470,7 +27468,6 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "Tout sélectionner", "xpack.snapshotRestore.restoreForm.stepLogistics.selectDataStreamsAndIndicesHelpText": "{indicesCount} {indicesCount, plural, other {index}} et {dataStreamsCount} {dataStreamsCount, plural,other {flux de données}} seront restaurés. {deselectAllLink}", "xpack.snapshotRestore.restoreForm.stepLogistics.selectDataStreamsAndIndicesLabel": "Sélectionner les flux de données et les index", - "xpack.snapshotRestore.restoreForm.stepLogistics.systemIndicesCallOut.title": "Une fois ce snapshot restauré, les index système seront écrasés avec les données du snapshot.", "xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "Restaurer les détails", "xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "Restaurer les paramètres à exécuter", "xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 71f752480d644..d8d83466fdb09 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27619,9 +27619,7 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesDescription": "インデックスエイリアスと関連付けられたインデックスを復元します。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesLabel": "エイリアスを復元", "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesTitle": "エイリアスを復元", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "現在クラスターに存在しないテンプレートを復元し、テンプレートを同じ名前で上書きします。永続設定とすべてのシステムインデックスも復元します。{learnMoreLink}", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "このスナップショットでは使用できません。", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink": "詳細情報", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "グローバル状態の復元", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "グローバル状態の復元", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "インデックスパターン", @@ -27640,7 +27638,6 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "すべて選択", "xpack.snapshotRestore.restoreForm.stepLogistics.selectDataStreamsAndIndicesHelpText": "{indicesCount} {indicesCount, plural, other {個のインデックス}} and {dataStreamsCount} {dataStreamsCount, plural, other {個のデータストリーム}}が復元されます。{deselectAllLink}", "xpack.snapshotRestore.restoreForm.stepLogistics.selectDataStreamsAndIndicesLabel": "データストリームとインデックスを選択", - "xpack.snapshotRestore.restoreForm.stepLogistics.systemIndicesCallOut.title": "このスナップショットが復元されるときに、システムインデックスはスナップショットのデータで上書きされます。", "xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "詳細を復元", "xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "実行する設定を復元", "xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3d391ce40f8c4..48011eaebd3cd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27653,9 +27653,7 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesDescription": "还原索引别名和它们的相关索引。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesLabel": "还原别名", "xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesTitle": "还原别名", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "还原当前在集群中不存在的模板并覆盖同名模板。同时还原永久性设置和所有系统索引。{learnMoreLink}", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "不适用于此快照。", - "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink": "了解详情。", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "还原全局状态", "xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "还原全局状态", "xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "索引模式", @@ -27674,7 +27672,6 @@ "xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "全选", "xpack.snapshotRestore.restoreForm.stepLogistics.selectDataStreamsAndIndicesHelpText": "将还原 {indicesCount} 个{indicesCount, plural, other {索引}}和 {dataStreamsCount} 个{dataStreamsCount, plural, other {数据流}}。{deselectAllLink}", "xpack.snapshotRestore.restoreForm.stepLogistics.selectDataStreamsAndIndicesLabel": "选择数据流和索引", - "xpack.snapshotRestore.restoreForm.stepLogistics.systemIndicesCallOut.title": "还原此快照时,将使用来自快照的数据覆盖系统索引。", "xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "还原详情", "xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "还原要执行的设置", "xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON",