Browse Source

SONAR-21909 Add version status badge in Footer and System Information

tags/10.5.0.89998
Ismail Cherri 1 month ago
parent
commit
62458cc73c

+ 26
- 1
server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts View 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,

+ 16
- 12
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx View 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>
)}

+ 32
- 2
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx View 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`,
}),
};

+ 7
- 3
server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx View File

@@ -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

+ 28
- 5
server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx View 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() {

+ 56
- 0
server/sonar-web/src/main/js/components/shared/AppVersionStatus.tsx View File

@@ -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>
) : (
''
),
},
);
}

+ 7
- 0
server/sonar-web/src/main/js/helpers/strings.ts View 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, '.');
}

+ 3
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View 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



Loading…
Cancel
Save