diff --git a/cypress/integration/snapshots_spec.js b/cypress/integration/snapshots_spec.js index 16dc09790..4eaa4631c 100644 --- a/cypress/integration/snapshots_spec.js +++ b/cypress/integration/snapshots_spec.js @@ -100,7 +100,7 @@ describe("Snapshots", () => { cy.get("button").contains("Restore snapshot").click({ force: true }); // Check for success toast - cy.contains("Restored snapshot test_snapshot to repository test_repo"); + cy.contains(`Restore from snapshot "test_snapshot" is in progress.`); }); }); diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index e1eb26f39..67020883c 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -44,6 +44,19 @@ interface ArgsWithError { } export type OnSearchChangeArgs = ArgsWithQuery | ArgsWithError; +export interface Toast { + id?: string; + title?: string; + iconType?: string; + color: string; + text?: JSX.Element; +} + +export interface RestoreError { + reason?: string, + type?: string +} + export interface LatestActivities { activityType: "Creation" | "Deletion"; status?: string; diff --git a/public/pages/Snapshots/components/ErrorModal/ErrorModal.tsx b/public/pages/Snapshots/components/ErrorModal/ErrorModal.tsx new file mode 100644 index 000000000..2f64646cf --- /dev/null +++ b/public/pages/Snapshots/components/ErrorModal/ErrorModal.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiModal, EuiText, EuiButton, EuiModalHeader, EuiModalFooter, EuiModalBody, EuiModalHeaderTitle } from "@elastic/eui"; +import React from "react"; + + +interface ErrorModalProps { + error: React.ErrorInfo; + onClick: (e: React.MouseEvent) => void; +} + +const ErrorModal = ({ onClick, error }: ErrorModalProps) => { + + return ( + <> + + +

{error.type}

+
+ + + {error.reason}. + + + + Close + +
+ + ); +}; + +export default ErrorModal; \ No newline at end of file diff --git a/public/pages/Snapshots/components/ErrorModal/index.ts b/public/pages/Snapshots/components/ErrorModal/index.ts new file mode 100644 index 000000000..be1bb1457 --- /dev/null +++ b/public/pages/Snapshots/components/ErrorModal/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import ErrorModal from "./ErrorModal"; + +export default ErrorModal; \ No newline at end of file diff --git a/public/pages/Snapshots/components/IndexList/IndexList.tsx b/public/pages/Snapshots/components/IndexList/IndexList.tsx index a4b85f2ed..f610c2fde 100644 --- a/public/pages/Snapshots/components/IndexList/IndexList.tsx +++ b/public/pages/Snapshots/components/IndexList/IndexList.tsx @@ -21,14 +21,10 @@ const IndexList = ({ indices, snapshot, onClick, title }: IndexListProps) => { { field: "index", name: "Index", - width: "70%", + width: "100%", + truncateText: true, sortable: true, - }, - { - field: "store.size", - name: "Total size", - sortable: true, - }, + } ]; return ( diff --git a/public/pages/Snapshots/components/RenameInput/RenameInput.test.tsx b/public/pages/Snapshots/components/RenameInput/RenameInput.test.tsx index a84ff53b2..b7a9e0264 100644 --- a/public/pages/Snapshots/components/RenameInput/RenameInput.test.tsx +++ b/public/pages/Snapshots/components/RenameInput/RenameInput.test.tsx @@ -34,11 +34,11 @@ describe("RenameInput component", () => { it("accepts user input", () => { // User enters text - userEvent.type(screen.getByTestId("renamePatternInput"), "(.+)"); + userEvent.type(screen.getByTestId("renamePatternInput"), "{selectall}{del}(.+)"); expect(screen.getByTestId("renamePatternInput")).toHaveValue("(.+)"); - userEvent.type(screen.getByTestId("renameReplacementInput"), "test_$1"); + userEvent.type(screen.getByTestId("renameReplacementInput"), "{selectall}{del}test_$1"); expect(screen.getByTestId("renameReplacementInput")).toHaveValue("test_$1"); }); diff --git a/public/pages/Snapshots/components/RenameInput/RenameInput.tsx b/public/pages/Snapshots/components/RenameInput/RenameInput.tsx index c94f87353..d509bb2ce 100644 --- a/public/pages/Snapshots/components/RenameInput/RenameInput.tsx +++ b/public/pages/Snapshots/components/RenameInput/RenameInput.tsx @@ -3,18 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiFormRow, EuiFieldText, EuiSpacer } from "@elastic/eui"; +import { EuiFormRow, EuiFieldText, EuiSpacer, EuiText, EuiLink } from "@elastic/eui"; import React, { useState, ChangeEvent } from "react"; -import CustomLabel from "../../../../components/CustomLabel"; - +import { RESTORE_SNAPSHOT_DOCUMENTATION_URL } from "../../../../utils/constants" interface RenameInputProps { getRenamePattern: (prefix: string) => void; getRenameReplacement: (prefix: string) => void; } const RenameInput = ({ getRenamePattern, getRenameReplacement }: RenameInputProps) => { - const [renamePattern, setRenamePattern] = useState(""); - const [renameReplacement, setRenameReplacement] = useState(""); + const [renamePattern, setRenamePattern] = useState("(.+)"); + const [renameReplacement, setRenameReplacement] = useState("restored_$1"); const onPatternChange = (e: ChangeEvent) => { setRenamePattern(e.target.value); @@ -26,23 +25,44 @@ const RenameInput = ({ getRenamePattern, getRenameReplacement }: RenameInputProp 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 ( <> - + +

Rename Pattern

+
+ +

+ Use regular expression to define how index names will be renamed. +
+ By default, input (.+) to reuse the entire index name.{" "} + + Learn more + +

+
- + +

Rename Replacement

+
+ +

+ 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 + +

+
diff --git a/public/pages/Snapshots/components/RenameInput/__snapshots__/RenameInput.test.tsx.snap b/public/pages/Snapshots/components/RenameInput/__snapshots__/RenameInput.test.tsx.snap index 938fc61a6..22d72d475 100644 --- a/public/pages/Snapshots/components/RenameInput/__snapshots__/RenameInput.test.tsx.snap +++ b/public/pages/Snapshots/components/RenameInput/__snapshots__/RenameInput.test.tsx.snap @@ -6,28 +6,39 @@ exports[`RenameInput component renders without error 1`] = ` class="euiSpacer euiSpacer--l" />
-
+ Rename Pattern + +
+
+

-

+ By default, input (.+) to reuse the entire index name. + + -

- Rename Pattern -

-
-
+ Learn more + EuiIconMock + + (opens in a new tab or window) + + +

- - Use regular expressiojn to define how index names will be renamed. By default, input (.+) to reuse the entire index name. [Learn more] - -
@@ -56,28 +67,41 @@ exports[`RenameInput component renders without error 1`] = ` class="euiSpacer euiSpacer--m" />
-
+ Rename Replacement + +
+
+

-

+ entire matching index name, $1 to include the content of the first +
+ capture group, etc. + + -

- Rename Replacement -

-
-
+ Learn more + EuiIconMock + + (opens in a new tab or window) + + +

- - 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] - -
diff --git a/public/pages/Snapshots/components/RestoreActivitiesPanel/RestoreActivitiesPanel.tsx b/public/pages/Snapshots/components/RestoreActivitiesPanel/RestoreActivitiesPanel.tsx index 6e03f3abc..5855c5f7a 100644 --- a/public/pages/Snapshots/components/RestoreActivitiesPanel/RestoreActivitiesPanel.tsx +++ b/public/pages/Snapshots/components/RestoreActivitiesPanel/RestoreActivitiesPanel.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiInMemoryTable, EuiSpacer, EuiLink, EuiFlyout, EuiButton } from "@elastic/eui"; +import { EuiInMemoryTable, EuiSpacer, EuiLink, EuiFlyout, EuiButton, EuiEmptyPrompt } from "@elastic/eui"; import _ from "lodash"; import React, { useEffect, useContext, useState, useMemo } from "react"; import { SnapshotManagementService } from "../../../../services"; @@ -17,11 +17,13 @@ import IndexList from "../IndexList"; interface RestoreActivitiesPanelProps { snapshotManagementService: SnapshotManagementService; snapshotId: string; - repository: string; restoreStartRef: number; + restoreCount: number } -export const RestoreActivitiesPanel: React.FC = ({ snapshotManagementService, snapshotId, restoreStartRef }: RestoreActivitiesPanelProps) => { +const intervalIds: ReturnType[] = []; + +export const RestoreActivitiesPanel = ({ snapshotManagementService, snapshotId, restoreStartRef, restoreCount }: RestoreActivitiesPanelProps) => { const context = useContext(CoreServicesContext); const [startTime, setStartTime] = useState(""); const [stopTime, setStopTime] = useState(""); @@ -30,16 +32,17 @@ export const RestoreActivitiesPanel: React.FC = ({ const [flyout, setFlyout] = useState(false); useEffect(() => { - context!.chrome.setBreadcrumbs([BREADCRUMBS.SNAPSHOT_MANAGEMENT, BREADCRUMBS.SNAPSHOTS, BREADCRUMBS.SNAPSHOT_RESTORE]); - - let getStatusInterval: ReturnType; + context?.chrome.setBreadcrumbs([BREADCRUMBS.SNAPSHOT_MANAGEMENT, BREADCRUMBS.SNAPSHOTS, BREADCRUMBS.SNAPSHOT_RESTORE]); - if (stage.slice(0, 4) !== "Done") { - getStatusInterval = setInterval(() => { + if (stage !== "Done (100%)" || indices.length < restoreCount) { + intervalIds.push(setInterval(() => { getRestoreStatus(); - }, 1000); + }, 2000)) + return () => { - clearInterval(getStatusInterval) + intervalIds.forEach((id) => { + clearInterval(id); + }) } } }, [stage]); @@ -52,24 +55,27 @@ export const RestoreActivitiesPanel: React.FC = ({ try { const res = await snapshotManagementService.getIndexRecovery(); - if (stage.indexOf("Done") >= 0) { - return; - } - if (res.ok) { const response: GetIndexRecoveryResponse = res.response; setRestoreStatus(response); } else { context?.notifications.toasts.addDanger(res.error); + const message = JSON.parse(res.error).error.root_cause[0].reason + const trimmedMessage = message.slice(message.indexOf("]") + 1, message.indexOf(".") + 1); + context?.notifications.toasts.addError(JSON.parse(res.error), { + title: `There was a problem loading the recovery status.`, + toastMessage: `${trimmedMessage} Open browser console & click below for details.` + }); } } catch (err) { - context?.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem loading the recovery.")); + context?.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem loading the recovery status.")); } }; - const onIndexesClick = (e: React.MouseEvent) => { + const onIndexesClick = async (e: React.MouseEvent) => { e.preventDefault(); + await getRestoreStatus(); setFlyout(true); }; @@ -80,7 +86,7 @@ export const RestoreActivitiesPanel: React.FC = ({ const setRestoreStatus = (response: GetIndexRecoveryResponse) => { let minStartTime: number = 0; let maxStopTime: number = 0; - let stageIndex: number = Infinity; + let stageIndex: number = 4; let doneCount: number = 0; const indexes: CatSnapshotIndex[] = []; const stages: string[] = ["START", "INIT", "INDEX", "FINALIZE", "DONE"]; @@ -88,45 +94,55 @@ export const RestoreActivitiesPanel: React.FC = ({ // Loop through indices in response, filter out kibana index, // gather progress info then use it to create progress field values. for (let item in response) { - if (item.indexOf("kibana") < 0) { - const info = response[item as keyof GetIndexRecoveryResponse].shards[0] + const responseItem = item as keyof GetIndexRecoveryResponse; + if ( + item.indexOf("kibana") < 0 && + response[responseItem].shards && + response[responseItem].shards[0].start_time_in_millis >= restoreStartRef + ) { + const info = response[responseItem].shards[0]; const stage = stages.indexOf(info.stage); const size = `${(info.index.size.total_in_bytes / 1024 ** 2).toFixed(2)}mb`; const time = { start_time: info.start_time_in_millis, - stop_time: info.stop_time_in_millis, + stop_time: info.stop_time_in_millis ? info.stop_time_in_millis : Date.now() }; - doneCount = stage === stages.length - 1 ? doneCount + 1 : doneCount; + doneCount = stage === 4 ? doneCount + 1 : doneCount; stageIndex = stage < stageIndex ? stage : stageIndex; - minStartTime = minStartTime && minStartTime < time.start_time ? minStartTime : time.start_time; + maxStopTime = maxStopTime && maxStopTime > time.stop_time ? maxStopTime : time.stop_time; if (info.source.index && info.source.snapshot === snapshotId) { + minStartTime = minStartTime && minStartTime < time.start_time ? minStartTime : time.start_time; indexes.push({ index: info.source.index, "store.size": size }); } } } - let percent = Math.floor((doneCount / indices.length) * 100); - percent = stageIndex === 4 ? 100 : percent; + let percent = Math.floor((doneCount / restoreCount) * 100); setIndices(indexes); - setStartTime(new Date(minStartTime).toLocaleString().replace(",", " ")); - setStopTime(new Date(maxStopTime).toLocaleString().replace(",", " ") || "In progress"); - setStage(`${stages[stageIndex][0] + stages[stageIndex].toLowerCase().slice(1)} (${percent}%)`); + setStopTime(new Date(maxStopTime).toLocaleString().replace(",", " ")); + setStartTime(new Date(minStartTime).toLocaleString().replace(",", " ")) + + if (stages[stageIndex]) { + stageIndex = (stageIndex === 4 && doneCount < restoreCount) ? 2 : stageIndex; + setStage(`${stages[stageIndex][0] + stages[stageIndex].toLowerCase().slice(1)} (${percent}%)`); + } + }; - const actions = useMemo(() => { + const actions = useMemo(() => ( [ - + Refresh , - ]; - }, []) + ] + ), []); - const indexText = `${indices.length === 1 && Object.keys(indices[0]).length > 0 ? "Index" : "Indices"}` - const indexes = `${indices.length === 1 && Object.keys(indices[0]).length === 0 ? "0" : indices.length} ${indexText}`; + const indexText = `${restoreCount === 1 ? "Index" : "Indices"}` + const indexes = `${restoreCount} ${indexText}`; const restoreStatus = [ { @@ -161,11 +177,23 @@ export const RestoreActivitiesPanel: React.FC = ({ }, ]; + const message = (There are no restore activities.

} titleSize="s">
) + return ( <> - {flyout && } + {flyout && + + + + } - + diff --git a/public/pages/Snapshots/components/RestoreSnapshotFlyout/RestoreSnapshotFlyout.tsx b/public/pages/Snapshots/components/RestoreSnapshotFlyout/RestoreSnapshotFlyout.tsx index 393a0991d..bd529982d 100644 --- a/public/pages/Snapshots/components/RestoreSnapshotFlyout/RestoreSnapshotFlyout.tsx +++ b/public/pages/Snapshots/components/RestoreSnapshotFlyout/RestoreSnapshotFlyout.tsx @@ -15,6 +15,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiAccordion, + EuiCheckbox, + EuiCallOut, + EuiText } from "@elastic/eui"; import _ from "lodash"; import React, { Component, ChangeEvent } from "react"; @@ -39,7 +42,7 @@ interface RestoreSnapshotProps { snapshotManagementService: SnapshotManagementService; indexService: IndexService; onCloseFlyout: () => void; - getTime: (time: number) => void + getRestoreInfo: (time: number, count: number) => void restoreSnapshot: (snapshotId: string, repository: string, options: object) => void; snapshotId: string; repository: string; @@ -53,16 +56,14 @@ interface RestoreSnapshotState { renamePattern: string; renameReplacement: string; listIndices: boolean; - indicesList: CatSnapshotIndex[]; - - repositories: CatRepository[], - selectedRepoValue: string, customIndexSettings: string; ignoreIndexSettings?: string; + indicesList: CatSnapshotIndex[]; + selectedRepoValue: string; + repositories: CatRepository[]; snapshot: GetSnapshot | null; restoreSpecific: boolean; partial: boolean; - repoError: string; snapshotIdError: string; } @@ -97,39 +98,41 @@ export default class RestoreSnapshotFlyout extends Component { - const { restoreSnapshot, snapshotId, repository, onCloseFlyout, getTime } = this.props; + const { restoreSnapshot, snapshotId, repository, onCloseFlyout, getRestoreInfo } = this.props; const { - selectedRepoValue, customIndexSettings, ignoreIndexSettings, restoreSpecific, selectedIndexOptions, indexOptions, - snapshot, 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(","); - // TODO replace unintelligible regex below with (.+) and add $1 to user provided prefix then add that to renameReplacement - const pattern = renameIndices === add_prefix ? "(? { + try { + return JSON.parse(testString); + } catch (err) { + this.context.notifications.toasts.addError(err, { title: `Please enter valid JSON.` }); + return false; + } + } + + checkSelectedIndices = (indices: string): string | boolean => { + const { restoreSpecific } = this.state; + try { + if (restoreSpecific && indices.length === 0) { + throw "No indices selected."; + } else { + return indices; + } + } catch (err) { + + return false; + } + } + onClickIndices = async () => { const { snapshot } = this.state; const indices = snapshot!.indices.join(","); @@ -183,6 +210,13 @@ export default class RestoreSnapshotFlyout extends Component ({ index: index.index, ["store.size"]: index["store.size"] })) - const inactiveIndices = snapshot?.indices.filter((index) => !activeIndexNames.includes(index) && index.length) + const inactiveIndices = snapshot?.indices.filter((index) => !activeIndexNames.includes(index) && index.length && index.indexOf("kibana") < 0) .map((index) => ({ index: index, "store.size": "unknown" })); this.setState({ indicesList: [...formattedIndices, ...inactiveIndices] }); } else { - this.context.notifications.toasts.addDanger(response.error); + 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 loading the indices for this 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 indices for this snapshot.")); @@ -306,6 +345,7 @@ export default class RestoreSnapshotFlyout extends Component @@ -325,18 +365,21 @@ export default class RestoreSnapshotFlyout extends Component - + + -

{snapshot?.snapshot}

+ +

{snapshot?.snapshot}

-

{snapshot?.state}

+ {status}
- {snapshot?.indices.length} + + {snapshot?.indices.length}
@@ -397,8 +440,6 @@ export default class RestoreSnapshotFlyout extends Component -
+ + + + {snapshot?.failed_shards && +

You are about to restore a partial snapshot. One or more shards may be missing in this
snapshot. Do you want to continue?

+ + Allow restore partial snapshots} + checked={String(_.get(snapshot, partial, false)) == "true"} + onChange={this.onToggle} + /> +
} + + - + - ) - } + )} ); } diff --git a/public/pages/Snapshots/components/SnapshotRenameOptions/SnapshotRenameOptions.tsx b/public/pages/Snapshots/components/SnapshotRenameOptions/SnapshotRenameOptions.tsx index aa7e9be72..e8e00e982 100644 --- a/public/pages/Snapshots/components/SnapshotRenameOptions/SnapshotRenameOptions.tsx +++ b/public/pages/Snapshots/components/SnapshotRenameOptions/SnapshotRenameOptions.tsx @@ -37,7 +37,7 @@ const SnapshotRenameOptions = ({ } + label="Do not rename" checked={doNotRename} onChange={onDoNotRenameToggle} /> @@ -47,7 +47,7 @@ const SnapshotRenameOptions = ({ } + label="Add prefix to restored index names" checked={addPrefix} onChange={onAddPrefixToggle} /> @@ -57,7 +57,7 @@ const SnapshotRenameOptions = ({ } + label="Rename using regular expression (Advanced)" checked={renameIndices} onChange={onRenameIndicesToggle} /> diff --git a/public/pages/Snapshots/components/SnapshotRenameOptions/__snapshots__/SnapshotRenameOptions.test.tsx.snap b/public/pages/Snapshots/components/SnapshotRenameOptions/__snapshots__/SnapshotRenameOptions.test.tsx.snap index 36f6b5d1e..12120e8f4 100644 --- a/public/pages/Snapshots/components/SnapshotRenameOptions/__snapshots__/SnapshotRenameOptions.test.tsx.snap +++ b/public/pages/Snapshots/components/SnapshotRenameOptions/__snapshots__/SnapshotRenameOptions.test.tsx.snap @@ -27,24 +27,7 @@ exports[`SnapshotRenameOptions component renders without error 1`] = ` class="euiRadio__label" for="do_not_rename" > -
-
-
-

- Do not rename -

-
-
-
-
+ Do not rename
-
-
-
-

- Add prefix to restored index names -

-
-
-
-
+ Add prefix to restored index names
-
-
-
-

- Rename using regular expression (Advanced) -

-
-
-
-
+ Rename using regular expression (Advanced)
diff --git a/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/SnapshotRestoreAdvancedOptions.tsx b/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/SnapshotRestoreAdvancedOptions.tsx index a5725711a..f51bfbd7b 100644 --- a/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/SnapshotRestoreAdvancedOptions.tsx +++ b/public/pages/Snapshots/components/SnapshotRestoreAdvancedOptions/SnapshotRestoreAdvancedOptions.tsx @@ -5,7 +5,7 @@ import React, { ChangeEvent } from "react"; import { EuiCheckbox, EuiSpacer, EuiText } from "@elastic/eui"; -import CustomLabel from "../../../../components/CustomLabel"; +import { CheckBoxLabel } from "../../helper" import IndexSettingsInput from "../../components/IndexSettingsInput"; import { RESTORE_OPTIONS } from "../../../../models/interfaces"; @@ -17,8 +17,6 @@ interface SnapshotAdvancedOptionsProps { onRestoreClusterStateToggle: (e: ChangeEvent) => void; ignoreUnavailable: boolean; onIgnoreUnavailableToggle: (e: ChangeEvent) => void; - restorePartial: boolean; - onRestorePartialToggle: (e: ChangeEvent) => void; customizeIndexSettings: boolean; onCustomizeIndexSettingsToggle: (e: ChangeEvent) => void; ignoreIndexSettings: boolean; @@ -34,8 +32,6 @@ const SnapshotRestoreAdvancedOptions = ({ onIgnoreUnavailableToggle, restoreClusterState, onRestoreClusterStateToggle, - restorePartial, - onRestorePartialToggle, customizeIndexSettings, onCustomizeIndexSettingsToggle, ignoreIndexSettings, @@ -46,7 +42,6 @@ const SnapshotRestoreAdvancedOptions = ({ restore_aliases, include_global_state, ignore_unavailable, - partial, customize_index_settings, ignore_index_settings, } = RESTORE_OPTIONS; @@ -55,7 +50,7 @@ const SnapshotRestoreAdvancedOptions = ({
} + label={} checked={restoreAliases} onChange={onRestoreAliasesToggle} /> @@ -64,7 +59,7 @@ const SnapshotRestoreAdvancedOptions = ({ } + label={Restore cluster state from snapshots} checked={restoreClusterState} onChange={onRestoreClusterStateToggle} /> @@ -74,7 +69,7 @@ const SnapshotRestoreAdvancedOptions = ({ @@ -83,15 +78,6 @@ const SnapshotRestoreAdvancedOptions = ({ onChange={onIgnoreUnavailableToggle} /> - - - } - checked={restorePartial} - onChange={onRestorePartialToggle} - /> -
Custom index settings
@@ -107,7 +93,7 @@ const SnapshotRestoreAdvancedOptions = ({ } + label={} checked={customizeIndexSettings} onChange={onCustomizeIndexSettingsToggle} /> @@ -119,7 +105,10 @@ const SnapshotRestoreAdvancedOptions = ({ + } checked={ignoreIndexSettings} onChange={onIgnoreIndexSettingsToggle} diff --git a/public/pages/Snapshots/components/SnapshotRestoreOption/SnapshotRestoreOption.tsx b/public/pages/Snapshots/components/SnapshotRestoreOption/SnapshotRestoreOption.tsx index 49dd2fec4..6398947ba 100644 --- a/public/pages/Snapshots/components/SnapshotRestoreOption/SnapshotRestoreOption.tsx +++ b/public/pages/Snapshots/components/SnapshotRestoreOption/SnapshotRestoreOption.tsx @@ -34,7 +34,7 @@ const SnapshotRestoreOption = ({ } + label="Restore all indices in snapshot" checked={restoreAllIndices} onChange={onRestoreAllIndicesToggle} /> @@ -44,7 +44,7 @@ const SnapshotRestoreOption = ({ } + label="Restore specific indices" checked={restoreSpecificIndices} onChange={onRestoreSpecificIndicesToggle} /> diff --git a/public/pages/Snapshots/components/SnapshotRestoreOption/__snapshots__/SnapshotRestoreOption.test.tsx.snap b/public/pages/Snapshots/components/SnapshotRestoreOption/__snapshots__/SnapshotRestoreOption.test.tsx.snap index 879e63c5c..5509d887b 100644 --- a/public/pages/Snapshots/components/SnapshotRestoreOption/__snapshots__/SnapshotRestoreOption.test.tsx.snap +++ b/public/pages/Snapshots/components/SnapshotRestoreOption/__snapshots__/SnapshotRestoreOption.test.tsx.snap @@ -27,24 +27,7 @@ exports[`SnapshotRestoreOption component renders without error 1`] = ` class="euiRadio__label" for="restore_all_indices" > -
-
-
-

- Restore all indices in snapshot -

-
-
-
-
+ Restore all indices in snapshot
-
-
-
-

- Restore specific indices -

-
-
-
-
+ Restore specific indices
diff --git a/public/pages/Snapshots/containers/Snapshots/Snapshots.tsx b/public/pages/Snapshots/containers/Snapshots/Snapshots.tsx index 922721a62..0c3c53514 100644 --- a/public/pages/Snapshots/containers/Snapshots/Snapshots.tsx +++ b/public/pages/Snapshots/containers/Snapshots/Snapshots.tsx @@ -6,11 +6,23 @@ import React, { Component } from "react"; import _ from "lodash"; import { RouteComponentProps } from "react-router-dom"; -import { EuiButton, EuiInMemoryTable, EuiLink, EuiTableFieldDataColumnType, EuiText, EuiPageHeader, EuiTabs, EuiTab } from "@elastic/eui"; +import { + EuiButton, + EuiInMemoryTable, + EuiLink, + EuiTableFieldDataColumnType, + EuiText, + EuiPageHeader, + EuiTabs, + EuiTab, + EuiOverlayMask, + EuiGlobalToastList, +} from "@elastic/eui"; import { FieldValueSelectionFilterConfigType } from "@elastic/eui/src/components/search_bar/filters/field_value_selection_filter"; import { CoreServicesContext } from "../../../../components/core_services"; import { SnapshotManagementService, IndexService } from "../../../../services"; import { getErrorMessage } from "../../../../utils/helpers"; +import { Toast, RestoreError } from "../../../../models/interfaces" import { CatSnapshotWithRepoAndPolicy as SnapshotsWithRepoAndPolicy } from "../../../../../server/models/interfaces"; import { ContentPanel } from "../../../../components/ContentPanel"; import SnapshotFlyout from "../../components/SnapshotFlyout/SnapshotFlyout"; @@ -18,9 +30,11 @@ import CreateSnapshotFlyout from "../../components/CreateSnapshotFlyout"; import RestoreSnapshotFlyout from "../../components/RestoreSnapshotFlyout"; import RestoreActivitiesPanel from "../../components/RestoreActivitiesPanel"; import { Snapshot } from "../../../../../models/interfaces"; -import { BREADCRUMBS, RESTORE_SNAPSHOT_DOCUMENTATION_URL, ROUTES } from "../../../../utils/constants"; +import { BREADCRUMBS, ROUTES } from "../../../../utils/constants"; import { renderTimestampMillis } from "../../../SnapshotPolicies/helpers"; +import ErrorModal from "../../../Snapshots/components/ErrorModal/ErrorModal" import DeleteModal from "../../../Repositories/components/DeleteModal/DeleteModal"; +import { getToasts } from "../../helper" import { snapshotStatusRender, truncateSpan } from "../../helper"; interface SnapshotsProps extends RouteComponentProps { @@ -34,6 +48,10 @@ interface SnapshotsState { loadingSnapshots: boolean; snapshotPanel: boolean; restoreStart: number; + restoreCount: number; + toasts: Toast[]; + viewError: boolean; + error: RestoreError; selectedItems: SnapshotsWithRepoAndPolicy[]; @@ -52,6 +70,7 @@ interface SnapshotsState { export default class Snapshots extends Component { static contextType = CoreServicesContext; columns: EuiTableFieldDataColumnType[]; + private tabsRef; constructor(props: SnapshotsProps) { super(props); @@ -62,6 +81,10 @@ export default class Snapshots extends Component loadingSnapshots: false, snapshotPanel: true, restoreStart: 0, + restoreCount: 0, + toasts: [], + error: {}, + viewError: false, selectedItems: [], showFlyout: false, flyoutSnapshotId: "", @@ -128,6 +151,7 @@ export default class Snapshots extends Component }, ]; + this.tabsRef = React.createRef(); this.getSnapshots = _.debounce(this.getSnapshots, 500, { leading: true }); } @@ -150,7 +174,12 @@ export default class Snapshots extends Component ] as string[]; this.setState({ snapshots, existingPolicyNames }); } else { - this.context.notifications.toasts.addDanger(response.error); + 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 snapshots.`, + toastMessage: `${trimmedMessage} Open browser console & click below for details.` + }); } } catch (err) { this.context.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem loading the snapshots.")); @@ -160,6 +189,7 @@ export default class Snapshots extends Component }; onSelectionChange = (selectedItems: SnapshotsWithRepoAndPolicy[]): void => { + if (this.state.showRestoreFlyout) return; this.setState({ selectedItems }); }; @@ -182,7 +212,12 @@ export default class Snapshots extends Component if (response.ok) { this.context.notifications.toasts.addSuccess(`Deleted snapshot ${snapshotId} from repository ${repository}.`); } else { - this.context.notifications.toasts.addDanger(response.error); + 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 deleting the snapshot.`, + toastMessage: `${trimmedMessage} Open browser console & click below for details.` + }); } } catch (err) { this.context.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem deleting the snapshot.")); @@ -206,7 +241,13 @@ export default class Snapshots extends Component this.context.notifications.toasts.addSuccess(`Created snapshot ${snapshotId} in repository ${repository}.`); await this.getSnapshots(); } else { - this.context.notifications.toasts.addDanger(response.error); + 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 creating the snapshot.`, + toastMessage: `${trimmedMessage} Open browser console & click below for details.` + }); } } catch (err) { this.context.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem creating the snapshot.")); @@ -215,27 +256,58 @@ export default class Snapshots extends Component restoreSnapshot = async (snapshotId: string, repository: string, options: object) => { try { + await this.setState({ toasts: [] }) 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}. View restore status in "Restore activities in progress" tab`); + this.onRestore(true, response); } else { - this.context.notifications.toasts.addDanger(response.error); + this.onRestore(false, JSON.parse(response.error).error); } } catch (err) { this.context.notifications.toasts.addDanger(getErrorMessage(err, "There was a problem restoring the snapshot.")); } }; - getRestoreReferenceTime = (time: number) => { - this.setState({ restoreStart: time }) + onRestore = (success: boolean, error: object = {}) => { + const { selectedItems } = this.state; + let errorMessage: string | undefined; + if (!success) { + const rawMessage = error.reason; + const index = rawMessage.indexOf("]") + 1; + const startIndex = rawMessage[index] === " " ? index + 1 : index; + const message = rawMessage.slice(startIndex).replace(/[\[\]]/g, '"'); + errorMessage = message.charAt(0).toUpperCase() + message.slice(1); + errorMessage = errorMessage?.slice(0, 125) + "..."; + } + + const toasts = success ? + getToasts("success_restore_toast", errorMessage, selectedItems[0].id, this.onClickTab) : + getToasts("error_restore_toast", errorMessage, selectedItems[0].id, this.onOpenError); + this.setState({ toasts, error: error }); + } + + onOpenError = () => { + this.setState({ viewError: true }); + } + + onCloseModal = () => { + this.setState({ viewError: false, error: {} }); + } + + getRestoreInfo = (time: number, count: number) => { + this.setState({ restoreStart: time, restoreCount: count }) } onClickRestore = async () => { this.setState({ showRestoreFlyout: true }); }; + onToastEnd = () => { + this.setState({ toasts: [] }); + } + onCloseRestoreFlyout = () => { this.setState({ showRestoreFlyout: false }); }; @@ -248,26 +320,38 @@ export default class Snapshots extends Component const prev = target.previousElementSibling; const next = target.nextElementSibling; - if (selectedItems.length === 0) { - this.context.notifications.toasts.addWarning("Please select a snapshot to view restore activities"); - return; + if (snapshotPanel) { + this.context.chrome.setBreadcrumbs([BREADCRUMBS.SNAPSHOT_MANAGEMENT, BREADCRUMBS.SNAPSHOTS]); } - this.context.chrome.setBreadcrumbs([BREADCRUMBS.SNAPSHOT_MANAGEMENT, BREADCRUMBS.SNAPSHOTS]); + if (target.textContent !== "View restore activities") { + target.ariaSelected = "true"; + target.classList.add("euiTab-isSelected"); - target.ariaSelected = "true"; - target.classList.add("euiTab-isSelected"); + if (prev) { + prev.classList.remove("euiTab-isSelected"); + prev.ariaSelected = "false"; + } - if (prev) { - prev.classList.remove("euiTab-isSelected"); - prev.ariaSelected = "false"; - } - if (next) { - next.classList.remove("euiTab-isSelected"); - next.ariaSelected = "false"; + if (next) { + next.classList.remove("euiTab-isSelected"); + next.ariaSelected = "false"; + } + } else { + const firstTab = this.tabsRef.current?.firstChild; + const secondTab = this.tabsRef.current?.lastChild; + + firstTab!.ariaSelected = "false"; + firstTab!.classList.remove("euiTab-isSelected"); + + secondTab!.ariaSelected = "true"; + secondTab!.classList.add("euiTab-isSelected"); } + let newState = { snapshotPanel: snapshotPanel, selectedItems } - this.setState({ snapshotPanel: snapshotPanel }); + if (snapshotPanel) newState.selectedItems = []; + + this.setState(newState); }; render() { @@ -277,7 +361,11 @@ export default class Snapshots extends Component selectedItems, loadingSnapshots, snapshotPanel, + toasts, + viewError, + error, restoreStart, + restoreCount, showFlyout, flyoutSnapshotId, flyoutSnapshotRepo, @@ -335,11 +423,8 @@ export default class Snapshots extends Component const subTitleText = (

- Snapshots are taken automatically from snapshot policies, or you can initiate manual snapshots to save to a repository.
- To restore a snapshot, use the snapshot restore API.{" "} - - Learn more - + Snapshots of indices are taken automatically from snapshot policies,
or you can initiate manual snapshots to save to a repository.
+ You can restore indices by selecting a snapshot.

); @@ -347,7 +432,7 @@ export default class Snapshots extends Component return ( <> - + Snapshots Restore activities in progress @@ -355,11 +440,12 @@ export default class Snapshots extends Component {snapshotPanel || ( )} + {snapshotPanel && ( /> )} + {/* Overlay added to preserve correct Delete/Restore button status, accurately depict selected snapshots upon leaving flyout */} {showRestoreFlyout && ( - + + + + )} + + + + {viewError && ( + )} {isDeleteModalVisible && ( diff --git a/public/pages/Snapshots/helper.tsx b/public/pages/Snapshots/helper.tsx index 7cc12680b..c45f6cb86 100644 --- a/public/pages/Snapshots/helper.tsx +++ b/public/pages/Snapshots/helper.tsx @@ -5,7 +5,8 @@ import React from "react"; import _ from "lodash"; -import { EuiHealth } from "@elastic/eui"; +import { Toast } from "../../models/interfaces" +import { EuiHealth, EuiButton, EuiFlexGroup, EuiSpacer, EuiText } from "@elastic/eui"; export function truncateLongText(text: string, truncateLen: number = 20): string { if (text.length > truncateLen) { @@ -28,3 +29,57 @@ export function snapshotStatusRender(value: string): React.ReactElement { return {capital}; } + + +export const getToasts = (id: string, message: string | undefined, snapshotId: string, onClick: (e: React.MouseEvent) => void): Toast[] => { + const toasts = [ + { + id: "success_restore_toast", + title: `Restore from snapshot "${snapshotId}" is in progress.`, + iconType: "check", + color: "success", + text: ( + <> + + + View restore activities + + + ) + }, + { + id: "error_restore_toast", + title: `Failed to restore snapshot "${snapshotId}"`, + color: "danger", + text: ( + <> + {message} + + + View full error + + + ) + } + ] + if (id === "success_restore_toast") { + return [toasts[0]] + } + return [toasts[1]]; +} + +interface CheckboxLabelProps { + title: string; + helpText: string; +} + +export const CheckBoxLabel = ({ title, helpText }: CheckboxLabelProps) => ( + <> + {title} + + {helpText} + + +); diff --git a/public/services/IndexService.ts b/public/services/IndexService.ts index 5e3e8248e..da2d058a4 100644 --- a/public/services/IndexService.ts +++ b/public/services/IndexService.ts @@ -38,7 +38,7 @@ export default class IndexService { getDataStreamsAndIndicesNames = async (searchValue: string): Promise> => { const [getIndicesResponse, getDataStreamsResponse] = await Promise.all([ - this.getIndices({ from: 0, size: 10, search: searchValue, sortDirection: "desc", sortField: "index", showDataStreams: true }), + this.getIndices({ from: 0, size: 100, search: searchValue, sortDirection: "desc", sortField: "index", showDataStreams: true }), this.getDataStreams({ search: searchValue }), ]); diff --git a/public/services/SnapshotManagementService.ts b/public/services/SnapshotManagementService.ts index cddf89378..75fe1fa22 100644 --- a/public/services/SnapshotManagementService.ts +++ b/public/services/SnapshotManagementService.ts @@ -64,7 +64,6 @@ export default class SnapshotManagementService { getIndexRecovery = async (): Promise> => { const url = NODE_API._RECOVERY; - console.log("URL", url); const response = (await this.httpClient.get(url)) as ServerResponse; return response; }; diff --git a/server/models/interfaces.ts b/server/models/interfaces.ts index 0b995160a..9d3be7544 100644 --- a/server/models/interfaces.ts +++ b/server/models/interfaces.ts @@ -385,7 +385,6 @@ export interface GetIndexRecoveryResponse { ]; }; } - export interface CatSnapshotWithRepoAndPolicy { id: string; status: string; @@ -434,6 +433,7 @@ export interface GetSnapshot { restore_aliases?: boolean; ignore_unavailable?: boolean; ignore_index_settings?: boolean; + failed_shards?: number; rename_pattern?: string; rename_replacement?: string; partial?: boolean; diff --git a/server/services/SnapshotManagementService.ts b/server/services/SnapshotManagementService.ts index 1aa3ebe46..fb236dc8a 100644 --- a/server/services/SnapshotManagementService.ts +++ b/server/services/SnapshotManagementService.ts @@ -314,7 +314,6 @@ 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, @@ -515,7 +514,6 @@ export default class SnapshotManagementService { request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory ): Promise>> => { - console.log(request); try { const { callAsCurrentUser: callWithRequest } = this.osDriver.asScoped(request); const res: CatSnapshotIndex[] = await callWithRequest("cat.indices", {