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



import { LogsLevels } from '../../apps/system/utils'; import { LogsLevels } from '../../apps/system/utils';
import { mockClusterSysInfo, mockLogs, mockStandaloneSysInfo } from '../../helpers/testMocks'; import { mockClusterSysInfo, mockLogs, mockStandaloneSysInfo } from '../../helpers/testMocks';
import { getSystemInfo, setLogLevel } from '../system';
import { getSystemInfo, getSystemUpgrades, setLogLevel } from '../system';


jest.mock('../system'); jest.mock('../system');


type SystemUpgrades = {
upgrades: [];
latestLTA: string;
updateCenterRefresh: string;
installedVersionActive: boolean;
};

export default class SystemServiceMock { export default class SystemServiceMock {
isCluster: boolean = false; isCluster: boolean = false;
logging: SysInfoLogging = mockLogs(); logging: SysInfoLogging = mockLogs();
systemInfo: SysInfoCluster | SysInfoStandalone = mockStandaloneSysInfo(); systemInfo: SysInfoCluster | SysInfoStandalone = mockStandaloneSysInfo();
systemUpgrades: SystemUpgrades = {
upgrades: [],
latestLTA: '7.9',
updateCenterRefresh: '2021-09-01',
installedVersionActive: true,
};


constructor() { constructor() {
this.updateSystemInfo(); this.updateSystemInfo();
jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo); jest.mocked(getSystemInfo).mockImplementation(this.handleGetSystemInfo);
jest.mocked(setLogLevel).mockImplementation(this.handleSetLogLevel); jest.mocked(setLogLevel).mockImplementation(this.handleSetLogLevel);
jest.mocked(getSystemUpgrades).mockImplementation(this.handleGetSystemUpgrades);
} }


handleGetSystemInfo = () => { handleGetSystemInfo = () => {
return this.reply(this.systemInfo); return this.reply(this.systemInfo);
}; };


handleGetSystemUpgrades = () => {
return this.reply(this.systemUpgrades);
};

setSystemUpgrades(systemUpgrades: Partial<SystemUpgrades>) {
this.systemUpgrades = {
...this.systemUpgrades,
...systemUpgrades,
};
}

setProvider(provider: Provider | null) { setProvider(provider: Provider | null) {
this.systemInfo = mockStandaloneSysInfo({ this.systemInfo = mockStandaloneSysInfo({
...this.systemInfo, ...this.systemInfo,

+ 16
- 12
server/sonar-web/src/main/js/app/components/GlobalFooter.tsx View File

themeBorder, themeBorder,
themeColor, themeColor,
} from 'design-system'; } from 'design-system';
import * as React from 'react';
import React from 'react';
import { useIntl } from 'react-intl';
import InstanceMessage from '../../components/common/InstanceMessage'; import InstanceMessage from '../../components/common/InstanceMessage';
import AppVersionStatus from '../../components/shared/AppVersionStatus';
import { useDocUrl } from '../../helpers/docs'; import { useDocUrl } from '../../helpers/docs';
import { getEdition } from '../../helpers/editions'; import { getEdition } from '../../helpers/editions';
import { translate, translateWithParameters } from '../../helpers/l10n';
import { useAppState } from './app-state/withAppStateContext';
import GlobalFooterBranding from './GlobalFooterBranding'; import GlobalFooterBranding from './GlobalFooterBranding';
import { AppStateContext } from './app-state/AppStateContext';


interface GlobalFooterProps { interface GlobalFooterProps {
hideLoggedInInfo?: boolean; hideLoggedInInfo?: boolean;
} }


export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooterProps>) { export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooterProps>) {
const appState = React.useContext(AppStateContext);
const appState = useAppState();
const currentEdition = appState?.edition && getEdition(appState.edition); const currentEdition = appState?.edition && getEdition(appState.edition);
const intl = useIntl();


const docUrl = useDocUrl(); const docUrl = useDocUrl();


<FlagMessage className="sw-mb-4" id="evaluation_warning" variant="warning"> <FlagMessage className="sw-mb-4" id="evaluation_warning" variant="warning">
<p> <p>
<span className="sw-body-md-highlight"> <span className="sw-body-md-highlight">
{translate('footer.production_database_warning')}
{intl.formatMessage({ id: 'footer.production_database_warning' })}
</span> </span>


<br /> <br />


<InstanceMessage message={translate('footer.production_database_explanation')} />
<InstanceMessage
message={intl.formatMessage({ id: 'footer.production_database_explanation' })}
/>
</p> </p>
</FlagMessage> </FlagMessage>
)} )}


{!hideLoggedInInfo && appState?.version && ( {!hideLoggedInInfo && appState?.version && (
<li className="sw-code"> <li className="sw-code">
{translateWithParameters('footer.version_x', appState.version)}
<AppVersionStatus />
</li> </li>
)} )}


highlight={LinkHighlight.CurrentColor} highlight={LinkHighlight.CurrentColor}
to="https://www.gnu.org/licenses/lgpl-3.0.txt" to="https://www.gnu.org/licenses/lgpl-3.0.txt"
> >
{translate('footer.license')}
{intl.formatMessage({ id: 'footer.license' })}
</LinkStandalone> </LinkStandalone>
</li> </li>


highlight={LinkHighlight.CurrentColor} highlight={LinkHighlight.CurrentColor}
to="https://community.sonarsource.com/c/help/sq" to="https://community.sonarsource.com/c/help/sq"
> >
{translate('footer.community')}
{intl.formatMessage({ id: 'footer.community' })}
</LinkStandalone> </LinkStandalone>
</li> </li>


<li> <li>
<LinkStandalone highlight={LinkHighlight.CurrentColor} to={docUrl('/')}> <LinkStandalone highlight={LinkHighlight.CurrentColor} to={docUrl('/')}>
{translate('footer.documentation')}
{intl.formatMessage({ id: 'footer.documentation' })}
</LinkStandalone> </LinkStandalone>
</li> </li>


highlight={LinkHighlight.CurrentColor} highlight={LinkHighlight.CurrentColor}
to={docUrl('/instance-administration/plugin-version-matrix/')} to={docUrl('/instance-administration/plugin-version-matrix/')}
> >
{translate('footer.plugins')}
{intl.formatMessage({ id: 'footer.plugins' })}
</LinkStandalone> </LinkStandalone>
</li> </li>


{!hideLoggedInInfo && ( {!hideLoggedInInfo && (
<li> <li>
<LinkStandalone highlight={LinkHighlight.CurrentColor} to="/web_api"> <LinkStandalone highlight={LinkHighlight.CurrentColor} to="/web_api">
{translate('footer.web_api')}
{intl.formatMessage({ id: 'footer.web_api' })}
</LinkStandalone> </LinkStandalone>
</li> </li>
)} )}

+ 32
- 2
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx View File

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react'; import * as React from 'react';
import SystemServiceMock from '../../../api/mocks/SystemServiceMock';
import { mockAppState } from '../../../helpers/testMocks'; import { mockAppState } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils'; import { renderComponent } from '../../../helpers/testReactTestingUtils';
import { byRole, byText } from '../../../helpers/testSelector'; import { byRole, byText } from '../../../helpers/testSelector';
import { FCProps } from '../../../types/misc'; import { FCProps } from '../../../types/misc';
import GlobalFooter from '../GlobalFooter'; 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(); renderGlobalFooter();


expect(ui.databaseWarningMessage.query()).not.toBeInTheDocument(); expect(ui.databaseWarningMessage.query()).not.toBeInTheDocument();


expect(byText('Community Edition').get()).toBeInTheDocument(); expect(byText('Community Edition').get()).toBeInTheDocument();
expect(ui.versionLabel('4.2').get()).toBeInTheDocument(); expect(ui.versionLabel('4.2').get()).toBeInTheDocument();
expect(await ui.ltaDocumentationLinkActive.find()).toBeInTheDocument();
expect(ui.apiLink.get()).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', () => { it('should not render missing logged-in information', () => {
renderGlobalFooter({}, { edition: undefined, version: '' }); renderGlobalFooter({}, { edition: undefined, version: '' });


databaseWarningMessage: byText('footer.production_database_warning'), databaseWarningMessage: byText('footer.production_database_warning'),


versionLabel: (version?: string) => versionLabel: (version?: string) =>
version ? byText(`footer.version_x.${version}`) : byText(/footer\.version_x/),
version ? byText(/footer\.version\.*(\d.\d)/) : byText(/footer\.version/),


// links // links
websiteLink: byRole('link', { name: 'SonarQube™' }), websiteLink: byRole('link', { name: 'SonarQube™' }),
docsLink: byRole('link', { name: 'opens_in_new_window footer.documentation' }), docsLink: byRole('link', { name: 'opens_in_new_window footer.documentation' }),
pluginsLink: byRole('link', { name: 'opens_in_new_window footer.plugins' }), pluginsLink: byRole('link', { name: 'opens_in_new_window footer.plugins' }),
apiLink: byRole('link', { name: 'footer.web_api' }), 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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 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 * as React from 'react';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import AppVersionStatus from '../../../components/shared/AppVersionStatus';
import { toShortISO8601String } from '../../../helpers/dates'; import { toShortISO8601String } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { AppState } from '../../../types/appstate'; import { AppState } from '../../../types/appstate';
<Title>{translate('system_info.page')}</Title> <Title>{translate('system_info.page')}</Title>


<div className="sw-flex sw-items-center"> <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 <PageActions
canDownloadLogs={!isCluster} canDownloadLogs={!isCluster}
</div> </div>
<div className="sw-flex sw-items-center"> <div className="sw-flex sw-items-center">
<strong className="sw-w-32">{translate('system.version')}</strong> <strong className="sw-w-32">{translate('system.version')}</strong>
<span>{version}</span>
<span>
<AppVersionStatus />
</span>
</div> </div>
</div> </div>
<ClipboardButton <ClipboardButton

+ 28
- 5
server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx View File

import SystemServiceMock from '../../../../api/mocks/SystemServiceMock'; import SystemServiceMock from '../../../../api/mocks/SystemServiceMock';
import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
import { byRole, byText } from '../../../../helpers/testSelector'; import { byRole, byText } from '../../../../helpers/testSelector';
import { AppState } from '../../../../types/appstate';
import routes from '../../routes'; import routes from '../../routes';
import { LogsLevels } from '../../utils'; import { LogsLevels } from '../../utils';


it('can change logs level', async () => { it('can change logs level', async () => {
const { user, ui } = getPageObjects(); const { user, ui } = getPageObjects();
renderSystemApp(); renderSystemApp();
expect(await ui.pageHeading.find()).toBeInTheDocument();
await ui.appIsLoaded();


await user.click(ui.changeLogLevelButton.get()); await user.click(ui.changeLogLevelButton.get());
expect(ui.logLevelWarning.queryAll()).toHaveLength(0); expect(ui.logLevelWarning.queryAll()).toHaveLength(0);
}); });
expect(ui.downloadSystemInfoButton.get()).toBeInTheDocument(); 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', () => { describe('System Info Cluster', () => {
renderSystemApp(); renderSystemApp();
await ui.appIsLoaded(); await ui.appIsLoaded();


expect(await ui.pageHeading.find()).toBeInTheDocument();

expect(ui.downloadLogsButton.query()).not.toBeInTheDocument(); expect(ui.downloadLogsButton.query()).not.toBeInTheDocument();
expect(ui.downloadSystemInfoButton.get()).toBeInTheDocument(); expect(ui.downloadSystemInfoButton.get()).toBeInTheDocument();


await user.click(first(ui.sectionButton('server1.example.com').getAll()) as HTMLElement); await user.click(first(ui.sectionButton('server1.example.com').getAll()) as HTMLElement);
expect(screen.getByRole('heading', { name: 'Web Logging' })).toBeInTheDocument(); 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() { function getPageObjects() {
logLevelWarningShort: byText('system.log_level.warning.short'), logLevelWarningShort: byText('system.log_level.warning.short'),
healthCauseWarning: byText('Friendly warning'), healthCauseWarning: byText('Friendly warning'),
saveButton: byRole('button', { name: 'save' }), 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() { async function appIsLoaded() {

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

/*
* 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

const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(window.atob(base64)); 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

footer.security=Security footer.security=Security
footer.status=Status footer.status=Status
footer.terms=Terms 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 footer.web_api=Web API





Loading…
Cancel
Save