[Snapshot restore] Add support for feature_states (#131310)

* Update copy to not include system indices

* Dont include system indices in ds/indices dropdown

* Start working on supporting feature states

* Store feature states array of options in local state

* Fix up server side integration and show deets in flyout

* Fix linter issues

* commit using @elastic.co

* Connect the dots in restore snapshot wizard

* Fix linter issues

* Finish up wiring up last features

* Fix copy

* CR

* Refactor tooltip implementation

* Fix tests

* Fix i18n

* Add tests

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* Add missing tests

* Add option for selecting none of the feature states

* Finish off refactoring label placement and fixing up tests

* Add tests

* Remove nextTick and refactor tests

* Refactor feature states into its own setting

* Fix docs link

* Copy review

* Fix tests

* Fix small bug and add more tests

* Fix linter issue

* Address CR

* Change duped locale id

* Address CR changes

* Copy updates

* CR changes
This commit is contained in:
Ignacio Rivas 2022-05-16 12:52:27 +02:00 committed by GitHub
parent dd2a9b5425
commit 8e33d5d569
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1359 additions and 252 deletions

View file

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

View file

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

View file

@ -40,6 +40,11 @@ export const formSetup = async (
export type PolicyFormTestSubjects =
| 'advancedCronInput'
| 'allIndicesToggle'
| 'globalStateToggle'
| 'featureStatesDropdown'
| 'toggleIncludeNone'
| 'noFeatureStatesCallout'
| 'featureStatesToggle'
| 'backButton'
| 'deselectIndicesLink'
| 'allDataStreamsToggle'

View file

@ -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<typeof createActions>;
};
export const setupPoliciesListPage = async (httpSetup: HttpSetup) => {
const initTestBed = registerTestBed(WithAppDependencies(PolicyList, httpSetup), testBedConfig);
const testBed = await initTestBed();
return {
...testBed,
actions: createActions(testBed),
};
};

View file

@ -51,6 +51,14 @@ const setupActions = (testBed: TestBed<RestoreSnapshotFormTestSubject>) => {
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'

View file

@ -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<string, unknown> = {}) =>
(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 (
<AppContextProvider value={appDependencies as any}>
<Comp {...props} />
</AppContextProvider>
);
};
return (
<AuthorizationContext.Provider
value={createAuthorizationContextValue(privileges as Privileges)}
>
<AppContextProvider value={merge(appDependencies, overrides) as any}>
<GlobalFlyoutProvider>
<Comp {...props} />
</GlobalFlyoutProvider>
</AppContextProvider>
</AuthorizationContext.Provider>
);
};

View file

@ -497,10 +497,12 @@ describe('<SnapshotRestoreHome />', () => {
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('<SnapshotRestoreHome />', () => {
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('<SnapshotRestoreHome />', () => {
);
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})`
);

View file

@ -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('<PolicyAdd />', () => {
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('<PolicyAdd />', () => {
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('<PolicyAdd />', () => {
await act(async () => {
// Toggle "All indices" switch
form.toggleEuiSwitch('allIndicesToggle');
await nextTick();
});
component.update();
@ -210,6 +212,123 @@ describe('<PolicyAdd />', () => {
});
});
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<string, any>).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<string, any>).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<string, any>).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('<PolicyAdd />', () => {
await act(async () => {
actions.clickSubmitButton();
await nextTick();
});
expect(httpSetup.post).toHaveBeenLastCalledWith(
@ -245,7 +363,7 @@ describe('<PolicyAdd />', () => {
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('<PolicyAdd />', () => {
await act(async () => {
actions.clickSubmitButton();
await nextTick();
component.update();
});
component.update();
expect(exists('savePolicyApiError')).toBe(true);
expect(find('savePolicyApiError').text()).toContain(error.message);

View file

@ -35,6 +35,9 @@ describe('<PolicyEdit />', () => {
httpRequestsMockHelpers.setLoadRepositoriesResponse({
repositories: [{ name: POLICY_EDIT.repository }],
});
httpRequestsMockHelpers.setLoadFeaturesResponse({
features: [{ name: 'kibana' }, { name: 'tasks' }],
});
testBed = await setup(httpSetup);
@ -151,6 +154,8 @@ describe('<PolicyEdit />', () => {
schedule,
repository,
config: {
featureStates: ['kibana'],
includeGlobalState: true,
ignoreUnavailable: true,
},
retention: {
@ -182,7 +187,7 @@ describe('<PolicyEdit />', () => {
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('<PolicyEdit />', () => {
snapshotName,
schedule,
repository,
config,
config: {
featureStates: ['kibana'],
includeGlobalState: true,
},
retention: {
...retention,
expireAfterUnit: TIME_UNITS.DAY, // default value

View file

@ -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('<PolicyList />', () => {
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');
});
});
});

View file

@ -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('<RestoreSnapshot />', () => {
});
});
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 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();
});
test('shows an info callout when include_global_state is enabled', () => {
const { exists, actions } = testBed;
expect(exists('systemIndicesInfoCallOut')).toBe(false);
actions.toggleGlobalState();
await actions.toggleFeatureState();
expect(exists('systemIndicesInfoCallOut')).toBe(true);
});
@ -137,6 +157,7 @@ describe('<RestoreSnapshot />', () => {
`${API_BASE_PATH}restore/${REPOSITORY_NAME}/${SNAPSHOT_NAME}`,
expect.objectContaining({
body: JSON.stringify({
featureStates: [FEATURE_STATES_NONE_OPTION],
includeAliases: false,
}),
})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,3 +9,10 @@ export interface PolicyIndicesResponse {
indices: string[];
dataStreams: string[];
}
export interface PolicyFeaturesResponse {
features: Array<{
name: string;
description: string;
}>;
}

View file

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

View file

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

View file

@ -28,15 +28,13 @@ export const CollapsibleDataStreamsList: React.FunctionComponent<Props> = ({ dat
) : (
<>
<EuiText>
<ul>
{items.map((dataStream) => (
<li key={dataStream}>
<EuiTitle size="xs">
<span>{dataStream}</span>
</EuiTitle>
</li>
))}
</ul>
{items.map((dataStream) => (
<div key={dataStream}>
<EuiTitle size="xs">
<span>{dataStream}</span>
</EuiTitle>
</div>
))}
</EuiText>
{hiddenItemsCount ? (
<>

View file

@ -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<Props> = ({ featureStates }) => {
const { isShowingFullList, setIsShowingFullList, items, hiddenItemsCount } = useCollapsibleList({
items: featureStates,
});
if (items === 'all' || items.length === 0) {
return (
<FormattedMessage
id="xpack.snapshotRestore.featureStatesList.allFeaturesLabel"
defaultMessage="All features"
/>
);
}
if (items.find((option) => option === FEATURE_STATES_NONE_OPTION)) {
return (
<FormattedMessage
id="xpack.snapshotRestore.featureStatesList.noneFeaturesLabel"
defaultMessage="No features"
/>
);
}
return (
<>
<EuiText data-test-subj="featureStatesList">
{items.map((feature) => (
<div key={feature}>
<EuiTitle size="xs">
<span>{feature}</span>
</EuiTitle>
</div>
))}
</EuiText>
{hiddenItemsCount ? (
<>
<EuiSpacer size="xs" />
<EuiLink
onClick={() =>
isShowingFullList ? setIsShowingFullList(false) : setIsShowingFullList(true)
}
>
{isShowingFullList ? (
<FormattedMessage
id="xpack.snapshotRestore.featureStatesList.featureStatesCollapseAllLink"
defaultMessage="Hide {count, plural, one {# feature} other {# features}}"
values={{ count: hiddenItemsCount }}
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.featureStatesList.featureStatesExpandAllLink"
defaultMessage="Show {count, plural, one {# feature} other {# features}}"
values={{ count: hiddenItemsCount }}
/>
)}{' '}
<EuiIcon type={isShowingFullList ? 'arrowUp' : 'arrowDown'} />
</EuiLink>
</>
) : null}
</>
);
};

View file

@ -27,15 +27,13 @@ export const CollapsibleIndicesList: React.FunctionComponent<Props> = ({ indices
) : (
<>
<EuiText>
<ul>
{items.map((index) => (
<li key={index}>
<EuiTitle size="xs">
<span>{index}</span>
</EuiTitle>
</li>
))}
</ul>
{items.map((index) => (
<div key={index}>
<EuiTitle size="xs">
<span>{index}</span>
</EuiTitle>
</div>
))}
</EuiText>
{hiddenItemsCount ? (
<>

View file

@ -7,3 +7,4 @@
export { CollapsibleIndicesList } from './collapsible_indices_list';
export { CollapsibleDataStreamsList } from './collapsible_data_streams_list';
export { CollapsibleFeatureStatesList } from './collapsible_feature_states';

View file

@ -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<string>;
interface Props {
featuresOptions: string[];
selectedOptions: FeaturesOption[];
onUpdateFormSettings: (
arg: Partial<SlmPolicyPayload['config']> & Partial<RestoreSettings>
) => void;
isLoadingFeatures?: boolean;
}
export const FeatureStatesFormField: FunctionComponent<Props> = ({
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 (
<EuiFormRow>
<EuiComboBox
data-test-subj="featureStatesDropdown"
placeholder={i18n.translate(
'xpack.snapshotRestore.featureStatesFormField.allFeaturesLabel',
{ defaultMessage: 'All features' }
)}
options={optionsList}
selectedOptions={selectedOptions}
onChange={onChange}
isLoading={isLoadingFeatures}
isClearable={true}
/>
</EuiFormRow>
);
};

View file

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

View file

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

View file

@ -67,6 +67,14 @@ export const PolicyForm: React.FunctionComponent<Props> = ({
const [policy, setPolicy] = useState<SlmPolicyPayload>({
...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: {

View file

@ -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<StepProps> = ({
policy,
@ -32,9 +33,10 @@ export const PolicyStepReview: React.FunctionComponent<StepProps> = ({
}) => {
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<StepProps> = ({
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiSpacer size="xxl" />
{/* Snapshot settings summary */}
<EuiTitle size="s">
@ -185,29 +187,6 @@ export const PolicyStepReview: React.FunctionComponent<StepProps> = ({
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialIndicesLabel"
defaultMessage="Allow partial indices"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{partial ? (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel"
defaultMessage="Yes"
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel"
defaultMessage="No"
/>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
@ -231,6 +210,37 @@ export const PolicyStepReview: React.FunctionComponent<StepProps> = ({
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
<PolicyFeatureStatesSummary
includeGlobalState={includeGlobalState}
featureStates={featureStates}
/>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialIndicesLabel"
defaultMessage="Allow partial indices"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{partial ? (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel"
defaultMessage="Yes"
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel"
defaultMessage="No"
/>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
</EuiFlexGroup>
{/* Retention summary */}

View file

@ -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<SlmPolicyPayload['config']>) => void;
errors: PolicyValidation['errors'];
}
export type FeaturesOption = EuiComboBoxOptionOption<string>;
export const IncludeFeatureStatesField: FunctionComponent<Props> = ({ 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 (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeFeatureStatesDescriptionTitle"
defaultMessage="Include feature state"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeFeatureStatesDescription"
defaultMessage="Includes the configuration, history, and other data stored in Elasticsearch by a feature such as Elasticsearch security."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="featureStatesToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.policyIncludeFeatureStatesLabel"
defaultMessage="Include feature state from"
/>
}
checked={isFeatureStatesToggleEnabled}
onChange={onFeatureStatesToggleChange}
/>
</EuiFormRow>
{isFeatureStatesToggleEnabled && (
<>
<EuiSpacer size="m" />
{!errorLoadingFeatures ? (
<FeatureStatesFormField
isLoadingFeatures={isLoadingFeatures}
featuresOptions={featuresOptions}
selectedOptions={selectedOptions}
onUpdateFormSettings={onUpdate}
/>
) : (
<EuiCallOut
color="warning"
iconType="alert"
title={
<FormattedMessage
id="xpack.snapshotRestore.errorLoadingFeatureStatesLabel"
defaultMessage="There was an error loading the list of feature states"
/>
}
/>
)}
</>
)}
</EuiDescribedFormGroup>
);
};

View file

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

View file

@ -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<SlmPolicyPayload['config']>) => void;
errors: PolicyValidation['errors'];
}
export type FeaturesOption = EuiComboBoxOptionOption<string>;
export const IncludeGlobalStateField: FunctionComponent<Props> = ({ 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 (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle"
defaultMessage="Include global state"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription"
defaultMessage="Stores the global cluster state as part of the snapshot."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="globalStateToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel"
defaultMessage="Include global state"
/>
}
checked={config.includeGlobalState === undefined || config.includeGlobalState}
onChange={onIncludeGlobalStateToggle}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
};

View file

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

View file

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

View file

@ -17,8 +17,8 @@ import {
EuiLink,
EuiPanel,
EuiSelectable,
EuiSelectableOption,
EuiSpacer,
EuiSelectableOption,
EuiSwitch,
EuiTitle,
EuiToolTip,
@ -115,7 +115,7 @@ export const IndicesAndDataStreamsField: FunctionComponent<Props> = ({
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.allDataStreamsAndIndicesLabel"
defaultMessage="All data streams and indices, including system indices"
defaultMessage="All data streams and indices"
/>
}
checked={isAllIndices}

View file

@ -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<StepProps> = ({
@ -127,45 +131,6 @@ export const PolicyStepSettings: React.FunctionComponent<StepProps> = ({
</EuiDescribedFormGroup>
);
const renderIncludeGlobalStateField = () => (
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle"
defaultMessage="Include global state"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription"
defaultMessage="Stores the global cluster state and system indices as part of the snapshot."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="globalStateToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel"
defaultMessage="Include global state"
/>
}
checked={config.includeGlobalState === undefined || config.includeGlobalState}
onChange={(e) => {
updatePolicyConfig({
includeGlobalState: e.target.checked,
});
}}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
);
return (
<div className="snapshotRestore__policyForm__stepSettings">
{/* Step title and doc link */}
@ -209,7 +174,9 @@ export const PolicyStepSettings: React.FunctionComponent<StepProps> = ({
{renderIgnoreUnavailableField()}
{renderPartialField()}
{renderIncludeGlobalStateField()}
<IncludeGlobalStateField errors={errors} policy={policy} onUpdate={updatePolicyConfig} />
<IncludeFeatureStatesField errors={errors} policy={policy} onUpdate={updatePolicyConfig} />
</div>
);
};

View file

@ -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<Props> = ({
const CurrentStepForm = stepMap[currentStep];
// Restore details state
const [restoreSettings, setRestoreSettings] = useState<RestoreSettings>({});
const [restoreSettings, setRestoreSettings] = useState<RestoreSettings>({
// 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<RestoreValidation>({

View file

@ -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<string>;
export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> = ({
snapshotDetails,
restoreSettings,
@ -54,6 +62,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
dataStreams: snapshotDataStreams = [],
includeGlobalState: snapshotIncludeGlobalState,
version,
featureStates: snapshotIncludeFeatureStates,
} = snapshotDetails;
const snapshotIndices = unfilteredSnapshotIndices.filter(
@ -81,6 +90,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
renameReplacement,
partial,
includeGlobalState,
featureStates,
includeAliases,
} = restoreSettings;
@ -148,6 +158,21 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
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 (
<div
data-test-subj="snapshotRestoreStepLogistics"
@ -218,7 +243,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
label={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.allDataStreamsAndIndicesLabel"
defaultMessage="All data streams and indices, including system indices"
defaultMessage="All data streams and indices"
/>
}
checked={isAllIndicesAndDataStreams}
@ -569,34 +594,10 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
</EuiTitle>
}
description={
<>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription"
defaultMessage="Restores templates that dont currently exist in the cluster and overrides
templates with the same name. Also restores persistent settings and all system indices. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink target="_blank" href={docLinks.links.snapshotRestore.restoreSnapshotApi}>
{i18n.translate(
'xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink',
{ defaultMessage: 'Learn more.' }
)}
</EuiLink>
),
}}
/>
{/* 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') && (
<>
<EuiSpacer size="s" />
<SystemIndicesOverwrittenCallOut />
</>
)}
</>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription"
defaultMessage="Restores the global cluster state as part of the snapshot."
/>
}
fullWidth
>
@ -627,6 +628,89 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
</EuiFormRow>
</EuiDescribedFormGroup>
{/* Include feature states */}
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeFeatureStatesTitle"
defaultMessage="Restore feature state"
/>
</h3>
</EuiTitle>
}
description={
<>
<FormattedMessage
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 includeFeatureState is enabled and the snapshot was created by ES 7.12+ */}
{semverGt(version, '7.12.0') && isFeatureStatesToggleEnabled && (
<>
<EuiSpacer size="s" />
<SystemIndicesOverwrittenCallOut featureStates={restoreSettings?.featureStates} />
</>
)}
</>
}
fullWidth
>
<EuiFormRow
hasEmptyLabelSpace={true}
fullWidth
helpText={
snapshotIncludeFeatureStates ? null : (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeFeatureStatesDisabledDescription"
defaultMessage="Not available for this snapshot."
/>
)
}
>
<EuiSwitch
label={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.restoreFeatureStatesLabel"
defaultMessage="Restore feature state from"
/>
}
checked={isFeatureStatesToggleEnabled}
onChange={onFeatureStatesToggleChange}
disabled={snapshotIncludeFeatureStates?.length === 0}
data-test-subj="includeFeatureStatesSwitch"
/>
</EuiFormRow>
{isFeatureStatesToggleEnabled && (
<>
<EuiSpacer size="m" />
<FeatureStatesFormField
featuresOptions={snapshotIncludeFeatureStates}
selectedOptions={selectedFeatureStateOptions}
onUpdateFormSettings={updateRestoreSettings}
/>
</>
)}
{snapshotIncludeFeatureStates?.length === 0 && (
<>
<EuiSpacer size="m" />
<EuiCallOut
size="s"
iconType="help"
color="warning"
data-test-subj="noFeatureStatesCallout"
title={i18n.translate(
'xpack.snapshotRestore.restoreForm.stepLogistics.noFeatureStates',
{ defaultMessage: 'No feature states are included in this snapshot.' }
)}
/>
</>
)}
</EuiDescribedFormGroup>
{/* Include aliases */}
<EuiDescribedFormGroup
title={

View file

@ -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 (
<EuiCallOut
data-test-subj="systemIndicesInfoCallOut"
@ -17,7 +19,11 @@ export const SystemIndicesOverwrittenCallOut: FunctionComponent = () => {
'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"

View file

@ -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<StepProps> = ({
restoreSettings,
@ -39,6 +40,7 @@ export const RestoreSnapshotStepReview: React.FunctionComponent<StepProps> = ({
renameReplacement,
partial,
includeGlobalState,
featureStates,
ignoreIndexSettings,
} = restoreSettings;
@ -129,33 +131,8 @@ export const RestoreSnapshotStepReview: React.FunctionComponent<StepProps> = ({
</Fragment>
) : null}
{partial !== undefined || includeGlobalState !== undefined ? (
{featureStates !== undefined || includeGlobalState !== undefined ? (
<EuiFlexGroup>
{partial !== undefined ? (
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel"
defaultMessage="Partial restore"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{partial ? (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialTrueValue"
defaultMessage="Yes"
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue"
defaultMessage="No"
/>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
) : null}
{includeGlobalState !== undefined ? (
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
@ -166,21 +143,52 @@ export const RestoreSnapshotStepReview: React.FunctionComponent<StepProps> = ({
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{includeGlobalState ? (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue"
defaultMessage="Yes"
/>
) : (
{includeGlobalState === false ? (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateFalseValue"
defaultMessage="No"
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue"
defaultMessage="Yes"
/>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
) : null}
{featureStates !== undefined ? (
<PolicyFeatureStatesSummary featureStates={featureStates} />
) : null}
</EuiFlexGroup>
) : null}
{partial !== undefined ? (
<EuiFlexGroup>
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel"
defaultMessage="Partial restore"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{partial ? (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialTrueValue"
defaultMessage="Yes"
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue"
defaultMessage="No"
/>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
</EuiFlexGroup>
) : null}

View file

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

View file

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

View file

@ -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<SnapshotConfig> = ({
includeGlobalState,
featureStates,
}) => {
const hasGlobalStateButNoFeatureStates = includeGlobalState && featureStates === undefined;
const hasNoFeatureStates = !featureStates || featureStates?.includes(FEATURE_STATES_NONE_OPTION);
const hasAllFeatureStates = hasGlobalStateButNoFeatureStates || featureStates?.length === 0;
return (
<EuiFlexItem data-test-subj="policyFeatureStatesSummary">
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.summary.policyFeatureStatesLabel"
defaultMessage="Include feature state {hasSpecificFeatures, plural, one {from} other {}}"
values={{ hasSpecificFeatures: !hasNoFeatureStates && !hasAllFeatureStates ? 1 : 0 }}
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription data-test-subj="value">
{!hasGlobalStateButNoFeatureStates && hasNoFeatureStates && (
<FormattedMessage
id="xpack.snapshotRestore.summary.policyNoFeatureStatesLabel"
defaultMessage="No"
/>
)}
{hasAllFeatureStates && (
<FormattedMessage
id="xpack.snapshotRestore.summary.policyAllFeatureStatesLabel"
defaultMessage="All features"
/>
)}
{!hasNoFeatureStates && !hasAllFeatureStates && (
<CollapsibleFeatureStatesList featureStates={featureStates} />
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
);
};

View file

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

View file

@ -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<SnapshotConfig> = ({
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 (
<EuiFlexItem data-test-subj="snapshotFeatureStatesSummary">
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.summary.snapshotFeatureStatesLabel"
defaultMessage="Include feature state {hasSpecificFeatures, plural, one {from} other {}}"
values={{ hasSpecificFeatures: !hasNoFeatureStates ? 1 : 0 }}
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription data-test-subj="value">
{hasNoFeatureStates ? (
<FormattedMessage
id="xpack.snapshotRestore.summary.snapshotNoFeatureStatesLabel"
defaultMessage="No"
/>
) : (
<CollapsibleFeatureStatesList featureStates={featureStates} />
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
);
};

View file

@ -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<Props> = ({ 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<Props> = ({ policy }) => {
</EuiDescriptionListDescription>
</EuiFlexItem>
<EuiFlexItem data-test-subj="includeGlobalState">
<EuiFlexItem data-test-subj="ignoreUnavailable">
<EuiDescriptionListTitle data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.policyDetails.ignoreUnavailableLabel"
@ -271,6 +273,38 @@ export const TabSummary: React.FunctionComponent<Props> = ({ policy }) => {
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem data-test-subj="includeGlobalState">
<EuiDescriptionListTitle data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.policyDetails.includeGlobalStateLabel"
defaultMessage="Include global state"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
{includeGlobalState === false ? (
<FormattedMessage
data-test-subj="withoutGlobalState"
id="xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel"
defaultMessage="No"
/>
) : (
<FormattedMessage
data-test-subj="withGlobalStateAndFeatureStates"
id="xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel"
defaultMessage="Yes"
/>
)}
</EuiDescriptionListDescription>
</EuiFlexItem>
<PolicyFeatureStatesSummary
includeGlobalState={includeGlobalState}
featureStates={featureStates}
/>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem data-test-subj="partial">
<EuiDescriptionListTitle data-test-subj="title">
@ -294,29 +328,6 @@ export const TabSummary: React.FunctionComponent<Props> = ({ policy }) => {
)}
</EuiDescriptionListDescription>
</EuiFlexItem>
<EuiFlexItem data-test-subj="includeGlobalState">
<EuiDescriptionListTitle data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.policyDetails.includeGlobalStateLabel"
defaultMessage="Include global state"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
{includeGlobalState === false ? (
<FormattedMessage
id="xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel"
defaultMessage="No"
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel"
defaultMessage="Yes"
/>
)}
</EuiDescriptionListDescription>
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescriptionList>

View file

@ -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<Props> = ({ 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<Props> = ({ snapshotDetails }) => {
</EuiDescriptionListDescription>
</EuiFlexItem>
<EuiFlexItem data-test-subj="duration">
<EuiDescriptionListTitle data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.snapshotDetails.itemDurationLabel"
defaultMessage="Duration"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
{state === SNAPSHOT_STATE.IN_PROGRESS ? (
<EuiLoadingSpinner size="m" />
) : (
<DataPlaceholder data={durationInMillis}>
<FormattedMessage
id="xpack.snapshotRestore.snapshotDetails.itemDurationValueLabel"
data-test-subj="srSnapshotDetailsDurationValue"
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
values={{ seconds: Math.ceil(durationInMillis / 1000) }}
/>
</DataPlaceholder>
)}
</EuiDescriptionListDescription>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem data-test-subj="includeGlobalState">
<EuiDescriptionListTitle data-test-subj="title">
<FormattedMessage
@ -106,19 +134,23 @@ export const TabSummary: React.FC<Props> = ({ snapshotDetails }) => {
</EuiDescriptionListTitle>
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
{includeGlobalState ? (
{includeGlobalState === false ? (
<FormattedMessage
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateYesLabel"
defaultMessage="Yes"
data-test-subj="withoutGlobalState"
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateNoLabel"
defaultMessage="No"
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateNoLabel"
defaultMessage="No"
data-test-subj="withGlobalState"
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateYesLabel"
defaultMessage="Yes"
/>
)}
</EuiDescriptionListDescription>
</EuiFlexItem>
<SnapshotFeatureStatesSummary featureStates={featureStates} />
</EuiFlexGroup>
<EuiFlexGroup>
@ -190,30 +222,6 @@ export const TabSummary: React.FC<Props> = ({ snapshotDetails }) => {
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem data-test-subj="duration">
<EuiDescriptionListTitle data-test-subj="title">
<FormattedMessage
id="xpack.snapshotRestore.snapshotDetails.itemDurationLabel"
defaultMessage="Duration"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
{state === SNAPSHOT_STATE.IN_PROGRESS ? (
<EuiLoadingSpinner size="m" />
) : (
<DataPlaceholder data={durationInMillis}>
<FormattedMessage
id="xpack.snapshotRestore.snapshotDetails.itemDurationValueLabel"
data-test-subj="srSnapshotDetailsDurationValue"
defaultMessage="{seconds} {seconds, plural, one {second} other {seconds}}"
values={{ seconds: Math.ceil(durationInMillis / 1000) }}
/>
</DataPlaceholder>
)}
</EuiDescriptionListDescription>
</EuiFlexItem>
{policyName ? (
<EuiFlexItem data-test-subj="policy">
<EuiDescriptionListTitle data-test-subj="title">

View file

@ -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<PolicyFeaturesResponse>({
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`,

View file

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

View file

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

View file

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

View file

@ -18,6 +18,7 @@ const defaultSnapshot = {
indices: [],
dataStreams: [],
includeGlobalState: undefined,
featureStates: [],
state: undefined,
startTime: undefined,
startTimeInMillis: undefined,

View file

@ -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())),

View file

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

View file

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

View file

@ -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",

View file

@ -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",

View file

@ -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",