diff --git a/e2e/playwright/tests/@smoke-test/test.spec.ts b/e2e/playwright/tests/@smoke-test/test.spec.ts index cc7ea645e0..5b0973de6b 100644 --- a/e2e/playwright/tests/@smoke-test/test.spec.ts +++ b/e2e/playwright/tests/@smoke-test/test.spec.ts @@ -283,6 +283,7 @@ test("smoke test", async ({ page }) => { .click(); await page.locator(".SnapshotRow--wrapper").click(); await expect(page.locator("#app")).toContainText("Snapshot timeline"); + await page.getByText("Infrastructure", { exact: true }).click(); await page.getByText("View logs").click(); await expect(page.locator(".view-lines")).toContainText("level=info"); await page.getByRole("button", { name: "Ok, got it!" }).click(); @@ -344,4 +345,4 @@ const validateDeployLogs = async (page, expect) => { await page.getByText("applyStdout").click(); await expect(page.locator(".view-lines")).toContainText("created"); await page.getByRole("button", { name: "Ok, got it!" }).click(); -}; +}; \ No newline at end of file diff --git a/web/src/components/snapshots/SnapshotDetails.jsx b/web/src/components/snapshots/SnapshotDetails.jsx index cc30f34029..cc41612a98 100644 --- a/web/src/components/snapshots/SnapshotDetails.jsx +++ b/web/src/components/snapshots/SnapshotDetails.jsx @@ -1,18 +1,20 @@ -import { Component } from "react"; -import { withRouter } from "@src/utilities/react-router-utilities"; import MonacoEditor from "@monaco-editor/react"; -import Modal from "react-modal"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import minMax from "dayjs/plugin/minMax"; import filter from "lodash/filter"; import isEmpty from "lodash/isEmpty"; +import { Component, useState } from "react"; import ReactApexChart from "react-apexcharts"; -import dayjs from "dayjs"; -import minMax from "dayjs/plugin/minMax"; -import duration from "dayjs/plugin/duration"; -import Loader from "../shared/Loader"; +import Modal from "react-modal"; + +import Icon from "@components/Icon"; +import { withRouter } from "@src/utilities/react-router-utilities"; +import { Utilities } from "../../utilities/utilities"; +import ErrorModal from "../modals/ErrorModal"; import ShowAllModal from "../modals/ShowAllModal"; import ViewSnapshotLogsModal from "../modals/ViewSnapshotLogsModal"; -import ErrorModal from "../modals/ErrorModal"; -import { Utilities } from "../../utilities/utilities"; +import Loader from "../shared/Loader"; dayjs.extend(minMax); dayjs.extend(duration); @@ -22,6 +24,7 @@ let mapColors = {}; class SnapshotDetails extends Component { state = { + activeIds: [], showScriptsOutput: false, scriptOutput: "", selectedTab: "stdout", @@ -40,6 +43,7 @@ class SnapshotDetails extends Component { snapshotLogsErrMsg: "", loading: true, + currentSnapshotIndex: 0, snapshotDetails: {}, errorMessage: "", errorTitle: "", @@ -127,6 +131,32 @@ class SnapshotDetails extends Component { } } + getSeriesDataForSnapshot = (snapshot) => { + let series = []; + if (!isEmpty(snapshot?.volumes)) { + if (snapshot?.hooks && !isEmpty(snapshot?.hooks)) { + series = this.getSeriesData( + [...snapshot?.volumes, ...snapshot?.hooks].sort( + (a, b) => new Date(a.started) - new Date(b.started) + ) + ); + } else { + series = this.getSeriesData( + (snapshot?.volumes).sort( + (a, b) => new Date(a.started) - new Date(b.started) + ) + ); + } + } else if (snapshot?.hooks && !isEmpty(snapshot?.hooks)) { + series = this.getSeriesData( + (snapshot?.hooks).sort( + (a, b) => new Date(a.started) - new Date(b.started) + ) + ); + } + return series; + }; + fetchSnapshotDetails = async () => { const { params } = this.props; const snapshotName = params.id; @@ -158,35 +188,10 @@ class SnapshotDetails extends Component { } const response = await res.json(); - const snapshotDetails = response.backupDetails?.[0]; - - let series = []; - if (!isEmpty(snapshotDetails?.volumes)) { - if (snapshotDetails?.hooks && !isEmpty(snapshotDetails?.hooks)) { - series = this.getSeriesData( - [...snapshotDetails?.volumes, ...snapshotDetails?.hooks].sort( - (a, b) => new Date(a.started) - new Date(b.started) - ) - ); - } else { - series = this.getSeriesData( - (snapshotDetails?.volumes).sort( - (a, b) => new Date(a.started) - new Date(b.started) - ) - ); - } - } else if (snapshotDetails?.hooks && !isEmpty(snapshotDetails?.hooks)) { - series = this.getSeriesData( - (snapshotDetails?.hooks).sort( - (a, b) => new Date(a.started) - new Date(b.started) - ) - ); - } - this.setState({ loading: false, - snapshotDetails: snapshotDetails, - series: series, + snapshotDetails: response, + currentSnapshotIndex: 0, errorMessage: "", errorTitle: "", }); @@ -202,14 +207,14 @@ class SnapshotDetails extends Component { } }; - preSnapshotScripts = () => { - return filter(this.state.snapshotDetails?.hooks, (hook) => { + preSnapshotScripts = (snapshotDetail) => { + return filter(snapshotDetail?.hooks, (hook) => { return hook.phase === "pre"; }); }; - postSnapshotScripts = () => { - return filter(this.state.snapshotDetails?.hooks, (hook) => { + postSnapshotScripts = (snapshotDetail) => { + return filter(snapshotDetail?.hooks, (hook) => { return hook.phase === "post"; }); }; @@ -246,14 +251,13 @@ class SnapshotDetails extends Component { this.setState({ showAllErrors: !this.state.showAllErrors }); }; - viewLogs = () => { + viewLogs = (name) => { this.setState( { toggleViewLogsModal: !this.state.toggleViewLogsModal, }, () => { this.setState({ loadingSnapshotLogs: true }); - const name = this.state.snapshotDetails?.name; const url = `${process.env.API_ENDPOINT}/snapshot/${name}/logs`; fetch(url, { credentials: "include", @@ -376,8 +380,8 @@ class SnapshotDetails extends Component { ); }; - renderErrorsWarningsTabs = () => { - const { snapshotDetails, selectedErrorsWarningTab } = this.state; + renderErrorsWarningsTabs = (snapshotDetail) => { + const { selectedErrorsWarningTab } = this.state; const tabs = ["Errors", "Warnings"]; return (
@@ -393,14 +397,14 @@ class SnapshotDetails extends Component { {tab === "Errors" ? ( {" "} - {snapshotDetails?.errors?.length}{" "} + {snapshotDetail?.errors?.length}{" "} ) : ( {" "} - {!snapshotDetails?.warnings + {!snapshotDetail?.warnings ? "0" - : snapshotDetails?.warnings?.length}{" "} + : snapshotDetail?.warnings?.length}{" "} )}
@@ -577,19 +581,16 @@ class SnapshotDetails extends Component { return series; }; - renderTimeInterval = () => { + renderTimeInterval = (snapshotDetail) => { let data; - if (!isEmpty(this.state.snapshotDetails?.volumes)) { - if (!isEmpty(this.state.snapshotDetails?.hooks)) { - data = [ - ...this.state.snapshotDetails?.volumes, - ...this.state.snapshotDetails?.hooks, - ]; + if (!isEmpty(snapshotDetail?.volumes)) { + if (!isEmpty(snapshotDetail?.hooks)) { + data = [...snapshotDetail?.volumes, ...snapshotDetail?.hooks]; } else { - data = this.state.snapshotDetails?.volumes; + data = snapshotDetail?.volumes; } - } else if (!isEmpty(this.state.snapshotDetails?.hooks)) { - data = this.state.snapshotDetails?.hooks; + } else if (!isEmpty(snapshotDetail?.hooks)) { + data = snapshotDetail?.hooks; } return (
@@ -649,284 +650,512 @@ class SnapshotDetails extends Component { ); }; - render() { - const { isEmbeddedCluster } = this.props; + componentDidUpdate = (lastProps, lastState) => { + const { snapshotDetails } = this.state; + const { backupDetails } = snapshotDetails; + if ( + lastState.snapshotDetails !== snapshotDetails && + backupDetails.length > 0 + ) { + // Filter snapshots with status not equal to "Completed" + const activeIds = backupDetails + .filter((snapshotDetail) => snapshotDetail.status !== "Completed") + .map((snapshotDetail) => snapshotDetail.name); + + // Set the filtered snapshot names in state + this.setState({ activeIds }); + } + }; + renderSnapshot = () => { + const { backupDetails, backup } = this.state.snapshotDetails; const { - loading, - showScriptsOutput, - selectedTab, + series, selectedScriptTab, - scriptOutput, + selectedErrorsWarningTab, + currentSnapshotIndex, + activeIds, showAllVolumes, showAllPreSnapshotScripts, showAllPostSnapshotScripts, - selectedErrorsWarningTab, - showAllErrors, showAllWarnings, - snapshotDetails, - series, - errorMessage, - errorTitle, + showAllErrors, + showAllScriptsOutput, } = this.state; - + const { isEmbeddedCluster, navigate } = this.props; let featureName = "snapshot"; if (isEmbeddedCluster) { featureName = "backup"; } + const { params } = this.props; + const snapshotName = params.id; - if (loading) { - return ( -
- -
- ); - } + const toggleAccordion = (name) => { + this.setState({ + activeIds: this.state.activeIds.includes(name) + ? this.state.activeIds.filter((id) => id !== name) + : [...this.state.activeIds, name], + }); + }; return ( -
-

- this.props.navigate(-1)}> - {Utilities.toTitleCase(featureName)}s - - > - {snapshotDetails?.name} -

-
-
-
-

- {snapshotDetails?.name} -

+
+
+
+

+ {snapshotName} +

+

Total size:{" "} - {snapshotDetails?.volumeSizeHuman} + {backup?.volumeSizeHuman}

-
-
-

+

Status:{" "} - {Utilities.snapshotStatusToDisplayName( - snapshotDetails?.status - )} - + className={`tw-mb-4 status-indicator ${backup?.status?.toLowerCase()} u-marginLeft--5`} + > + {Utilities.snapshotStatusToDisplayName(backup?.status)}

-
- {snapshotDetails?.status !== "InProgress" && ( - this.viewLogs()}> - View logs - - )} -
- - {snapshotDetails?.status === "InProgress" ? ( -
- -

- {" "} - This {featureName} has not completed yet, check back soon{" "} -

-
- ) : ( -
- {!isEmpty(snapshotDetails?.volumes) || - !isEmpty(this.preSnapshotScripts()) || - !isEmpty(this.postSnapshotScripts()) ? ( -
-

- {Utilities.toTitleCase(featureName)} timeline -

-
- { + const isActive = activeIds.includes(snapshotDetail.name); + return ( +
+ {showAllVolumes && ( + - {this.renderTimeInterval()} -
-
- ) : null} - -
-
-
-
-

- Volumes -

- {snapshotDetails?.volumes?.length > 3 ? ( -
- this.toggleShowAllVolumes()} - > - Show all {snapshotDetails?.volumes?.length} volumes + )} + {showAllPreSnapshotScripts && ( + + )} + {showAllPostSnapshotScripts && ( + + )} + {showAllWarnings && ( + + )} + {showAllErrors && ( + + )} +
+
+
- {!isEmpty(snapshotDetails?.volumes) ? ( - this.renderShowAllVolumes( - snapshotDetails?.volumes?.slice(0, 3) - ) - ) : ( -
-

- {" "} - No volumes to display{" "} -

-
- )} -
-
-
-
-

- Scripts -

- {this.preSnapshotScripts()?.length > 3 && - selectedScriptTab === "Pre-snapshot scripts" ? ( -
- this.toggleShowAllPreScripts()} - > - Show all {this.preSnapshotScripts()?.length}{" "} - pre-scripts - -
- ) : null} - {this.postSnapshotScripts()?.length > 3 && - selectedScriptTab === "Post-snapshot scripts" ? ( -
+ + {snapshotDetail.status !== "Completed" ? ( this.toggleShowAllPostScripts()} - > - Show all {this.postSnapshotScripts()?.length}{" "} - post-scripts - -
- ) : null} -
-
- {this.renderScriptsTabs()} -
-
-
- {selectedScriptTab === "Pre-snapshot scripts" ? ( - !isEmpty(this.preSnapshotScripts()) ? ( - this.renderShowAllScripts( - this.preSnapshotScripts().slice(0, 3) - ) - ) : ( -
-

- {" "} - No pre-{featureName} scripts to display{" "} -

-
- ) - ) : selectedScriptTab === "Post-snapshot scripts" && - !isEmpty(this.postSnapshotScripts()) ? ( - this.renderShowAllScripts( - this.postSnapshotScripts().slice(0, 3) - ) - ) : ( -
-

- {" "} - No post-{featureName} scripts to display{" "} -

+ className={`status-indicator ${snapshotDetail?.status?.toLowerCase()} tw-mx-2 tw-mb-4`} + > + ) : null}
- )} -
-
-
-
+ + - {(!isEmpty(snapshotDetails?.errors) || - !isEmpty(snapshotDetails?.warnings)) && ( -
-
-
-
-
-

- Errors and warnings -

- {snapshotDetails?.errors?.length > 3 && - selectedErrorsWarningTab === "Errors" ? ( -
- this.toggleShowAllErrors()} - > - Show all {snapshotDetails?.errors?.length}{" "} - errors{" "} +
+
+
+

+ Size:{" "} + + {snapshotDetail?.volumeSizeHuman} -

- ) : null} - {snapshotDetails?.warnings?.length > 3 && - selectedErrorsWarningTab === "Warnings" ? ( -
+

+

+ Status:{" "} this.toggleShowAllWarnings()} + className={`tw-mb-4 status-indicator ${snapshotDetail?.status?.toLowerCase()} u-marginLeft--5`} + > + {Utilities.snapshotStatusToDisplayName( + snapshotDetail?.status + )} +

+
+
+ {snapshotDetail?.status !== "InProgress" && ( + + this.viewLogs(snapshotDetail?.name) + } > - Show all {snapshotDetails?.warnings?.length}{" "} - warnings{" "} + View logs -
- ) : null} -
-
- {this.renderErrorsWarningsTabs()} + )} +
-
-
- {selectedErrorsWarningTab === "Errors" ? ( - !isEmpty(snapshotDetails?.errors) ? ( - this.renderShowAllErrors( - snapshotDetails?.errors.slice(0, 3) - ) - ) : ( -
-

- {" "} - No errors to display{" "} -

-
- ) - ) : selectedErrorsWarningTab === "Warnings" && - !isEmpty(snapshotDetails?.warnings) ? ( - this.renderShowAllWarnings( - snapshotDetails?.warnings?.slice(0, 3) - ) - ) : ( -
-

+ + {snapshotDetail?.status === "InProgress" ? ( +

+ +

{" "} - No warnings to display{" "} + This {featureName} has not completed yet, check + back soon{" "}

+ ) : ( +
+ {!isEmpty(snapshotDetail?.volumes) || + !isEmpty(this.preSnapshotScripts(snapshotDetail)) || + !isEmpty( + this.postSnapshotScripts(snapshotDetail) + ) ? ( +
+

+ {Utilities.toTitleCase(featureName)} timeline +

+
+ + {/* {this.renderTimeInterval(snapshotDetail)} */} +
+
+ ) : null} + +
+
+
+
+

+ Volumes +

+ {snapshotDetail?.volumes?.length > 3 ? ( +
+ + this.toggleShowAllVolumes() + } + > + Show all{" "} + {snapshotDetail?.volumes?.length}{" "} + volumes + +
+ ) : null} +
+ {!isEmpty(snapshotDetail?.volumes) ? ( + this.renderShowAllVolumes( + snapshotDetail?.volumes?.slice(0, 3) + ) + ) : ( +
+

+ {" "} + No volumes to display{" "} +

+
+ )} +
+
+
+
+

+ Scripts +

+ {this.preSnapshotScripts()?.length > 3 && + selectedScriptTab === + "Pre-snapshot scripts" ? ( +
+ + this.toggleShowAllPreScripts() + } + > + Show all{" "} + {this.preSnapshotScripts()?.length}{" "} + pre-scripts + +
+ ) : null} + {this.postSnapshotScripts(snapshotDetail) + ?.length > 3 && + selectedScriptTab === + "Post-snapshot scripts" ? ( +
+ + this.toggleShowAllPostScripts() + } + > + Show all{" "} + { + this.postSnapshotScripts( + snapshotDetail + )?.length + }{" "} + post-scripts + +
+ ) : null} +
+
+ {this.renderScriptsTabs()} +
+
+
+ {selectedScriptTab === + "Pre-snapshot scripts" ? ( + !isEmpty( + this.preSnapshotScripts(snapshotDetail) + ) ? ( + this.renderShowAllScripts( + this.preSnapshotScripts( + snapshotDetail + ).slice(0, 3) + ) + ) : ( +
+

+ {" "} + No pre-{featureName} scripts to + display{" "} +

+
+ ) + ) : selectedScriptTab === + "Post-snapshot scripts" && + !isEmpty( + this.postSnapshotScripts(snapshotDetail) + ) ? ( + this.renderShowAllScripts( + this.postSnapshotScripts( + snapshotDetail + ).slice(0, 3) + ) + ) : ( +
+

+ {" "} + No post-{featureName} scripts to + display{" "} +

+
+ )} +
+
+
+
+ + {(!isEmpty(snapshotDetail?.errors) || + !isEmpty(snapshotDetail?.warnings)) && ( +
+
+
+
+
+

+ Errors and warnings +

+ {snapshotDetail?.errors?.length > 3 && + selectedErrorsWarningTab === + "Errors" ? ( +
+ + this.toggleShowAllErrors() + } + > + Show all{" "} + {snapshotDetail?.errors?.length}{" "} + errors{" "} + +
+ ) : null} + {snapshotDetail?.warnings?.length > 3 && + selectedErrorsWarningTab === + "Warnings" ? ( +
+ + this.toggleShowAllWarnings() + } + > + Show all{" "} + {snapshotDetail?.warnings?.length}{" "} + warnings{" "} + +
+ ) : null} +
+
+ {this.renderErrorsWarningsTabs( + snapshotDetail + )} +
+
+
+ {selectedErrorsWarningTab === "Errors" ? ( + !isEmpty(snapshotDetail?.errors) ? ( + this.renderShowAllErrors( + snapshotDetail?.errors.slice(0, 3) + ) + ) : ( +
+

+ {" "} + No errors to display{" "} +

+
+ ) + ) : selectedErrorsWarningTab === + "Warnings" && + !isEmpty(snapshotDetail?.warnings) ? ( + this.renderShowAllWarnings( + snapshotDetail?.warnings?.slice(0, 3) + ) + ) : ( +
+

+ {" "} + No warnings to display{" "} +

+
+ )} +
+
+
+
+ )} +
)}
- )} -
- )} + ); + })} +
+
+ ); + }; + + render() { + const { isEmbeddedCluster, navigate } = this.props; + + const { + loading, + showScriptsOutput, + selectedTab, + selectedScriptTab, + scriptOutput, + showAllVolumes, + showAllPreSnapshotScripts, + showAllPostSnapshotScripts, + selectedErrorsWarningTab, + showAllErrors, + showAllWarnings, + snapshotDetails, + currentSnapshotIndex, + series, + errorMessage, + errorTitle, + } = this.state; + + let featureName = "snapshot"; + if (isEmbeddedCluster) { + featureName = "backup"; + } + + const { params } = this.props; + + const snapshotName = params.id; + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

+ this.props.navigate(-1)}> + {Utilities.toTitleCase(featureName)} + + > + {snapshotName} +

+ + {this.renderSnapshot()} {showScriptsOutput && scriptOutput && ( )} - {showAllVolumes && ( - - )} - {showAllPreSnapshotScripts && ( - - )} - {showAllPostSnapshotScripts && ( - - )} - {showAllWarnings && ( - - )} - {showAllErrors && ( - - )} + {this.state.toggleViewLogsModal && ( )} -

- {" "} - {snapshot?.volumeSuccessCount}/{snapshot?.volumeCount} -

{snapshot?.status === "Completed" ? (

diff --git a/web/src/scss/utilities/base.scss b/web/src/scss/utilities/base.scss index 480333a6a2..863e1a4432 100644 --- a/web/src/scss/utilities/base.scss +++ b/web/src/scss/utilities/base.scss @@ -247,7 +247,7 @@ body a { background-color: #4999ad; } &.partiallyfailed::before { - background-color: #f1843c; + background-color: #bc4752; } &.deleting::before { background-color: #f7b500;