aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorIsmail Cherri <ismail.cherri@sonarsource.com>2024-03-28 18:08:51 +0100
committersonartech <sonartech@sonarsource.com>2024-04-03 20:02:41 +0000
commit62458cc73c69ff89de412fe3fcaff19cb88dd0bb (patch)
treeb72c5e7e38bd20824f7663a9bfde869820eca0a5 /server/sonar-web
parenta5ad37c33725d0bebd76c00cfad10eb0ff985f23 (diff)
downloadsonarqube-62458cc73c69ff89de412fe3fcaff19cb88dd0bb.tar.gz
sonarqube-62458cc73c69ff89de412fe3fcaff19cb88dd0bb.zip
SONAR-21909 Add version status badge in Footer and System Information
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts27
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalFooter.tsx28
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx33
-rw-r--r--server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx56
-rw-r--r--server/sonar-web/src/main/js/helpers/strings.ts7
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, '.');
+}