From: Ismail Cherri Date: Thu, 28 Mar 2024 17:08:51 +0000 (+0100) Subject: SONAR-21909 Add version status badge in Footer and System Information X-Git-Tag: 10.5.0.89998~46 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=62458cc73c69ff89de412fe3fcaff19cb88dd0bb;p=sonarqube.git SONAR-21909 Add version status badge in Footer and System Information --- diff --git a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts index c52b9d1cde0..224338dd86d 100644 --- a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts @@ -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) { + this.systemUpgrades = { + ...this.systemUpgrades, + ...systemUpgrades, + }; + } + setProvider(provider: Provider | null) { this.systemInfo = mockStandaloneSysInfo({ ...this.systemInfo, diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx index e98c67615ad..085e75a378d 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx @@ -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) { - 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

- {translate('footer.production_database_warning')} + {intl.formatMessage({ id: 'footer.production_database_warning' })}
- +

)} @@ -70,7 +74,7 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly - {translateWithParameters('footer.version_x', appState.version)} + )} @@ -79,7 +83,7 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly - {translate('footer.license')} + {intl.formatMessage({ id: 'footer.license' })} @@ -88,13 +92,13 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly - {translate('footer.community')} + {intl.formatMessage({ id: 'footer.community' })}
  • - {translate('footer.documentation')} + {intl.formatMessage({ id: 'footer.documentation' })}
  • @@ -103,14 +107,14 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly - {translate('footer.plugins')} + {intl.formatMessage({ id: 'footer.plugins' })} {!hideLoggedInInfo && (
  • - {translate('footer.web_api')} + {intl.formatMessage({ id: 'footer.web_api' })}
  • )} 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 1314125f386..4c0503d5c2d 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 @@ -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`, + }), }; diff --git a/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx index 9bf4545eb35..a3b75cbf96d 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx @@ -17,9 +17,11 @@ * 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) { {translate('system_info.page')}
    - + ) {
    {translate('system.version')} - {version} + + +
    { 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 index 00000000000..99a26e972e0 --- /dev/null +++ b/server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx @@ -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 ? ( + + + + ) : ( + '' + ), + }, + ); +} diff --git a/server/sonar-web/src/main/js/helpers/strings.ts b/server/sonar-web/src/main/js/helpers/strings.ts index 2cb504d7072..ad92fb85eb4 100644 --- a/server/sonar-web/src/main/js/helpers/strings.ts +++ b/server/sonar-web/src/main/js/helpers/strings.ts @@ -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, '.'); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index f5c5d4cabd1..e6c4cc5e3df 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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