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,
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();
<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>
)}
{!hideLoggedInInfo && appState?.version && (
<li className="sw-code">
- {translateWithParameters('footer.version_x', appState.version)}
+ <AppVersionStatus />
</li>
)}
highlight={LinkHighlight.CurrentColor}
to="https://www.gnu.org/licenses/lgpl-3.0.txt"
>
- {translate('footer.license')}
+ {intl.formatMessage({ id: 'footer.license' })}
</LinkStandalone>
</li>
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>
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>
)}
* 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';
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();
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: '' });
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â„¢' }),
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`,
+ }),
};
* 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';
<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}
</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
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';
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);
});
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', () => {
renderSystemApp();
await ui.appIsLoaded();
- expect(await ui.pageHeading.find()).toBeInTheDocument();
-
expect(ui.downloadLogsButton.query()).not.toBeInTheDocument();
expect(ui.downloadSystemInfoButton.get()).toBeInTheDocument();
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() {
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() {
--- /dev/null
+/*
+ * 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>
+ ) : (
+ ''
+ ),
+ },
+ );
+}
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, '.');
+}
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