]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21909 Add version status badge in Footer and System Information
authorIsmail Cherri <ismail.cherri@sonarsource.com>
Thu, 28 Mar 2024 17:08:51 +0000 (18:08 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 3 Apr 2024 20:02:41 +0000 (20:02 +0000)
server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx
server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx
server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/strings.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index c52b9d1cde0caecc513d4fd58d39a17e8b1cc20c..224338dd86df55cef67768e308149dcca2a9b954 100644 (file)
@@ -22,25 +22,50 @@ import { Provider, SysInfoCluster, SysInfoLogging, SysInfoStandalone } from '../
 
 import { LogsLevels } from '../../apps/system/utils';
 import { mockClusterSysInfo, mockLogs, mockStandaloneSysInfo } from '../../helpers/testMocks';
-import { getSystemInfo, setLogLevel } from '../system';
+import { getSystemInfo, getSystemUpgrades, setLogLevel } from '../system';
 
 jest.mock('../system');
 
+type SystemUpgrades = {
+  upgrades: [];
+  latestLTA: string;
+  updateCenterRefresh: string;
+  installedVersionActive: boolean;
+};
+
 export default class SystemServiceMock {
   isCluster: boolean = false;
   logging: SysInfoLogging = mockLogs();
   systemInfo: SysInfoCluster | SysInfoStandalone = mockStandaloneSysInfo();
+  systemUpgrades: SystemUpgrades = {
+    upgrades: [],
+    latestLTA: '7.9',
+    updateCenterRefresh: '2021-09-01',
+    installedVersionActive: true,
+  };
 
   constructor() {
     this.updateSystemInfo();
     jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
     jest.mocked(setLogLevel).mockImplementation(this.handleSetLogLevel);
+    jest.mocked(getSystemUpgrades).mockImplementation(this.handleGetSystemUpgrades);
   }
 
   handleGetSystemInfo = () => {
     return this.reply(this.systemInfo);
   };
 
+  handleGetSystemUpgrades = () => {
+    return this.reply(this.systemUpgrades);
+  };
+
+  setSystemUpgrades(systemUpgrades: Partial<SystemUpgrades>) {
+    this.systemUpgrades = {
+      ...this.systemUpgrades,
+      ...systemUpgrades,
+    };
+  }
+
   setProvider(provider: Provider | null) {
     this.systemInfo = mockStandaloneSysInfo({
       ...this.systemInfo,
index e98c67615ad95bcaedaee9459dbae7c7f3f7bb3a..085e75a378dc979d999744170db394776343062b 100644 (file)
@@ -27,21 +27,23 @@ import {
   themeBorder,
   themeColor,
 } from 'design-system';
-import * as React from 'react';
+import React from 'react';
+import { useIntl } from 'react-intl';
 import InstanceMessage from '../../components/common/InstanceMessage';
+import AppVersionStatus from '../../components/shared/AppVersionStatus';
 import { useDocUrl } from '../../helpers/docs';
 import { getEdition } from '../../helpers/editions';
-import { translate, translateWithParameters } from '../../helpers/l10n';
+import { useAppState } from './app-state/withAppStateContext';
 import GlobalFooterBranding from './GlobalFooterBranding';
-import { AppStateContext } from './app-state/AppStateContext';
 
 interface GlobalFooterProps {
   hideLoggedInInfo?: boolean;
 }
 
 export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooterProps>) {
-  const appState = React.useContext(AppStateContext);
+  const appState = useAppState();
   const currentEdition = appState?.edition && getEdition(appState.edition);
+  const intl = useIntl();
 
   const docUrl = useDocUrl();
 
@@ -52,12 +54,14 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooter
           <FlagMessage className="sw-mb-4" id="evaluation_warning" variant="warning">
             <p>
               <span className="sw-body-md-highlight">
-                {translate('footer.production_database_warning')}
+                {intl.formatMessage({ id: 'footer.production_database_warning' })}
               </span>
 
               <br />
 
-              <InstanceMessage message={translate('footer.production_database_explanation')} />
+              <InstanceMessage
+                message={intl.formatMessage({ id: 'footer.production_database_explanation' })}
+              />
             </p>
           </FlagMessage>
         )}
@@ -70,7 +74,7 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooter
 
             {!hideLoggedInInfo && appState?.version && (
               <li className="sw-code">
-                {translateWithParameters('footer.version_x', appState.version)}
+                <AppVersionStatus />
               </li>
             )}
 
@@ -79,7 +83,7 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooter
                 highlight={LinkHighlight.CurrentColor}
                 to="https://www.gnu.org/licenses/lgpl-3.0.txt"
               >
-                {translate('footer.license')}
+                {intl.formatMessage({ id: 'footer.license' })}
               </LinkStandalone>
             </li>
 
@@ -88,13 +92,13 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooter
                 highlight={LinkHighlight.CurrentColor}
                 to="https://community.sonarsource.com/c/help/sq"
               >
-                {translate('footer.community')}
+                {intl.formatMessage({ id: 'footer.community' })}
               </LinkStandalone>
             </li>
 
             <li>
               <LinkStandalone highlight={LinkHighlight.CurrentColor} to={docUrl('/')}>
-                {translate('footer.documentation')}
+                {intl.formatMessage({ id: 'footer.documentation' })}
               </LinkStandalone>
             </li>
 
@@ -103,14 +107,14 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooter
                 highlight={LinkHighlight.CurrentColor}
                 to={docUrl('/instance-administration/plugin-version-matrix/')}
               >
-                {translate('footer.plugins')}
+                {intl.formatMessage({ id: 'footer.plugins' })}
               </LinkStandalone>
             </li>
 
             {!hideLoggedInInfo && (
               <li>
                 <LinkStandalone highlight={LinkHighlight.CurrentColor} to="/web_api">
-                  {translate('footer.web_api')}
+                  {intl.formatMessage({ id: 'footer.web_api' })}
                 </LinkStandalone>
               </li>
             )}
index 1314125f3861d7266b4f3a20e02018749cea044e..4c0503d5c2d79981ab67aac2a9a2604a0279fef9 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
 import { mockAppState } from '../../../helpers/testMocks';
 import { renderComponent } from '../../../helpers/testReactTestingUtils';
 import { byRole, byText } from '../../../helpers/testSelector';
@@ -26,7 +27,13 @@ import { EditionKey } from '../../../types/editions';
 import { FCProps } from '../../../types/misc';
 import GlobalFooter from '../GlobalFooter';
 
-it('should render the logged-in information', () => {
+const systemMock = new SystemServiceMock();
+
+afterEach(() => {
+  systemMock.reset();
+});
+
+it('should render the logged-in information', async () => {
   renderGlobalFooter();
 
   expect(ui.databaseWarningMessage.query()).not.toBeInTheDocument();
@@ -35,9 +42,26 @@ it('should render the logged-in information', () => {
 
   expect(byText('Community Edition').get()).toBeInTheDocument();
   expect(ui.versionLabel('4.2').get()).toBeInTheDocument();
+  expect(await ui.ltaDocumentationLinkActive.find()).toBeInTheDocument();
   expect(ui.apiLink.get()).toBeInTheDocument();
 });
 
+it('should render the inactive version and cleanup build number', async () => {
+  systemMock.setSystemUpgrades({ installedVersionActive: false });
+  renderGlobalFooter({}, { version: '4.2 (build 12345)' });
+
+  expect(ui.versionLabel('4.2.12345').get()).toBeInTheDocument();
+  expect(await ui.ltaDocumentationLinkInactive.find()).toBeInTheDocument();
+});
+
+it('should active status if undefined', () => {
+  systemMock.setSystemUpgrades({ installedVersionActive: undefined });
+  renderGlobalFooter({}, { version: '4.2 (build 12345)' });
+
+  expect(ui.ltaDocumentationLinkInactive.query()).not.toBeInTheDocument();
+  expect(ui.ltaDocumentationLinkActive.query()).not.toBeInTheDocument();
+});
+
 it('should not render missing logged-in information', () => {
   renderGlobalFooter({}, { edition: undefined, version: '' });
 
@@ -84,7 +108,7 @@ const ui = {
   databaseWarningMessage: byText('footer.production_database_warning'),
 
   versionLabel: (version?: string) =>
-    version ? byText(`footer.version_x.${version}`) : byText(/footer\.version_x/),
+    version ? byText(/footer\.version\.*(\d.\d)/) : byText(/footer\.version/),
 
   // links
   websiteLink: byRole('link', { name: 'SonarQubeâ„¢' }),
@@ -94,4 +118,10 @@ const ui = {
   docsLink: byRole('link', { name: 'opens_in_new_window footer.documentation' }),
   pluginsLink: byRole('link', { name: 'opens_in_new_window footer.plugins' }),
   apiLink: byRole('link', { name: 'footer.web_api' }),
+  ltaDocumentationLinkActive: byRole('link', {
+    name: `footer.version.status.active open_in_new_window`,
+  }),
+  ltaDocumentationLinkInactive: byRole('link', {
+    name: `footer.version.status.inactive open_in_new_window`,
+  }),
 };
index 9bf4545eb352f221cde14472fd2ab473679c35d0..a3b75cbf96d08512a2356858056db5ea0a9b76c9 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { Card, ClipboardButton, FlagMessage, Spinner, Title } from 'design-system';
+import { Spinner } from '@sonarsource/echoes-react';
+import { Card, ClipboardButton, FlagMessage, Title } from 'design-system';
 import * as React from 'react';
 import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
+import AppVersionStatus from '../../../components/shared/AppVersionStatus';
 import { toShortISO8601String } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
 import { AppState } from '../../../types/appstate';
@@ -43,7 +45,7 @@ function PageHeader(props: Readonly<Props>) {
         <Title>{translate('system_info.page')}</Title>
 
         <div className="sw-flex sw-items-center">
-          <Spinner className="sw-mr-4 sw-mt-1" loading={loading} />
+          <Spinner className="sw-mr-4 sw-mt-1" isLoading={loading} />
 
           <PageActions
             canDownloadLogs={!isCluster}
@@ -70,7 +72,9 @@ function PageHeader(props: Readonly<Props>) {
               </div>
               <div className="sw-flex sw-items-center">
                 <strong className="sw-w-32">{translate('system.version')}</strong>
-                <span>{version}</span>
+                <span>
+                  <AppVersionStatus />
+                </span>
               </div>
             </div>
             <ClipboardButton
index fc45c6112b68a04222a223d59658a071af2c8421..38e281122b1337b0f09500e5a9211a83aa0c1585 100644 (file)
@@ -23,6 +23,7 @@ import { first } from 'lodash';
 import SystemServiceMock from '../../../../api/mocks/SystemServiceMock';
 import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
 import { byRole, byText } from '../../../../helpers/testSelector';
+import { AppState } from '../../../../types/appstate';
 import routes from '../../routes';
 import { LogsLevels } from '../../utils';
 
@@ -52,7 +53,7 @@ describe('System Info Standalone', () => {
   it('can change logs level', async () => {
     const { user, ui } = getPageObjects();
     renderSystemApp();
-    expect(await ui.pageHeading.find()).toBeInTheDocument();
+    await ui.appIsLoaded();
 
     await user.click(ui.changeLogLevelButton.get());
     expect(ui.logLevelWarning.queryAll()).toHaveLength(0);
@@ -81,6 +82,15 @@ describe('System Info Standalone', () => {
     });
     expect(ui.downloadSystemInfoButton.get()).toBeInTheDocument();
   });
+
+  it('should render current version and status', async () => {
+    const { ui } = getPageObjects();
+    renderSystemApp();
+    await ui.appIsLoaded();
+
+    expect(ui.versionLabel('7.8').get()).toBeInTheDocument();
+    expect(ui.ltaDocumentationLinkActive.get()).toBeInTheDocument();
+  });
 });
 
 describe('System Info Cluster', () => {
@@ -90,8 +100,6 @@ describe('System Info Cluster', () => {
     renderSystemApp();
     await ui.appIsLoaded();
 
-    expect(await ui.pageHeading.find()).toBeInTheDocument();
-
     expect(ui.downloadLogsButton.query()).not.toBeInTheDocument();
     expect(ui.downloadSystemInfoButton.get()).toBeInTheDocument();
 
@@ -109,10 +117,20 @@ describe('System Info Cluster', () => {
     await user.click(first(ui.sectionButton('server1.example.com').getAll()) as HTMLElement);
     expect(screen.getByRole('heading', { name: 'Web Logging' })).toBeInTheDocument();
   });
+
+  it('should render current version and status', async () => {
+    systemMock.setIsCluster(true);
+    const { ui } = getPageObjects();
+    renderSystemApp();
+    await ui.appIsLoaded();
+
+    expect(ui.versionLabel('7.8').get()).toBeInTheDocument();
+    expect(ui.ltaDocumentationLinkActive.get()).toBeInTheDocument();
+  });
 });
 
-function renderSystemApp() {
-  return renderAppRoutes('system', routes);
+function renderSystemApp(appState?: AppState) {
+  return renderAppRoutes('system', routes, { appState });
 }
 
 function getPageObjects() {
@@ -130,6 +148,11 @@ function getPageObjects() {
     logLevelWarningShort: byText('system.log_level.warning.short'),
     healthCauseWarning: byText('Friendly warning'),
     saveButton: byRole('button', { name: 'save' }),
+    versionLabel: (version?: string) =>
+      version ? byText(/footer\.version\s*(\d.\d)/) : byText(/footer\.version/),
+    ltaDocumentationLinkActive: byRole('link', {
+      name: `footer.version.status.active open_in_new_window`,
+    }),
   };
 
   async function appIsLoaded() {
diff --git a/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx b/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx
new file mode 100644 (file)
index 0000000..99a26e9
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { LinkHighlight, LinkStandalone } from '@sonarsource/echoes-react';
+import React from 'react';
+import { FormattedMessage, useIntl } from 'react-intl';
+import { useAppState } from '../../app/components/app-state/withAppStateContext';
+import { useDocUrl } from '../../helpers/docs';
+import { getInstanceVersionNumber } from '../../helpers/strings';
+import { useSystemUpgrades } from '../../queries/system';
+
+export default function AppVersionStatus() {
+  const { data } = useSystemUpgrades();
+  const { version } = useAppState();
+
+  const docUrl = useDocUrl();
+  const intl = useIntl();
+
+  return intl.formatMessage(
+    { id: `footer.version` },
+    {
+      version: getInstanceVersionNumber(version),
+      status:
+        data?.installedVersionActive !== undefined ? (
+          <LinkStandalone
+            className="sw-ml-1"
+            highlight={LinkHighlight.CurrentColor}
+            to={docUrl('/setup-and-upgrade/upgrade-the-server/active-versions/')}
+          >
+            <FormattedMessage
+              id={`footer.version.status.${data.installedVersionActive ? 'active' : 'inactive'}`}
+            />
+          </LinkStandalone>
+        ) : (
+          ''
+        ),
+    },
+  );
+}
index 2cb504d7072390a860ef110544b0b8ecb580dac5..ad92fb85eb4153d3ef52ebd61bd0b0a8bbafcb94 100644 (file)
@@ -414,3 +414,10 @@ export function decodeJwt(token: string) {
   const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
   return JSON.parse(window.atob(base64));
 }
+
+const VERSION_REGEX = /[\s()]/g;
+const VERSION_BUILD = 'build';
+export function getInstanceVersionNumber(version: string) {
+  // e.g. "10.5 (build 12345)" => "10.5.12345"
+  return version.replace(VERSION_REGEX, '').replace(VERSION_BUILD, '.');
+}
index f5c5d4cabd1849f73317496ae08e589b5e693090..e6c4cc5e3dff0bb5dea51ad4311caee35329e1a1 100644 (file)
@@ -4241,7 +4241,9 @@ footer.production_database_warning=Embedded database should be used for evaluati
 footer.security=Security
 footer.status=Status
 footer.terms=Terms
-footer.version_x=Version {0}
+footer.version=v{version}{status}
+footer.version.status.active=(ACTIVE)
+footer.version.status.inactive=(NO LONGER ACTIVE)
 footer.web_api=Web API