@@ -36,7 +36,8 @@ export function getSystemStatus(): Promise<{ id: string; version: string; status | |||
export function getSystemUpgrades(): Promise<{ | |||
upgrades: SystemUpgrade[]; | |||
latestLTS: string; | |||
latestLTA: string; | |||
installedVersionActive: boolean; | |||
updateCenterRefresh: string; | |||
}> { | |||
return getJSON('/api/system/upgrades'); |
@@ -43,3 +43,7 @@ export default function withAppStateContext<P>( | |||
} | |||
}; | |||
} | |||
export function useAppState() { | |||
return React.useContext(AppStateContext); | |||
} |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Banner, Variant } from 'design-system'; | |||
import { Banner } from 'design-system'; | |||
import { groupBy, isEmpty, mapValues } from 'lodash'; | |||
import * as React from 'react'; | |||
import DismissableAlert from '../../../components/ui/DismissableAlert'; | |||
@@ -26,33 +26,26 @@ import { UpdateUseCase } from '../../../components/upgrade/utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { hasGlobalPermission } from '../../../helpers/users'; | |||
import { useSystemUpgrades } from '../../../queries/system'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Permissions } from '../../../types/permissions'; | |||
import { Dict } from '../../../types/types'; | |||
import { CurrentUser, isLoggedIn } from '../../../types/users'; | |||
import withAppStateContext from '../app-state/withAppStateContext'; | |||
import withCurrentUserContext from '../current-user/withCurrentUserContext'; | |||
import { isMinorUpdate, isPatchUpdate, isPreLTSUpdate, isPreviousLTSUpdate } from './helpers'; | |||
const MAP_VARIANT: Dict<Variant> = { | |||
[UpdateUseCase.NewMinorVersion]: 'info', | |||
[UpdateUseCase.NewPatch]: 'warning', | |||
[UpdateUseCase.PreLTS]: 'warning', | |||
[UpdateUseCase.PreviousLTS]: 'error', | |||
}; | |||
import { isLoggedIn } from '../../../types/users'; | |||
import { useAppState } from '../app-state/withAppStateContext'; | |||
import { useCurrentUser } from '../current-user/CurrentUserContext'; | |||
import { BANNER_VARIANT, isCurrentVersionLTA, isMinorUpdate, isPatchUpdate } from './helpers'; | |||
interface Props { | |||
dismissable: boolean; | |||
appState: AppState; | |||
currentUser: CurrentUser; | |||
dismissable?: boolean; | |||
} | |||
const VERSION_PARSER = /^(\d+)\.(\d+)(\.(\d+))?/; | |||
export function UpdateNotification({ dismissable, appState, currentUser }: Readonly<Props>) { | |||
export default function UpdateNotification({ dismissable }: Readonly<Props>) { | |||
const appState = useAppState(); | |||
const { currentUser } = useCurrentUser(); | |||
const canUserSeeNotification = | |||
isLoggedIn(currentUser) && hasGlobalPermission(currentUser, Permissions.Admin); | |||
const regExpParsedVersion = VERSION_PARSER.exec(appState.version); | |||
const { data } = useSystemUpgrades({ | |||
enabled: canUserSeeNotification && regExpParsedVersion !== null, | |||
}); | |||
@@ -66,7 +59,8 @@ export function UpdateNotification({ dismissable, appState, currentUser }: Reado | |||
return null; | |||
} | |||
const { upgrades, latestLTS } = data; | |||
const { upgrades, installedVersionActive, latestLTA } = data; | |||
const parsedVersion = regExpParsedVersion | |||
.slice(1) | |||
.map(Number) | |||
@@ -84,16 +78,15 @@ export function UpdateNotification({ dismissable, appState, currentUser }: Reado | |||
}), | |||
); | |||
let useCase = UpdateUseCase.NewMinorVersion; | |||
let useCase = UpdateUseCase.NewVersion; | |||
if (isPreviousLTSUpdate(parsedVersion, latestLTS, systemUpgrades)) { | |||
useCase = UpdateUseCase.PreviousLTS; | |||
} else if (isPreLTSUpdate(parsedVersion, latestLTS)) { | |||
useCase = UpdateUseCase.PreLTS; | |||
} else if (isPatchUpdate(parsedVersion, systemUpgrades)) { | |||
if (!installedVersionActive) { | |||
useCase = UpdateUseCase.CurrentVersionInactive; | |||
} else if ( | |||
isPatchUpdate(parsedVersion, systemUpgrades) && | |||
(isCurrentVersionLTA(parsedVersion, latestLTA) || !isMinorUpdate(parsedVersion, systemUpgrades)) | |||
) { | |||
useCase = UpdateUseCase.NewPatch; | |||
} else if (isMinorUpdate(parsedVersion, systemUpgrades)) { | |||
useCase = UpdateUseCase.NewMinorVersion; | |||
} | |||
const latest = [...upgrades].sort( | |||
@@ -107,26 +100,24 @@ export function UpdateNotification({ dismissable, appState, currentUser }: Reado | |||
return dismissable ? ( | |||
<DismissableAlert | |||
alertKey={dismissKey} | |||
variant={MAP_VARIANT[useCase]} | |||
variant={BANNER_VARIANT[useCase]} | |||
className={`it__promote-update-notification it__upgrade-prompt-${useCase}`} | |||
> | |||
{translate('admin_notification.update', useCase)} | |||
<SystemUpgradeButton | |||
systemUpgrades={upgrades} | |||
updateUseCase={useCase} | |||
latestLTS={latestLTS} | |||
latestLTA={latestLTA} | |||
/> | |||
</DismissableAlert> | |||
) : ( | |||
<Banner variant={MAP_VARIANT[useCase]} className={`it__upgrade-prompt-${useCase}`}> | |||
<Banner variant={BANNER_VARIANT[useCase]} className={`it__upgrade-prompt-${useCase}`}> | |||
{translate('admin_notification.update', useCase)} | |||
<SystemUpgradeButton | |||
systemUpgrades={upgrades} | |||
updateUseCase={useCase} | |||
latestLTS={latestLTS} | |||
latestLTA={latestLTA} | |||
/> | |||
</Banner> | |||
); | |||
} | |||
export default withCurrentUserContext(withAppStateContext(UpdateNotification)); |
@@ -18,44 +18,20 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Variant } from 'design-system'; | |||
import { isEmpty } from 'lodash'; | |||
import { sortUpgrades } from '../../../components/upgrade/utils'; | |||
import { UpdateUseCase, sortUpgrades } from '../../../components/upgrade/utils'; | |||
import { SystemUpgrade } from '../../../types/system'; | |||
const MONTH_BEFOR_PREVIOUS_LTS_NOTIFICATION = 6; | |||
import { Dict } from '../../../types/types'; | |||
type GroupedSystemUpdate = { | |||
[x: string]: Record<string, SystemUpgrade[]>; | |||
}; | |||
export const isPreLTSUpdate = (parsedVersion: number[], latestLTS: string) => { | |||
export const isCurrentVersionLTA = (parsedVersion: number[], latestLTS: string) => { | |||
const [currentMajor, currentMinor] = parsedVersion; | |||
const [ltsMajor, ltsMinor] = latestLTS.split('.').map(Number); | |||
return currentMajor < ltsMajor || (currentMajor === ltsMajor && currentMinor < ltsMinor); | |||
}; | |||
export const isPreviousLTSUpdate = ( | |||
parsedVersion: number[], | |||
latestLTS: string, | |||
systemUpgrades: GroupedSystemUpdate, | |||
) => { | |||
const [ltsMajor, ltsMinor] = latestLTS.split('.').map(Number); | |||
let ltsOlderThan6Month = false; | |||
const beforeLts = isPreLTSUpdate(parsedVersion, latestLTS); | |||
if (beforeLts) { | |||
const allLTS = sortUpgrades(systemUpgrades[ltsMajor][ltsMinor]); | |||
const ltsReleaseDate = new Date(allLTS[allLTS.length - 1]?.releaseDate ?? ''); | |||
if (isNaN(ltsReleaseDate.getTime())) { | |||
// We can not parse the LTS date. | |||
// It is unlikly that this could happen but consider LTS to be old. | |||
return true; | |||
} | |||
ltsOlderThan6Month = | |||
ltsReleaseDate.setMonth(ltsReleaseDate.getMonth() + MONTH_BEFOR_PREVIOUS_LTS_NOTIFICATION) - | |||
Date.now() < | |||
0; | |||
} | |||
return ltsOlderThan6Month && beforeLts; | |||
return currentMajor === ltsMajor && currentMinor === ltsMinor; | |||
}; | |||
export const isMinorUpdate = (parsedVersion: number[], systemUpgrades: GroupedSystemUpdate) => { | |||
@@ -69,7 +45,7 @@ export const isMinorUpdate = (parsedVersion: number[], systemUpgrades: GroupedSy | |||
export const isPatchUpdate = (parsedVersion: number[], systemUpgrades: GroupedSystemUpdate) => { | |||
const [currentMajor, currentMinor, currentPatch] = parsedVersion; | |||
const allMinor = systemUpgrades[currentMajor]; | |||
const allPatch = sortUpgrades(allMinor[currentMinor] || []); | |||
const allPatch = sortUpgrades(allMinor?.[currentMinor] ?? []); | |||
if (!isEmpty(allPatch)) { | |||
const [, , latestPatch] = allPatch[0].version.split('.').map(Number); | |||
@@ -79,3 +55,9 @@ export const isPatchUpdate = (parsedVersion: number[], systemUpgrades: GroupedSy | |||
} | |||
return false; | |||
}; | |||
export const BANNER_VARIANT: Dict<Variant> = { | |||
[UpdateUseCase.NewVersion]: 'info', | |||
[UpdateUseCase.CurrentVersionInactive]: 'error', | |||
[UpdateUseCase.NewPatch]: 'warning', | |||
}; |
@@ -129,7 +129,7 @@ class SystemApp extends React.PureComponent<Props, State> { | |||
<Helmet defer={false} title={translate('system_info.page')} /> | |||
<PageContentFontWrapper className="sw-body-sm sw-pb-8"> | |||
<div> | |||
<UpdateNotification dismissable={false} /> | |||
<UpdateNotification /> | |||
</div> | |||
{sysInfoData && ( | |||
<PageHeader |
@@ -25,13 +25,13 @@ import SystemUpgradeForm from './SystemUpgradeForm'; | |||
import { groupUpgrades, sortUpgrades, UpdateUseCase } from './utils'; | |||
interface Props { | |||
latestLTS: string; | |||
latestLTA: string; | |||
systemUpgrades: SystemUpgrade[]; | |||
updateUseCase?: UpdateUseCase; | |||
updateUseCase: UpdateUseCase; | |||
} | |||
export default function SystemUpgradeButton(props: Readonly<Props>) { | |||
const { latestLTS, systemUpgrades, updateUseCase } = props; | |||
const { latestLTA, systemUpgrades, updateUseCase } = props; | |||
const [isSystemUpgradeFormOpen, setSystemUpgradeFormOpen] = React.useState(false); | |||
@@ -52,7 +52,7 @@ export default function SystemUpgradeButton(props: Readonly<Props>) { | |||
<SystemUpgradeForm | |||
onClose={closeSystemUpgradeForm} | |||
systemUpgrades={groupUpgrades(sortUpgrades(systemUpgrades))} | |||
latestLTS={latestLTS} | |||
latestLTA={latestLTA} | |||
updateUseCase={updateUseCase} | |||
/> | |||
)} |
@@ -17,35 +17,30 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { FlagMessage, Link, Modal, Variant } from 'design-system'; | |||
import { FlagMessage, Link, Modal } from 'design-system'; | |||
import { filter, flatMap, isEmpty, negate } from 'lodash'; | |||
import * as React from 'react'; | |||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | |||
import { useAppState } from '../../app/components/app-state/withAppStateContext'; | |||
import { BANNER_VARIANT } from '../../app/components/update-notification/helpers'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { AppState } from '../../types/appstate'; | |||
import { SystemUpgrade } from '../../types/system'; | |||
import SystemUpgradeItem from './SystemUpgradeItem'; | |||
import { SYSTEM_VERSION_REGEXP, UpdateUseCase } from './utils'; | |||
interface Props { | |||
appState: AppState; | |||
onClose: () => void; | |||
systemUpgrades: SystemUpgrade[][]; | |||
latestLTS: string; | |||
updateUseCase?: UpdateUseCase; | |||
latestLTA: string; | |||
updateUseCase: UpdateUseCase; | |||
} | |||
const MAP_ALERT: { [key in UpdateUseCase]?: Variant } = { | |||
[UpdateUseCase.NewPatch]: 'warning', | |||
[UpdateUseCase.PreLTS]: 'warning', | |||
[UpdateUseCase.PreviousLTS]: 'error', | |||
}; | |||
export function SystemUpgradeForm(props: Readonly<Props>) { | |||
const { appState, latestLTS, onClose, updateUseCase, systemUpgrades } = props; | |||
export default function SystemUpgradeForm(props: Readonly<Props>) { | |||
const appState = useAppState(); | |||
const { latestLTA, onClose, updateUseCase, systemUpgrades } = props; | |||
let systemUpgradesWithPatch: SystemUpgrade[][] = []; | |||
const alertVariant = updateUseCase ? MAP_ALERT[updateUseCase] : undefined; | |||
const alertVariant = | |||
updateUseCase !== UpdateUseCase.NewVersion ? BANNER_VARIANT[updateUseCase] : undefined; | |||
const header = translate('system.system_upgrade'); | |||
const parsedVersion = SYSTEM_VERSION_REGEXP.exec(appState.version); | |||
let patches: SystemUpgrade[] = []; | |||
@@ -65,12 +60,12 @@ export function SystemUpgradeForm(props: Readonly<Props>) { | |||
systemUpgradesWithPatch.push(patches); | |||
} else { | |||
let untilLTS = false; | |||
let untilLTA = false; | |||
for (const upgrades of systemUpgrades) { | |||
if (untilLTS === false) { | |||
if (untilLTA === false) { | |||
systemUpgradesWithPatch.push(upgrades); | |||
untilLTS = upgrades.some((upgrade) => upgrade.version.startsWith(latestLTS)); | |||
untilLTA = upgrades.some((upgrade) => upgrade.version.startsWith(latestLTA)); | |||
} | |||
} | |||
} | |||
@@ -81,7 +76,7 @@ export function SystemUpgradeForm(props: Readonly<Props>) { | |||
onClose={onClose} | |||
body={ | |||
<> | |||
{alertVariant && updateUseCase && ( | |||
{alertVariant && ( | |||
<FlagMessage variant={alertVariant} className={`it__upgrade-alert-${updateUseCase}`}> | |||
{translate('admin_notification.update', updateUseCase)} | |||
</FlagMessage> | |||
@@ -92,7 +87,7 @@ export function SystemUpgradeForm(props: Readonly<Props>) { | |||
key={upgrades[upgrades.length - 1].version} | |||
systemUpgrades={upgrades} | |||
isPatch={upgrades === patches} | |||
isLTSVersion={upgrades.some((upgrade) => upgrade.version.startsWith(latestLTS))} | |||
isLTAVersion={upgrades.some((upgrade) => upgrade.version.startsWith(latestLTA))} | |||
/> | |||
))} | |||
</> | |||
@@ -106,5 +101,3 @@ export function SystemUpgradeForm(props: Readonly<Props>) { | |||
/> | |||
); | |||
} | |||
export default withAppStateContext(SystemUpgradeForm); |
@@ -34,21 +34,21 @@ import SystemUpgradeIntermediate from './SystemUpgradeIntermediate'; | |||
export interface SystemUpgradeItemProps { | |||
edition: EditionKey | undefined; | |||
isLTSVersion: boolean; | |||
isLTAVersion: boolean; | |||
isPatch: boolean; | |||
systemUpgrades: SystemUpgrade[]; | |||
} | |||
export default function SystemUpgradeItem(props: SystemUpgradeItemProps) { | |||
const { edition, isPatch, isLTSVersion, systemUpgrades } = props; | |||
const { edition, isPatch, isLTAVersion, systemUpgrades } = props; | |||
const lastUpgrade = systemUpgrades[0]; | |||
const downloadUrl = getEditionDownloadUrl( | |||
getEdition(edition || EditionKey.community), | |||
lastUpgrade, | |||
); | |||
let header = translate('system.latest_version'); | |||
if (isLTSVersion) { | |||
header = translate('system.lts_version'); | |||
if (isLTAVersion) { | |||
header = translate('system.lta_version'); | |||
} else if (isPatch) { | |||
header = translate('system.latest_patch'); | |||
} |
@@ -31,7 +31,7 @@ const ui = { | |||
header: byRole('heading', { name: 'system.system_upgrade' }), | |||
downloadLink: byRole('link', { name: /system.see_sonarqube_downloads/ }), | |||
ltsVersionHeader: byRole('heading', { name: /system.lts_version/ }), | |||
ltaVersionHeader: byRole('heading', { name: /system.lta_version/ }), | |||
newPatchWarning: byText(/admin_notification.update/), | |||
}; | |||
@@ -44,7 +44,7 @@ it('should render properly', async () => { | |||
await user.click(ui.learnMoreButton.get()); | |||
expect(ui.header.get()).toBeInTheDocument(); | |||
expect(ui.ltsVersionHeader.get()).toBeInTheDocument(); | |||
expect(ui.ltaVersionHeader.get()).toBeInTheDocument(); | |||
expect(ui.downloadLink.get()).toBeInTheDocument(); | |||
}); | |||
@@ -54,7 +54,7 @@ it('should render properly for new patch', async () => { | |||
renderSystemUpgradeButton( | |||
{ | |||
updateUseCase: UpdateUseCase.NewPatch, | |||
latestLTS: '9.9', | |||
latestLTA: '9.9', | |||
systemUpgrades: [{ downloadUrl: '', version: '9.9.1' }], | |||
}, | |||
'9.9', | |||
@@ -64,7 +64,7 @@ it('should render properly for new patch', async () => { | |||
expect(ui.header.get()).toBeInTheDocument(); | |||
expect(ui.newPatchWarning.get()).toBeInTheDocument(); | |||
expect(ui.ltsVersionHeader.get()).toBeInTheDocument(); | |||
expect(ui.ltaVersionHeader.get()).toBeInTheDocument(); | |||
expect(ui.downloadLink.get()).toBeInTheDocument(); | |||
}); | |||
@@ -74,7 +74,8 @@ function renderSystemUpgradeButton( | |||
) { | |||
renderComponent( | |||
<SystemUpgradeButton | |||
latestLTS="9.9" | |||
updateUseCase={UpdateUseCase.NewVersion} | |||
latestLTA="9.9" | |||
systemUpgrades={[ | |||
{ downloadUrl: 'eight', version: '9.8' }, | |||
{ downloadUrl: 'lts', version: '9.9' }, |
@@ -21,10 +21,9 @@ import { groupBy, sortBy } from 'lodash'; | |||
import { SystemUpgrade } from '../../types/system'; | |||
export enum UpdateUseCase { | |||
NewMinorVersion = 'new_minor_version', | |||
NewVersion = 'new_version', | |||
CurrentVersionInactive = 'current_version_inactive', | |||
NewPatch = 'new_patch', | |||
PreLTS = 'pre_lts', | |||
PreviousLTS = 'previous_lts', | |||
} | |||
export const SYSTEM_VERSION_REGEXP = /^(\d+)\.(\d+)(\.(\d+))?/; |
@@ -494,10 +494,9 @@ qualifier.description.APP=Single-level aggregation with a technical focus and a | |||
# Admin notification | |||
# | |||
#------------------------------------------------------------------------------ | |||
admin_notification.update.new_minor_version=There’s a new version of SonarQube available. Update to enjoy the latest updates and features. | |||
admin_notification.update.new_patch=There’s an update available for your SonarQube instance. Please update to make sure you benefit from the latest security and bug fixes. | |||
admin_notification.update.pre_lts=You’re running a version of SonarQube that has reached end of life. Please upgrade to a supported version at your earliest convenience. | |||
admin_notification.update.previous_lts=You’re running a version of SonarQube that is past end of life. Please upgrade to a supported version immediately. | |||
admin_notification.update.new_version=There’s a new version of SonarQube available. Upgrade to the latest active version to access new updates and features. | |||
admin_notification.update.current_version_inactive=You’re running a version of SonarQube that is no longer active. Please upgrade to an active version immediately. | |||
#------------------------------------------------------------------------------ | |||
# | |||
@@ -3837,7 +3836,7 @@ system.hide_intermediate_versions=Hide intermediate versions | |||
system.how_to_upgrade=How to upgrade? | |||
system.latest_version=Latest Version | |||
system.latest_patch=Patch Release | |||
system.lts_version=Latest LTS Version | |||
system.lta_version=Latest LTA Version | |||
system.log_level.warning=This level has performance impacts, please make sure to get back to INFO level once your investigation is done. Please note that when the server is restarted, logging will revert to the level configured in sonar.properties. | |||
system.log_level.warning.short=Current logs level has performance impacts, get back to INFO level. | |||
system.log_level.info=Your selection does not affect the Search Engine. |