mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
dd2a9b5425
commit
8e33d5d569
57 changed files with 1359 additions and 252 deletions
|
@ -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'] },
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -40,6 +40,11 @@ export const formSetup = async (
|
|||
export type PolicyFormTestSubjects =
|
||||
| 'advancedCronInput'
|
||||
| 'allIndicesToggle'
|
||||
| 'globalStateToggle'
|
||||
| 'featureStatesDropdown'
|
||||
| 'toggleIncludeNone'
|
||||
| 'noFeatureStatesCallout'
|
||||
| 'featureStatesToggle'
|
||||
| 'backButton'
|
||||
| 'deselectIndicesLink'
|
||||
| 'allDataStreamsToggle'
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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})`
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -9,3 +9,10 @@ export interface PolicyIndicesResponse {
|
|||
indices: string[];
|
||||
dataStreams: string[];
|
||||
}
|
||||
|
||||
export interface PolicyFeaturesResponse {
|
||||
features: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
}>;
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ? (
|
||||
<>
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 ? (
|
||||
<>
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export { CollapsibleIndicesList } from './collapsible_indices_list';
|
||||
export { CollapsibleDataStreamsList } from './collapsible_data_streams_list';
|
||||
export { CollapsibleFeatureStatesList } from './collapsible_feature_states';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>({
|
||||
|
|
|
@ -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 don’t 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={
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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()', () => {
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -18,6 +18,7 @@ const defaultSnapshot = {
|
|||
indices: [],
|
||||
dataStreams: [],
|
||||
includeGlobalState: undefined,
|
||||
featureStates: [],
|
||||
state: undefined,
|
||||
startTime: undefined,
|
||||
startTimeInMillis: undefined,
|
||||
|
|
|
@ -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())),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue