diff options
author | Ismail Cherri <ismail.cherri@sonarsource.com> | 2024-03-28 18:08:51 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-04-03 20:02:41 +0000 |
commit | 62458cc73c69ff89de412fe3fcaff19cb88dd0bb (patch) | |
tree | b72c5e7e38bd20824f7663a9bfde869820eca0a5 /server/sonar-web | |
parent | a5ad37c33725d0bebd76c00cfad10eb0ff985f23 (diff) | |
download | sonarqube-62458cc73c69ff89de412fe3fcaff19cb88dd0bb.tar.gz sonarqube-62458cc73c69ff89de412fe3fcaff19cb88dd0bb.zip |
SONAR-21909 Add version status badge in Footer and System Information
Diffstat (limited to 'server/sonar-web')
7 files changed, 172 insertions, 23 deletions
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<SystemUpgrades>) { + 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<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> )} 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<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 diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx index fc45c6112b6..38e281122b1 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx @@ -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 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 ? ( + <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> + ) : ( + '' + ), + }, + ); +} 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, '.'); +} |