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