Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: version upgrade notification bug [CM-411] #10069

Merged
merged 7 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: */}
johnkim-det marked this conversation as resolved.
Show resolved Hide resolved
<ClusterMessageBanner message={info.clusterMessage} />
<JupyterLabGlobal
enabled={
Loadable.isLoaded(loadableUser) &&
(workspace ? canCreateWorkspaceNSC({ workspace }) : canCreateNSC)
}
workspace={workspace ?? undefined}
/>
<Omnibar />
<VersionChecker version={info.version} />
johnkim-det marked this conversation as resolved.
Show resolved Hide resolved
<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) {
johnkim-det marked this conversation as resolved.
Show resolved Hide resolved
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
Loading