diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2021-10-14 18:06:41 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-10-21 20:04:01 +0000 |
commit | 667542fa30857ee17c1317e19d67f62e62e44b44 (patch) | |
tree | 921b5837c2e68b86c2c4a5f9e35662bb007fc755 /server | |
parent | 8ce408e17dd34e2401bd34394d5b21e8b790f836 (diff) | |
download | sonarqube-667542fa30857ee17c1317e19d67f62e62e44b44.tar.gz sonarqube-667542fa30857ee17c1317e19d67f62e62e44b44.zip |
SONAR-15507 Add a prompt to warn when SonarQube need an update
Diffstat (limited to 'server')
29 files changed, 784 insertions, 216 deletions
diff --git a/server/sonar-web/src/main/js/api/system.ts b/server/sonar-web/src/main/js/api/system.ts index 9adf55ea9dc..7a613374ec3 100644 --- a/server/sonar-web/src/main/js/api/system.ts +++ b/server/sonar-web/src/main/js/api/system.ts @@ -35,6 +35,7 @@ export function getSystemStatus(): Promise<{ id: string; version: string; status export function getSystemUpgrades(): Promise<{ upgrades: SystemUpgrade[]; + latestLTS: string; updateCenterRefresh: string; }> { return getJSON('/api/system/upgrades'); diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 68763f336ea..e31bf86e066 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -28,6 +28,7 @@ import IndexationContextProvider from './indexation/IndexationContextProvider'; import IndexationNotification from './indexation/IndexationNotification'; import GlobalNav from './nav/global/GlobalNav'; import StartupModal from './StartupModal'; +import UpdateNotification from './update-notification/UpdateNotification'; export interface Props { children: React.ReactNode; @@ -52,6 +53,7 @@ export default function GlobalContainer(props: Props) { <GlobalNav location={props.location} /> <GlobalMessagesContainer /> <IndexationNotification /> + <UpdateNotification /> {props.children} </IndexationContextProvider> </Workspace> diff --git a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap index cf87ae0d4f2..7f04cba74c2 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/GlobalContainer-test.tsx.snap @@ -32,6 +32,7 @@ exports[`should render correctly 1`] = ` /> <Connect(GlobalMessages) /> <Connect(withCurrentUser(withIndexationContext(IndexationNotification))) /> + <Connect(withCurrentUser(Connect(withAppState(UpdateNotification)))) /> <ChildComponent /> </Connect(withAppState(IndexationContextProvider))> </Workspace> diff --git a/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.css b/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.css new file mode 100644 index 00000000000..fc23a2cb9f8 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.css @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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. + */ +.promote-update-notification.dismissable-alert-wrapper { + height: 42px; +} + +.promote-update-notification .dismissable-alert-banner { + margin-bottom: 0 !important; + position: fixed; + width: 100%; + z-index: var(--globalBannerZIndex); +} diff --git a/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx b/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx new file mode 100644 index 00000000000..1a3a23b4f5f --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/update-notification/UpdateNotification.tsx @@ -0,0 +1,241 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { groupBy, isEmpty, mapValues } from 'lodash'; +import * as React from 'react'; +import { getSystemUpgrades } from '../../../api/system'; +import { withAppState } from '../../../components/hoc/withAppState'; +import { withCurrentUser } from '../../../components/hoc/withCurrentUser'; +import { AlertVariant } from '../../../components/ui/Alert'; +import DismissableAlert from '../../../components/ui/DismissableAlert'; +import SystemUpgradeButton from '../../../components/upgrade/SystemUpgradeButton'; +import { sortUpgrades } from '../../../components/upgrade/utils'; +import { translate } from '../../../helpers/l10n'; +import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; +import { Permissions } from '../../../types/permissions'; +import { SystemUpgrade } from '../../../types/system'; +import './UpdateNotification.css'; + +const MONTH_BEFOR_PREVIOUS_LTS_NOTIFICATION = 6; + +type GroupedSystemUpdate = { + [x: string]: T.Dict<SystemUpgrade[]>; +}; + +enum UseCase { + NewMinorVersion = 'new_minor_version', + NewPatch = 'new_patch', + PreLTS = 'pre_lts', + PreviousLTS = 'previous_lts' +} + +const MAP_VARIANT: T.Dict<AlertVariant> = { + [UseCase.NewMinorVersion]: 'info', + [UseCase.NewPatch]: 'warning', + [UseCase.PreLTS]: 'warning', + [UseCase.PreviousLTS]: 'error' +}; + +interface Props { + appState: Pick<T.AppState, 'version'>; + currentUser: T.CurrentUser; +} + +interface State { + dismissKey: string; + useCase: UseCase; + systemUpgrades: SystemUpgrade[]; + canSeeNotification: boolean; +} + +export class UpdateNotification extends React.PureComponent<Props, State> { + mounted = false; + versionParser = /^(\d+)\.(\d+)(\.(\d+))?/; + + constructor(props: Props) { + super(props); + this.state = { + dismissKey: '', + systemUpgrades: [], + canSeeNotification: false, + useCase: UseCase.NewMinorVersion + }; + this.fetchSystemUpgradeInformation(); + } + + componentDidMount() { + this.mounted = true; + } + + componentDidUpdate(prevProps: Props) { + if ( + prevProps.currentUser !== this.props.currentUser || + this.props.appState.version !== prevProps.appState.version + ) { + this.fetchSystemUpgradeInformation(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + isPreLTSUpdate(parsedVersion: number[], latestLTS: string) { + const [currentMajor, currentMinor] = parsedVersion; + const [ltsMajor, ltsMinor] = latestLTS.split('.').map(Number); + return currentMajor < ltsMajor || (currentMajor === ltsMajor && currentMinor < ltsMinor); + } + + isPreviousLTSUpdate( + parsedVersion: number[], + latestLTS: string, + systemUpgrades: GroupedSystemUpdate + ) { + const [ltsMajor, ltsMinor] = latestLTS.split('.').map(Number); + let ltsOlderThan6Month = false; + const beforeLts = this.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; + } + + isMinorUpdate(parsedVersion: number[], systemUpgrades: GroupedSystemUpdate) { + const [currentMajor, currentMinor] = parsedVersion; + const allMinor = systemUpgrades[currentMajor]; + return Object.keys(allMinor) + .map(Number) + .some(minor => minor > currentMinor); + } + + isPatchUpdate(parsedVersion: number[], systemUpgrades: GroupedSystemUpdate) { + const [currentMajor, currentMinor, currentPatch] = parsedVersion; + const allMinor = systemUpgrades[currentMajor]; + const allPatch = sortUpgrades(allMinor[currentMinor] || []); + + if (!isEmpty(allPatch)) { + const [, , latestPatch] = allPatch[0].version.split('.').map(Number); + const effectiveCurrentPatch = isNaN(currentPatch) ? 0 : currentPatch; + const effectiveLatestPatch = isNaN(latestPatch) ? 0 : latestPatch; + return effectiveCurrentPatch < effectiveLatestPatch; + } + return false; + } + + async fetchSystemUpgradeInformation() { + if ( + !isLoggedIn(this.props.currentUser) || + !hasGlobalPermission(this.props.currentUser, Permissions.Admin) + ) { + this.noPromptToShow(); + return; + } + + const regExpParsedVersion = this.versionParser.exec(this.props.appState.version); + if (regExpParsedVersion === null) { + this.noPromptToShow(); + return; + } + regExpParsedVersion.shift(); + const parsedVersion = regExpParsedVersion.map(Number).map(n => (isNaN(n) ? 0 : n)); + + const { upgrades, latestLTS } = await getSystemUpgrades(); + + if (isEmpty(upgrades)) { + // No new upgrades + this.noPromptToShow(); + return; + } + const systemUpgrades = mapValues( + groupBy(upgrades, upgrade => { + const [major] = upgrade.version.split('.'); + return major; + }), + upgrades => + groupBy(upgrades, upgrade => { + const [, minor] = upgrade.version.split('.'); + return minor; + }) + ); + + let useCase = UseCase.NewMinorVersion; + + if (this.isPreviousLTSUpdate(parsedVersion, latestLTS, systemUpgrades)) { + useCase = UseCase.PreviousLTS; + } else if (this.isPreLTSUpdate(parsedVersion, latestLTS)) { + useCase = UseCase.PreLTS; + } else if (this.isPatchUpdate(parsedVersion, systemUpgrades)) { + useCase = UseCase.NewPatch; + } else if (this.isMinorUpdate(parsedVersion, systemUpgrades)) { + useCase = UseCase.NewMinorVersion; + } + + const latest = [...upgrades].sort( + (upgrade1, upgrade2) => + new Date(upgrade2.releaseDate || '').getTime() - + new Date(upgrade1.releaseDate || '').getTime() + )[0]; + + const dismissKey = useCase + latest.version; + + if (this.mounted) { + this.setState({ + useCase, + dismissKey, + systemUpgrades: upgrades, + canSeeNotification: true + }); + } + } + + noPromptToShow() { + if (this.mounted) { + this.setState({ canSeeNotification: false }); + } + } + + render() { + const { systemUpgrades, canSeeNotification, useCase, dismissKey } = this.state; + if (!canSeeNotification) { + return null; + } + return ( + <DismissableAlert + alertKey={dismissKey} + variant={MAP_VARIANT[useCase]} + className="promote-update-notification"> + {translate('admin_notification.update', useCase)} + <SystemUpgradeButton systemUpgrades={systemUpgrades} /> + </DismissableAlert> + ); + } +} + +export default withCurrentUser(withAppState(UpdateNotification)); diff --git a/server/sonar-web/src/main/js/app/components/update-notification/__tests__/UpdateNotification-test.tsx b/server/sonar-web/src/main/js/app/components/update-notification/__tests__/UpdateNotification-test.tsx new file mode 100644 index 00000000000..6c284a04610 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/update-notification/__tests__/UpdateNotification-test.tsx @@ -0,0 +1,153 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { getSystemUpgrades } from '../../../../api/system'; +import DismissableAlert from '../../../../components/ui/DismissableAlert'; +import { mockUpgrades } from '../../../../helpers/mocks/system-upgrades'; +import { mockAppState, mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { Permissions } from '../../../../types/permissions'; +import { UpdateNotification } from '../UpdateNotification'; + +jest.mock('../../../../api/system', () => { + const { mockUpgrades } = jest.requireActual('../../../../helpers/mocks/system-upgrades'); + return { + getSystemUpgrades: jest.fn().mockResolvedValue({ upgrades: [mockUpgrades()], latestLTS: '8.9' }) + }; +}); + +function formatDate(date: Date): string { + return `${date.getFullYear()}-${date.getMonth()}-${date.getDay()}`; +} + +it('should not show prompt when not admin', async () => { + //As anonymous + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(wrapper.type()).toBeNull(); + + // As non admin user + wrapper.setProps({ currentUser: mockLoggedInUser() }); + await waitAndUpdate(wrapper); + expect(wrapper.type()).toBeNull(); +}); + +it('should not show prompt when no current version', async () => { + const wrapper = shallowRender({ appState: mockAppState({ version: 'NOVERSION' }) }); + await waitAndUpdate(wrapper); + expect(wrapper.type()).toBeNull(); +}); + +it('should not show prompt when no upgrade', async () => { + (getSystemUpgrades as jest.Mock).mockResolvedValueOnce({ upgrades: [], latestLTS: '8.9' }); + const wrapper = shallowRender({ + appState: mockAppState({ version: '9.1' }), + currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }) + }); + await waitAndUpdate(wrapper); + expect(wrapper.type()).toBeNull(); +}); + +it('should show prompt when no lts date', async () => { + (getSystemUpgrades as jest.Mock).mockResolvedValueOnce({ + upgrades: [mockUpgrades({ version: '8.9', releaseDate: 'INVALID' })], + latestLTS: '8.9' + }); + const wrapper = shallowRender({ + appState: mockAppState({ version: '8.1' }), + currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }) + }); + await waitAndUpdate(wrapper); + expect(wrapper.find(DismissableAlert).props().alertKey).toBe('previous_lts8.9'); + expect(wrapper.contains('admin_notification.update.previous_lts')).toBe(true); +}); + +it('should show prompt when minor upgrade', async () => { + (getSystemUpgrades as jest.Mock).mockResolvedValueOnce({ + upgrades: [mockUpgrades({ version: '9.2' }), mockUpgrades({ version: '9.1' })], + latestLTS: '8.9' + }); + const wrapper = shallowRender({ + appState: mockAppState({ version: '9.1' }), + currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }) + }); + await waitAndUpdate(wrapper); + expect(wrapper.find(DismissableAlert).props().alertKey).toBe('new_minor_version9.2'); + expect(wrapper.contains('admin_notification.update.new_minor_version')).toBe(true); +}); + +it('should show prompt when patch upgrade', async () => { + (getSystemUpgrades as jest.Mock).mockResolvedValueOnce({ + upgrades: [mockUpgrades({ version: '9.2' }), mockUpgrades({ version: '9.1.1' })], + latestLTS: '8.9' + }); + const wrapper = shallowRender({ + appState: mockAppState({ version: '9.1' }), + currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }) + }); + await waitAndUpdate(wrapper); + expect(wrapper.find(DismissableAlert).props().alertKey).toBe('new_patch9.2'); + expect(wrapper.contains('admin_notification.update.new_patch')).toBe(true); +}); + +it('should show prompt when lts upgrade', async () => { + (getSystemUpgrades as jest.Mock).mockResolvedValueOnce({ + upgrades: [ + mockUpgrades({ version: '8.9', releaseDate: formatDate(new Date(Date.now())) }), + mockUpgrades({ version: '9.2' }), + mockUpgrades({ version: '9.1.1' }) + ], + latestLTS: '8.9' + }); + const wrapper = shallowRender({ + appState: mockAppState({ version: '8.8' }), + currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }) + }); + await waitAndUpdate(wrapper); + expect(wrapper.find(DismissableAlert).props().alertKey).toBe('pre_lts8.9'); + expect(wrapper.contains('admin_notification.update.pre_lts')).toBe(true); +}); + +it('should show prompt when lts upgrade is more than 6 month', async () => { + const ltsDate = new Date(Date.now()); + ltsDate.setMonth(ltsDate.getMonth() - 7); + (getSystemUpgrades as jest.Mock).mockResolvedValueOnce({ + upgrades: [ + mockUpgrades({ version: '8.9', releaseDate: formatDate(ltsDate) }), + mockUpgrades({ version: '9.2' }), + mockUpgrades({ version: '9.1.1' }) + ], + latestLTS: '8.9' + }); + const wrapper = shallowRender({ + appState: mockAppState({ version: '8.8' }), + currentUser: mockLoggedInUser({ permissions: { global: [Permissions.Admin] } }) + }); + await waitAndUpdate(wrapper); + expect(wrapper.find(DismissableAlert).props().alertKey).toBe('previous_lts8.9'); + expect(wrapper.contains('admin_notification.update.previous_lts')).toBe(true); +}); + +function shallowRender(props: Partial<UpdateNotification['props']> = {}) { + return shallow( + <UpdateNotification appState={mockAppState()} currentUser={mockCurrentUser()} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts index 331497b902d..3194894a6ab 100644 --- a/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts @@ -19,7 +19,6 @@ */ import { mockClusterSysInfo, mockStandaloneSysInfo } from '../../../helpers/testMocks'; -import { SystemUpgrade } from '../../../types/system'; import * as u from '../utils'; describe('parseQuery', () => { @@ -73,54 +72,6 @@ describe('getSystemLogsLevel', () => { }); }); -describe('sortUpgrades', () => { - it('should sort correctly versions', () => { - expect( - u.sortUpgrades([ - { version: '5.4.2' }, - { version: '5.10' }, - { version: '5.1' }, - { version: '5.4' } - ] as SystemUpgrade[]) - ).toEqual([{ version: '5.10' }, { version: '5.4.2' }, { version: '5.4' }, { version: '5.1' }]); - expect( - u.sortUpgrades([ - { version: '5.10' }, - { version: '5.1.2' }, - { version: '6.0' }, - { version: '6.9' } - ] as SystemUpgrade[]) - ).toEqual([{ version: '6.9' }, { version: '6.0' }, { version: '5.10' }, { version: '5.1.2' }]); - }); -}); - -describe('groupUpgrades', () => { - it('should group correctly', () => { - expect( - u.groupUpgrades([ - { version: '5.10' }, - { version: '5.4.2' }, - { version: '5.4' }, - { version: '5.1' } - ] as SystemUpgrade[]) - ).toEqual([ - [{ version: '5.10' }, { version: '5.4.2' }, { version: '5.4' }, { version: '5.1' }] - ]); - expect( - u.groupUpgrades([ - { version: '6.9' }, - { version: '6.7' }, - { version: '6.0' }, - { version: '5.10' }, - { version: '5.4.2' } - ] as SystemUpgrade[]) - ).toEqual([ - [{ version: '6.9' }, { version: '6.7' }, { version: '6.0' }], - [{ version: '5.10' }, { version: '5.4.2' }] - ]); - }); -}); - describe('isCluster', () => { it('should return the correct information', () => { expect(u.isCluster(mockClusterSysInfo())).toBe(true); diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeNotif.tsx b/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeNotif.tsx index 5655488d6d8..e6f7c4c0903 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeNotif.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeNotif.tsx @@ -19,21 +19,18 @@ */ import * as React from 'react'; import { getSystemUpgrades } from '../../../../api/system'; -import { Button } from '../../../../components/controls/buttons'; import { Alert } from '../../../../components/ui/Alert'; +import SystemUpgradeButton from '../../../../components/upgrade/SystemUpgradeButton'; import { translate } from '../../../../helpers/l10n'; import { SystemUpgrade } from '../../../../types/system'; -import { groupUpgrades, sortUpgrades } from '../../utils'; -import SystemUpgradeForm from './SystemUpgradeForm'; interface State { - systemUpgrades: SystemUpgrade[][]; - openSystemUpgradeForm: boolean; + systemUpgrades: SystemUpgrade[]; } export default class SystemUpgradeNotif extends React.PureComponent<{}, State> { mounted = false; - state: State = { openSystemUpgradeForm: false, systemUpgrades: [] }; + state: State = { systemUpgrades: [] }; componentDidMount() { this.mounted = true; @@ -48,20 +45,12 @@ export default class SystemUpgradeNotif extends React.PureComponent<{}, State> { getSystemUpgrades().then( ({ upgrades }) => { if (this.mounted) { - this.setState({ systemUpgrades: groupUpgrades(sortUpgrades(upgrades)) }); + this.setState({ systemUpgrades: upgrades }); } }, () => {} ); - handleOpenSystemUpgradeForm = () => { - this.setState({ openSystemUpgradeForm: true }); - }; - - handleCloseSystemUpgradeForm = () => { - this.setState({ openSystemUpgradeForm: false }); - }; - render() { const { systemUpgrades } = this.state; @@ -73,16 +62,8 @@ export default class SystemUpgradeNotif extends React.PureComponent<{}, State> { <div className="page-notifs"> <Alert variant="info"> {translate('system.new_version_available')} - <Button className="spacer-left" onClick={this.handleOpenSystemUpgradeForm}> - {translate('learn_more')} - </Button> + <SystemUpgradeButton systemUpgrades={systemUpgrades} /> </Alert> - {this.state.openSystemUpgradeForm && ( - <SystemUpgradeForm - onClose={this.handleCloseSystemUpgradeForm} - systemUpgrades={systemUpgrades} - /> - )} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeNotif-test.tsx b/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeNotif-test.tsx index 7ad1e031157..61673af52a2 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeNotif-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeNotif-test.tsx @@ -20,7 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { getSystemUpgrades } from '../../../../../api/system'; -import { click, waitAndUpdate } from '../../../../../helpers/testUtils'; +import { waitAndUpdate } from '../../../../../helpers/testUtils'; import SystemUpgradeNotif from '../SystemUpgradeNotif'; jest.mock('../../../../../api/system', () => ({ @@ -83,9 +83,6 @@ it('should render correctly', async () => { expect(getSystemUpgrades).toHaveBeenCalled(); expect(wrapper).toMatchSnapshot(); - - click(wrapper.find('Button')); - expect(wrapper).toMatchSnapshot(); }); it('should display nothing', async () => { diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeNotif-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeNotif-test.tsx.snap index 88e3fa2d032..3e3d05a2f7b 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeNotif-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeNotif-test.tsx.snap @@ -8,61 +8,32 @@ exports[`should render correctly 1`] = ` variant="info" > system.new_version_available - <Button - className="spacer-left" - onClick={[Function]} - > - learn_more - </Button> - </Alert> -</div> -`; - -exports[`should render correctly 2`] = ` -<div - className="page-notifs" -> - <Alert - variant="info" - > - system.new_version_available - <Button - className="spacer-left" - onClick={[Function]} - > - learn_more - </Button> - </Alert> - <Connect(withAppState(SystemUpgradeForm)) - onClose={[Function]} - systemUpgrades={ - Array [ + <SystemUpgradeButton + systemUpgrades={ Array [ Object { "changeLogUrl": "changelogurl", - "description": "Version 6.4 description", + "description": "Version 5.6.7 description", "downloadUrl": "downloadurl", "plugins": Object {}, - "releaseDate": "2017-06-02", - "version": "6.4", + "releaseDate": "2017-03-01", + "version": "5.6.7", }, Object { "changeLogUrl": "changelogurl", - "description": "Version 6.3 description", + "description": "Version 5.6.5 description", "downloadUrl": "downloadurl", "plugins": Object {}, - "releaseDate": "2017-05-02", - "version": "6.3", + "releaseDate": "2017-03-01", + "version": "5.6.5", }, - ], - Array [ Object { "changeLogUrl": "changelogurl", - "description": "Version 5.6.7 description", + "description": "Version 6.3 description", "downloadUrl": "downloadurl", "plugins": Object {}, - "releaseDate": "2017-03-01", - "version": "5.6.7", + "releaseDate": "2017-05-02", + "version": "6.3", }, Object { "changeLogUrl": "changelogurl", @@ -74,15 +45,15 @@ exports[`should render correctly 2`] = ` }, Object { "changeLogUrl": "changelogurl", - "description": "Version 5.6.5 description", + "description": "Version 6.4 description", "downloadUrl": "downloadurl", "plugins": Object {}, - "releaseDate": "2017-03-01", - "version": "5.6.5", + "releaseDate": "2017-06-02", + "version": "6.4", }, - ], - ] - } - /> + ] + } + /> + </Alert> </div> `; diff --git a/server/sonar-web/src/main/js/apps/system/utils.ts b/server/sonar-web/src/main/js/apps/system/utils.ts index 4a4c7f28242..a9857503449 100644 --- a/server/sonar-web/src/main/js/apps/system/utils.ts +++ b/server/sonar-web/src/main/js/apps/system/utils.ts @@ -17,10 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { each, groupBy, memoize, omit, omitBy, pickBy, sortBy } from 'lodash'; +import { each, memoize, omit, omitBy, pickBy, sortBy } from 'lodash'; import { formatMeasure } from '../../helpers/measures'; import { cleanQuery, parseAsArray, parseAsString, serializeStringArray } from '../../helpers/query'; -import { SystemUpgrade } from '../../types/system'; export interface Query { expandedCards: string[]; @@ -229,17 +228,3 @@ export const serializeQuery = memoize( expand: serializeStringArray(query.expandedCards) }) ); - -export function sortUpgrades(upgrades: SystemUpgrade[]): SystemUpgrade[] { - return sortBy(upgrades, [ - (upgrade: SystemUpgrade) => -Number(upgrade.version.split('.')[0]), - (upgrade: SystemUpgrade) => -Number(upgrade.version.split('.')[1] || 0), - (upgrade: SystemUpgrade) => -Number(upgrade.version.split('.')[2] || 0) - ]); -} - -export function groupUpgrades(upgrades: SystemUpgrade[]): SystemUpgrade[][] { - const groupedVersions = groupBy(upgrades, upgrade => upgrade.version.split('.')[0]); - const sortedMajor = sortBy(Object.keys(groupedVersions), key => -Number(key)); - return sortedMajor.map(key => groupedVersions[key]); -} diff --git a/server/sonar-web/src/main/js/components/ui/Alert.tsx b/server/sonar-web/src/main/js/components/ui/Alert.tsx index 0c7f2435632..5ed07a1d665 100644 --- a/server/sonar-web/src/main/js/components/ui/Alert.tsx +++ b/server/sonar-web/src/main/js/components/ui/Alert.tsx @@ -30,7 +30,7 @@ import InfoIcon from '../icons/InfoIcon'; import DeferredSpinner from './DeferredSpinner'; type AlertDisplay = 'banner' | 'inline' | 'block'; -type AlertVariant = 'error' | 'warning' | 'success' | 'info' | 'loading'; +export type AlertVariant = 'error' | 'warning' | 'success' | 'info' | 'loading'; export interface AlertProps { display?: AlertDisplay; diff --git a/server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx b/server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx index b6f5e08fb33..2d3ae9f0a30 100644 --- a/server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx +++ b/server/sonar-web/src/main/js/components/ui/DismissableAlert.tsx @@ -45,25 +45,25 @@ export default function DismissableAlert(props: DismissableAlertProps) { }, [alertKey]); const hideAlert = () => { + window.dispatchEvent(new Event('resize')); save(DISMISSED_ALERT_STORAGE_KEY, 'true', alertKey); }; return !show ? null : ( - <Alert - className={classNames(`dismissable-alert-${display}`, className)} - display={display} - variant={variant}> - <div className="display-flex-center dismissable-alert-content"> - <div className="flex-1">{children}</div> - <ButtonIcon - aria-label={translate('alert.dismiss')} - onClick={() => { - hideAlert(); - setShow(false); - }}> - <ClearIcon size={12} thin={true} /> - </ButtonIcon> - </div> - </Alert> + <div className={classNames('dismissable-alert-wrapper', className)}> + <Alert className={`dismissable-alert-${display}`} display={display} variant={variant}> + <div className="display-flex-center dismissable-alert-content"> + <div className="flex-1">{children}</div> + <ButtonIcon + aria-label={translate('alert.dismiss')} + onClick={() => { + hideAlert(); + setShow(false); + }}> + <ClearIcon size={12} thin={true} /> + </ButtonIcon> + </div> + </Alert> + </div> ); } diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap index 7583076acd6..4c3e6bb89e7 100644 --- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DismissableAlert-test.tsx.snap @@ -1,59 +1,67 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -<Alert - className="dismissable-alert-banner" - display="banner" - variant="info" +<div + className="dismissable-alert-wrapper" > - <div - className="display-flex-center dismissable-alert-content" + <Alert + className="dismissable-alert-banner" + display="banner" + variant="info" > <div - className="flex-1" + className="display-flex-center dismissable-alert-content" > - <div> - My content + <div + className="flex-1" + > + <div> + My content + </div> </div> + <ButtonIcon + aria-label="alert.dismiss" + onClick={[Function]} + > + <ClearIcon + size={12} + thin={true} + /> + </ButtonIcon> </div> - <ButtonIcon - aria-label="alert.dismiss" - onClick={[Function]} - > - <ClearIcon - size={12} - thin={true} - /> - </ButtonIcon> - </div> -</Alert> + </Alert> +</div> `; exports[`should render correctly with a non-default display 1`] = ` -<Alert - className="dismissable-alert-block" - display="block" - variant="info" +<div + className="dismissable-alert-wrapper" > - <div - className="display-flex-center dismissable-alert-content" + <Alert + className="dismissable-alert-block" + display="block" + variant="info" > <div - className="flex-1" + className="display-flex-center dismissable-alert-content" > - <div> - My content + <div + className="flex-1" + > + <div> + My content + </div> </div> + <ButtonIcon + aria-label="alert.dismiss" + onClick={[Function]} + > + <ClearIcon + size={12} + thin={true} + /> + </ButtonIcon> </div> - <ButtonIcon - aria-label="alert.dismiss" - onClick={[Function]} - > - <ClearIcon - size={12} - thin={true} - /> - </ButtonIcon> - </div> -</Alert> + </Alert> +</div> `; diff --git a/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx new file mode 100644 index 00000000000..cd3a527ef9b --- /dev/null +++ b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeButton.tsx @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 * as React from 'react'; +import { translate } from '../../helpers/l10n'; +import { SystemUpgrade } from '../../types/system'; +import { Button } from '../controls/buttons'; +import SystemUpgradeForm from './SystemUpgradeForm'; +import { groupUpgrades, sortUpgrades } from './utils'; + +interface Props { + systemUpgrades: SystemUpgrade[]; +} + +interface State { + openSystemUpgradeForm: boolean; +} + +export default class SystemUpgradeButton extends React.PureComponent<Props, State> { + state: State = { openSystemUpgradeForm: false }; + + handleOpenSystemUpgradeForm = () => { + this.setState({ openSystemUpgradeForm: true }); + }; + + handleCloseSystemUpgradeForm = () => { + this.setState({ openSystemUpgradeForm: false }); + }; + + render() { + const { systemUpgrades } = this.props; + const { openSystemUpgradeForm } = this.state; + return ( + <> + <Button className="spacer-left" onClick={this.handleOpenSystemUpgradeForm}> + {translate('learn_more')} + </Button> + {openSystemUpgradeForm && ( + <SystemUpgradeForm + onClose={this.handleCloseSystemUpgradeForm} + systemUpgrades={groupUpgrades(sortUpgrades(systemUpgrades))} + /> + )} + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeForm.tsx b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx index b4a25a71963..9a845dbc573 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeForm.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeForm.tsx @@ -18,12 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { ResetButtonLink } from '../../../../components/controls/buttons'; -import Modal from '../../../../components/controls/Modal'; -import { withAppState } from '../../../../components/hoc/withAppState'; -import { translate } from '../../../../helpers/l10n'; -import { EditionKey } from '../../../../types/editions'; -import { SystemUpgrade } from '../../../../types/system'; +import { translate } from '../../helpers/l10n'; +import { EditionKey } from '../../types/editions'; +import { SystemUpgrade } from '../../types/system'; +import { ResetButtonLink } from '../controls/buttons'; +import Modal from '../controls/Modal'; +import { withAppState } from '../hoc/withAppState'; import SystemUpgradeItem from './SystemUpgradeItem'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeIntermediate.tsx b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeIntermediate.tsx index b059fe41805..db09a7ef8ce 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeIntermediate.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeIntermediate.tsx @@ -18,11 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { ButtonLink } from '../../../../components/controls/buttons'; -import DropdownIcon from '../../../../components/icons/DropdownIcon'; -import DateFormatter from '../../../../components/intl/DateFormatter'; -import { translate } from '../../../../helpers/l10n'; -import { SystemUpgrade } from '../../../../types/system'; +import { translate } from '../../helpers/l10n'; +import { SystemUpgrade } from '../../types/system'; +import { ButtonLink } from '../controls/buttons'; +import DropdownIcon from '../icons/DropdownIcon'; +import DateFormatter from '../intl/DateFormatter'; interface Props { className?: string; diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeItem.tsx b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeItem.tsx index d5c605cbc4b..756455472c6 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/SystemUpgradeItem.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/SystemUpgradeItem.tsx @@ -19,15 +19,15 @@ */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import DateFormatter from '../../../../components/intl/DateFormatter'; import { getEdition, getEditionDownloadFilename, getEditionDownloadUrl -} from '../../../../helpers/editions'; -import { translate, translateWithParameters } from '../../../../helpers/l10n'; -import { EditionKey } from '../../../../types/editions'; -import { SystemUpgrade } from '../../../../types/system'; +} from '../../helpers/editions'; +import { translate, translateWithParameters } from '../../helpers/l10n'; +import { EditionKey } from '../../types/editions'; +import { SystemUpgrade } from '../../types/system'; +import DateFormatter from '../intl/DateFormatter'; import SystemUpgradeIntermediate from './SystemUpgradeIntermediate'; export interface SystemUpgradeItemProps { diff --git a/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeButton-test.tsx b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeButton-test.tsx new file mode 100644 index 00000000000..df853f7207c --- /dev/null +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeButton-test.tsx @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { click } from '../../../helpers/testUtils'; +import { Button } from '../../controls/buttons'; +import SystemUpgradeButton from '../SystemUpgradeButton'; +import SystemUpgradeForm from '../SystemUpgradeForm'; + +it('should open modal correctly', () => { + const wrapper = shallowRender(); + expect(wrapper).toMatchSnapshot(); + click(wrapper.find(Button)); + expect(wrapper.find(SystemUpgradeForm)).toBeDefined(); +}); + +function shallowRender(props: Partial<SystemUpgradeButton['props']> = {}) { + return shallow<SystemUpgradeButton['props']>( + <SystemUpgradeButton systemUpgrades={[]} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeForm-test.tsx b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeForm-test.tsx index 02bf8a136bb..7e33af49609 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeForm-test.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeForm-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { EditionKey } from '../../../../../types/editions'; +import { EditionKey } from '../../../types/editions'; import { SystemUpgradeForm } from '../SystemUpgradeForm'; const UPGRADES = [ diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeIntermediate-test.tsx b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeIntermediate-test.tsx index c2d05b1853f..23d79ea71d5 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeIntermediate-test.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeIntermediate-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { click } from '../../../../../helpers/testUtils'; +import { click } from '../../../helpers/testUtils'; import SystemUpgradeIntermediate from '../SystemUpgradeIntermediate'; const UPGRADES = [ diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeItem-test.tsx b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeItem-test.tsx index 6b775f034df..ba705ab5238 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/SystemUpgradeItem-test.tsx +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/SystemUpgradeItem-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import { EditionKey } from '../../../../../types/editions'; +import { EditionKey } from '../../../types/editions'; import SystemUpgradeItem, { SystemUpgradeItemProps } from '../SystemUpgradeItem'; it('should display correctly', () => { diff --git a/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeButton-test.tsx.snap b/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeButton-test.tsx.snap new file mode 100644 index 00000000000..037b14bb219 --- /dev/null +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeButton-test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should open modal correctly 1`] = ` +<Fragment> + <Button + className="spacer-left" + onClick={[Function]} + > + learn_more + </Button> +</Fragment> +`; diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeForm-test.tsx.snap b/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeForm-test.tsx.snap index e1c80419910..e1c80419910 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeForm-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeIntermediate-test.tsx.snap b/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeIntermediate-test.tsx.snap index 763ab35301a..763ab35301a 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeIntermediate-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeIntermediate-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeItem-test.tsx.snap b/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeItem-test.tsx.snap index 51d35d94dc6..51d35d94dc6 100644 --- a/server/sonar-web/src/main/js/apps/system/components/system-upgrade/__tests__/__snapshots__/SystemUpgradeItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/__snapshots__/SystemUpgradeItem-test.tsx.snap diff --git a/server/sonar-web/src/main/js/components/upgrade/__tests__/utils-test.ts b/server/sonar-web/src/main/js/components/upgrade/__tests__/utils-test.ts new file mode 100644 index 00000000000..34d19105097 --- /dev/null +++ b/server/sonar-web/src/main/js/components/upgrade/__tests__/utils-test.ts @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { SystemUpgrade } from '../../../types/system'; +import * as u from '../utils'; + +describe('sortUpgrades', () => { + it('should sort correctly versions', () => { + expect( + u.sortUpgrades([ + { version: '5.4.2' }, + { version: '5.10' }, + { version: '5.1' }, + { version: '5.4' } + ] as SystemUpgrade[]) + ).toEqual([{ version: '5.10' }, { version: '5.4.2' }, { version: '5.4' }, { version: '5.1' }]); + expect( + u.sortUpgrades([ + { version: '5.10' }, + { version: '5.1.2' }, + { version: '6.0' }, + { version: '6.9' } + ] as SystemUpgrade[]) + ).toEqual([{ version: '6.9' }, { version: '6.0' }, { version: '5.10' }, { version: '5.1.2' }]); + }); +}); + +describe('groupUpgrades', () => { + it('should group correctly', () => { + expect( + u.groupUpgrades([ + { version: '5.10' }, + { version: '5.4.2' }, + { version: '5.4' }, + { version: '5.1' } + ] as SystemUpgrade[]) + ).toEqual([ + [{ version: '5.10' }, { version: '5.4.2' }, { version: '5.4' }, { version: '5.1' }] + ]); + expect( + u.groupUpgrades([ + { version: '6.9' }, + { version: '6.7' }, + { version: '6.0' }, + { version: '5.10' }, + { version: '5.4.2' } + ] as SystemUpgrade[]) + ).toEqual([ + [{ version: '6.9' }, { version: '6.7' }, { version: '6.0' }], + [{ version: '5.10' }, { version: '5.4.2' }] + ]); + }); +}); diff --git a/server/sonar-web/src/main/js/components/upgrade/utils.ts b/server/sonar-web/src/main/js/components/upgrade/utils.ts new file mode 100644 index 00000000000..e58b76a2bc0 --- /dev/null +++ b/server/sonar-web/src/main/js/components/upgrade/utils.ts @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { groupBy, sortBy } from 'lodash'; +import { SystemUpgrade } from '../../types/system'; + +export function sortUpgrades(upgrades: SystemUpgrade[]): SystemUpgrade[] { + return sortBy(upgrades, [ + (upgrade: SystemUpgrade) => -Number(upgrade.version.split('.')[0]), + (upgrade: SystemUpgrade) => -Number(upgrade.version.split('.')[1] || 0), + (upgrade: SystemUpgrade) => -Number(upgrade.version.split('.')[2] || 0) + ]); +} + +export function groupUpgrades(upgrades: SystemUpgrade[]): SystemUpgrade[][] { + const groupedVersions = groupBy(upgrades, upgrade => upgrade.version.split('.')[0]); + const sortedMajor = sortBy(Object.keys(groupedVersions), key => -Number(key)); + return sortedMajor.map(key => groupedVersions[key]); +} diff --git a/server/sonar-web/src/main/js/helpers/mocks/system-upgrades.ts b/server/sonar-web/src/main/js/helpers/mocks/system-upgrades.ts new file mode 100644 index 00000000000..ba1249c30cf --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/mocks/system-upgrades.ts @@ -0,0 +1,31 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { SystemUpgrade } from '../../types/system'; + +export function mockUpgrades(override: Partial<SystemUpgrade>): SystemUpgrade { + return { + version: '5.6.7', + description: 'Version 5.6.7 description', + releaseDate: '2017-03-01', + changeLogUrl: 'changelogurl', + downloadUrl: 'downloadurl', + ...override + }; +} |