]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22017 Update Notification banner - no network connection case
authorViktor Vorona <viktor.vorona@sonarsource.com>
Tue, 9 Apr 2024 10:25:51 +0000 (12:25 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 10 Apr 2024 20:02:56 +0000 (20:02 +0000)
15 files changed:
server/sonar-web/package.json
server/sonar-web/src/main/js/api/system.ts
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/StartupModal-test.tsx
server/sonar-web/src/main/js/app/components/__tests__/UpdateNotification-it.tsx
server/sonar-web/src/main/js/app/components/app-state/AppStateContext.tsx
server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx
server/sonar-web/src/main/js/apps/projects/components/project-card/__tests__/ProjectCardMeasures-test.tsx
server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx
server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx
server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx
server/sonar-web/src/main/js/helpers/system.ts
server/sonar-web/src/main/js/helpers/testMocks.ts
server/sonar-web/src/main/js/helpers/testReactTestingUtils.tsx
server/sonar-web/src/main/js/types/appstate.ts

index 05e90164e98d03c272b300f9576f7006d1526ed7..024a231aff9d4c1f9c8bef16687b90559e73fb1e 100644 (file)
     "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}\"",
index 21202786a953a3ddce87e5578a368b20c68bf805..50bc95278d6d35d3d4baff5b3878580f99842b84 100644 (file)
@@ -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');
 }
index cc174d6ee814950d5018c6a81fb71fb528e046bf..ce965fcd3cdcf42081680dc010ad6132e483fb06 100644 (file)
@@ -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();
index 99f1d7f37ecc0b39ad50ccfe5462c3960d358ea9..835c9c51211c56dc61f9da444922748f6f4af574 100644 (file)
@@ -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: [],
index 1cf63a94a3ef604c65d1030532b8899693309253..91e350399c91bfb5e3ced970cea05fbf67e44e28 100644 (file)
  * 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<CurrentUser>,
-  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<AppState> = { version: '10.5.0', versionEOL: '2020-01-01' },
 ) {
   return renderComponent(
     <CurrentUserContext.Provider
@@ -438,7 +475,7 @@ function renderUpdateNotification(
         updateDismissedNotices: () => {},
       }}
     >
-      <AppStateContext.Provider value={mockAppState({ version })}>
+      <AppStateContext.Provider value={mockAppState(appState)}>
         <UpdateNotification dismissable={dissmissable} />
       </AppStateContext.Provider>
     </CurrentUserContext.Provider>,
index bfd1538af22846ef949c7d53b152627b0b207c99..4492d1c18d61270d4575f1daaa63db1fc1dad62b 100644 (file)
@@ -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<AppState>(DEFAULT_APP_STATE);
index 5a8bb6a20f7d0e72d94e7de3b6645605450f13a2..8e6109a8c3ec519675c52ca65abf536493508af6 100644 (file)
@@ -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<Props>) {
     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<Props>) {
 
   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<Props>) {
       new Date(upgrade1.releaseDate ?? '').getTime(),
   )[0];
 
-  const dismissKey = useCase + latest.version;
+  const dismissKey = useCase + (latest?.version ?? appState.version);
 
   return dismissable ? (
     <DismissableAlert
index 04c1197884b339047297e3f033a74b813ebeb259..7efa8f2e5f3c97fb5c126b4c858a490048ad5df9 100644 (file)
@@ -27,6 +27,7 @@ import { Dict } from '../../../../../types/types';
 import ProjectCardMeasures, { ProjectCardMeasuresProps } from '../ProjectCardMeasures';
 
 jest.mock('date-fns', () => ({
+  ...jest.requireActual('date-fns'),
   differenceInMilliseconds: () => 1000 * 60 * 60 * 24 * 30 * 8, // ~ 8 months
 }));
 
index 7fe3277d0e030a932b8a28a5ef7321fcdb055002..49d8f811c41c1018ccba1e281af07d66d03efe53 100644 (file)
@@ -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() {
           />
         </LinkStandalone>
       ),
-    }
+    },
   );
 }
index 9607fa5e7bbf41dad67b0ea2f9453fd166951f94..7744d356114798ee88f63c5f6a080efdee820484 100644 (file)
@@ -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<Props>) {
     setSystemUpgradeFormOpen(false);
   }, [setSystemUpgradeFormOpen]);
 
+  if (systemUpgrades.length === 0) {
+    return null;
+  }
+
   return (
     <>
       <ButtonSecondary className="sw-ml-2" onClick={openSystemUpgradeForm}>
index c9772ff86fa1969468def99abdf90869410e4583..c10de327d078782e3ba390bdfb16b5c4d43204d6 100644 (file)
@@ -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<Props>) {
     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<Props>) {
               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),
+              )}
             />
           ))}
         </>
index 1503850fe67ce22e979e72546454798c66141aa5..8319ff4d41efd90fa00a310115e61fbe2cdfe0a1 100644 (file)
@@ -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());
 }
index 1ea0bc60e69ec0def05b48ae7585984a78e04272..7cb81dfb4b7592fbec383fa915b4e58955b671a6 100644 (file)
@@ -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> = {}): 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,
   };
 }
index 750d09ffc76d82d3eb06b7431933d8e85682efa6..b8e7bcf983de471742a56b28a7fc535397f23d68 100644 (file)
@@ -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 (
       <IntlWrapper>
index f9bac3f35fddd456e3ce25927ef33b78afdcd863..308a01fa688e40f19b41ead6771760e31ddb8957 100644 (file)
@@ -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;
 }