Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Snapshot restore] Add support for feature_states #131310

Merged
Merged
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
837cb19
Update copy to not include system indices
sabarasaba Apr 28, 2022
5de8d29
Dont include system indices in ds/indices dropdown
sabarasaba Apr 28, 2022
236f978
Start working on supporting feature states
sabarasaba Apr 28, 2022
a3bda45
Store feature states array of options in local state
sabarasaba May 2, 2022
8a6473c
Fix up server side integration and show deets in flyout
sabarasaba May 2, 2022
181d500
Fix linter issues
sabarasaba May 2, 2022
cb899ef
commit using @elastic.co
sabarasaba May 2, 2022
8e16971
Connect the dots in restore snapshot wizard
sabarasaba May 2, 2022
0080dcd
Fix linter issues
sabarasaba May 2, 2022
469a53c
Finish up wiring up last features
sabarasaba May 2, 2022
5e5ada2
Fix copy
sabarasaba May 2, 2022
b8a7dd9
CR
sabarasaba May 2, 2022
3d4c2ca
Refactor tooltip implementation
sabarasaba May 2, 2022
13c83ef
Fix tests
sabarasaba May 2, 2022
1595fa7
Fix i18n
sabarasaba May 2, 2022
7711a1c
Add tests
sabarasaba May 3, 2022
d6f0a7d
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine May 3, 2022
e0b2f84
Add missing tests
sabarasaba May 4, 2022
e845207
Merge branch 'main' into snapshot_restore/system_indices_fix
kibanamachine May 4, 2022
ec9d815
Add option for selecting none of the feature states
sabarasaba May 5, 2022
d750fee
Finish off refactoring label placement and fixing up tests
sabarasaba May 5, 2022
79f5736
Add tests
sabarasaba May 6, 2022
433b30b
Remove nextTick and refactor tests
sabarasaba May 9, 2022
dbebbfc
Refactor feature states into its own setting
sabarasaba May 9, 2022
5cea032
Fix docs link
sabarasaba May 9, 2022
38fe3d1
Copy review
sabarasaba May 9, 2022
fa8c67b
Fix tests
sabarasaba May 10, 2022
3aad8f3
Fix small bug and add more tests
sabarasaba May 10, 2022
d8f782f
Merge branch 'main' into snapshot_restore/system_indices_fix
kibanamachine May 10, 2022
8f16d3b
Fix linter issue
sabarasaba May 10, 2022
1c514ee
Address CR
sabarasaba May 10, 2022
63c69e6
Change duped locale id
sabarasaba May 10, 2022
5b3dc19
Address CR changes
sabarasaba May 10, 2022
1413dd6
Copy updates
sabarasaba May 12, 2022
fef1131
Merge branch 'main' into snapshot_restore/system_indices_fix
kibanamachine May 12, 2022
eca5016
CR changes
sabarasaba May 16, 2022
9124877
Merge branch 'main' into snapshot_restore/system_indices_fix
kibanamachine May 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor feature states into its own setting
sabarasaba committed May 9, 2022

Verified

This commit was signed with the committer’s verified signature.
sabarasaba Ignacio Rivas
commit dbebbfcb4aad84be11b5f477e4e03851c097aa92
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/get_doc_links.ts
Original file line number Diff line number Diff line change
@@ -539,6 +539,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
registerUrl: `${ELASTICSEARCH_DOCS}snapshots-read-only-repository.html#read-only-url-repository-settings`,
restoreSnapshot: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html`,
restoreSnapshotApi: `${ELASTICSEARCH_DOCS}restore-snapshot-api.html#restore-snapshot-api-request-body`,
restoreSnapshotFeatureStates: `${ELASTICSEARCH_DOCS}snapshots-restore-snapshot.html#restore-feature-state`,
searchableSnapshotSharedCache: `${ELASTICSEARCH_DOCS}searchable-snapshots.html#searchable-snapshots-shared-cache`,
},
ingest: {
Original file line number Diff line number Diff line change
@@ -6,114 +6,42 @@
*/

import React, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';

import {
EuiFormRow,
EuiSwitch,
EuiIconTip,
EuiComboBox,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';

import { useServices } from '../../app_context';
import { FEATURE_STATES_NONE_OPTION } from '../../../../common/constants';
import { SlmPolicyPayload, RestoreSettings } from '../../../../common/types';

export type FeaturesOption = EuiComboBoxOptionOption<string>;

interface Props {
featuresOptions: FeaturesOption[];
selectedOptions: FeaturesOption[];
setSelectedOptions: (features: FeaturesOption[]) => void;
onUpdateFormSettings: (
arg: Partial<SlmPolicyPayload['config']> & Partial<RestoreSettings>
) => void;
hasNoneOptionSelected: boolean;
isLoadingFeatures?: boolean;
}

export const FeatureStatesFormField: FunctionComponent<Props> = ({
isLoadingFeatures = false,
featuresOptions,
selectedOptions,
setSelectedOptions,
onUpdateFormSettings,
hasNoneOptionSelected,
}) => {
const { i18n } = useServices();

const onChange = (selected: FeaturesOption[]) => {
setSelectedOptions(selected);
onUpdateFormSettings({
featureStates: selected.map((option) => option.label),
});
};

const onIncludeNoneSwitchChange = () => {
if (!hasNoneOptionSelected) {
setSelectedOptions([{ label: FEATURE_STATES_NONE_OPTION }]);
onUpdateFormSettings({ featureStates: [FEATURE_STATES_NONE_OPTION] });
} else {
setSelectedOptions([]);
onUpdateFormSettings({ featureStates: [] });
}
};

return (
<EuiFormRow
label={
<>
<FormattedMessage
id="xpack.snapshotRestore.featureStatesFormField.formRowLabel"
defaultMessage="Include feature states from"
/>{' '}
<EuiIconTip
type="questionInCircle"
content={
<span>
<FormattedMessage
id="xpack.snapshotRestore.featureStatesFormField.formRowLabelTooltip"
defaultMessage="A feature state contains the indices, system indices and data streams used to store configurations, history, and other data for an Elastic feature."
/>
</span>
}
iconProps={{
className: 'eui-alignTop',
}}
/>
</>
}
labelAppend={
<EuiSwitch
data-test-subj="toggleIncludeNone"
compressed
label={
<>
<FormattedMessage
id="xpack.snapshotRestore.featureStatesFormField.includeNoneLabel"
defaultMessage="Include none"
/>{' '}
<EuiIconTip
type="questionInCircle"
content={
<span>
<FormattedMessage
id="xpack.snapshotRestore.featureStatesFormField.includeNoneDescription"
defaultMessage="All system indices can be omitted by including none of the feature states."
/>
</span>
}
iconProps={{
className: 'eui-alignTop',
}}
/>
</>
}
checked={hasNoneOptionSelected}
onChange={onIncludeNoneSwitchChange}
/>
}
label={i18n.translate('xpack.snapshotRestore.featureStatesFormField.formRowLabel', {
defaultMessage: 'Include feature states from',
})}
>
<EuiComboBox
data-test-subj="featureStatesDropdown"
@@ -122,8 +50,7 @@ export const FeatureStatesFormField: FunctionComponent<Props> = ({
{ defaultMessage: 'All features' }
)}
options={featuresOptions}
selectedOptions={hasNoneOptionSelected ? [] : selectedOptions}
isDisabled={hasNoneOptionSelected}
selectedOptions={selectedOptions}
onChange={onChange}
isLoading={isLoadingFeatures}
isClearable={true}
Original file line number Diff line number Diff line change
@@ -67,10 +67,10 @@ export const PolicyForm: React.FunctionComponent<Props> = ({
const [policy, setPolicy] = useState<SlmPolicyPayload>({
...originalPolicy,
config: {
// By default, the policy has includeGlobalState enabled but it doesnt get
// reflected in the form state. We need to have it here because the feature_states
// dropdown depends on it.
includeGlobalState: true,
// By default, the policy has includeGlobalState enabled but since when
// a policy is created with just that option it also includes all featureStates
// we also want to set all featureStates to be enabled by default.
featureStates: [],
...(originalPolicy.config || {}),
},
retention: {
Original file line number Diff line number Diff line change
@@ -24,7 +24,8 @@ import {
import { serializePolicy } from '../../../../../common/lib';
import { useServices } from '../../../app_context';
import { StepProps } from '.';
import { CollapsibleIndicesList, CollapsibleFeatureStatesList } from '../../collapsible_lists';
import { CollapsibleIndicesList } from '../../collapsible_lists';
import { PolicyFeatureStatesSummary } from '../../summaries';

export const PolicyStepReview: React.FunctionComponent<StepProps> = ({
policy,
@@ -190,47 +191,49 @@ export const PolicyStepReview: React.FunctionComponent<StepProps> = ({
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialIndicesLabel"
defaultMessage="Allow partial indices"
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel"
defaultMessage="Include global state"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{partial ? (
{includeGlobalState === false ? (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel"
defaultMessage="Yes"
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel"
defaultMessage="No"
/>
) : (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel"
defaultMessage="No"
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel"
defaultMessage="Yes"
/>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
<PolicyFeatureStatesSummary featureStates={featureStates} />
</EuiFlexGroup>

<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel"
defaultMessage="Include global state"
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialIndicesLabel"
defaultMessage="Allow partial indices"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{includeGlobalState === false ? (
{partial ? (
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel"
defaultMessage="No, without any feature states"
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel"
defaultMessage="Yes"
/>
) : (
<>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel"
defaultMessage="Yes, including feature states from:"
/>{' '}
<CollapsibleFeatureStatesList featureStates={featureStates} />
</>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel"
defaultMessage="No"
/>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';

import {
EuiDescribedFormGroup,
EuiFormRow,
EuiSwitch,
EuiSwitchEvent,
EuiTitle,
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 } = useLoadFeatures();

const features = useMemo(() => {
if (!isLoadingFeatures && !errorLoadingFeatures) {
const featuresList = data?.features.map((feature) => ({
label: feature.name,
}));

return sortBy(featuresList, 'label');
}

return [];
}, [isLoadingFeatures, errorLoadingFeatures, data]);

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 states"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeFeatureStatesDescription"
defaultMessage="Stores the state for all the features as part of the snapshot. This will capture all system indices and other required indices and data streams in addition to any specific indices that have been selected for capture."
/>
}
fullWidth
>
<EuiFormRow hasEmptyLabelSpace fullWidth>
<EuiSwitch
data-test-subj="featureStatesToggle"
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.policyIncludeFeatureStatesLabel"
defaultMessage="Include feature states"
/>
}
checked={isFeatureStatesToggleEnabled}
onChange={onFeatureStatesToggleChange}
/>
</EuiFormRow>

{isFeatureStatesToggleEnabled && (
<>
<EuiSpacer size="m" />
<FeatureStatesFormField
isLoadingFeatures={isLoadingFeatures}
featuresOptions={features}
selectedOptions={selectedOptions}
onUpdateFormSettings={onUpdate}
/>
</>
)}
</EuiDescribedFormGroup>
);
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -5,24 +5,21 @@
* 2.0.
*/

import React, { FunctionComponent, useState, useMemo } from 'react';
import { sortBy } from 'lodash';
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';

import {
EuiDescribedFormGroup,
EuiFormRow,
EuiSwitch,
EuiSwitchEvent,
EuiTitle,
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;
@@ -34,27 +31,18 @@ export type FeaturesOption = EuiComboBoxOptionOption<string>;

export const IncludeGlobalStateField: FunctionComponent<Props> = ({ policy, onUpdate }) => {
const { config = {} } = policy;
const { error: errorLoadingFeatures, isLoading: isLoadingFeatures, data } = useLoadFeatures();

const features = useMemo(() => {
if (!isLoadingFeatures && !errorLoadingFeatures) {
const featuresList = data?.features.map((feature) => ({
label: feature.name,
}));
const onIncludeGlobalStateToggle = (event: EuiSwitchEvent) => {
const { checked } = event.target;
const hasFeatureStates = !config?.featureStates?.includes(FEATURE_STATES_NONE_OPTION);

return sortBy(featuresList, 'label');
}

return [];
}, [isLoadingFeatures, errorLoadingFeatures, data]);

const [selectedOptions, setSelected] = useState(
config?.featureStates?.map((feature) => ({ label: feature })) as FeaturesOption[]
);

const hasNoneOptionSelected = !!selectedOptions?.find(
(option) => option.label === 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
@@ -63,15 +51,15 @@ export const IncludeGlobalStateField: FunctionComponent<Props> = ({ policy, onUp
<h3>
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle"
defaultMessage="Include global state and feature states"
defaultMessage="Include global state"
/>
</h3>
</EuiTitle>
}
description={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription"
defaultMessage="Stores the global cluster state and the state for all the features as part of the snapshot. This will capture all system indices and other required indices and data streams in addition to any specific indices that have been selected for capture."
defaultMessage="Stores the global cluster state as part of the snapshot."
/>
}
fullWidth
@@ -82,31 +70,13 @@ export const IncludeGlobalStateField: FunctionComponent<Props> = ({ policy, onUp
label={
<FormattedMessage
id="xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel"
defaultMessage="Include global state and feature states"
defaultMessage="Include global state"
/>
}
checked={config.includeGlobalState === undefined || config.includeGlobalState}
onChange={(e) => {
onUpdate({
featureStates: undefined,
includeGlobalState: e.target.checked,
});
}}
onChange={onIncludeGlobalStateToggle}
/>
</EuiFormRow>
{config.includeGlobalState && (
<>
<EuiSpacer size="m" />
<FeatureStatesFormField
isLoadingFeatures={isLoadingFeatures}
featuresOptions={features}
selectedOptions={hasNoneOptionSelected ? [] : selectedOptions}
setSelectedOptions={setSelected}
onUpdateFormSettings={onUpdate}
hasNoneOptionSelected={hasNoneOptionSelected}
/>
</>
)}
</EuiDescribedFormGroup>
);
};
Original file line number Diff line number Diff line change
@@ -7,3 +7,4 @@

export { IndicesAndDataStreamsField } from './indices_and_data_streams_field';
export { IncludeGlobalStateField } from './include_global_state_field';
export { IncludeFeatureStatesField } from './include_feature_states_field';
Original file line number Diff line number Diff line change
@@ -21,7 +21,11 @@ import {
import { SlmPolicyPayload } from '../../../../../../common/types';
import { StepProps } from '..';

import { IndicesAndDataStreamsField, IncludeGlobalStateField } from './fields';
import {
IndicesAndDataStreamsField,
IncludeGlobalStateField,
IncludeFeatureStatesField,
} from './fields';
import { useCore } from '../../../../app_context';

export const PolicyStepSettings: React.FunctionComponent<StepProps> = ({
@@ -172,6 +176,7 @@ export const PolicyStepSettings: React.FunctionComponent<StepProps> = ({
{renderPartialField()}

<IncludeGlobalStateField errors={errors} policy={policy} onUpdate={updatePolicyConfig} />
<IncludeFeatureStatesField errors={errors} policy={policy} onUpdate={updatePolicyConfig} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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>({
Original file line number Diff line number Diff line change
@@ -5,9 +5,10 @@
* 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 { sortBy } from 'lodash';
import {
EuiButtonEmpty,
EuiDescribedFormGroup,
@@ -62,7 +63,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
dataStreams: snapshotDataStreams = [],
includeGlobalState: snapshotIncludeGlobalState,
version,
featureStates,
featureStates: snapshotIncludeFeatureStates,
} = snapshotDetails;

const snapshotIndices = unfilteredSnapshotIndices.filter(
@@ -90,6 +91,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
renameReplacement,
partial,
includeGlobalState,
featureStates,
includeAliases,
} = restoreSettings;

@@ -157,21 +159,23 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
renameReplacement: '',
});

const [selectedFeatureStateOptions, setSelectedFeatureStateOptions] = useState(
restoreSettings?.featureStates?.map((feature) => ({ label: feature })) as FeaturesOption[]
);
const onRestoreGlobalStateToggleChange = (event: EuiSwitchEvent) => {
updateRestoreSettings({
includeGlobalState: event.target.checked,
});
};

const hasNoneOptionSelected = !!selectedFeatureStateOptions?.find(
(option) => option.label === FEATURE_STATES_NONE_OPTION
);
const selectedFeatureStateOptions = useMemo(() => {
return featureStates?.map((feature) => ({ label: feature })) as FeaturesOption[];
}, [featureStates]);

const onRestoreGlobalStateToggleChange = (event: EuiSwitchEvent) => {
const isFeatureStatesToggleEnabled =
featureStates !== undefined && !featureStates?.includes(FEATURE_STATES_NONE_OPTION);
const onFeatureStatesToggleChange = (event: EuiSwitchEvent) => {
const { checked } = event.target;

updateRestoreSettings({
includeGlobalState: checked,
featureStates:
checked && featureStates.length === 0 ? [FEATURE_STATES_NONE_OPTION] : undefined,
featureStates: checked ? [] : [FEATURE_STATES_NONE_OPTION],
});
};

@@ -590,33 +594,90 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
<h3>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle"
defaultMessage="Restore global state and feature states"
defaultMessage="Restore global state"
/>
</h3>
</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. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink target="_blank" href={docLinks.links.snapshotRestore.restoreSnapshotApi}>
{i18n.translate(
'xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink',
{ defaultMessage: 'Learn more.' }
)}
</EuiLink>
),
}}
/>
}
fullWidth
>
<EuiFormRow
hasEmptyLabelSpace={true}
fullWidth
helpText={
snapshotIncludeGlobalState ? null : (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription"
defaultMessage="Not available for this snapshot."
/>
)
}
>
<EuiSwitch
label={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel"
defaultMessage="Restore global state"
/>
}
checked={includeGlobalState === undefined ? false : includeGlobalState}
onChange={onRestoreGlobalStateToggleChange}
disabled={!snapshotIncludeGlobalState}
data-test-subj="includeGlobalStateSwitch"
/>
</EuiFormRow>
</EuiDescribedFormGroup>

{/* Include feature states */}
<EuiDescribedFormGroup
title={
<EuiTitle size="s">
<h3>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeFeatureStatesTitle"
defaultMessage="Restore feature states"
/>
</h3>
</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 from all features. {learnMoreLink}"
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeFeatureStatesDescription"
defaultMessage="Restores persistent settings and all system indices from features. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink target="_blank" href={docLinks.links.snapshotRestore.restoreSnapshotApi}>
<EuiLink target="_blank" href={docLinks.links.snapshotRestore.restoreSnapshotFeatureStates}>
{i18n.translate(
'xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDocLink',
'xpack.snapshotRestore.restoreForm.stepLogistics.includeFeatureStatesDocLink',
{ defaultMessage: 'Learn more.' }
)}
</EuiLink>
),
}}
/>

{includeGlobalState &&
semverGt(version, '7.12.0') &&
featureStates.length > 0 &&
!hasNoneOptionSelected && (
{semverGt(version, '7.12.0') &&
featureStates &&
featureStates?.length >= 0 &&
isFeatureStatesToggleEnabled && (
<>
<EuiSpacer size="s" />
<SystemIndicesOverwrittenCallOut featureStates={restoreSettings?.featureStates} />
@@ -630,7 +691,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
hasEmptyLabelSpace={true}
fullWidth
helpText={
snapshotIncludeGlobalState ? null : (
snapshotIncludeFeatureStates ? null : (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription"
defaultMessage="Not available for this snapshot."
@@ -641,30 +702,28 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
<EuiSwitch
label={
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel"
defaultMessage="Restore global state and feature states"
id="xpack.snapshotRestore.restoreForm.stepLogistics.restoreFeatureStatesLabel"
defaultMessage="Restore feature states"
/>
}
checked={includeGlobalState === undefined ? false : includeGlobalState}
onChange={onRestoreGlobalStateToggleChange}
disabled={!snapshotIncludeGlobalState}
data-test-subj="includeGlobalStateSwitch"
checked={isFeatureStatesToggleEnabled}
onChange={onFeatureStatesToggleChange}
disabled={snapshotIncludeFeatureStates?.length === 0}
data-test-subj="includeFeatureStatesSwitch"
/>
</EuiFormRow>

{includeGlobalState && featureStates.length > 0 && (
{isFeatureStatesToggleEnabled && (
<>
<EuiSpacer size="m" />
<FeatureStatesFormField
featuresOptions={featureStates.map((feature) => ({ label: feature }))}
selectedOptions={hasNoneOptionSelected ? [] : selectedFeatureStateOptions}
setSelectedOptions={setSelectedFeatureStateOptions}
featuresOptions={snapshotIncludeFeatureStates?.map((feature) => ({ label: feature }))}
selectedOptions={selectedFeatureStateOptions}
onUpdateFormSettings={updateRestoreSettings}
hasNoneOptionSelected={hasNoneOptionSelected}
/>
</>
)}
{includeGlobalState && featureStates.length === 0 && (
{snapshotIncludeFeatureStates?.length === 0 && (
<>
<EuiSpacer size="m" />
<EuiCallOut
Original file line number Diff line number Diff line change
@@ -26,7 +26,8 @@ import { serializeRestoreSettings } from '../../../../../common/lib';
import { EuiCodeEditor } from '../../../../shared_imports';
import { useServices } from '../../../app_context';
import { StepProps } from '.';
import { CollapsibleIndicesList, CollapsibleFeatureStatesList } from '../../collapsible_lists';
import { CollapsibleIndicesList } from '../../collapsible_lists';
import { PolicyFeatureStatesSummary } from '../../summaries';

export const RestoreSnapshotStepReview: React.FunctionComponent<StepProps> = ({
restoreSettings,
@@ -130,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">
@@ -170,21 +146,49 @@ export const RestoreSnapshotStepReview: React.FunctionComponent<StepProps> = ({
{includeGlobalState === false ? (
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateFalseValue"
defaultMessage="No, without any feature states"
defaultMessage="No"
/>
) : (
<>
<FormattedMessage
id="xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue"
defaultMessage="Yes, including feature states from:"
/>{' '}
<CollapsibleFeatureStatesList featureStates={featureStates} />
</>
<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}

Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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> = ({
featureStates = [],
}) => {
const hasNoFeatureStates = featureStates?.includes(FEATURE_STATES_NONE_OPTION);
const hasAllFeatureStates = featureStates?.length === 0;

return (
<EuiFlexItem data-test-subj="policyFeatureStatesSummary">
<EuiDescriptionList textStyle="reverse">
<EuiDescriptionListTitle>
<FormattedMessage
id="xpack.snapshotRestore.summary.policyFeatureStatesLabel"
defaultMessage="Include feature states"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{hasNoFeatureStates && (
<FormattedMessage
id="xpack.snapshotRestore.summary.policyNoFeatureStatesLabel"
defaultMessage="No"
/>
)}
{hasAllFeatureStates && (
<FormattedMessage
id="xpack.snapshotRestore.summary.policyAllFeatureStatesLabel"
defaultMessage="From all features"
sabarasaba marked this conversation as resolved.
Show resolved Hide resolved
/>
)}
{!hasNoFeatureStates && !hasAllFeatureStates && (
<>
<FormattedMessage
id="xpack.snapshotRestore.summary.policyFeatureStatesFromLabel"
defaultMessage="Only from:"
/>{' '}
<CollapsibleFeatureStatesList featureStates={featureStates} />
</>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
);
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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 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 states"
/>
</EuiDescriptionListTitle>
<EuiDescriptionListDescription>
{hasNoFeatureStates ? (
<FormattedMessage
id="xpack.snapshotRestore.summary.snapshotNoFeatureStatesLabel"
defaultMessage="No"
/>
) : (
<>
<FormattedMessage
id="xpack.snapshotRestore.summary.snapshotFeatureStatesFromLabel"
defaultMessage="Only from:"
/>{' '}
<CollapsibleFeatureStatesList featureStates={featureStates} />
</>
)}
</EuiDescriptionListDescription>
</EuiDescriptionList>
</EuiFlexItem>
);
};
Original file line number Diff line number Diff line change
@@ -25,12 +25,9 @@ import {
import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public';
import { SlmPolicy } from '../../../../../../../common/types';
import { useServices } from '../../../../../app_context';
import {
FormattedDateTime,
CollapsibleIndicesList,
CollapsibleFeatureStatesList,
} from '../../../../../components';
import { FormattedDateTime, CollapsibleIndicesList } from '../../../../../components';
import { linkToSnapshots, linkToRepository } from '../../../../../services/navigation';
import { PolicyFeatureStatesSummary } from '../../../../../components/summaries';

interface Props {
policy: SlmPolicy;
@@ -316,18 +313,19 @@ export const TabSummary: React.FunctionComponent<Props> = ({ policy }) => {
defaultMessage="No"
/>
) : (
<>
<FormattedMessage
data-test-subj="withGlobalStateAndFeatureStates"
id="xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel"
defaultMessage="Yes, including feature states from:"
/>{' '}
<CollapsibleFeatureStatesList featureStates={featureStates} />
</>
<FormattedMessage
data-test-subj="withGlobalStateAndFeatureStates"
id="xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel"
defaultMessage="Yes"
/>
)}
</EuiDescriptionListDescription>
</EuiFlexItem>
</EuiFlexGroup>

<EuiFlexGroup>
<PolicyFeatureStatesSummary featureStates={featureStates} />
</EuiFlexGroup>
</EuiDescriptionList>

{retention && (
Original file line number Diff line number Diff line change
@@ -25,11 +25,11 @@ import {
FormattedDateTime,
CollapsibleIndicesList,
CollapsibleDataStreamsList,
CollapsibleFeatureStatesList,
} from '../../../../../components';
import { linkToPolicy } from '../../../../../services/navigation';
import { SnapshotState } from './snapshot_state';
import { useServices } from '../../../../../app_context';
import { SnapshotFeatureStatesSummary } from '../../../../../components/summaries';

interface Props {
snapshotDetails: SnapshotDetails;
@@ -99,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
@@ -115,31 +141,16 @@ export const TabSummary: React.FC<Props> = ({ snapshotDetails }) => {
defaultMessage="No"
/>
) : (
<>
{/*
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.
*/}
{featureStates.length === 0 ? (
<FormattedMessage
id="xpack.snapshotRestore.featureStatesList.noFeatureStates"
defaultMessage="Yes, without any feature states"
/>
) : (
<>
<FormattedMessage
data-test-subj="withGlobalStateAndFeatureStates"
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateYesLabel"
defaultMessage="Yes, including feature states from:"
/>{' '}
<CollapsibleFeatureStatesList featureStates={featureStates} />
</>
)}
</>
<FormattedMessage
data-test-subj="withGlobalState"
id="xpack.snapshotRestore.snapshotDetails.itemIncludeGlobalStateYesLabel"
defaultMessage="Yes"
/>
)}
</EuiDescriptionListDescription>
</EuiFlexItem>

<SnapshotFeatureStatesSummary featureStates={featureStates} />
</EuiFlexGroup>

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