From 8be3c7a3d3cb85e3b27df6fbbb49c569cce38b38 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Fri, 26 Nov 2021 15:30:25 +1100 Subject: [PATCH] add update check badge --- package.json | 1 + src/components/Navigation/NavDrawer.tsx | 2 + .../Navigation/UpdateCheckBadge.tsx | 22 +++ src/components/Navigation/index.tsx | 11 +- .../Settings/ProjectSettings/About.tsx | 92 +++--------- .../Settings/ProjectSettings/RowyRun.tsx | 137 ++++++------------ src/hooks/useUpdateCheck.ts | 102 +++++++++++++ src/pages/Settings/ProjectSettings.tsx | 6 +- src/pages/Settings/UserSettings.tsx | 6 +- yarn.lock | 5 + 10 files changed, 211 insertions(+), 173 deletions(-) create mode 100644 src/components/Navigation/UpdateCheckBadge.tsx create mode 100644 src/hooks/useUpdateCheck.ts diff --git a/package.json b/package.json index c1caa907..acf28d3d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "algoliasearch": "^4.8.6", "ansi-to-react": "^6.1.5", "colord": "^2.7.0", + "compare-versions": "^4.1.1", "craco-swc": "^0.1.3", "csv-parse": "^4.15.3", "date-fns": "^2.19.0", diff --git a/src/components/Navigation/NavDrawer.tsx b/src/components/Navigation/NavDrawer.tsx index a223d857..2bcdeb14 100644 --- a/src/components/Navigation/NavDrawer.tsx +++ b/src/components/Navigation/NavDrawer.tsx @@ -23,6 +23,7 @@ import { APP_BAR_HEIGHT } from "."; import Logo from "@src/assets/Logo"; import NavItem from "./NavItem"; import NavTableSection from "./NavTableSection"; +import UpdateCheckBadge from "./UpdateCheckBadge"; import { useAppContext } from "@src/contexts/AppContext"; import { useProjectContext } from "@src/contexts/ProjectContext"; @@ -157,6 +158,7 @@ export default function NavDrawer({ + )} diff --git a/src/components/Navigation/UpdateCheckBadge.tsx b/src/components/Navigation/UpdateCheckBadge.tsx new file mode 100644 index 00000000..e41ade67 --- /dev/null +++ b/src/components/Navigation/UpdateCheckBadge.tsx @@ -0,0 +1,22 @@ +import { Badge, BadgeProps } from "@mui/material"; +import useUpdateCheck from "@src/hooks/useUpdateCheck"; + +export default function UpdateCheckBadge(props: Partial) { + const [latestUpdate] = useUpdateCheck(); + + if (!latestUpdate.rowy && !latestUpdate.rowyRun) return <>{props.children}; + + return ( + + ); +} diff --git a/src/components/Navigation/index.tsx b/src/components/Navigation/index.tsx index 4838307d..26b1ceb4 100644 --- a/src/components/Navigation/index.tsx +++ b/src/components/Navigation/index.tsx @@ -19,6 +19,7 @@ import NavDrawer, { NAV_DRAWER_WIDTH } from "./NavDrawer"; import UserMenu from "./UserMenu"; import ErrorBoundary from "@src/components/ErrorBoundary"; import Loading from "@src/components/Loading"; +import UpdateCheckBadge from "./UpdateCheckBadge"; import { useAppContext } from "@src/contexts/AppContext"; import useDocumentTitle from "@src/hooks/useDocumentTitle"; @@ -43,7 +44,7 @@ export default function Navigation({ currentSection, titleTransitionProps, }: INavigationProps) { - const { projectId } = useAppContext(); + const { projectId, userRoles } = useAppContext(); useDocumentTitle(projectId, title); const [open, setOpen] = useOpenState(false); @@ -114,7 +115,13 @@ export default function Navigation({ size="large" edge="start" > - + {userRoles.includes("ADMIN") ? ( + + + + ) : ( + + )} )} diff --git a/src/components/Settings/ProjectSettings/About.tsx b/src/components/Settings/ProjectSettings/About.tsx index 23dcccd1..4cdd6c5c 100644 --- a/src/components/Settings/ProjectSettings/About.tsx +++ b/src/components/Settings/ProjectSettings/About.tsx @@ -1,7 +1,3 @@ -import { useState, useCallback, useEffect } from "react"; -import createPersistedState from "use-persisted-state"; -import { differenceInDays } from "date-fns"; - import { Grid, Typography, Button, Link, Divider } from "@mui/material"; import LoadingButton from "@mui/lab/LoadingButton"; import GitHubIcon from "@mui/icons-material/GitHub"; @@ -11,72 +7,15 @@ import TwitterIcon from "@mui/icons-material/Twitter"; import Logo from "@src/assets/Logo"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; -import { name, version, repository } from "@root/package.json"; +import { name, version } from "@root/package.json"; import { useAppContext } from "@src/contexts/AppContext"; +import useUpdateCheck from "@src/hooks/useUpdateCheck"; import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks"; -const useLastCheckedUpdateState = createPersistedState( - "__ROWY__LAST_CHECKED_UPDATE" -); -export const useLatestUpdateState = createPersistedState( - "__ROWY__LATEST_UPDATE" -); - export default function About() { const { projectId } = useAppContext(); - const [lastCheckedUpdate, setLastCheckedUpdate] = - useLastCheckedUpdateState(); - const [latestUpdate, setLatestUpdate] = useLatestUpdateState>(null); - - const [checkState, setCheckState] = useState( - null - ); - - const checkForUpdate = useCallback(async () => { - setCheckState("LOADING"); - - // https://docs.github.com/en/rest/reference/repos#get-the-latest-release - const endpoint = repository.url - .replace("github.com", "api.github.com/repos") - .replace(/.git$/, "/releases/latest"); - try { - const req = await fetch(endpoint, { - headers: { - Accept: "application/vnd.github.v3+json", - }, - }); - const res = await req.json(); - - if (res.tag_name > "v" + version) { - setLatestUpdate(res); - setCheckState(null); - } else { - setCheckState("NO_UPDATE"); - } - - setLastCheckedUpdate(new Date().toISOString()); - } catch (e) { - console.error(e); - setLatestUpdate(null); - setCheckState("NO_UPDATE"); - } - }, [setLastCheckedUpdate, setLatestUpdate]); - - // Check for new updates on page load, if last check was more than 7 days ago - useEffect(() => { - if (!lastCheckedUpdate) checkForUpdate(); - else if (differenceInDays(new Date(), new Date(lastCheckedUpdate)) > 7) - checkForUpdate(); - }, [lastCheckedUpdate, checkForUpdate]); - - // Verify latest update is not installed yet - useEffect(() => { - if (latestUpdate?.tag_name <= "v" + version) setLatestUpdate(null); - }, [latestUpdate, setLatestUpdate]); + const [latestUpdate, checkForUpdates, loading] = useUpdateCheck(); return ( <> @@ -132,19 +71,29 @@ export default function About() {
- {checkState === "LOADING" ? ( + {loading ? ( Checking for updates… - ) : latestUpdate === null ? ( + ) : latestUpdate.rowy === null ? ( Up to date ) : ( + Update available:{" "} - {latestUpdate.tag_name} + {latestUpdate.rowy.tag_name} @@ -156,11 +105,8 @@ export default function About() { - {latestUpdate === null ? ( - + {latestUpdate.rowy === null ? ( + Check for updates ) : ( diff --git a/src/components/Settings/ProjectSettings/RowyRun.tsx b/src/components/Settings/ProjectSettings/RowyRun.tsx index 491e36de..901adf51 100644 --- a/src/components/Settings/ProjectSettings/RowyRun.tsx +++ b/src/components/Settings/ProjectSettings/RowyRun.tsx @@ -1,6 +1,4 @@ -import { useState, useCallback, useEffect } from "react"; -import createPersistedState from "use-persisted-state"; -import { differenceInDays } from "date-fns"; +import { useState } from "react"; import { Typography, @@ -12,19 +10,14 @@ import { } from "@mui/material"; import LoadingButton from "@mui/lab/LoadingButton"; import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings"; import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks"; +import useUpdateCheck from "@src/hooks/useUpdateCheck"; import { name } from "@root/package.json"; import { runRoutes } from "@src/constants/runRoutes"; -const useLastCheckedUpdateState = createPersistedState( - "__ROWY__RUN_LAST_CHECKED_UPDATE" -); -export const useLatestUpdateState = createPersistedState( - "__ROWY__RUN_LATEST_UPDATE" -); - export default function RowyRun({ settings, updateSettings, @@ -41,7 +34,12 @@ export default function RowyRun({ if (!versionReq.version) throw new Error("No version found"); else { setVerified(true); - setVersion(versionReq.version); + + // If the deployed version is different from the last update check, + // check for updates again to clear update + if (versionReq.version !== latestUpdate.deployedRowyRun) + checkForUpdates(); + updateSettings({ rowyRunUrl: inputRowyRunUrl }); } } catch (e) { @@ -50,76 +48,7 @@ export default function RowyRun({ } }; - const [lastCheckedUpdate, setLastCheckedUpdate] = - useLastCheckedUpdateState(); - const [latestUpdate, setLatestUpdate] = useLatestUpdateState>(null); - - const [checkState, setCheckState] = useState( - null - ); - const [version, setVersion] = useState(""); - useEffect(() => { - fetch(settings.rowyRunUrl + runRoutes.version.path, { - method: runRoutes.version.method, - }) - .then((res) => res.json()) - .then((data) => setVersion(data.version)); - }, [settings.rowyRunUrl]); - - const checkForUpdate = useCallback(async () => { - setCheckState("LOADING"); - - // https://docs.github.com/en/rest/reference/repos#get-the-latest-release - const endpoint = - EXTERNAL_LINKS.rowyRunGitHub.replace( - "github.com", - "api.github.com/repos" - ) + "/releases/latest"; - try { - const versionReq = await fetch( - settings.rowyRunUrl + runRoutes.version.path, - { method: runRoutes.version.method } - ).then((res) => res.json()); - const version = versionReq.version; - setVersion(version); - - const req = await fetch(endpoint, { - headers: { - Accept: "application/vnd.github.v3+json", - }, - }); - const res = await req.json(); - - if (res.tag_name > "v" + version) { - setLatestUpdate(res); - setCheckState(null); - } else { - setCheckState("NO_UPDATE"); - } - - setLastCheckedUpdate(new Date().toISOString()); - } catch (e) { - console.error(e); - setLatestUpdate(null); - setCheckState("NO_UPDATE"); - } - }, [setLastCheckedUpdate, setLatestUpdate, settings.rowyRunUrl]); - - // Check for new updates on page load, if last check was more than 7 days ago - useEffect(() => { - if (!lastCheckedUpdate) checkForUpdate(); - else if (differenceInDays(new Date(), new Date(lastCheckedUpdate)) > 7) - checkForUpdate(); - }, [lastCheckedUpdate, checkForUpdate]); - - // Verify latest update is not installed yet - useEffect(() => { - if (version && latestUpdate?.tag_name <= "v" + version) - setLatestUpdate(null); - }, [latestUpdate, setLatestUpdate, version]); + const [latestUpdate, checkForUpdates, loading] = useUpdateCheck(); const deployButton = window.location.hostname.includes( EXTERNAL_LINKS.rowyAppHostName @@ -179,35 +108,42 @@ export default function RowyRun({
- {checkState === "LOADING" ? ( + {loading ? ( Checking for updates… - ) : latestUpdate === null ? ( + ) : latestUpdate.rowyRun === null ? ( Up to date ) : ( + Update available:{" "} - {latestUpdate.tag_name} + {latestUpdate.rowyRun.tag_name} )} - {name} Run v{version} + {name} Run v{latestUpdate.deployedRowyRun} - {latestUpdate === null ? ( - + {latestUpdate.rowyRun === null ? ( + Check for updates ) : ( @@ -254,11 +190,20 @@ export default function RowyRun({ autoComplete="url" error={verified === false} helperText={ - verified === true - ? `${name} Run is set up correctly` - : verified === false - ? `${name} Run is not set up correctly` - : " " + verified === true ? ( + <> + +   + {name} Run is set up correctly + + ) : verified === false ? ( + `${name} Run is not set up correctly` + ) : ( + " " + ) } /> diff --git a/src/hooks/useUpdateCheck.ts b/src/hooks/useUpdateCheck.ts new file mode 100644 index 00000000..1da13e04 --- /dev/null +++ b/src/hooks/useUpdateCheck.ts @@ -0,0 +1,102 @@ +import { useState, useCallback, useEffect } from "react"; +import createPersistedState from "use-persisted-state"; +import { differenceInDays } from "date-fns"; +import { compare } from "compare-versions"; + +import { useProjectContext } from "@src/contexts/ProjectContext"; +import { repository, version } from "@root/package.json"; +import { EXTERNAL_LINKS } from "@src/constants/externalLinks"; +import { runRoutes } from "@src/constants/runRoutes"; + +// https://docs.github.com/en/rest/reference/repos#get-the-latest-release +const UPDATE_ENDPOINTS = { + rowy: repository.url + .replace("github.com", "api.github.com/repos") + .replace(/.git$/, "/releases/latest"), + + rowyRun: + EXTERNAL_LINKS.rowyRunGitHub.replace("github.com", "api.github.com/repos") + + "/releases/latest", +}; + +export const useLatestUpdateState = createPersistedState( + "__ROWY__UPDATE_CHECK" +); +type LatestUpdateState = { + lastChecked: string; + rowy: null | Record; + rowyRun: null | Record; + deployedRowy: string; + deployedRowyRun: string; +}; + +/** + * Get the latest version of Rowy and Rowy Run from GitHub releases, + * and the currently deployed versions + * @returns [latestUpdate, checkForUpdates, loading] + */ +export default function useUpdateCheck() { + const { rowyRun } = useProjectContext(); + const [loading, setLoading] = useState(false); + + // Store latest release from GitHub + const [latestUpdate, setLatestUpdate] = + useLatestUpdateState({ + lastChecked: "", + rowy: null, + rowyRun: null, + deployedRowy: version, + deployedRowyRun: "", + }); + + // Check for updates using latest releases from GitHub + const checkForUpdates = useCallback(async () => { + if (!rowyRun) return; + setLoading(true); + + const newState = { + lastChecked: new Date().toISOString(), + rowy: null, + rowyRun: null, + deployedRowy: version, + deployedRowyRun: "", + }; + + // Make all requests simultaneously + const [resRowy, resRowyRun, deployedRowyRun] = await Promise.all([ + fetch(UPDATE_ENDPOINTS.rowy, { + headers: { Accept: "application/vnd.github.v3+json" }, + }).then((r) => r.json()), + fetch(UPDATE_ENDPOINTS.rowyRun, { + headers: { Accept: "application/vnd.github.v3+json" }, + }).then((r) => r.json()), + rowyRun({ route: runRoutes.version }), + ]); + + // Only store the latest release + if (compare(resRowy.tag_name, version, ">")) newState.rowy = resRowy; + if (compare(resRowyRun.tag_name, deployedRowyRun.version, ">")) + newState.rowyRun = resRowyRun; + + // Save deployed version + newState.deployedRowyRun = deployedRowyRun.version; + + setLatestUpdate(newState); + setLoading(false); + }, [setLoading, setLatestUpdate, rowyRun]); + + // Check for new updates on page load if last check was more than 7 days ago + // or if deployed version has changed + useEffect(() => { + if (loading) return; + + if ( + !latestUpdate.lastChecked || + differenceInDays(new Date(), new Date(latestUpdate.lastChecked)) > 7 || + latestUpdate.deployedRowy !== version + ) + checkForUpdates(); + }, [latestUpdate, loading, checkForUpdates]); + + return [latestUpdate, checkForUpdates, loading] as const; +} diff --git a/src/pages/Settings/ProjectSettings.tsx b/src/pages/Settings/ProjectSettings.tsx index b9df3ec8..8ee5dc9b 100644 --- a/src/pages/Settings/ProjectSettings.tsx +++ b/src/pages/Settings/ProjectSettings.tsx @@ -86,7 +86,11 @@ export default function ProjectSettingsPage() { ) : ( {sections.map(({ title, Component, props }, i) => ( - + ))} diff --git a/src/pages/Settings/UserSettings.tsx b/src/pages/Settings/UserSettings.tsx index 0619a147..04386566 100644 --- a/src/pages/Settings/UserSettings.tsx +++ b/src/pages/Settings/UserSettings.tsx @@ -65,7 +65,11 @@ export default function UserSettingsPage() { ) : ( {sections.map(({ title, Component, props }, i) => ( - + ))} diff --git a/yarn.lock b/yarn.lock index f42bb79e..541f2a3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5426,6 +5426,11 @@ compare-versions@^3.6.0: resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== +compare-versions@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-4.1.1.tgz#d881fc9f47d6eb2b8f63109dc5e82dae39c3680c" + integrity sha512-jHQA7zMUpbO+FhPz/kADChZVSk3edtD7c3WkEAjleBtwgAl0ji6wGrYxryaBhViGgq0A+Pb6JPhjhg9jpth4mQ== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"