From 22ad4572aea60c8e8a5f3d81432f970875fabb23 Mon Sep 17 00:00:00 2001
From: John Kim <97752292+johnkim-det@users.noreply.github.com>
Date: Tue, 22 Oct 2024 16:16:16 -0400
Subject: [PATCH] fix: version upgrade notification bug [CM-411] (#10069)
---
webui/react/src/App.tsx | 60 +++-----------
webui/react/src/components/AuthToken.tsx | 48 -----------
.../react/src/components/JupyterLabButton.tsx | 2 +-
.../src/components/VersionChecker.test.tsx | 81 +++++++++++++++++++
webui/react/src/components/VersionChecker.tsx | 62 ++++++++++++++
webui/react/src/pages/SignIn.tsx | 14 ++--
webui/react/src/utils/browser.ts | 9 ++-
7 files changed, 171 insertions(+), 105 deletions(-)
delete mode 100644 webui/react/src/components/AuthToken.tsx
create mode 100644 webui/react/src/components/VersionChecker.test.tsx
create mode 100644 webui/react/src/components/VersionChecker.tsx
diff --git a/webui/react/src/App.tsx b/webui/react/src/App.tsx
index 080fc0e31b7..c8e57ce827a 100644
--- a/webui/react/src/App.tsx
+++ b/webui/react/src/App.tsx
@@ -1,7 +1,6 @@
import Button from 'hew/Button';
import Spinner from 'hew/Spinner';
import UIProvider from 'hew/Theme';
-import { notification } from 'hew/Toast';
import { ConfirmationProvider } from 'hew/useConfirm';
import { Loadable } from 'hew/utils/loadable';
import { useObservable } from 'micro-observables';
@@ -13,11 +12,11 @@ import { useParams } from 'react-router-dom';
import ClusterMessageBanner from 'components/ClusterMessage';
import JupyterLabGlobal from 'components/JupyterLabGlobal';
-import Link from 'components/Link';
import Navigation from 'components/Navigation';
import PageMessage from 'components/PageMessage';
import Router from 'components/Router';
import useUI, { Mode, ThemeProvider } from 'components/ThemeProvider';
+import VersionChecker from 'components/VersionChecker';
import useAuthCheck from 'hooks/useAuthCheck';
import useFeature from 'hooks/useFeature';
import useKeyTracker from 'hooks/useKeyTracker';
@@ -30,7 +29,7 @@ import useTelemetry from 'hooks/useTelemetry';
import { STORAGE_PATH, settings as themeSettings } from 'hooks/useTheme.settings';
import Omnibar from 'omnibar/Omnibar';
import appRoutes from 'routes';
-import { paths, serverAddress } from 'routes/utils';
+import { serverAddress } from 'routes/utils';
import authStore from 'stores/auth';
import clusterStore from 'stores/cluster';
import determinedStore from 'stores/determinedInfo';
@@ -90,43 +89,6 @@ const AppView: React.FC = () => {
);
useEffect(() => determinedStore.startPolling({ delay: 60_000 }), []);
- useEffect(() => {
- /*
- * Check to make sure the WebUI version matches the platform version.
- * Skip this check for development version.
- */
- Loadable.quickMatch(loadableInfo, undefined, undefined, (info) => {
- if (!process.env.IS_DEV && info.version !== process.env.VERSION) {
- const btn = (
-
- );
- const message = 'New WebUI Version';
- const description = (
-
- WebUI version v{info.version} is available. Check out what's new in
- our
-
- release notes
-
- .
-
- );
- setTimeout(() => {
- notification.warning({
- btn,
- description,
- duration: 0,
- key: 'version-mismatch',
- message,
- placement: 'bottomRight',
- });
- }, 10);
- }
- });
- }, [loadableInfo]);
-
// Detect telemetry settings changes and update telemetry library.
useEffect(() => {
Loadable.quickMatch(
@@ -171,16 +133,18 @@ const AppView: React.FC = () => {
<>
{isServerReachable ? (
+ {/* Global app components: */}
+
+
+
-
-
}>
diff --git a/webui/react/src/components/AuthToken.tsx b/webui/react/src/components/AuthToken.tsx
deleted file mode 100644
index ae0c19a24ae..00000000000
--- a/webui/react/src/components/AuthToken.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import Button from 'hew/Button';
-import Icon from 'hew/Icon';
-import Message from 'hew/Message';
-import { useToast } from 'hew/Toast';
-import React, { useCallback } from 'react';
-
-import { globalStorage } from 'globalStorage';
-import { copyToClipboard } from 'utils/dom';
-
-const AuthToken: React.FC = () => {
- const { openToast } = useToast();
- const token = globalStorage.authToken || 'Auth token not found.';
-
- const handleCopyToClipboard = useCallback(async () => {
- try {
- await copyToClipboard(token);
- openToast({
- description: 'Auth token copied to the clipboard.',
- title: 'Auth Token Copied',
- });
- } catch (e) {
- openToast({
- description: (e as Error)?.message,
- severity: 'Warning',
- title: 'Unable to Copy to Clipboard',
- });
- }
- }, [token, openToast]);
-
- return (
- }
- key="copy"
- type="primary"
- onClick={handleCopyToClipboard}>
- Copy token to clipboard
-
- }
- description={token}
- icon="checkmark"
- title="Your Determined Authentication Token"
- />
- );
-};
-
-export default AuthToken;
diff --git a/webui/react/src/components/JupyterLabButton.tsx b/webui/react/src/components/JupyterLabButton.tsx
index 6692a5ae466..2925732633e 100644
--- a/webui/react/src/components/JupyterLabButton.tsx
+++ b/webui/react/src/components/JupyterLabButton.tsx
@@ -22,7 +22,7 @@ const JupyterLabButton: React.FC = ({ enabled, workspace }: Props) => {
} = useSettings(shortCutSettingsConfig);
return (
-
+
{enabled ? (
<>
diff --git a/webui/react/src/components/VersionChecker.test.tsx b/webui/react/src/components/VersionChecker.test.tsx
new file mode 100644
index 00000000000..f1fe09163e3
--- /dev/null
+++ b/webui/react/src/components/VersionChecker.test.tsx
@@ -0,0 +1,81 @@
+import { render, waitFor } from '@testing-library/react';
+import UIProvider, { DefaultTheme } from 'hew/Theme';
+
+import VersionChecker from 'components/VersionChecker';
+
+import { ThemeProvider } from './ThemeProvider';
+
+const THEME_CLASS = 'ui-provider-test';
+const OLDER_VERSION = '1';
+const NEWER_VERSION = '2';
+
+const mockWarning = vi.hoisted(() => vi.fn());
+vi.mock('hew/Toast', () => ({
+ notification: {
+ warning: mockWarning,
+ },
+}));
+
+vi.mock('hew/Theme', async (importOriginal) => {
+ const useTheme = () => {
+ return {
+ themeSettings: {
+ className: THEME_CLASS,
+ },
+ };
+ };
+
+ return {
+ __esModule: true,
+ ...(await importOriginal()),
+ useTheme,
+ };
+});
+
+const setup = () => {
+ render(
+
+
+
+
+ ,
+ );
+};
+
+describe('VersionChecker', () => {
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ mockWarning.mockReset();
+ });
+
+ it('shows warning if version mismatch in production mode', async () => {
+ vi.stubEnv('IS_DEV', 'false');
+ vi.stubEnv('VERSION', OLDER_VERSION);
+ setup();
+ await waitFor(() => {
+ expect(mockWarning).toHaveBeenCalledWith(
+ expect.objectContaining({
+ className: THEME_CLASS,
+ duration: 0,
+ key: 'version-mismatch',
+ message: 'New WebUI Version',
+ placement: 'bottomRight',
+ }),
+ );
+ });
+ });
+
+ it('does not show warning in development mode', () => {
+ vi.stubEnv('IS_DEV', 'true');
+ vi.stubEnv('VERSION', OLDER_VERSION);
+ setup();
+ expect(mockWarning).not.toBeCalled();
+ });
+
+ it('does not show warning if version matches', () => {
+ vi.stubEnv('IS_DEV', 'false');
+ vi.stubEnv('VERSION', NEWER_VERSION);
+ setup();
+ expect(mockWarning).not.toBeCalled();
+ });
+});
diff --git a/webui/react/src/components/VersionChecker.tsx b/webui/react/src/components/VersionChecker.tsx
new file mode 100644
index 00000000000..833e8766ed1
--- /dev/null
+++ b/webui/react/src/components/VersionChecker.tsx
@@ -0,0 +1,62 @@
+import Button from 'hew/Button';
+import { useTheme } from 'hew/Theme';
+import { notification } from 'hew/Toast';
+import { useState } from 'react';
+
+import Link from 'components/Link';
+import { paths } from 'routes/utils';
+import { refreshPage } from 'utils/browser';
+import { isBoolean } from 'utils/data';
+
+interface Props {
+ version: string;
+}
+
+const VersionChecker: React.FC = ({ version }: Props) => {
+ const {
+ themeSettings: { className: themeClass },
+ } = useTheme();
+ const [closed, setClosed] = useState(false);
+ // process.env.IS_DEV must be string type for vi.stubEnv, otherwise is boolean:
+ const isDev = isBoolean(process.env.IS_DEV) ? process.env.IS_DEV : process.env.IS_DEV === 'true';
+
+ /*
+ * Check to make sure the WebUI version matches the platform version.
+ * Skip this check for development version.
+ */
+ if (!isDev && version !== process.env.VERSION) {
+ const btn = (
+
+ );
+ const message = 'New WebUI Version';
+ const description = (
+
+ WebUI version v{version} is available. Check out what's new in our
+
+ release notes
+
+ .
+
+ );
+ if (!closed) {
+ setTimeout(() => {
+ notification.warning({
+ btn,
+ className: themeClass,
+ description,
+ duration: 0,
+ key: 'version-mismatch',
+ message,
+ onClose: () => setClosed(true),
+ placement: 'bottomRight',
+ });
+ }, 0); // 0ms setTimeout needed to make sure UIProvider is available.
+ }
+ }
+
+ return null;
+};
+
+export default VersionChecker;
diff --git a/webui/react/src/pages/SignIn.tsx b/webui/react/src/pages/SignIn.tsx
index 1da1ee7d6a1..6fe5b103fcb 100644
--- a/webui/react/src/pages/SignIn.tsx
+++ b/webui/react/src/pages/SignIn.tsx
@@ -1,6 +1,8 @@
import Button from 'hew/Button';
+import CodeSample from 'hew/CodeSample';
import Divider from 'hew/Divider';
import Form from 'hew/Form';
+import { useTheme } from 'hew/Theme';
import { notification } from 'hew/Toast';
import { useObservable } from 'micro-observables';
import React, { useEffect, useMemo, useState } from 'react';
@@ -8,7 +10,6 @@ import { useLocation } from 'react-router-dom';
import LogoGoogle from 'assets/images/logo-sso-google-white.svg?url';
import LogoOkta from 'assets/images/logo-sso-okta-white.svg?url';
-import AuthToken from 'components/AuthToken';
import DeterminedAuth from 'components/DeterminedAuth';
import Logo from 'components/Logo';
import Page from 'components/Page';
@@ -41,7 +42,9 @@ const SignIn: React.FC = () => {
const info = useObservable(determinedStore.info);
const [canceler] = useState(new AbortController());
const { rbacEnabled } = useObservable(determinedStore.info);
-
+ const {
+ themeSettings: { className: themeClass },
+ } = useTheme();
const queries = useMemo(() => new URLSearchParams(location.search), [location.search]);
const ssoQueries = handleRelayState(queries);
@@ -74,9 +77,10 @@ const SignIn: React.FC = () => {
// Show auth token via notification if requested via query parameters.
if (queries.get('cli') === 'true')
notification.open({
- description: ,
+ className: themeClass,
+ description: ,
duration: 0,
- message: '',
+ message: 'Your Determined Authentication Token',
});
// Reroute the authenticated user to the app.
@@ -94,7 +98,7 @@ const SignIn: React.FC = () => {
} else if (isAuthChecked) {
uiActions.hideSpinner();
}
- }, [isAuthenticated, isAuthChecked, info, location, queries, uiActions, rbacEnabled]);
+ }, [isAuthenticated, isAuthChecked, info, location, queries, uiActions, rbacEnabled, themeClass]);
useEffect(() => {
uiActions.hideChrome();
diff --git a/webui/react/src/utils/browser.ts b/webui/react/src/utils/browser.ts
index c3bec391ecf..ffa27e579dc 100644
--- a/webui/react/src/utils/browser.ts
+++ b/webui/react/src/utils/browser.ts
@@ -3,7 +3,7 @@ import { V1TrialLogsResponse } from 'services/api-ts-sdk';
import { detApi } from 'services/apiConfig';
import { readStream } from 'services/utils';
import { BrandingType } from 'stores/determinedInfo';
-import { parseUrl, routeToExternalUrl } from 'utils/routes';
+import { routeToExternalUrl } from 'utils/routes';
/*
* In mobile view the definition of viewport height varies between
@@ -83,8 +83,11 @@ export const setCookie = (name: string, value: string): void => {
*/
export const refreshPage = (): void => {
const now = Date.now();
- const url = parseUrl(window.location.href);
- url.search = url.search ? `${url.search}&ts=${now}` : `ts=${now}`;
+ const url = new URL(window.location.href);
+ const params = url.searchParams;
+
+ params.set('ts', now.toString());
+
routeToExternalUrl(url.toString());
};