From 2585a7b8d18564d1e08b63c2aa0a3913efd6e655 Mon Sep 17 00:00:00 2001 From: Viktor Vorona Date: Tue, 9 Apr 2024 12:25:51 +0200 Subject: [PATCH] SONAR-22017 Update Notification banner - no network connection case --- server/sonar-web/package.json | 2 +- server/sonar-web/src/main/js/api/system.ts | 6 +- .../__tests__/GlobalFooter-test.tsx | 4 +- .../__tests__/StartupModal-test.tsx | 5 +- .../__tests__/UpdateNotification-it.tsx | 57 +++++++++++++++---- .../components/app-state/AppStateContext.tsx | 2 +- .../UpdateNotification.tsx | 29 ++++++---- .../__tests__/ProjectCardMeasures-test.tsx | 1 + .../js/components/shared/AppVersionStatus.tsx | 8 +-- .../upgrade/SystemUpgradeButton.tsx | 6 +- .../components/upgrade/SystemUpgradeForm.tsx | 10 +++- .../sonar-web/src/main/js/helpers/system.ts | 4 +- .../src/main/js/helpers/testMocks.ts | 3 +- .../main/js/helpers/testReactTestingUtils.tsx | 8 ++- .../sonar-web/src/main/js/types/appstate.ts | 2 +- 15 files changed, 105 insertions(+), 42 deletions(-) diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json index 05e90164e98..024a231aff9 100644 --- a/server/sonar-web/package.json +++ b/server/sonar-web/package.json @@ -121,7 +121,7 @@ "start:force": "node scripts/start.js --force-build-design-system", "build": "node scripts/build.js", "build-release": "yarn install --immutable && node scripts/build.js release", - "test": "NODE_OPTIONS=--max-old-space-size=1024 jest", + "test": "jest --workerIdleMemoryLimit=1G", "test-ci": "NODE_OPTIONS=\"-r dd-trace/ci/init\" jest --coverage --maxWorkers=5 --workerIdleMemoryLimit=2G --ci", "test-eslint-local-rules": "jest -c eslint-local-rules/jest.config.js", "format": "prettier --write --list-different \"src/main/js/**/*.{js,ts,tsx,css}\"", diff --git a/server/sonar-web/src/main/js/api/system.ts b/server/sonar-web/src/main/js/api/system.ts index 21202786a95..50bc95278d6 100644 --- a/server/sonar-web/src/main/js/api/system.ts +++ b/server/sonar-web/src/main/js/api/system.ts @@ -36,9 +36,9 @@ export function getSystemStatus(): Promise<{ id: string; version: string; status export function getSystemUpgrades(): Promise<{ upgrades: SystemUpgrade[]; - latestLTA: string; - installedVersionActive: boolean; - updateCenterRefresh: string; + latestLTA?: string; + installedVersionActive?: boolean; + updateCenterRefresh?: string; }> { return getJSON('/api/system/upgrades'); } diff --git a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx index cc174d6ee81..ce965fcd3cd 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx @@ -59,7 +59,7 @@ it('should show active status if offline and did not reach EOL', async () => { systemMock.setSystemUpgrades({ installedVersionActive: undefined }); renderGlobalFooter( {}, - { version: '4.2 (build 12345)', installedVersionEOL: addDays(new Date(), 10).toISOString() }, + { version: '4.2 (build 12345)', versionEOL: addDays(new Date(), 10).toISOString() }, ); expect(await ui.ltaDocumentationLinkActive.find()).toBeInTheDocument(); @@ -69,7 +69,7 @@ it('should show inactive status if offline and reached EOL', async () => { systemMock.setSystemUpgrades({ installedVersionActive: undefined }); renderGlobalFooter( {}, - { version: '4.2 (build 12345)', installedVersionEOL: subDays(new Date(), 10).toISOString() }, + { version: '4.2 (build 12345)', versionEOL: subDays(new Date(), 10).toISOString() }, ); expect(await ui.ltaDocumentationLinkInactive.find()).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx index 99f1d7f37ec..835c9c51211 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx @@ -42,7 +42,10 @@ jest.mock('../../../helpers/dates', () => ({ toShortISO8601String: jest.fn().mockReturnValue('short-not-iso-date'), })); -jest.mock('date-fns', () => ({ differenceInDays: jest.fn().mockReturnValue(1) })); +jest.mock('date-fns', () => ({ + ...jest.requireActual('date-fns'), + differenceInDays: jest.fn().mockReturnValue(1), +})); const LOGGED_IN_USER: LoggedInUser = { groups: [], diff --git a/server/sonar-web/src/main/js/app/components/__tests__/UpdateNotification-it.tsx b/server/sonar-web/src/main/js/app/components/__tests__/UpdateNotification-it.tsx index 1cf63a94a3e..91e350399c9 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/UpdateNotification-it.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/UpdateNotification-it.tsx @@ -18,12 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import userEvent from '@testing-library/user-event'; +import { addDays, formatISO, subDays } from 'date-fns'; import * as React from 'react'; import { getSystemUpgrades } from '../../../api/system'; import { UpdateUseCase } from '../../../components/upgrade/utils'; import { mockAppState, mockCurrentUser } from '../../../helpers/testMocks'; import { renderComponent } from '../../../helpers/testReactTestingUtils'; import { byRole } from '../../../helpers/testSelector'; +import { AppState } from '../../../types/appstate'; import { Permissions } from '../../../types/permissions'; import { CurrentUser } from '../../../types/users'; import { AppStateContext } from '../app-state/AppStateContext'; @@ -72,6 +74,40 @@ it('should not render update notification if no upgrades', () => { expect(ui.updateMessage.query()).not.toBeInTheDocument(); }); +it('should show error message if upgrades call failed and the version has reached eol', async () => { + jest.mocked(getSystemUpgrades).mockReturnValue(Promise.reject(new Error('error'))); + renderUpdateNotification(undefined, undefined, { + versionEOL: formatISO(subDays(new Date(), 1), { representation: 'date' }), + }); + expect(await ui.updateMessage.find()).toHaveTextContent( + `admin_notification.update.${UpdateUseCase.CurrentVersionInactive}`, + ); + expect(ui.openDialogBtn.query()).not.toBeInTheDocument(); +}); + +it('should not show the notification banner if there is no network connection and version has not reached the eol', () => { + jest.mocked(getSystemUpgrades).mockResolvedValue({ + upgrades: [], + }); + renderUpdateNotification(undefined, undefined, { + versionEOL: formatISO(addDays(new Date(), 1), { representation: 'date' }), + }); + expect(ui.updateMessage.query()).not.toBeInTheDocument(); +}); + +it('should show the error banner if there is no network connection and version has reached the eol', async () => { + jest.mocked(getSystemUpgrades).mockResolvedValue({ + upgrades: [], + }); + renderUpdateNotification(undefined, undefined, { + versionEOL: formatISO(subDays(new Date(), 1), { representation: 'date' }), + }); + expect(await ui.updateMessage.find()).toHaveTextContent( + `admin_notification.update.${UpdateUseCase.CurrentVersionInactive}`, + ); + expect(ui.openDialogBtn.query()).not.toBeInTheDocument(); +}); + it('active / latest / patch', async () => { jest.mocked(getSystemUpgrades).mockResolvedValue({ upgrades: [{ downloadUrl: '', version: '10.5.1' }], @@ -224,7 +260,7 @@ it('active / lta / patch', async () => { installedVersionActive: true, }); const user = userEvent.setup(); - renderUpdateNotification(undefined, undefined, '9.9.0'); + renderUpdateNotification(undefined, undefined, { version: '9.9.0' }); expect(await ui.updateMessage.find()).toHaveTextContent( `admin_notification.update.${UpdateUseCase.NewPatch}`, ); @@ -245,7 +281,7 @@ it('active / lta / new minor', async () => { installedVersionActive: true, }); const user = userEvent.setup(); - renderUpdateNotification(undefined, undefined, '9.9.0'); + renderUpdateNotification(undefined, undefined, { version: '9.9.0' }); expect(await ui.updateMessage.find()).toHaveTextContent( `admin_notification.update.${UpdateUseCase.NewVersion}`, ); @@ -269,7 +305,7 @@ it('active / lta / new minor + patch', async () => { installedVersionActive: true, }); const user = userEvent.setup(); - renderUpdateNotification(undefined, undefined, '9.9.0'); + renderUpdateNotification(undefined, undefined, { version: '9.9.0' }); expect(await ui.updateMessage.find()).toHaveTextContent( `admin_notification.update.${UpdateUseCase.NewPatch}`, ); @@ -295,7 +331,7 @@ it('active / prev lta / new lta + patch', async () => { installedVersionActive: true, }); const user = userEvent.setup(); - renderUpdateNotification(undefined, undefined, '8.9.0'); + renderUpdateNotification(undefined, undefined, { version: '8.9.0' }); expect(await ui.updateMessage.find()).toHaveTextContent( `admin_notification.update.${UpdateUseCase.NewPatch}`, ); @@ -321,7 +357,7 @@ it('active / prev lta / new lta + new minor + patch', async () => { installedVersionActive: true, }); const user = userEvent.setup(); - renderUpdateNotification(undefined, undefined, '8.9.0'); + renderUpdateNotification(undefined, undefined, { version: '8.9.0' }); expect(await ui.updateMessage.find()).toHaveTextContent( `admin_notification.update.${UpdateUseCase.NewPatch}`, ); @@ -344,7 +380,7 @@ it('no longer active / prev lta / new lta', async () => { installedVersionActive: false, }); const user = userEvent.setup(); - renderUpdateNotification(undefined, undefined, '8.9.0'); + renderUpdateNotification(undefined, undefined, { version: '8.9.0' }); expect(await ui.updateMessage.find()).toHaveTextContent( `admin_notification.update.${UpdateUseCase.CurrentVersionInactive}`, ); @@ -366,7 +402,7 @@ it('no longer active / prev lta / new lta + patch', async () => { installedVersionActive: false, }); const user = userEvent.setup(); - renderUpdateNotification(undefined, undefined, '8.9.0'); + renderUpdateNotification(undefined, undefined, { version: '8.9.0' }); expect(await ui.updateMessage.find()).toHaveTextContent( `admin_notification.update.${UpdateUseCase.CurrentVersionInactive}`, ); @@ -394,7 +430,7 @@ it('no longer active / prev lta / new lta + patch + new minors', async () => { installedVersionActive: false, }); const user = userEvent.setup(); - renderUpdateNotification(undefined, undefined, '8.9.0'); + renderUpdateNotification(undefined, undefined, { version: '8.9.0' }); expect(await ui.updateMessage.find()).toHaveTextContent( `admin_notification.update.${UpdateUseCase.CurrentVersionInactive}`, ); @@ -424,7 +460,8 @@ it('no longer active / prev lta / new lta + patch + new minors', async () => { function renderUpdateNotification( dissmissable: boolean = false, user?: Partial, - version: string = '10.5.0', + // versionEOL is a date in the past to be sure that it is not used when we have data from upgrades endpoint + appState: Partial = { version: '10.5.0', versionEOL: '2020-01-01' }, ) { return renderComponent( {}, }} > - + , diff --git a/server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx b/server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx index bfd1538af22..4492d1c18d6 100644 --- a/server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx +++ b/server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx @@ -21,7 +21,6 @@ import * as React from 'react'; import { AppState } from '../../../types/appstate'; export const DEFAULT_APP_STATE = { - installedVersionEOL: '', authenticationError: false, authorizationError: false, edition: undefined, @@ -29,6 +28,7 @@ export const DEFAULT_APP_STATE = { qualifiers: [], settings: {}, version: '', + versionEOL: '', documentationUrl: 'https://docs.sonarsource.com/sonarqube/latest', }; export const AppStateContext = React.createContext(DEFAULT_APP_STATE); diff --git a/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx b/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx index 5a8bb6a20f7..8e6109a8c3e 100644 --- a/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx +++ b/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx @@ -24,6 +24,7 @@ import DismissableAlert from '../../../components/ui/DismissableAlert'; import SystemUpgradeButton from '../../../components/upgrade/SystemUpgradeButton'; import { UpdateUseCase } from '../../../components/upgrade/utils'; import { translate } from '../../../helpers/l10n'; +import { isCurrentVersionEOLActive } from '../../../helpers/system'; import { hasGlobalPermission } from '../../../helpers/users'; import { useSystemUpgrades } from '../../../queries/system'; import { Permissions } from '../../../types/permissions'; @@ -46,20 +47,25 @@ export default function UpdateNotification({ dismissable }: Readonly) { isLoggedIn(currentUser) && hasGlobalPermission(currentUser, Permissions.Admin); const regExpParsedVersion = VERSION_PARSER.exec(appState.version); - const { data } = useSystemUpgrades({ + const { data, isLoading } = useSystemUpgrades({ enabled: canUserSeeNotification && regExpParsedVersion !== null, }); - if ( - !canUserSeeNotification || - regExpParsedVersion === null || - data === undefined || - isEmpty(data.upgrades) - ) { + if (!canUserSeeNotification || regExpParsedVersion === null || isLoading) { return null; } - const { upgrades, installedVersionActive, latestLTA } = data; + const { upgrades = [], installedVersionActive, latestLTA } = data ?? {}; + + let active = installedVersionActive; + + if (installedVersionActive === undefined) { + active = isCurrentVersionEOLActive(appState.versionEOL); + } + + if (active && isEmpty(upgrades)) { + return null; + } const parsedVersion = regExpParsedVersion .slice(1) @@ -80,11 +86,12 @@ export default function UpdateNotification({ dismissable }: Readonly) { let useCase = UpdateUseCase.NewVersion; - if (!installedVersionActive) { + if (!active) { useCase = UpdateUseCase.CurrentVersionInactive; } else if ( isPatchUpdate(parsedVersion, systemUpgrades) && - (isCurrentVersionLTA(parsedVersion, latestLTA) || !isMinorUpdate(parsedVersion, systemUpgrades)) + ((latestLTA !== undefined && isCurrentVersionLTA(parsedVersion, latestLTA)) || + !isMinorUpdate(parsedVersion, systemUpgrades)) ) { useCase = UpdateUseCase.NewPatch; } @@ -95,7 +102,7 @@ export default function UpdateNotification({ dismissable }: Readonly) { new Date(upgrade1.releaseDate ?? '').getTime(), )[0]; - const dismissKey = useCase + latest.version; + const dismissKey = useCase + (latest?.version ?? appState.version); return dismissable ? ( ({ + ...jest.requireActual('date-fns'), differenceInMilliseconds: () => 1000 * 60 * 60 * 24 * 30 * 8, // ~ 8 months })); diff --git a/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx b/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx index 7fe3277d0e0..49d8f811c41 100644 --- a/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx +++ b/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx @@ -29,15 +29,15 @@ import { useSystemUpgrades } from '../../queries/system'; export default function AppVersionStatus() { const { data } = useSystemUpgrades(); - const { version, installedVersionEOL } = useAppState(); + const { version, versionEOL } = useAppState(); const isActiveVersion = useMemo(() => { if (data?.installedVersionActive !== undefined) { return data.installedVersionActive; } - return isCurrentVersionEOLActive(installedVersionEOL); - }, [data?.installedVersionActive, installedVersionEOL]); + return isCurrentVersionEOLActive(versionEOL); + }, [data?.installedVersionActive, versionEOL]); const docUrl = useDocUrl(); const intl = useIntl(); @@ -57,6 +57,6 @@ export default function AppVersionStatus() { /> ), - } + }, ); } diff --git a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx index 9607fa5e7bb..7744d356114 100644 --- a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx @@ -25,7 +25,7 @@ import SystemUpgradeForm from './SystemUpgradeForm'; import { groupUpgrades, sortUpgrades, UpdateUseCase } from './utils'; interface Props { - latestLTA: string; + latestLTA?: string; systemUpgrades: SystemUpgrade[]; updateUseCase: UpdateUseCase; } @@ -43,6 +43,10 @@ export default function SystemUpgradeButton(props: Readonly) { setSystemUpgradeFormOpen(false); }, [setSystemUpgradeFormOpen]); + if (systemUpgrades.length === 0) { + return null; + } + return ( <> diff --git a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx index c9772ff86fa..c10de327d07 100644 --- a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx @@ -30,7 +30,7 @@ import { SYSTEM_VERSION_REGEXP, UpdateUseCase } from './utils'; interface Props { onClose: () => void; systemUpgrades: SystemUpgrade[][]; - latestLTA: string; + latestLTA?: string; updateUseCase: UpdateUseCase; } @@ -65,7 +65,9 @@ export default function SystemUpgradeForm(props: Readonly) { for (const upgrades of systemUpgrades) { if (untilLTA === false) { systemUpgradesWithPatch.push(upgrades); - untilLTA = upgrades.some((upgrade) => upgrade.version.startsWith(latestLTA)); + untilLTA = upgrades.some( + (upgrade) => latestLTA !== undefined && upgrade.version.startsWith(latestLTA), + ); } } } @@ -87,7 +89,9 @@ export default function SystemUpgradeForm(props: Readonly) { key={upgrades[upgrades.length - 1].version} systemUpgrades={upgrades} isPatch={upgrades === patches} - isLTAVersion={upgrades.some((upgrade) => upgrade.version.startsWith(latestLTA))} + isLTAVersion={upgrades.some( + (upgrade) => latestLTA !== undefined && upgrade.version.startsWith(latestLTA), + )} /> ))} diff --git a/server/sonar-web/src/main/js/helpers/system.ts b/server/sonar-web/src/main/js/helpers/system.ts index 1503850fe67..8319ff4d41e 100644 --- a/server/sonar-web/src/main/js/helpers/system.ts +++ b/server/sonar-web/src/main/js/helpers/system.ts @@ -56,6 +56,6 @@ export function initAppVariables() { getEnhancedWindow().official = Boolean(appVariablesDiv.dataset.official); } -export function isCurrentVersionEOLActive(installedVersionEOL: string) { - return isAfter(parseDate(installedVersionEOL), new Date()); +export function isCurrentVersionEOLActive(versionEOL: string) { + return isAfter(parseDate(versionEOL), new Date()); } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 1ea0bc60e69..7cb81dfb4b7 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { addMonths, formatISO } from 'date-fns'; import { omit } from 'lodash'; import { To } from 'react-router-dom'; import { CompareResponse } from '../api/quality-profiles'; @@ -94,8 +95,8 @@ export function mockAppState(overrides: Partial = {}): AppState { qualifiers: [ComponentQualifier.Project], settings: {}, version: '1.0', + versionEOL: formatISO(addMonths(new Date(), 1), { representation: 'date' }), documentationUrl: 'https://docs.sonarsource.com/sonarqube/10.0', - installedVersionEOL: '2024-01-01T00:00:00Z', ...overrides, }; } diff --git a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx index 750d09ffc76..b8e7bcf983d 100644 --- a/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx +++ b/server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx @@ -111,7 +111,13 @@ export function renderComponent( }: RenderContext = {}, ) { function Wrapper({ children }: { children: React.ReactElement }) { - const queryClient = new QueryClient(); + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); return ( diff --git a/server/sonar-web/src/main/js/types/appstate.ts b/server/sonar-web/src/main/js/types/appstate.ts index f9bac3f35fd..308a01fa688 100644 --- a/server/sonar-web/src/main/js/types/appstate.ts +++ b/server/sonar-web/src/main/js/types/appstate.ts @@ -28,13 +28,13 @@ export interface AppState { edition?: EditionKey; globalPages?: Extension[]; instanceUsesDefaultAdminCredentials?: boolean; - installedVersionEOL: string; needIssueSync?: boolean; productionDatabase: boolean; qualifiers: string[]; settings: { [key in GlobalSettingKeys]?: string }; standalone?: boolean; version: string; + versionEOL: string; webAnalyticsJsPath?: string; documentationUrl: string; } -- 2.39.5