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()); };