fix(ui): version link and fetch logic with 1 hour wait time (#21932)

* fix(ui): version link and fetch logic with 1 hour wait time

* fix tests

* fix test
This commit is contained in:
Chirag Madlani 2025-06-25 20:54:07 +05:30 committed by GitHub
parent 693292406d
commit 1e50fc1376
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 91 additions and 34 deletions

View File

@ -124,7 +124,6 @@
.slick-active { .slick-active {
button { button {
background-color: @primary-color; background-color: @primary-color;
width: inherit;
border-radius: 5px; border-radius: 5px;
} }
} }

View File

@ -12,7 +12,10 @@
*/ */
import { act, render, screen } from '@testing-library/react'; import { act, render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { ONE_HOUR_MS } from '../../constants/constants'; import {
LAST_VERSION_FETCH_TIME_KEY,
ONE_HOUR_MS,
} from '../../constants/constants';
import { HELP_ITEMS_ENUM } from '../../constants/Navbar.constants'; import { HELP_ITEMS_ENUM } from '../../constants/Navbar.constants';
import { getVersion } from '../../rest/miscAPI'; import { getVersion } from '../../rest/miscAPI';
import { getHelpDropdownItems } from '../../utils/NavbarUtils'; import { getHelpDropdownItems } from '../../utils/NavbarUtils';
@ -177,13 +180,13 @@ describe('Test NavBar Component', () => {
}); });
}); });
// --- Additional tests for fetchOMVersion one hour threshold --- // --- Tests for handleDocumentVisibilityChange one hour threshold ---
describe('fetchOMVersion one hour threshold', () => { describe('handleDocumentVisibilityChange one hour threshold', () => {
const OLD_DATE_NOW = Date.now; const OLD_DATE_NOW = Date.now;
beforeEach(() => { beforeEach(() => {
jest.resetModules(); jest.resetModules();
jest.clearAllMocks();
global.Date.now = jest.fn(); global.Date.now = jest.fn();
}); });
@ -191,7 +194,7 @@ describe('fetchOMVersion one hour threshold', () => {
global.Date.now = OLD_DATE_NOW; global.Date.now = OLD_DATE_NOW;
}); });
it('should NOT call getVersion if less than one hour since last fetch', async () => { it('should NOT call getVersion on window focus if less than one hour since last fetch', async () => {
const now = 2000000; const now = 2000000;
const lastFetch = now - (ONE_HOUR_MS - 1000); // less than 1 hour ago const lastFetch = now - (ONE_HOUR_MS - 1000); // less than 1 hour ago
mockGetItem.mockReturnValue(String(lastFetch)); mockGetItem.mockReturnValue(String(lastFetch));
@ -200,26 +203,63 @@ describe('fetchOMVersion one hour threshold', () => {
render(<NavBarComponent />); render(<NavBarComponent />);
await screen.findByTestId('global-search-bar'); await screen.findByTestId('global-search-bar');
// Clear the initial getVersion call from mount
jest.clearAllMocks();
// Simulate window focus event
await act(async () => {
window.dispatchEvent(new Event('focus'));
});
expect(getVersion).not.toHaveBeenCalled(); expect(getVersion).not.toHaveBeenCalled();
}); });
it('should call getVersion and setItem if more than one hour since last fetch', async () => { it('should call getVersion and setItem on window focus if more than one hour since last fetch', async () => {
const now = 3000000; const now = 3000000;
const lastFetch = now - (ONE_HOUR_MS + 1000); // more than 1 hour ago const lastFetch = now - (ONE_HOUR_MS + 1000); // more than 1 hour ago
mockGetItem.mockReturnValue(String(lastFetch)); mockGetItem.mockReturnValue(String(lastFetch));
(global.Date.now as jest.Mock).mockReturnValue(now); (global.Date.now as jest.Mock).mockReturnValue(now);
render(<NavBarComponent />); render(<NavBarComponent />);
await Promise.resolve(); await screen.findByTestId('global-search-bar');
// Clear the initial getVersion call from mount
jest.clearAllMocks();
// Simulate window focus event
await act(async () => { await act(async () => {
expect(getVersion).toHaveBeenCalled(); window.dispatchEvent(new Event('focus'));
}); });
expect(getVersion).toHaveBeenCalled();
expect(mockSetItem).toHaveBeenCalledWith( expect(mockSetItem).toHaveBeenCalledWith(
'versionFetchTime', LAST_VERSION_FETCH_TIME_KEY,
'3000000', '3000000',
expect.objectContaining({ expires: expect.any(Date) }) expect.objectContaining({ expires: expect.any(Date) })
); );
}); });
it('should call getVersion on window focus if no previous fetch time exists', async () => {
const now = 4000000;
mockGetItem.mockReturnValue(null); // No previous fetch time
(global.Date.now as jest.Mock).mockReturnValue(now);
render(<NavBarComponent />);
await screen.findByTestId('global-search-bar');
// Clear the initial getVersion call from mount
jest.clearAllMocks();
// Simulate window focus event
await act(async () => {
window.dispatchEvent(new Event('focus'));
});
expect(getVersion).toHaveBeenCalled();
expect(mockSetItem).toHaveBeenCalledWith(
LAST_VERSION_FETCH_TIME_KEY,
'4000000',
expect.objectContaining({ expires: expect.any(Date) })
);
});
}); });

View File

@ -45,10 +45,10 @@ import { ReactComponent as SidebarCollapsedIcon } from '../../assets/svg/ic-side
import { ReactComponent as SidebarExpandedIcon } from '../../assets/svg/ic-sidebar-expanded.svg'; import { ReactComponent as SidebarExpandedIcon } from '../../assets/svg/ic-sidebar-expanded.svg';
import { import {
DEFAULT_DOMAIN_VALUE, DEFAULT_DOMAIN_VALUE,
LAST_VERSION_FETCH_TIME_KEY,
NOTIFICATION_READ_TIMER, NOTIFICATION_READ_TIMER,
ONE_HOUR_MS, ONE_HOUR_MS,
SOCKET_EVENTS, SOCKET_EVENTS,
VERSION_FETCH_TIME_KEY,
} from '../../constants/constants'; } from '../../constants/constants';
import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants'; import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants';
import { HELP_ITEMS_ENUM } from '../../constants/Navbar.constants'; import { HELP_ITEMS_ENUM } from '../../constants/Navbar.constants';
@ -130,22 +130,16 @@ const NavBar = () => {
} = useCurrentUserPreferences(); } = useCurrentUserPreferences();
const fetchOMVersion = async () => { const fetchOMVersion = async () => {
// If version fetch happens within an hour, skip fetching
const lastFetchTime = cookieStorage.getItem(VERSION_FETCH_TIME_KEY);
const now = Date.now();
if (lastFetchTime && now - Number(lastFetchTime) < ONE_HOUR_MS) {
// Less than an hour since last fetch, skip fetching
return;
}
try { try {
const res = await getVersion(); const res = await getVersion();
setVersion(res.version);
// Set/update the cookie with current time, expires in 1 hour const now = Date.now();
cookieStorage.setItem(VERSION_FETCH_TIME_KEY, String(now), { // Update the cache timestamp
expires: new Date(now + ONE_HOUR_MS), cookieStorage.setItem(LAST_VERSION_FETCH_TIME_KEY, String(now), {
expires: new Date(Date.now() + ONE_HOUR_MS),
}); });
setVersion(res.version);
} catch (err) { } catch (err) {
showErrorToast( showErrorToast(
err as AxiosError, err as AxiosError,
@ -325,7 +319,27 @@ const NavBar = () => {
) { ) {
return; return;
} }
// Check if we need to fetch based on cache timing
// This is to block the API call for 1 hour
const lastFetchTime = cookieStorage.getItem(LAST_VERSION_FETCH_TIME_KEY);
const now = Date.now();
if (lastFetchTime) {
const timeSinceLastFetch = now - parseInt(lastFetchTime);
if (timeSinceLastFetch < ONE_HOUR_MS) {
// Less than 1 hour since last fetch, skip API call
return;
}
}
const newVersion = await getVersion(); const newVersion = await getVersion();
// Update the cache timestamp
cookieStorage.setItem(LAST_VERSION_FETCH_TIME_KEY, String(now), {
expires: new Date(Date.now() + ONE_HOUR_MS),
});
// Compare version only if version is set previously to have fair comparison // Compare version only if version is set previously to have fair comparison
if (version && version !== newVersion.version) { if (version && version !== newVersion.version) {
setShowVersionMissMatchAlert(true); setShowVersionMissMatchAlert(true);

View File

@ -21,7 +21,7 @@ import documentationLinksClassBase from '../utils/DocumentationLinksClassBase';
import i18n from '../utils/i18next/LocalUtil'; import i18n from '../utils/i18next/LocalUtil';
import { ROUTES } from './constants'; import { ROUTES } from './constants';
import { URL_GITHUB_REPO, URL_JOIN_SLACK } from './URL.constants'; import { URL_JOIN_SLACK, URL_OM_RELEASE_UPDATES } from './URL.constants';
export enum HELP_ITEMS_ENUM { export enum HELP_ITEMS_ENUM {
TOUR = 'tour', TOUR = 'tour',
@ -80,7 +80,7 @@ export const HELP_ITEMS = [
key: HELP_ITEMS_ENUM.VERSION, key: HELP_ITEMS_ENUM.VERSION,
label: i18n.t('label.version'), label: i18n.t('label.version'),
icon: IconVersionBlack, icon: IconVersionBlack,
link: URL_GITHUB_REPO, link: URL_OM_RELEASE_UPDATES,
isExternal: true, isExternal: true,
}, },
]; ];

View File

@ -14,3 +14,6 @@
export const URL_JOIN_SLACK = 'https://slack.open-metadata.org'; export const URL_JOIN_SLACK = 'https://slack.open-metadata.org';
export const URL_OPEN_METADATA_DOCS = 'https://docs.open-metadata.org/'; export const URL_OPEN_METADATA_DOCS = 'https://docs.open-metadata.org/';
export const URL_GITHUB_REPO = 'https://github.com/open-metadata/OpenMetadata'; export const URL_GITHUB_REPO = 'https://github.com/open-metadata/OpenMetadata';
export const URL_OM_RELEASE_UPDATES =
'https://open-metadata.org/product-updates#{{currentVersion}}';

View File

@ -65,6 +65,8 @@ export const MAX_CHAR_LIMIT_ENTITY_SUMMARY = 130;
export const TEST_CASE_FEED_GRAPH_HEIGHT = 250; export const TEST_CASE_FEED_GRAPH_HEIGHT = 250;
export const ONE_MINUTE_IN_MILLISECOND = 60000; export const ONE_MINUTE_IN_MILLISECOND = 60000;
export const TWO_MINUTE_IN_MILLISECOND = 120000; export const TWO_MINUTE_IN_MILLISECOND = 120000;
export const ONE_HOUR_MS = 3600000; // 1 hour in milliseconds
export const LAST_VERSION_FETCH_TIME_KEY = 'versionFetchTime';
export const LOCALSTORAGE_RECENTLY_VIEWED = `recentlyViewedData_${COOKIE_VERSION}`; export const LOCALSTORAGE_RECENTLY_VIEWED = `recentlyViewedData_${COOKIE_VERSION}`;
export const LOCALSTORAGE_RECENTLY_SEARCHED = `recentlySearchedData_${COOKIE_VERSION}`; export const LOCALSTORAGE_RECENTLY_SEARCHED = `recentlySearchedData_${COOKIE_VERSION}`;
export const REDIRECT_PATHNAME = 'redirectUrlPath'; export const REDIRECT_PATHNAME = 'redirectUrlPath';
@ -432,6 +434,3 @@ export const MAX_VISIBLE_OWNERS_FOR_FEED_TAB = 4;
export const MAX_VISIBLE_OWNERS_FOR_FEED_CARD = 2; export const MAX_VISIBLE_OWNERS_FOR_FEED_CARD = 2;
export const BREADCRUMB_SEPARATOR = '/'; export const BREADCRUMB_SEPARATOR = '/';
export const VERSION_FETCH_TIME_KEY = 'versionFetchTime';
export const ONE_HOUR_MS = 60 * 60 * 1000;

View File

@ -13,8 +13,8 @@
import { ROUTES } from '../constants/constants'; import { ROUTES } from '../constants/constants';
import { import {
URL_GITHUB_REPO,
URL_JOIN_SLACK, URL_JOIN_SLACK,
URL_OM_RELEASE_UPDATES,
URL_OPEN_METADATA_DOCS, URL_OPEN_METADATA_DOCS,
} from '../constants/URL.constants'; } from '../constants/URL.constants';
import navbarUtilClassBase from './NavbarUtilClassBase'; import navbarUtilClassBase from './NavbarUtilClassBase';
@ -28,7 +28,7 @@ describe('NavbarUtilClassBase', () => {
expect(stringifyResult).toContain(URL_OPEN_METADATA_DOCS); expect(stringifyResult).toContain(URL_OPEN_METADATA_DOCS);
expect(stringifyResult).toContain(ROUTES.SWAGGER); expect(stringifyResult).toContain(ROUTES.SWAGGER);
expect(stringifyResult).toContain(URL_JOIN_SLACK); expect(stringifyResult).toContain(URL_JOIN_SLACK);
expect(stringifyResult).toContain(URL_GITHUB_REPO); expect(stringifyResult).toContain(URL_OM_RELEASE_UPDATES);
expect(stringifyResult).toContain('label.tour'); expect(stringifyResult).toContain('label.tour');
expect(stringifyResult).toContain('label.doc-plural'); expect(stringifyResult).toContain('label.doc-plural');
expect(stringifyResult).toContain('label.api-uppercase'); expect(stringifyResult).toContain('label.api-uppercase');

View File

@ -13,7 +13,7 @@
import { ROUTES } from '../constants/constants'; import { ROUTES } from '../constants/constants';
import { HELP_ITEMS_ENUM } from '../constants/Navbar.constants'; import { HELP_ITEMS_ENUM } from '../constants/Navbar.constants';
import { URL_GITHUB_REPO } from '../constants/URL.constants'; import { URL_OM_RELEASE_UPDATES } from '../constants/URL.constants';
import { getHelpDropdownItems } from './NavbarUtils'; import { getHelpDropdownItems } from './NavbarUtils';
describe('NavbarUtils test', () => { describe('NavbarUtils test', () => {
@ -30,7 +30,9 @@ describe('NavbarUtils test', () => {
// Test external link // Test external link
const externalLink = helpDropdownItems[5].label.props.href; const externalLink = helpDropdownItems[5].label.props.href;
expect(externalLink).toBe(URL_GITHUB_REPO); expect(externalLink).toBe(
URL_OM_RELEASE_UPDATES.replace('{{currentVersion}}', version)
);
expect(helpDropdownItems[5].label.props.target).toBe('_blank'); expect(helpDropdownItems[5].label.props.target).toBe('_blank');
expect(helpDropdownItems[5].label.props.rel).toBe('noreferrer'); expect(helpDropdownItems[5].label.props.rel).toBe('noreferrer');

View File

@ -56,7 +56,7 @@ const getHelpDropdownLabel = (item: SupportItem, version?: string) => {
return ( return (
<a <a
className="no-underline" className="no-underline"
href={item.link} href={item.link?.replace('{{currentVersion}}', version ?? '')}
rel="noreferrer" rel="noreferrer"
target="_blank"> target="_blank">
{getHelpDropdownLabelContentRenderer(item, version)} {getHelpDropdownLabelContentRenderer(item, version)}