/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ import { EuiComboBoxOptionOption, EuiHealth, EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, EuiSpacer, EuiTitle, EuiFlexGroup, EuiFlexItem, EuiAccordion, EuiCheckbox, EuiCallOut, EuiText } from "@elastic/eui"; import _ from "lodash"; import React, { Component, ChangeEvent } from "react"; import FlyoutFooter from "../../../VisualCreatePolicy/components/FlyoutFooter"; import { CoreServicesContext } from "../../../../components/core_services"; import { IndexService, SnapshotManagementService } from "../../../../services"; import { RESTORE_OPTIONS } from "../../../../models/interfaces"; import { getErrorMessage } from "../../../../utils/helpers"; import { checkBadJSON, checkBadRegex, checkBadReplacement, checkCustomIgnoreConflict, checkNoSelectedIndices } from "../../helper" import { browseIndicesCols } from "../../../../utils/constants" import { ERROR_TOAST_TITLE } from "../../constants" import { IndexItem } from "../../../../../models/interfaces"; import { CatRepository, GetSnapshot } from "../../../../../server/models/interfaces"; import CustomLabel from "../../../../components/CustomLabel"; import SnapshotRestoreAdvancedOptions from "../SnapshotRestoreAdvancedOptions"; import SnapshotRestoreOption from "../SnapshotRestoreOption"; import SnapshotRenameOptions from "../SnapshotRenameOptions"; import AddPrefixInput from "../AddPrefixInput"; import RenameInput from "../RenameInput"; import SnapshotIndicesInput from "../SnapshotIndicesInput"; import IndexList from "../IndexList"; import { ERROR_PROMPT } from "../../../CreateSnapshotPolicy/constants"; interface RestoreSnapshotProps { snapshotManagementService: SnapshotManagementService; indexService: IndexService; onCloseFlyout: () => void; getRestoreTime: (time: number) => void restoreSnapshot: (snapshotId: string, repository: string, options: object) => void; snapshotId: string; repository: string; } interface RestoreSnapshotState { indexOptions: EuiComboBoxOptionOption<IndexItem>[]; selectedIndexOptions: EuiComboBoxOptionOption<IndexItem>[]; renameIndices: string; prefix: string; renamePattern: string; renameReplacement: string; listIndices: boolean; customIndexSettings?: string; ignoreIndexSettings?: string; indicesList: string[]; selectedRepoValue: string; repositories: CatRepository[]; snapshot: GetSnapshot | null; restoreSpecific: boolean; partial: boolean; badPattern: boolean; badRename: boolean; badJSON: boolean; badIgnore: boolean repoError: string; noIndicesSelected: boolean; snapshotIdError: string; } export default class RestoreSnapshotFlyout extends Component<RestoreSnapshotProps, RestoreSnapshotState> { static contextType = CoreServicesContext; constructor(props: RestoreSnapshotProps) { super(props); this.state = { indexOptions: [], selectedIndexOptions: [], renameIndices: "add_prefix", prefix: "restored_", renamePattern: "(.+)", renameReplacement: "restored_$1", listIndices: false, customIndexSettings: "", ignoreIndexSettings: "", indicesList: [], repositories: [], selectedRepoValue: "", snapshot: null, restoreSpecific: false, partial: false, badPattern: false, badRename: false, badJSON: false, badIgnore: false, noIndicesSelected: false, repoError: "", snapshotIdError: "", }; } async componentDidMount() { await this.getIndexOptions(); } onClickAction = () => { const { restoreSnapshot, snapshotId, repository, onCloseFlyout, getRestoreTime } = this.props; const { customIndexSettings, ignoreIndexSettings, restoreSpecific, selectedIndexOptions, indexOptions, renameIndices, prefix, snapshot, renamePattern, renameReplacement, } = this.state; const { add_prefix } = RESTORE_OPTIONS; const selectedIndices = selectedIndexOptions.map((option) => option.label).join(","); const allIndices = indexOptions.map((option) => option.label).join(","); const pattern = renameIndices === add_prefix ? "(.+)" : renamePattern; const options = { indices: restoreSpecific ? selectedIndices : allIndices, index_settings: customIndexSettings, ignore_index_settings: ignoreIndexSettings, ignore_unavailable: snapshot?.ignore_unavailable || false, include_global_state: snapshot?.include_global_state, rename_pattern: pattern, rename_replacement: renameIndices === add_prefix ? `${prefix}$1` : renameReplacement, include_aliases: snapshot?.restore_aliases ? snapshot.restore_aliases : true, partial: snapshot?.partial || false, }; let repoError = ""; if (options.index_settings?.length == 0) { delete options.index_settings; } if (options.ignore_index_settings?.length == 0) { delete options.ignore_index_settings; } const badJSON = options.index_settings ? checkBadJSON(options.index_settings) : false; const badPattern = checkBadRegex(options.rename_pattern); const badRename = checkBadReplacement(options.rename_replacement); const badIgnore = options.ignore_index_settings ? checkCustomIgnoreConflict(customIndexSettings, ignoreIndexSettings) : false; const noIndicesSelected = checkNoSelectedIndices(options.indices, restoreSpecific); if ((!badIgnore) && (badPattern || badRename || noIndicesSelected || badJSON)) { this.context.notifications.toasts.addDanger(null, { title: ERROR_TOAST_TITLE }); } if (badIgnore) { this.context.notifications.toasts.addDanger(null, { title: "Cannot apply and ignore the same index settings", text: "One or more index settings was declared in both Custom index settings and Ignore index settings. Remove the index setting from either fields." }); } this.setState({ badPattern, badRename, badIgnore, noIndicesSelected, badJSON }); if (badPattern || badRename || badIgnore || noIndicesSelected || badJSON) { return; } if (!options.ignore_index_settings) { delete options.ignore_index_settings; } if (!snapshotId.trim()) { this.setState({ snapshotIdError: "Required." }); return; } if (!repository) { repoError = ERROR_PROMPT.REPO; this.setState({ repoError }); return; } if (options.index_settings) { options.index_settings = JSON.parse(options.index_settings) } getRestoreTime(Date.now()); restoreSnapshot(snapshotId, repository, options); onCloseFlyout() }; onClickIndices = async () => { this.setState({ listIndices: true }); }; onBackArrowClick = () => { this.setState({ listIndices: false }); }; onIndicesSelectionChange = (selectedOptions: EuiComboBoxOptionOption<IndexItem>[]) => { const selectedIndexOptions = selectedOptions.map((o) => o.label); let newJSON = this.state.snapshot; newJSON!.indices = [...selectedIndexOptions]; this.setState({ snapshot: newJSON, selectedIndexOptions: selectedOptions }); }; getSnapshot = async (snapshotId: string, repository: string) => { const { snapshotManagementService } = this.props; try { const response = await snapshotManagementService.getSnapshot(snapshotId, repository); if (response.ok) { const newOptions = response.response.indices.map((index) => { return { label: index }; }); this.setState({ snapshot: response.response, indexOptions: [...newOptions] }); } else { const message = JSON.parse(response.error).error.root_cause[0].reason const trimmedMessage = message.slice(message.indexOf("]") + 1, message.indexOf(".") + 1); this.context.notifications.toasts.addError(response.error, { title: `There was a problem getting the snapshot`, toastMessage: `${trimmedMessage} Open browser console & click below for details.` }); } } catch (err) { this.context.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem loading the snapshot.")); } }; getIndexOptions = () => { const { snapshotId, repository } = this.props; this.getSnapshot(snapshotId, repository); }; getIndexSettings = (indexSettings: string, ignore: boolean) => { !ignore && this.setState({ customIndexSettings: indexSettings }); ignore && this.setState({ ignoreIndexSettings: indexSettings }); }; onCreateOption = (searchValue: string, options: Array<EuiComboBoxOptionOption<IndexItem>>) => { const normalizedSearchValue = searchValue.trim().toLowerCase(); if (!normalizedSearchValue) { return; } const newOption = { label: searchValue, }; // Create the option if it doesn't exist. if (options.findIndex((option) => option.label.trim().toLowerCase() === normalizedSearchValue) === -1) { this.setState({ indexOptions: [...this.state.indexOptions, newOption] }); } const selectedIndexOptions = [...this.state.selectedIndexOptions, newOption]; this.setState({ selectedIndexOptions: selectedIndexOptions }); }; getPrefix = (prefix: string) => { this.setState({ prefix: prefix }); }; getRenamePattern = (renamePattern: string) => { this.setState({ renamePattern: renamePattern }); }; getRenameReplacement = (renameReplacement: string) => { this.setState({ renameReplacement: renameReplacement }); }; onToggle = (e: ChangeEvent<HTMLInputElement>) => { const { restore_specific_indices, restore_all_indices, customize_index_settings, ignore_index_settings } = RESTORE_OPTIONS; if (e.target.id === restore_specific_indices) { this.setState({ restoreSpecific: true, snapshot: _.set(this.state.snapshot!, e.target.id, e.target.checked) }); return; } if (e.target.id === restore_all_indices) { this.setState({ restoreSpecific: false, noIndicesSelected: false, snapshot: _.set(this.state.snapshot!, e.target.id, e.target.checked) }); return; } if (e.target.id === customize_index_settings) { if (!e.target.checked) { this.setState({ customIndexSettings: "", badJSON: false }); } } if (e.target.id === ignore_index_settings) { if (!e.target.checked) { this.setState({ ignoreIndexSettings: "", badIgnore: false }); } } if (e.target.name === "rename_option") { let renamePattern = this.state.renamePattern; let renameReplacement = this.state.renameReplacement; if (e.target.id !== "rename_indices") { renamePattern = "(.+)" renameReplacement = "restored_$1" } this.setState({ renameIndices: e.target.id, renamePattern, badPattern: false, renameReplacement, badRename: false, snapshot: _.set(this.state.snapshot!, e.target.id, e.target.checked) }); return; } this.setState({ snapshot: _.set(this.state.snapshot!, e.target.id, e.target.checked) }); }; render() { const { onCloseFlyout, snapshotId } = this.props; const { indexOptions, selectedIndexOptions, selectedRepoValue, restoreSpecific, snapshot, renameIndices, listIndices, badPattern, badRename, badJSON, badIgnore, noIndicesSelected } = this.state; const { do_not_rename, add_prefix, rename_indices, restore_aliases, include_global_state, ignore_unavailable, partial, customize_index_settings, ignore_index_settings, } = RESTORE_OPTIONS; const status = snapshot ? snapshot?.state[0] + snapshot?.state.slice(1).toLowerCase() : undefined; const restoreDisabled = snapshot?.failed_shards && !snapshot?.partial; const snapshotIndices: IndexItem[] = snapshot?.indices.map((index) => ({ index: index })); return ( <EuiFlyout ownFocus={false} maxWidth={600} onClose={onCloseFlyout} size="m" hideCloseButton> {listIndices ? ( <IndexList indices={snapshotIndices} snapshot={snapshotId} columns={browseIndicesCols} onClick={this.onBackArrowClick} title="Indices in snapshot" /> ) : ( <> <EuiFlyoutHeader hasBorder> <EuiTitle size="m"> <h2 id="flyoutTitle">Restore snapshot</h2> </EuiTitle> </EuiFlyoutHeader> <EuiFlyoutBody> <EuiFlexGroup alignItems="flexStart"> <EuiFlexItem> <CustomLabel title="Snapshot name" /> <EuiSpacer size="xs" /> <h3 style={{ fontSize: "1.1rem" }}>{snapshot?.snapshot}</h3> </EuiFlexItem> <EuiFlexItem> <CustomLabel title="Status" /> <EuiHealth textSize="m" color={`${status?.toLowerCase()}`} title={`${status} indicator icon`}> {status}</EuiHealth> </EuiFlexItem> <EuiFlexItem> <CustomLabel title="Indices" /> <EuiSpacer size="xs" /> <a onClick={this.onClickIndices} style={{ fontSize: "1.1rem" }}>{snapshot?.indices.length}</a> </EuiFlexItem> </EuiFlexGroup> <EuiSpacer size="xxl" /> <SnapshotRestoreOption restoreAllIndices={!restoreSpecific} onRestoreAllIndicesToggle={this.onToggle} restoreSpecificIndices={restoreSpecific} onRestoreSpecificIndicesToggle={this.onToggle} width="200%" /> <EuiSpacer size="l" /> { restoreSpecific && ( <SnapshotIndicesInput indexOptions={indexOptions} selectedIndexOptions={selectedIndexOptions} onIndicesSelectionChange={this.onIndicesSelectionChange} getIndexOptions={this.getIndexOptions} onCreateOption={this.onCreateOption} selectedRepoValue={selectedRepoValue} showError={noIndicesSelected} isClearable={true} /> ) } <EuiSpacer size="l" /> <SnapshotRenameOptions doNotRename={renameIndices === do_not_rename} onDoNotRenameToggle={this.onToggle} addPrefix={renameIndices === add_prefix} onAddPrefixToggle={this.onToggle} renameIndices={renameIndices === rename_indices} onRenameIndicesToggle={this.onToggle} width="200%" /> {renameIndices === add_prefix && <AddPrefixInput getPrefix={this.getPrefix} />} { renameIndices === rename_indices && ( <RenameInput getRenamePattern={this.getRenamePattern} getRenameReplacement={this.getRenameReplacement} showPatternError={badPattern} showRenameError={badRename} /> ) } <EuiSpacer size="xxl" /> <EuiAccordion id="advanced_restore_options" buttonContent="Advanced options"> <EuiSpacer size="m" /> <SnapshotRestoreAdvancedOptions getIndexSettings={this.getIndexSettings} restoreAliases={String(_.get(snapshot, restore_aliases, true)) == "true"} onRestoreAliasesToggle={this.onToggle} restoreClusterState={String(_.get(snapshot, include_global_state, false)) == "true"} onRestoreClusterStateToggle={this.onToggle} ignoreUnavailable={String(_.get(snapshot, ignore_unavailable, false)) == "true"} onIgnoreUnavailableToggle={this.onToggle} customizeIndexSettings={String(_.get(snapshot, customize_index_settings, false)) == "true"} onCustomizeIndexSettingsToggle={this.onToggle} ignoreIndexSettings={String(_.get(snapshot, ignore_index_settings, false)) == "true"} onIgnoreIndexSettingsToggle={this.onToggle} width="200%" badJSONInput={badJSON} badIgnoreInput={badIgnore} /> </EuiAccordion> <EuiSpacer size="l" /> {snapshot?.failed_shards && <EuiCallOut title="Restoring a partial snapshot" color="warning" > <p> You are about to restore a partial snapshot. One or more shards may be missing in this <br /> snapshot. Do you want to continue? </p> <EuiSpacer size="s" /> <EuiCheckbox id={partial} label={<EuiText size="s">Allow restore partial snapshots</EuiText>} checked={String(_.get(snapshot, partial, false)) == "true"} onChange={this.onToggle} /> </EuiCallOut>} </EuiFlyoutBody> <EuiFlyoutFooter> <FlyoutFooter edit={true} restore={true} action="" onClickAction={this.onClickAction} onClickCancel={onCloseFlyout} disabledAction={!!restoreDisabled} /> </EuiFlyoutFooter> </> )} </EuiFlyout> ); } }