diff --git a/.github/workflows/unit-tests-workflow.yml b/.github/workflows/unit-tests-workflow.yml index 07697c9eb..88f75e664 100644 --- a/.github/workflows/unit-tests-workflow.yml +++ b/.github/workflows/unit-tests-workflow.yml @@ -7,7 +7,7 @@ on: branches: - "*" env: - OPENSEARCH_DASHBOARDS_VERSION: '2.2' + OPENSEARCH_DASHBOARDS_VERSION: '2.3' jobs: tests: name: Run unit tests diff --git a/cypress/integration/rollups_spec.js b/cypress/integration/rollups_spec.js index 866e49ab1..25fa7c53d 100644 --- a/cypress/integration/rollups_spec.js +++ b/cypress/integration/rollups_spec.js @@ -14,10 +14,12 @@ describe("Rollups", () => { localStorage.setItem("home:welcome:show", "true"); // Go to sample data page - cy.visit(`${Cypress.env("opensearch_dashboards")}/app/home#/tutorial_directory/sampleData`); + cy.visit(`${Cypress.env("opensearch_dashboards")}/app/home#/tutorial_directory`); - // Click on "Sample data" tab - cy.contains("Sample data").click({ force: true }); + cy.wait(10000) + + // // Click on "Sample data" tab + // cy.contains("Add data").click({ force: true }); // Load sample eCommerce data cy.get(`button[data-test-subj="addSampleDataSetecommerce"]`).click({ force: true }); diff --git a/cypress/integration/snapshots_spec.js b/cypress/integration/snapshots_spec.js new file mode 100644 index 000000000..b412fbd74 --- /dev/null +++ b/cypress/integration/snapshots_spec.js @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PLUGIN_NAME } from "../support/constants"; + +describe("Snapshots", () => { + before(() => { + // Delete any existing indices + cy.deleteAllIndices(); + + // Load ecommerce data + cy.request({ + method: "POST", + url: `${Cypress.env("opensearch_dashboards")}/api/sample_data/ecommerce`, + headers: { + "osd-xsrf": true, + }, + }).then((response) => { + expect(response.status).equal(200); + }); + + // Load flight data + cy.request({ + method: "POST", + url: `${Cypress.env("opensearch_dashboards")}/api/sample_data/flights`, + headers: { + "osd-xsrf": true, + }, + }).then((response) => { + expect(response.status).equal(200); + }); + + // Load web log data + cy.request({ + method: "POST", + url: `${Cypress.env("opensearch_dashboards")}/api/sample_data/logs`, + headers: { + "osd-xsrf": true, + }, + }).then((response) => { + expect(response.status).equal(200); + }); + }); + + beforeEach(() => { + // Set welcome screen tracking to false + localStorage.setItem("home:welcome:show", "false"); + + // Visit ISM Snapshots Dashboard + cy.visit(`${Cypress.env("opensearch_dashboards")}/app/${PLUGIN_NAME}#/snapshots`); + + // Common text to wait for to confirm page loaded, give up to 60 seconds for initial load + cy.contains("Restore", { timeout: 60000 }); + }); + + describe("Repository can be created", () => { + it("successfully creates a new repository", () => { + // Create repository to store snapshots + cy.visit(`${Cypress.env("opensearch_dashboards")}/app/${PLUGIN_NAME}#/repositories`); + + // Route to create repository page + cy.contains("Create repository").click({ force: true }); + + // Type in repository name + cy.get(`input[data-test-subj="repoNameInput"]`).focus().type("test_repo"); + + // Type in repository location + cy.get(`input[placeholder="e.g., /mnt/snapshots"]`).focus().type("~/Desktop"); + + // Click Add button + cy.get("button").contains("Add").click({ force: true }); + + // Confirm repository created + cy.contains("test_repo"); + }); + }); + + describe("Snapshot can be created", () => { + it("successfully creates a new snapshot", () => { + // Click Take snapshot button + cy.get("button").contains("Take snapshot").click({ force: true }); + + // Type in Snapshot name + cy.get(`input[data-test-subj="snapshotNameInput"]`).type("test_snapshot{enter}"); + + // Select indexes to be included + cy.get(`[data-test-subj="indicesComboBoxInput"]`).type("open*{enter}"); + + // Confirm test_repo exists + cy.contains("test_repo"); + + // Click 'Add' button to create snapshot + cy.get("button").contains("Add").click({ force: true }); + + // check for success status and snapshot name + cy.contains("In_progress"); + cy.contains("test_snapshot"); + }); + }); +}); diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index 5a645fecc..e1eb26f39 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -55,3 +55,17 @@ export interface LatestActivities { cause?: string; }; } + +export enum RESTORE_OPTIONS { + restore_specific_indices = "restore_specific_indices", + restore_all_indices = "restore_all_indices", + do_not_rename = "do_not_rename", + add_prefix = "add_prefix", + rename_indices = "rename_indices", + restore_aliases = "restore_aliases", + include_global_state = "include_global_state", + ignore_unavailable = "ignore_unavailable", + partial = "partial", + customize_index_settings = "customize_index_settings", + ignore_index_settings = "ignore_index_settings", +} diff --git a/public/pages/CreateSnapshotPolicy/components/SnapshotIndicesRepoInput/SnapshotIndicesRepoInput.tsx b/public/pages/CreateSnapshotPolicy/components/SnapshotIndicesRepoInput/SnapshotIndicesRepoInput.tsx index 9c63bbec6..590889cdc 100644 --- a/public/pages/CreateSnapshotPolicy/components/SnapshotIndicesRepoInput/SnapshotIndicesRepoInput.tsx +++ b/public/pages/CreateSnapshotPolicy/components/SnapshotIndicesRepoInput/SnapshotIndicesRepoInput.tsx @@ -73,6 +73,7 @@ const SnapshotIndicesRepoInput = ({ onSearchChange={getIndexOptions} onCreateOption={onCreateOption} isClearable={true} + data-test-subj="indicesComboBoxInput" /> diff --git a/public/pages/Repositories/components/CreateRepositoryFlyout/CreateRepositoryFlyout.tsx b/public/pages/Repositories/components/CreateRepositoryFlyout/CreateRepositoryFlyout.tsx index 160af0edc..c3c80787b 100644 --- a/public/pages/Repositories/components/CreateRepositoryFlyout/CreateRepositoryFlyout.tsx +++ b/public/pages/Repositories/components/CreateRepositoryFlyout/CreateRepositoryFlyout.tsx @@ -258,7 +258,12 @@ export default class CreateRepositoryFlyout extends Component - this.setState({ repoName: e.target.value })} /> + this.setState({ repoName: e.target.value })} + /> diff --git a/public/pages/Snapshots/components/AddPrefixInput/AddPrefixInput.tsx b/public/pages/Snapshots/components/AddPrefixInput/AddPrefixInput.tsx new file mode 100644 index 000000000..4f6dd08c3 --- /dev/null +++ b/public/pages/Snapshots/components/AddPrefixInput/AddPrefixInput.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiFormRow, EuiFieldText, EuiSpacer } from "@elastic/eui"; +import React, { useState, ChangeEvent } from "react"; +import CustomLabel from "../../../../components/CustomLabel"; + +interface AddPrefixesInputProps { + getPrefix: (prefix: string) => void; +} + +const AddPrefixesInput = ({ getPrefix }: AddPrefixesInputProps) => { + const [prefix, setPrefix] = useState(""); + + const onPrefixChange = (e: ChangeEvent) => { + setPrefix(e.target.value); + getPrefix(e.target.value); + }; + + return ( + <> + + + + + + + + + + ); +}; + +export default AddPrefixesInput; diff --git a/public/pages/Snapshots/components/AddPrefixInput/index.ts b/public/pages/Snapshots/components/AddPrefixInput/index.ts new file mode 100644 index 000000000..2ae205f75 --- /dev/null +++ b/public/pages/Snapshots/components/AddPrefixInput/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import AddPrefixInput from "./AddPrefixInput"; + +export default AddPrefixInput; diff --git a/public/pages/Snapshots/components/CreateSnapshotFlyout/CreateSnapshotFlyout.tsx b/public/pages/Snapshots/components/CreateSnapshotFlyout/CreateSnapshotFlyout.tsx index 2ab6397d9..4657ad5f6 100644 --- a/public/pages/Snapshots/components/CreateSnapshotFlyout/CreateSnapshotFlyout.tsx +++ b/public/pages/Snapshots/components/CreateSnapshotFlyout/CreateSnapshotFlyout.tsx @@ -208,6 +208,7 @@ export default class CreateSnapshotFlyout extends Component { this.setState({ snapshotId: e.target.value }); }} + data-test-subj="snapshotNameInput" /> diff --git a/public/pages/Snapshots/components/RenameInput/RenameInput.tsx b/public/pages/Snapshots/components/RenameInput/RenameInput.tsx new file mode 100644 index 000000000..b549d1b92 --- /dev/null +++ b/public/pages/Snapshots/components/RenameInput/RenameInput.tsx @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiFormRow, EuiFieldText, EuiSpacer } from "@elastic/eui"; +import React, { useState, ChangeEvent } from "react"; +import CustomLabel from "../../../../components/CustomLabel"; + +interface RenameInputProps { + getRenamePattern: (prefix: string) => void; + getRenameReplacement: (prefix: string) => void; +} + +const RenameInput = ({ getRenamePattern, getRenameReplacement }: RenameInputProps) => { + const [renamePattern, setRenamePattern] = useState(""); + const [renameReplacement, setRenameReplacement] = useState(""); + + const onPatternChange = (e: ChangeEvent) => { + setRenamePattern(e.target.value); + getRenamePattern(e.target.value); + }; + + const onReplacementChange = (e: ChangeEvent) => { + setRenameReplacement(e.target.value); + getRenameReplacement(e.target.value); + }; + + const patternHelpText = + "Use regular expressiojn to define how index names will be renamed. By default, input (.+) to reuse the entire index name. [Learn more]"; + const replacementHelpText = + "Define the format of renamed indices. Use $0 to include the entire matching index name, $1 to include the content of the first capture group, etc. [Learn more]"; + + return ( + <> + + + + + + + + + + + + + + + ); +}; + +export default RenameInput; diff --git a/public/pages/Snapshots/components/RenameInput/index.ts b/public/pages/Snapshots/components/RenameInput/index.ts new file mode 100644 index 000000000..5fe87f5f0 --- /dev/null +++ b/public/pages/Snapshots/components/RenameInput/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import RenameInput from "./RenameInput"; + +export default RenameInput; diff --git a/public/pages/Snapshots/components/RestoreSnapshotFlyout/RestoreSnapshotFlyout.tsx b/public/pages/Snapshots/components/RestoreSnapshotFlyout/RestoreSnapshotFlyout.tsx new file mode 100644 index 000000000..b9aba4925 --- /dev/null +++ b/public/pages/Snapshots/components/RestoreSnapshotFlyout/RestoreSnapshotFlyout.tsx @@ -0,0 +1,320 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiComboBoxOptionOption, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiAccordion, +} 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 { IndexItem } from "../../../../../models/interfaces"; +import { 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 { ERROR_PROMPT } from "../../../CreateSnapshotPolicy/constants"; + +interface RestoreSnapshotProps { + snapshotManagementService: SnapshotManagementService; + indexService: IndexService; + onCloseFlyout: () => void; + restoreSnapshot: (snapshotId: string, repository: string, options: object) => void; + snapshotId: string; + repository: string; +} + +interface RestoreSnapshotState { + indexOptions: EuiComboBoxOptionOption[]; + selectedIndexOptions: EuiComboBoxOptionOption[]; + renameIndices: string; + prefix: string; + renamePattern: string; + renameReplacement: string; + listIndices: boolean; + + snapshot: GetSnapshot | null; + snapshotId: string; + restoreSpecific: boolean; + partial: boolean; + + repoError: string; + snapshotIdError: string; +} + +export default class RestoreSnapshotFlyout extends Component { + static contextType = CoreServicesContext; + constructor(props: RestoreSnapshotProps) { + super(props); + this.state = { + indexOptions: [], + selectedIndexOptions: [], + renameIndices: "add_prefix", + prefix: "", + renamePattern: "", + renameReplacement: "", + listIndices: false, + snapshot: null, + snapshotId: "", + restoreSpecific: false, + partial: false, + repoError: "", + snapshotIdError: "", + }; + } + + async componentDidMount() { + await this.getIndexOptions(); + } + + onClickAction = () => { + const { restoreSnapshot, snapshotId, repository } = this.props; + const { + restoreSpecific, + selectedIndexOptions, + indexOptions, + snapshot, + renameIndices, + prefix, + 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(","); + // TODO replace unintelligible regex below with (.+) and add $1 to user provided prefix then add that to renameReplacement + const pattern = renameIndices === add_prefix ? "(? { + this.setState({ listIndices: true }); + }; + + onIndicesSelectionChange = (selectedOptions: EuiComboBoxOptionOption[]) => { + 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] }); + } + } catch (err) { + this.context.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem loading the snapshot.")); + } + }; + + getIndexOptions = () => { + this.getSnapshot(this.props.snapshotId, this.props.repository); + }; + + onCreateOption = (searchValue: string, options: Array>) => { + 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) => { + const { restore_specific_indices, restore_all_indices } = 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, snapshot: _.set(this.state.snapshot!, e.target.id, e.target.checked) }); + return; + } + if (e.target.name === "rename_option") { + this.setState({ renameIndices: e.target.id, 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, repository } = this.props; + const { indexOptions, selectedIndexOptions, restoreSpecific, snapshot, renameIndices } = 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; + + return ( + + + +

Restore snapshot

+
+
+ + + + + +

{snapshot?.snapshot}

+
+ + +

{snapshot?.state}

+
+ + + {snapshot?.indices.length} + +
+ + + + + + + + {restoreSpecific && ( + + )} + + + + + + {renameIndices === add_prefix && } + {renameIndices === rename_indices && ( + + )} + + + + + + + +
+ + + + +
+ ); + } +} diff --git a/public/pages/Snapshots/components/RestoreSnapshotFlyout/index.ts b/public/pages/Snapshots/components/RestoreSnapshotFlyout/index.ts new file mode 100644 index 000000000..b5a24d1da --- /dev/null +++ b/public/pages/Snapshots/components/RestoreSnapshotFlyout/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import RestoreSnapshotFlyout from "./RestoreSnapshotFlyout"; + +export default RestoreSnapshotFlyout; diff --git a/public/pages/Snapshots/components/SnapshotIndicesInput/SnapshotIndicesInput.tsx b/public/pages/Snapshots/components/SnapshotIndicesInput/SnapshotIndicesInput.tsx new file mode 100644 index 000000000..06c2dd536 --- /dev/null +++ b/public/pages/Snapshots/components/SnapshotIndicesInput/SnapshotIndicesInput.tsx @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiComboBox, EuiComboBoxOptionOption, EuiSpacer } from "@elastic/eui"; +import React from "react"; +import { IndexItem } from "../../../../../models/interfaces"; +import CustomLabel from "../../../../components/CustomLabel"; + +interface SnapshotIndicesInputProps { + indexOptions: EuiComboBoxOptionOption[]; + selectedIndexOptions: EuiComboBoxOptionOption[]; + onIndicesSelectionChange: (selectedOptions: EuiComboBoxOptionOption[]) => void; + getIndexOptions: (searchValue: string) => void; + onCreateOption: (searchValue: string, options: Array>) => void; + selectedRepoValue: string; + isClearable: boolean; +} + +const SnapshotIndicesInput = ({ + indexOptions, + selectedIndexOptions, + onIndicesSelectionChange, + getIndexOptions, + onCreateOption, +}: SnapshotIndicesInputProps) => { + return ( + <> + + + + + + ); +}; + +export default SnapshotIndicesInput; diff --git a/public/pages/Snapshots/components/SnapshotIndicesInput/index.ts b/public/pages/Snapshots/components/SnapshotIndicesInput/index.ts new file mode 100644 index 000000000..2029cf583 --- /dev/null +++ b/public/pages/Snapshots/components/SnapshotIndicesInput/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import SnapshotIndicesInput from "./SnapshotIndicesInput"; + +export default SnapshotIndicesInput; diff --git a/public/pages/Snapshots/components/SnapshotRenameOptions/SnapshotRenameOptions.tsx b/public/pages/Snapshots/components/SnapshotRenameOptions/SnapshotRenameOptions.tsx new file mode 100644 index 000000000..aa7e9be72 --- /dev/null +++ b/public/pages/Snapshots/components/SnapshotRenameOptions/SnapshotRenameOptions.tsx @@ -0,0 +1,68 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ChangeEvent } from "react"; +import { EuiRadio, EuiSpacer } from "@elastic/eui"; +import CustomLabel from "../../../../components/CustomLabel"; +import { RESTORE_OPTIONS } from "../../../../models/interfaces"; +interface SnapshotRenameOptionsProps { + doNotRename: boolean; + onDoNotRenameToggle: (e: ChangeEvent) => void; + addPrefix: boolean; + onAddPrefixToggle: (e: ChangeEvent) => void; + renameIndices: boolean; + onRenameIndicesToggle: (e: ChangeEvent) => void; + width: string; +} + +const SnapshotRenameOptions = ({ + doNotRename, + onDoNotRenameToggle, + addPrefix, + onAddPrefixToggle, + renameIndices, + onRenameIndicesToggle, + width, +}: SnapshotRenameOptionsProps) => { + const { do_not_rename, add_prefix, rename_indices } = RESTORE_OPTIONS; + + return ( +
+
Rename restored indices
+ + + + } + checked={doNotRename} + onChange={onDoNotRenameToggle} + /> + + + + } + checked={addPrefix} + onChange={onAddPrefixToggle} + /> + + + + } + checked={renameIndices} + onChange={onRenameIndicesToggle} + /> +
+ ); +}; + +export default SnapshotRenameOptions; diff --git a/public/pages/Snapshots/components/SnapshotRenameOptions/index.ts b/public/pages/Snapshots/components/SnapshotRenameOptions/index.ts new file mode 100644 index 000000000..532fb263d --- /dev/null +++ b/public/pages/Snapshots/components/SnapshotRenameOptions/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import SnapshotRenameOptions from "./SnapshotRenameOptions"; + +export default SnapshotRenameOptions; diff --git a/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/SnapshotRestoreAdvancedOptions.tsx b/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/SnapshotRestoreAdvancedOptions.tsx new file mode 100644 index 000000000..a7a583b23 --- /dev/null +++ b/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/SnapshotRestoreAdvancedOptions.tsx @@ -0,0 +1,125 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ChangeEvent } from "react"; +import { EuiCheckbox, EuiSpacer, EuiText } from "@elastic/eui"; +import CustomLabel from "../../../../components/CustomLabel"; +import { RESTORE_OPTIONS } from "../../../../models/interfaces"; + +interface SnapshotAdvancedOptionsProps { + restoreAliases: boolean; + onRestoreAliasesToggle: (e: ChangeEvent) => void; + restoreClusterState: boolean; + onRestoreClusterStateToggle: (e: ChangeEvent) => void; + ignoreUnavailable: boolean; + onIgnoreUnavailableToggle: (e: ChangeEvent) => void; + restorePartial: boolean; + onRestorePartialToggle: (e: ChangeEvent) => void; + customizeIndexSettings: boolean; + onCustomizeIndexSettingsToggle: (e: ChangeEvent) => void; + ignoreIndexSettings: boolean; + onIgnoreIndexSettingsToggle: (e: ChangeEvent) => void; + width?: string; +} + +const SnapshotRestoreAdvancedOptions = ({ + restoreAliases, + onRestoreAliasesToggle, + ignoreUnavailable, + onIgnoreUnavailableToggle, + restoreClusterState, + onRestoreClusterStateToggle, + restorePartial, + onRestorePartialToggle, + customizeIndexSettings, + onCustomizeIndexSettingsToggle, + ignoreIndexSettings, + onIgnoreIndexSettingsToggle, + width, +}: SnapshotAdvancedOptionsProps) => { + const { + restore_aliases, + include_global_state, + ignore_unavailable, + partial, + customize_index_settings, + ignore_index_settings, + } = RESTORE_OPTIONS; + return ( +
+ } + checked={restoreAliases} + onChange={onRestoreAliasesToggle} + /> + + + + } + checked={restoreClusterState} + onChange={onRestoreClusterStateToggle} + /> + + + + + } + checked={ignoreUnavailable} + onChange={onIgnoreUnavailableToggle} + /> + + + + } + checked={restorePartial} + onChange={onRestorePartialToggle} + /> + + + +
Custom index settings
+ +

+ By default, index settings are restored from indices in snapshots. You can choose to +
+ customize index settings on restore. +

+
+ + + + } + checked={customizeIndexSettings} + onChange={onCustomizeIndexSettingsToggle} + /> + + + + + } + checked={ignoreIndexSettings} + onChange={onIgnoreIndexSettingsToggle} + /> +
+ ); +}; + +export default SnapshotRestoreAdvancedOptions; diff --git a/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/index.ts b/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/index.ts new file mode 100644 index 000000000..940d333c3 --- /dev/null +++ b/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import SnapshotRestoreAdvancedOptions from "./SnapshotRestoreAdvancedOptions"; + +export default SnapshotRestoreAdvancedOptions; diff --git a/public/pages/Snapshots/components/SnapshotRestoreOption/SnapshotRestoreOption.tsx b/public/pages/Snapshots/components/SnapshotRestoreOption/SnapshotRestoreOption.tsx new file mode 100644 index 000000000..49dd2fec4 --- /dev/null +++ b/public/pages/Snapshots/components/SnapshotRestoreOption/SnapshotRestoreOption.tsx @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ChangeEvent } from "react"; +import { EuiRadio, EuiSpacer } from "@elastic/eui"; +import CustomLabel from "../../../../components/CustomLabel"; +import { RESTORE_OPTIONS } from "../../../../models/interfaces"; + +interface SnapshotRestoreOptionProps { + restoreAllIndices: boolean; + onRestoreAllIndicesToggle: (e: ChangeEvent) => void; + restoreSpecificIndices: boolean; + onRestoreSpecificIndicesToggle: (e: ChangeEvent) => void; + width: string; +} + +const SnapshotRestoreOption = ({ + restoreAllIndices, + onRestoreAllIndicesToggle, + restoreSpecificIndices, + onRestoreSpecificIndicesToggle, + width, +}: SnapshotRestoreOptionProps) => { + const { restore_all_indices, restore_specific_indices } = RESTORE_OPTIONS; + + return ( +
+
Specify restore option
+ + + + } + checked={restoreAllIndices} + onChange={onRestoreAllIndicesToggle} + /> + + + + } + checked={restoreSpecificIndices} + onChange={onRestoreSpecificIndicesToggle} + /> +
+ ); +}; + +export default SnapshotRestoreOption; diff --git a/public/pages/Snapshots/components/SnapshotRestoreOption/index.ts b/public/pages/Snapshots/components/SnapshotRestoreOption/index.ts new file mode 100644 index 000000000..50f5c8958 --- /dev/null +++ b/public/pages/Snapshots/components/SnapshotRestoreOption/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import SnapshotRestoreOption from "./SnapshotRestoreOption"; + +export default SnapshotRestoreOption; diff --git a/public/pages/Snapshots/containers/Snapshots/Snapshots.tsx b/public/pages/Snapshots/containers/Snapshots/Snapshots.tsx index c82ca30c8..68ed3784b 100644 --- a/public/pages/Snapshots/containers/Snapshots/Snapshots.tsx +++ b/public/pages/Snapshots/containers/Snapshots/Snapshots.tsx @@ -14,7 +14,8 @@ import { getErrorMessage } from "../../../../utils/helpers"; import { CatSnapshotWithRepoAndPolicy as SnapshotsWithRepoAndPolicy } from "../../../../../server/models/interfaces"; import { ContentPanel } from "../../../../components/ContentPanel"; import SnapshotFlyout from "../../components/SnapshotFlyout/SnapshotFlyout"; -import CreateSnapshotFlyout from "../../components/CreateSnapshotFlyout/CreateSnapshotFlyout"; +import CreateSnapshotFlyout from "../../components/CreateSnapshotFlyout"; +import RestoreSnapshotFlyout from "../../components/RestoreSnapshotFlyout"; import { Snapshot } from "../../../../../models/interfaces"; import { BREADCRUMBS, RESTORE_SNAPSHOT_DOCUMENTATION_URL, ROUTES } from "../../../../utils/constants"; import { renderTimestampMillis } from "../../../SnapshotPolicies/helpers"; @@ -38,6 +39,7 @@ interface SnapshotsState { flyoutSnapshotRepo: string; showCreateFlyout: boolean; + showRestoreFlyout: boolean; message?: React.ReactNode; @@ -60,6 +62,7 @@ export default class Snapshots extends Component flyoutSnapshotId: "", flyoutSnapshotRepo: "", showCreateFlyout: false, + showRestoreFlyout: false, message: null, isDeleteModalVisible: false, }; @@ -215,6 +218,28 @@ export default class Snapshots extends Component } }; + restoreSnapshot = async (snapshotId: string, repository: string, options: object) => { + try { + const { snapshotManagementService } = this.props; + const response = await snapshotManagementService.restoreSnapshot(snapshotId, repository, options); + if (response.ok) { + this.context.notifications.toasts.addSuccess(`Restored snapshot ${snapshotId} to repository ${repository}.`); + } else { + this.context.notifications.toasts.addDanger(response.error); + } + } catch (err) { + this.context.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem restoring the snapshot.")); + } + }; + + onClickRestore = async () => { + this.setState({ showRestoreFlyout: true }); + }; + + onCloseRestoreFlyout = () => { + this.setState({ showRestoreFlyout: false }); + }; + render() { const { snapshots, @@ -225,6 +250,7 @@ export default class Snapshots extends Component flyoutSnapshotId, flyoutSnapshotRepo, showCreateFlyout, + showRestoreFlyout, isDeleteModalVisible, } = this.state; @@ -266,12 +292,12 @@ export default class Snapshots extends Component Delete , + + Restore + , Take snapshot , - - Restore - , ]; const subTitleText = ( @@ -326,6 +352,17 @@ export default class Snapshots extends Component /> )} + {showRestoreFlyout && ( + + )} + {isDeleteModalVisible && ( void; onClickAction: () => void; save?: boolean; + restore?: boolean; } -const FlyoutFooter = ({ edit, action, disabledAction = false, onClickCancel, onClickAction, save }: FlyoutFooterProps) => ( +const FlyoutFooter = ({ edit, action, disabledAction = false, onClickCancel, onClickAction, save, restore }: FlyoutFooterProps) => ( @@ -24,7 +25,7 @@ const FlyoutFooter = ({ edit, action, disabledAction = false, onClickCancel, onC - {!save ? `${edit ? "Edit" : "Add"} ${action}` : save ? "Save" : "Create"} + {restore ? "Restore snapshot" : !save ? `${edit ? "Edit" : "Add"} ${action}` : save ? "Save" : "Create"} diff --git a/public/services/SnapshotManagementService.ts b/public/services/SnapshotManagementService.ts index 7035bed04..f98272a80 100644 --- a/public/services/SnapshotManagementService.ts +++ b/public/services/SnapshotManagementService.ts @@ -13,6 +13,7 @@ import { CreateRepositoryBody, AcknowledgedResponse, CreateSnapshotResponse, + RestoreSnapshotResponse, } from "../../server/models/interfaces"; import { ServerResponse } from "../../server/models/types"; import { DocumentSMPolicy, DocumentSMPolicyWithMetadata, SMPolicy, Snapshot } from "../../models/interfaces"; @@ -50,6 +51,15 @@ export default class SnapshotManagementService { return response; }; + restoreSnapshot = async (snapshotId: string, repository: string, options: object): Promise> => { + let url = `..${NODE_API._SNAPSHOTS}/${snapshotId}`; + const response = (await this.httpClient.post(url, { + query: { repository }, + body: JSON.stringify(options), + })) as ServerResponse; + return response; + }; + createPolicy = async (policyId: string, policy: SMPolicy): Promise> => { let url = `..${NODE_API.SMPolicies}/${policyId}`; const response = (await this.httpClient.post(url, { body: JSON.stringify(policy) })) as ServerResponse; diff --git a/release-notes/opensearch-index-management-dashboards-plugin.release-notes-2.3.0.0.md b/release-notes/opensearch-index-management-dashboards-plugin.release-notes-2.3.0.0.md new file mode 100644 index 000000000..59c5d3a92 --- /dev/null +++ b/release-notes/opensearch-index-management-dashboards-plugin.release-notes-2.3.0.0.md @@ -0,0 +1,17 @@ +## Version 2.3.0.0 2022-09-09 + +Compatible with OpenSearch 2.3.0 + +### Enhancements +* Change alignment of Snapshot Management panels in pages/Main/Main.tsx ([#236](https://github.com/opensearch-project/index-management-dashboards-plugin/pull/236)) + +### Bug fixes +* Remove extra forward slash for URL to snapshot management docs ([#231](https://github.com/opensearch-project/index-management-dashboards-plugin/pull/231)) + +### Maintenance +* Version bump 2.3.0 ([#247](https://github.com/opensearch-project/index-management-dashboards-plugin/pull/247)) +* Bumped moment version to resolve dependabot alert ([#230](https://github.com/opensearch-project/index-management-dashboards-plugin/pull/230)) +* Refactored dependency used by test mock. Adjusted OSD version used by test workflows ([#229](https://github.com/opensearch-project/index-management-dashboards-plugin/pull/229)) + +### Documentation +* Added release notes for 2.3 ([#250](https://github.com/opensearch-project/index-management-dashboards-plugin/pull/250)) \ No newline at end of file diff --git a/server/models/interfaces.ts b/server/models/interfaces.ts index f93626352..8b380c943 100644 --- a/server/models/interfaces.ts +++ b/server/models/interfaces.ts @@ -386,6 +386,10 @@ export interface CreateSnapshotResponse { snapshot: GetSnapshot; } +export interface RestoreSnapshotResponse { + snapshot: GetSnapshot; +} + export interface GetSnapshot { snapshot: string; uuid: string; @@ -405,6 +409,11 @@ export interface GetSnapshot { successful: number; failed: number; }; + restore_aliases?: boolean; + ignore_unavailable?: boolean; + rename_pattern?: string; + rename_replacement?: string; + partial?: boolean; metadata?: { sm_policy?: string; }; diff --git a/server/routes/snapshotManagement.ts b/server/routes/snapshotManagement.ts index 13cf02a4b..6a280b78d 100644 --- a/server/routes/snapshotManagement.ts +++ b/server/routes/snapshotManagement.ts @@ -65,6 +65,22 @@ export default function (services: NodeServices, router: IRouter) { snapshotManagementService.createSnapshot ); + router.post( + { + path: `${NODE_API._SNAPSHOTS}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + query: schema.object({ + repository: schema.string(), + }), + body: schema.any(), + }, + }, + snapshotManagementService.restoreSnapshot + ); + router.post( { path: `${NODE_API.SMPolicies}/{id}`, diff --git a/server/services/SnapshotManagementService.ts b/server/services/SnapshotManagementService.ts index 84d405fca..379055b7a 100644 --- a/server/services/SnapshotManagementService.ts +++ b/server/services/SnapshotManagementService.ts @@ -23,6 +23,7 @@ import { GetRepositoryResponse, AcknowledgedResponse, CreateSnapshotResponse, + RestoreSnapshotResponse, } from "../models/interfaces"; import { FailedServerResponse, ServerResponse } from "../models/types"; @@ -187,6 +188,38 @@ export default class SnapshotManagementService { } }; + restoreSnapshot = async ( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory + ): Promise>> => { + try { + const { id } = request.params as { + id: string; + }; + const { repository } = request.query as { + repository: string; + }; + const params = { + repository: repository, + snapshot: id, + body: JSON.stringify(request.body), + }; + const { callAsCurrentUser: callWithRequest } = this.osDriver.asScoped(request); + const resp: RestoreSnapshotResponse = await callWithRequest("snapshot.restore", params); + + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: resp, + }, + }); + } catch (err) { + return this.errorResponse(response, err, "restoreSnapshot"); + } + }; + createPolicy = async ( context: RequestHandlerContext, request: OpenSearchDashboardsRequest, @@ -279,7 +312,7 @@ export default class SnapshotManagementService { queryString: queryString.trim() ? `${queryString.trim()}` : "*", }; const res = await callWithRequest("ism.getSMPolicies", params); - + console.log("policy response", res); const policies: DocumentSMPolicy[] = res.policies.map( (p: { _id: string; _seq_no: number; _primary_term: number; sm_policy: SMPolicy }) => ({ seqNo: p._seq_no,