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