Add "Include aliases" toggle to the Restore Snapshot Wizard (#95882)

* Add support for includeAliases to restore API endpoint, with unit tests.
* Remove unused deserializeRestoreSettings function.
* Add 'Include aliases' option to the UI, with default value of true.
* Add client integration test.
This commit is contained in:
CJ Cenizal 2021-03-31 12:29:07 -07:00 committed by GitHub
parent f7caf44876
commit b531d28364
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 111 additions and 80 deletions

View file

@ -108,6 +108,14 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
const setRestoreSnapshotResponse = (response?: HttpResponse) => {
server.respondWith('POST', `${API_BASE_PATH}restore/:repository/:snapshot`, [
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response),
]);
};
return {
setLoadRepositoriesResponse,
setLoadRepositoryTypesResponse,
@ -119,6 +127,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
setAddPolicyResponse,
setGetPolicyResponse,
setCleanupRepositoryResponse,
setRestoreSnapshotResponse,
};
};

View file

@ -25,6 +25,7 @@ const initTestBed = registerTestBed<RestoreSnapshotFormTestSubject>(
const setupActions = (testBed: TestBed<RestoreSnapshotFormTestSubject>) => {
const { find, component, form } = testBed;
return {
findDataStreamCallout() {
return find('dataStreamWarningCallOut');
@ -37,6 +38,28 @@ const setupActions = (testBed: TestBed<RestoreSnapshotFormTestSubject>) => {
component.update();
},
toggleIncludeAliases() {
act(() => {
form.toggleEuiSwitch('includeAliasesSwitch');
});
component.update();
},
goToStep(step: number) {
while (--step > 0) {
find('nextButton').simulate('click');
}
component.update();
},
async clickRestore() {
await act(async () => {
find('restoreButton').simulate('click');
});
component.update();
},
};
};
@ -58,5 +81,8 @@ export const setup = async (): Promise<RestoreSnapshotTestBed> => {
export type RestoreSnapshotFormTestSubject =
| 'snapshotRestoreStepLogistics'
| 'includeGlobalStateSwitch'
| 'includeAliasesSwitch'
| 'nextButton'
| 'restoreButton'
| 'systemIndicesInfoCallOut'
| 'dataStreamWarningCallOut';

View file

@ -33,7 +33,7 @@ describe('<RestoreSnapshot />', () => {
testBed.component.update();
});
it('shows the data streams warning when the snapshot has data streams', () => {
test('shows the data streams warning when the snapshot has data streams', () => {
const { exists } = testBed;
expect(exists('dataStreamWarningCallOut')).toBe(true);
});
@ -49,7 +49,7 @@ describe('<RestoreSnapshot />', () => {
testBed.component.update();
});
it('hides the data streams warning when the snapshot has data streams', () => {
test('hides the data streams warning when the snapshot has data streams', () => {
const { exists } = testBed;
expect(exists('dataStreamWarningCallOut')).toBe(false);
});
@ -65,7 +65,7 @@ describe('<RestoreSnapshot />', () => {
testBed.component.update();
});
it('shows an info callout when include_global_state is enabled', () => {
test('shows an info callout when include_global_state is enabled', () => {
const { exists, actions } = testBed;
expect(exists('systemIndicesInfoCallOut')).toBe(false);
@ -75,4 +75,30 @@ describe('<RestoreSnapshot />', () => {
expect(exists('systemIndicesInfoCallOut')).toBe(true);
});
});
// NOTE: This suite can be expanded to simulate the user setting non-default values for all of
// the form controls and asserting that the correct payload is sent to the API.
describe('include aliases', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setGetSnapshotResponse(fixtures.getSnapshot());
httpRequestsMockHelpers.setRestoreSnapshotResponse({});
await act(async () => {
testBed = await setup();
});
testBed.component.update();
});
test('is sent to the API', async () => {
const { actions } = testBed;
actions.toggleIncludeAliases();
actions.goToStep(3);
await actions.clickRestore();
const expectedPayload = { includeAliases: false };
const latestRequest = server.requests[server.requests.length - 1];
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expectedPayload);
});
});
});

View file

@ -6,10 +6,7 @@
*/
export { flatten } from './flatten';
export {
deserializeRestoreSettings,
serializeRestoreSettings,
} from './restore_settings_serialization';
export { serializeRestoreSettings } from './restore_settings_serialization';
export {
deserializeSnapshotDetails,
deserializeSnapshotConfig,

View file

@ -5,10 +5,7 @@
* 2.0.
*/
import {
deserializeRestoreSettings,
serializeRestoreSettings,
} from './restore_settings_serialization';
import { serializeRestoreSettings } from './restore_settings_serialization';
describe('restore_settings_serialization()', () => {
it('should serialize blank restore settings', () => {
@ -56,6 +53,7 @@ describe('restore_settings_serialization()', () => {
indexSettings: '{"modified_setting":123}',
ignoreIndexSettings: ['setting1'],
ignoreUnavailable: true,
includeAliases: true,
})
).toEqual({
indices: ['foo', 'bar'],
@ -66,6 +64,7 @@ describe('restore_settings_serialization()', () => {
index_settings: { modified_setting: 123 },
ignore_index_settings: ['setting1'],
ignore_unavailable: true,
include_aliases: true,
});
});
@ -76,47 +75,4 @@ describe('restore_settings_serialization()', () => {
})
).toEqual({});
});
it('should deserialize blank restore settings', () => {
expect(deserializeRestoreSettings({})).toEqual({});
});
it('should deserialize partial restore settings', () => {
expect(deserializeRestoreSettings({})).toEqual({});
expect(
deserializeRestoreSettings({
indices: ['foo', 'bar'],
ignore_index_settings: ['setting1'],
partial: true,
})
).toEqual({
indices: ['foo', 'bar'],
ignoreIndexSettings: ['setting1'],
partial: true,
});
});
it('should deserialize full restore settings', () => {
expect(
deserializeRestoreSettings({
indices: ['foo', 'bar'],
rename_pattern: 'capture_pattern',
rename_replacement: 'replacement_pattern',
include_global_state: true,
partial: true,
index_settings: { modified_setting: 123 },
ignore_index_settings: ['setting1'],
ignore_unavailable: true,
})
).toEqual({
indices: ['foo', 'bar'],
renamePattern: 'capture_pattern',
renameReplacement: 'replacement_pattern',
includeGlobalState: true,
partial: true,
indexSettings: '{"modified_setting":123}',
ignoreIndexSettings: ['setting1'],
ignoreUnavailable: true,
});
});
});

View file

@ -26,6 +26,7 @@ export function serializeRestoreSettings(restoreSettings: RestoreSettings): Rest
indexSettings,
ignoreIndexSettings,
ignoreUnavailable,
includeAliases,
} = restoreSettings;
let parsedIndexSettings: RestoreSettingsEs['index_settings'] | undefined;
@ -47,32 +48,7 @@ export function serializeRestoreSettings(restoreSettings: RestoreSettings): Rest
index_settings: parsedIndexSettings,
ignore_index_settings: ignoreIndexSettings,
ignore_unavailable: ignoreUnavailable,
};
return removeUndefinedSettings(settings);
}
export function deserializeRestoreSettings(restoreSettingsEs: RestoreSettingsEs): RestoreSettings {
const {
indices,
rename_pattern: renamePattern,
rename_replacement: renameReplacement,
include_global_state: includeGlobalState,
partial,
index_settings: indexSettings,
ignore_index_settings: ignoreIndexSettings,
ignore_unavailable: ignoreUnavailable,
} = restoreSettingsEs;
const settings: RestoreSettings = {
indices,
renamePattern,
renameReplacement,
includeGlobalState,
partial,
indexSettings: indexSettings ? JSON.stringify(indexSettings) : undefined,
ignoreIndexSettings,
ignoreUnavailable,
include_aliases: includeAliases,
};
return removeUndefinedSettings(settings);

View file

@ -14,6 +14,7 @@ export interface RestoreSettings {
indexSettings?: string;
ignoreIndexSettings?: string[];
ignoreUnavailable?: boolean;
includeAliases?: boolean;
}
export interface RestoreSettingsEs {
@ -25,6 +26,7 @@ export interface RestoreSettingsEs {
index_settings?: { [key: string]: any };
ignore_index_settings?: string[];
ignore_unavailable?: boolean;
include_aliases?: boolean;
}
export interface SnapshotRestore {

View file

@ -140,6 +140,7 @@ export const RestoreSnapshotForm: React.FunctionComponent<Props> = ({
iconType="arrowRight"
onClick={() => onNext()}
disabled={!validation.isValid}
data-test-subj="nextButton"
>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.nextButtonLabel"
@ -156,6 +157,7 @@ export const RestoreSnapshotForm: React.FunctionComponent<Props> = ({
iconType="check"
onClick={() => executeRestore()}
isLoading={isSaving}
data-test-subj="restoreButton"
>
{isSaving ? (
<FormattedMessage

View file

@ -81,6 +81,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
renameReplacement,
partial,
includeGlobalState,
includeAliases,
} = restoreSettings;
// States for choosing all indices, or a subset, including caching previously chosen subset list
@ -625,6 +626,41 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
/>
</EuiFormRow>
</EuiDescribedFormGroup>
{/* Include aliases */}
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesTitle"
defaultMessage="Restore aliases"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesDescription"
defaultMessage="Restores index aliases along with their associated indices."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace={true} fullWidth>
<EuiSwitch
label={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeAliasesLabel"
defaultMessage="Restore aliases"
/>
}
checked={includeAliases === undefined ? true : includeAliases}
onChange={(e) => updateRestoreSettings({ includeAliases: e.target.checked })}
data-test-subj="includeAliasesSwitch"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</div>
);
};

View file

@ -176,4 +176,5 @@ export const restoreSettingsSchema = schema.object({
indexSettings: schema.maybe(schema.string()),
ignoreIndexSettings: schema.maybe(schema.arrayOf(schema.string())),
ignoreUnavailable: schema.maybe(schema.boolean()),
includeAliases: schema.maybe(schema.boolean()),
});