From c9ecaf0e9839cbef64235cce9a4e28f034ad054d Mon Sep 17 00:00:00 2001 From: Mia Wong Date: Fri, 23 Aug 2024 15:33:10 -0400 Subject: [PATCH] Show available updates in Dashboard (#4828) --- .../apps/AvailableUpdatesComponent.tsx | 171 ++++++++++-------- .../components/AvailableUpdateCard.tsx | 54 ++++++ .../components/DashboardVersionCard.tsx | 102 +++++++++-- 3 files changed, 241 insertions(+), 86 deletions(-) create mode 100644 web/src/features/Dashboard/components/AvailableUpdateCard.tsx diff --git a/web/src/components/apps/AvailableUpdatesComponent.tsx b/web/src/components/apps/AvailableUpdatesComponent.tsx index c817d1c57c..cc47821f65 100644 --- a/web/src/components/apps/AvailableUpdatesComponent.tsx +++ b/web/src/components/apps/AvailableUpdatesComponent.tsx @@ -5,6 +5,80 @@ import { Utilities } from "@src/utilities/utilities"; import { AvailableUpdate } from "@types"; import ReactTooltip from "react-tooltip"; +export const AvailableUpdateRow = ({ + update, + index, + showReleaseNotes, + children, + upgradeService, +}: { + update: AvailableUpdate; + index: number; + showReleaseNotes: (releaseNotes: string) => void; + children: React.ReactNode; + upgradeService?: { + versionLabel?: string; + isLoading?: boolean; + error?: string; + } | null; +}) => { + return ( +
+
+
+
+

+ {update.versionLabel} +

+ {update.isRequired && ( + + {" "} + Required{" "} + + )} +
+ {update.upstreamReleasedAt && ( +

+ {" "} + Released{" "} + + {Utilities.dateFormat( + update.upstreamReleasedAt, + "MM/DD/YY @ hh:mm a z" + )} + +

+ )} +
+
+ {update?.releaseNotes && ( + <> + showReleaseNotes(update?.releaseNotes)} + data-tip="View release notes" + className="u-marginRight--5 clickable" + /> + + + )} + {children} +
+
+ {upgradeService?.error && + upgradeService?.versionLabel === update.versionLabel && ( +
+ + {upgradeService.error} + error + +
+ )} +
+ ); +}; + const AvailableUpdatesComponent = ({ updates, showReleaseNotes, @@ -71,78 +145,33 @@ const AvailableUpdatesComponent = ({ upgradeService?.versionLabel === update.versionLabel && upgradeService.isLoading; return ( -
-
-
-
-

- {update.versionLabel} -

- {update.isRequired && ( - - {" "} - Required{" "} - - )} -
- {update.upstreamReleasedAt && ( -

- {" "} - Released{" "} - - {Utilities.dateFormat( - update.upstreamReleasedAt, - "MM/DD/YY @ hh:mm a z" - )} - -

- )} -
-
- {update?.releaseNotes && ( - <> - showReleaseNotes(update?.releaseNotes)} - data-tip="View release notes" - className="u-marginRight--5 clickable" - /> - - - )} - - -
-
- {upgradeService?.error && - upgradeService?.versionLabel === update.versionLabel && ( -
- - {upgradeService.error} - -
- )} -
+ {isCurrentVersionLoading ? "Preparing..." : "Deploy"} + + + + + ); })} diff --git a/web/src/features/Dashboard/components/AvailableUpdateCard.tsx b/web/src/features/Dashboard/components/AvailableUpdateCard.tsx new file mode 100644 index 0000000000..be93b6dce8 --- /dev/null +++ b/web/src/features/Dashboard/components/AvailableUpdateCard.tsx @@ -0,0 +1,54 @@ +import { useNavigate } from "react-router-dom"; + +import { AvailableUpdate } from "@types"; +import { AvailableUpdateRow } from "@components/apps/AvailableUpdatesComponent"; + +const AvailableUpdateCard = ({ + updates, + showReleaseNotes, + appSlug, +}: { + updates: AvailableUpdate[]; + showReleaseNotes: (releaseNotes: string) => void; + appSlug: string; +}) => { + const navigate = useNavigate(); + const update = updates[0]; + return ( +
+
+

+ Latest Available Update +

+

+ + ({updates.length} available) + +

+
+
+ + + +
+
+ ); +}; + +export default AvailableUpdateCard; diff --git a/web/src/features/Dashboard/components/DashboardVersionCard.tsx b/web/src/features/Dashboard/components/DashboardVersionCard.tsx index 32e9c87294..7f8bc99dad 100644 --- a/web/src/features/Dashboard/components/DashboardVersionCard.tsx +++ b/web/src/features/Dashboard/components/DashboardVersionCard.tsx @@ -1,32 +1,31 @@ +import classNames from "classnames"; import { useEffect, useReducer } from "react"; +import Modal from "react-modal"; import { Link, useNavigate, useParams } from "react-router-dom"; import ReactTooltip from "react-tooltip"; -import DashboardGitOpsCard from "./DashboardGitOpsCard"; -import MarkdownRenderer from "@src/components/shared/MarkdownRenderer"; + +import EditConfigIcon from "@components/shared/EditConfigIcon"; +import { useSelectedApp } from "@features/App"; import VersionDiff from "@features/VersionDiff/VersionDiff"; -import Modal from "react-modal"; import AirgapUploadProgress from "@src/components/AirgapUploadProgress"; -import Loader from "@src/components/shared/Loader"; -import MountAware from "@src/components/shared/MountAware"; +import Icon from "@src/components/Icon"; import ShowDetailsModal from "@src/components/modals/ShowDetailsModal"; import ShowLogsModal from "@src/components/modals/ShowLogsModal"; +import Loader from "@src/components/shared/Loader"; +import MarkdownRenderer from "@src/components/shared/MarkdownRenderer"; import DeployWarningModal from "@src/components/shared/modals/DeployWarningModal"; import SkipPreflightsModal from "@src/components/shared/modals/SkipPreflightsModal"; -import classNames from "classnames"; +import MountAware from "@src/components/shared/MountAware"; +import { AirgapUploader } from "@src/utilities/airgapUploader"; +import { Repeater } from "@src/utilities/repeater"; import { getPreflightResultState, getReadableGitOpsProviderName, secondsAgo, Utilities, } from "@src/utilities/utilities"; -import { useNextAppVersionWithIntercept } from "../api/useNextAppVersion"; -import { useSelectedApp } from "@features/App"; -import { Repeater } from "@src/utilities/repeater"; - -import "@src/scss/components/watches/DashboardCard.scss"; -import Icon from "@src/components/Icon"; - import { + AvailableUpdate, Downstream, KotsParams, Metadata, @@ -34,8 +33,11 @@ import { VersionDownloadStatus, VersionStatus, } from "@types"; -import { AirgapUploader } from "@src/utilities/airgapUploader"; -import EditConfigIcon from "@components/shared/EditConfigIcon"; +import { useNextAppVersionWithIntercept } from "../api/useNextAppVersion"; +import AvailableUpdateCard from "./AvailableUpdateCard"; +import DashboardGitOpsCard from "./DashboardGitOpsCard"; + +import "@src/scss/components/watches/DashboardCard.scss"; type Props = { adminConsoleMetadata: Metadata | null; @@ -71,6 +73,7 @@ type Props = { }; type State = { + availableUpdates: AvailableUpdate[]; confirmType: string; deployView: boolean; displayConfirmDeploymentModal: boolean; @@ -78,6 +81,7 @@ type State = { displayShowDetailsModal: boolean; firstSequence: string; secondSequence: string; + isFetchingAvailableUpdates: boolean; isRedeploy: boolean; isSkipPreflights: boolean; kotsUpdateChecker: Repeater; @@ -119,12 +123,14 @@ const DashboardVersionCard = (props: Props) => { ...newState, }), { + availableUpdates: [], confirmType: "", deployView: false, displayConfirmDeploymentModal: false, displayKotsUpdateModal: false, displayShowDetailsModal: false, firstSequence: "", + isFetchingAvailableUpdates: false, isSkipPreflights: false, isRedeploy: false, kotsUpdateChecker: new Repeater(), @@ -168,6 +174,32 @@ const DashboardVersionCard = (props: Props) => { } = useNextAppVersionWithIntercept(); const { latestDeployableVersion } = newAppVersionWithInterceptData || {}; + const fetchAvailableUpdates = async () => { + const appSlug = params.slug; + setState({ isFetchingAvailableUpdates: true }); + const res = await fetch( + `${process.env.API_ENDPOINT}/app/${appSlug}/updates`, + { + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + method: "GET", + } + ); + if (!res.ok) { + setState({ isFetchingAvailableUpdates: false }); + return; + } + const response = await res.json(); + + setState({ + isFetchingAvailableUpdates: false, + availableUpdates: response.updates, + }); + return response; + }; + // moving this out of the state because new repeater instances were getting created // and it doesn't really affect the UI const versionDownloadStatusJobs: { @@ -178,6 +210,9 @@ const DashboardVersionCard = (props: Props) => { if (props.links && props.links.length > 0) { setState({ selectedAction: props.links[0] }); } + if (props.adminConsoleMetadata?.isEmbeddedCluster) { + fetchAvailableUpdates(); + } }, []); useEffect(() => { @@ -1507,6 +1542,35 @@ const DashboardVersionCard = (props: Props) => { )} )} + {props.adminConsoleMetadata?.isEmbeddedCluster && ( +
+ {!state.isFetchingAvailableUpdates && ( + fetchAvailableUpdates()} + > + + Check for update + + )} + {state.isFetchingAvailableUpdates && ( +
+ + + Checking for updates + +
+ )} +
+ )} {currentVersion?.deployedAt ? (
@@ -1520,6 +1584,14 @@ const DashboardVersionCard = (props: Props) => {

)} + {props.adminConsoleMetadata?.isEmbeddedCluster && + state.availableUpdates?.length > 0 && ( + + )} {renderBottomSection()}