Skip to content

Commit

Permalink
fix: version upgrade notification bug [CM-411] (#10069)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnkim-det authored Oct 22, 2024
1 parent 935fa66 commit 22ad457
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 105 deletions.
60 changes: 12 additions & 48 deletions webui/react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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 = (
<Button type="primary" onClick={refreshPage}>
Update Now
</Button>
);
const message = 'New WebUI Version';
const description = (
<div>
WebUI version <b>v{info.version}</b> is available. Check out what&apos;s new in
our&nbsp;
<Link external path={paths.docs('/release-notes.html')}>
release notes
</Link>
.
</div>
);
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(
Expand Down Expand Up @@ -171,16 +133,18 @@ const AppView: React.FC = () => {
<>
{isServerReachable ? (
<ConfirmationProvider>
{/* Global app components: */}
<ClusterMessageBanner message={info.clusterMessage} />
<JupyterLabGlobal
enabled={
Loadable.isLoaded(loadableUser) &&
(workspace ? canCreateWorkspaceNSC({ workspace }) : canCreateNSC)
}
workspace={workspace ?? undefined}
/>
<Omnibar />
<VersionChecker version={info.version} />
<Navigation isClusterMessagePresent={!!info.clusterMessage}>
<JupyterLabGlobal
enabled={
Loadable.isLoaded(loadableUser) &&
(workspace ? canCreateWorkspaceNSC({ workspace }) : canCreateNSC)
}
workspace={workspace ?? undefined}
/>
<Omnibar />
<main>
<Suspense fallback={<Spinner center spinning />}>
<Router routes={appRoutes} />
Expand Down
48 changes: 0 additions & 48 deletions webui/react/src/components/AuthToken.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion webui/react/src/components/JupyterLabButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const JupyterLabButton: React.FC<Props> = ({ enabled, workspace }: Props) => {
} = useSettings<ShortcutSettings>(shortCutSettingsConfig);

return (
<div data-testId="jupyter-lab-button">
<div data-testid="jupyter-lab-button">
{enabled ? (
<>
<Tooltip content={shortcutToString(jupyterLabShortcut)}>
Expand Down
81 changes: 81 additions & 0 deletions webui/react/src/components/VersionChecker.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof import('hew/Theme')>()),
useTheme,
};
});

const setup = () => {
render(
<UIProvider theme={DefaultTheme.Light}>
<ThemeProvider>
<VersionChecker version={NEWER_VERSION} />
</ThemeProvider>
</UIProvider>,
);
};

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();
});
});
62 changes: 62 additions & 0 deletions webui/react/src/components/VersionChecker.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ 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 = (
<Button type="primary" onClick={refreshPage}>
Update Now
</Button>
);
const message = 'New WebUI Version';
const description = (
<div>
WebUI version <b>v{version}</b> is available. Check out what&apos;s new in our&nbsp;
<Link external path={paths.docs('/release-notes.html')}>
release notes
</Link>
.
</div>
);
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;
14 changes: 9 additions & 5 deletions webui/react/src/pages/SignIn.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';
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';
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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: <AuthToken />,
className: themeClass,
description: <CodeSample text={globalStorage.authToken || 'Auth token not found.'} />,
duration: 0,
message: '',
message: 'Your Determined Authentication Token',
});

// Reroute the authenticated user to the app.
Expand All @@ -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();
Expand Down
9 changes: 6 additions & 3 deletions webui/react/src/utils/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
};

Expand Down

0 comments on commit 22ad457

Please sign in to comment.