Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 520x 520x 2481x 2481x 520x 520x 1961x 1961x 3756x 1961x 1961x 1961x 1961x 1961x 1961x 1961x 1743x 1743x 1743x 1734x 9x 9x 1961x 1921x 6x 3x 1x | import { ReactElement, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import styled from "styled-components";
import { config } from "shared/config";
import { Button } from "client/components/Button";
import { DismissibleBanner } from "client/components/DismissibleBanner";
import { ButtonStyle, HighlightStyle } from "client/components/componentStyles";
import { useAppSelector } from "client/utils/hooks";
import {
getAppUpdateReloadedBuildTime,
saveAppUpdateReloadedBuildTime,
} from "client/utils/sessionStorage";
const { appBuildTime } = config.client();
// Empty when a build carried no timestamp, and any non-numeric value is
// treated the same way: without a usable time there is nothing to order by
const parseBuildTime = (value: string): number | null => {
const parsed = Number(value);
return value === "" || Number.isNaN(parsed) ? null : parsed;
};
const ownBuildTime = parseBuildTime(appBuildTime);
// Shown when the server reports a build newer than the one this bundle came
// from, i.e. a new Konsti version was deployed after this page was loaded.
// Reloading fetches it
export const AppUpdateBanner = (): ReactElement | null => {
const { t } = useTranslation();
const serverAppBuildTime = useAppSelector(
(state) => state.admin.serverAppBuildTime,
);
// Keyed to the dismissed build so a later deploy notifies again
const [dismissedBuildTime, setDismissedBuildTime] = useState<string>("");
const routerLocation = useLocation();
const lastLocationKey = useRef(routerLocation.key);
// Strictly newer, not merely different. Images are built in the order they
// are deployed, so this distinguishes a server that has moved on from one
// that simply hasn't rolled yet: mid-rollout a page served by an
// already-updated instance keeps polling instances still on the old build,
// and must not be told to update to the version it is already running.
// Either side missing a build time means there is nothing to compare, which
// is the case in every environment that doesn't stamp its builds
const serverBuildTime = parseBuildTime(serverAppBuildTime);
const updateAvailable =
ownBuildTime !== null &&
serverBuildTime !== null &&
serverBuildTime > ownBuildTime;
const dismissed = dismissedBuildTime === serverAppBuildTime;
// Reload transparently on the first route navigation after an update is
// detected: a navigation replaces the whole view anyway, so a full page
// load is not disruptive there. Only an actual navigation may trigger the
// reload - never the update detection itself, which can happen mid-view.
// Attempt it only once per server build so a reload that fails to deliver
// it can't loop; after that the banner's reload button remains as the
// manual path. Keyed to the build rather than the revision so redeploying
// the same code - a rollback - is still allowed its own reload
useEffect(() => {
const isNavigation = routerLocation.key !== lastLocationKey.current;
lastLocationKey.current = routerLocation.key;
if (
!isNavigation ||
!updateAvailable ||
dismissed ||
getAppUpdateReloadedBuildTime() === serverAppBuildTime
) {
return;
}
saveAppUpdateReloadedBuildTime(serverAppBuildTime);
location.reload();
}, [routerLocation.key, updateAvailable, dismissed, serverAppBuildTime]);
if (!updateAvailable || dismissed) {
return null;
}
return (
<DismissibleBanner
data-testid="app-update-banner"
icon="arrows-rotate"
highlightStyle={HighlightStyle.INFO}
onDismiss={() => {
setDismissedBuildTime(serverAppBuildTime);
}}
dismissAriaLabel={t("iconAltText.closeAppUpdateNotification")}
>
<Message>{t("appUpdateBanner.message")}</Message>
<Button
buttonStyle={ButtonStyle.PRIMARY}
onClick={() => {
location.reload();
}}
>
{t("appUpdateBanner.reload")}
</Button>
</DismissibleBanner>
);
};
const Message = styled.span`
flex: 1;
`;
|