Преглед на файлове

SONAR-10696 Remove ability to upgrade/downgrade an edition from Marketplace (#269)

* SONAR-10699 Remove upgrade/downgrade buttons
* SONAR-10697 Drop edition.json support in the marketplace
* SONAR-10717 Drop 'sonar.editions.jsonUrl' property
* SONAR-10702 Edition's "Learn more" links redirect to the form page with arguments
* SONAR-10698 Get Edition data from the doc
* SONAR-10700 Remove LicenseEditionForm and LicenseEditionSet from Marketplace
tags/7.5
Grégoire Aubert преди 6 години
родител
ревизия
bdde08fff5
променени са 48 файла, в които са добавени 305 реда и са изтрити 2346 реда
  1. 1
    1
      server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
  2. 3
    0
      server/sonar-docs/src/tooltips/editions/community.md
  3. 3
    0
      server/sonar-docs/src/tooltips/editions/datacenter.md
  4. 3
    0
      server/sonar-docs/src/tooltips/editions/developer.md
  5. 3
    0
      server/sonar-docs/src/tooltips/editions/enterprise.md
  6. 0
    2
      server/sonar-server/src/main/java/org/sonar/server/ui/ws/GlobalAction.java
  7. 0
    2
      server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java
  8. 1
    37
      server/sonar-web/src/main/js/api/marketplace.ts
  9. 15
    32
      server/sonar-web/src/main/js/app/components/AdminContainer.tsx
  10. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  11. 0
    179
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx
  12. 0
    49
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotifContainer.tsx
  13. 12
    29
      server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
  14. 0
    130
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx
  15. 25
    0
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
  16. 0
    62
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap
  17. 20
    1
      server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
  18. 0
    6
      server/sonar-web/src/main/js/app/styles/components/alerts.css
  19. 0
    2
      server/sonar-web/src/main/js/app/utils/exposeLibraries.ts
  20. 4
    23
      server/sonar-web/src/main/js/apps/marketplace/App.tsx
  21. 13
    21
      server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
  22. 36
    121
      server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx
  23. 10
    98
      server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx
  24. 23
    53
      server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap
  25. 26
    34
      server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
  26. 0
    62
      server/sonar-web/src/main/js/apps/marketplace/components/EditionBoxBadge.tsx
  27. 0
    114
      server/sonar-web/src/main/js/apps/marketplace/components/LicenseEditionForm.tsx
  28. 0
    269
      server/sonar-web/src/main/js/apps/marketplace/components/LicenseEditionSet.tsx
  29. 0
    94
      server/sonar-web/src/main/js/apps/marketplace/components/UninstallEditionForm.tsx
  30. 6
    30
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx
  31. 0
    81
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBoxBadge-test.tsx
  32. 0
    98
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/LicenseEditionForm-test.tsx
  33. 0
    126
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/LicenseEditionSet-test.tsx
  34. 0
    72
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/UninstallEditionForm-test.tsx
  35. 18
    88
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap
  36. 0
    29
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBoxBadge-test.tsx.snap
  37. 0
    81
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/LicenseEditionForm-test.tsx.snap
  38. 0
    105
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/LicenseEditionSet-test.tsx.snap
  39. 0
    39
      server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/UninstallEditionForm-test.tsx.snap
  40. 5
    0
      server/sonar-web/src/main/js/apps/marketplace/style.css
  41. 53
    14
      server/sonar-web/src/main/js/apps/marketplace/utils.ts
  42. 13
    56
      server/sonar-web/src/main/js/store/marketplace/actions.ts
  43. 8
    26
      server/sonar-web/src/main/js/store/marketplace/reducer.ts
  44. 0
    49
      server/sonar-web/src/main/js/store/marketplace/utils.ts
  45. 2
    4
      server/sonar-web/src/main/js/store/rootReducer.js
  46. 0
    10
      sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java
  47. 0
    15
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  48. 1
    1
      sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java

+ 1
- 1
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java Целия файл

@@ -121,7 +121,7 @@ public class ComputeEngineContainerImplTest {
+ 27 // level 1
+ 55 // content of DaoModule
+ 3 // content of EsModule
+ 59 // content of CorePropertyDefinitions
+ 58 // content of CorePropertyDefinitions
+ 1 // StopFlagContainer
);
assertThat(

+ 3
- 0
server/sonar-docs/src/tooltips/editions/community.md Целия файл

@@ -0,0 +1,3 @@
### Community Edition

Comes with support for 9 programming languages, numerous plugins, integration with DevOps tool chains, and ability to connect to SonarLint in the IDE.

+ 3
- 0
server/sonar-docs/src/tooltips/editions/datacenter.md Целия файл

@@ -0,0 +1,3 @@
### Data Center Edition

Enterprise Edition + component redundancy and data integrity

+ 3
- 0
server/sonar-docs/src/tooltips/editions/developer.md Целия файл

@@ -0,0 +1,3 @@
### Developer Edition

Community Edition + branch analysis, SonarLint push notifications, and 16 languages.

+ 3
- 0
server/sonar-docs/src/tooltips/editions/enterprise.md Целия файл

@@ -0,0 +1,3 @@
### Enterprise Edition

Developer Edition + portfolio management, executive reporting, parallel processing of analysis reports and 20 languages.

+ 0
- 2
server/sonar-server/src/main/java/org/sonar/server/ui/ws/GlobalAction.java Целия файл

@@ -45,7 +45,6 @@ import org.sonar.server.ui.VersionFormatter;
import org.sonar.server.user.UserSession;

import static org.sonar.api.CoreProperties.RATING_GRID;
import static org.sonar.core.config.CorePropertyDefinitions.EDITIONS_CONFIG_URL;
import static org.sonar.core.config.WebConstants.SONAR_LF_ENABLE_GRAVATAR;
import static org.sonar.core.config.WebConstants.SONAR_LF_GRAVATAR_SERVER_URL;
import static org.sonar.core.config.WebConstants.SONAR_LF_LOGO_URL;
@@ -59,7 +58,6 @@ public class GlobalAction implements NavigationWsAction, Startable {
SONAR_LF_LOGO_WIDTH_PX,
SONAR_LF_ENABLE_GRAVATAR,
SONAR_LF_GRAVATAR_SERVER_URL,
EDITIONS_CONFIG_URL,
RATING_GRID);

private final Map<String, String> systemSettingValuesByKey;

+ 0
- 2
server/sonar-server/src/test/java/org/sonar/server/ui/ws/GlobalActionTest.java Целия файл

@@ -105,7 +105,6 @@ public class GlobalActionTest {
settings.setProperty("sonar.lf.gravatarServerUrl", "https://secure.gravatar.com/avatar/{EMAIL_MD5}.jpg?s={SIZE}&d=identicon");
settings.setProperty("sonar.lf.enableGravatar", true);
settings.setProperty("sonar.updatecenter.activate", false);
settings.setProperty("sonar.editions.jsonUrl", "https://foo.bar/editions.json");
settings.setProperty("sonar.technicalDebt.ratingGrid", "0.05,0.1,0.2,0.5");
// This setting should be ignored as it's not needed
settings.setProperty("sonar.defaultGroup", "sonar-users");
@@ -117,7 +116,6 @@ public class GlobalActionTest {
" \"sonar.lf.logoWidthPx\": \"135\"," +
" \"sonar.lf.gravatarServerUrl\": \"https://secure.gravatar.com/avatar/{EMAIL_MD5}.jpg?s={SIZE}&d=identicon\"," +
" \"sonar.lf.enableGravatar\": \"true\"," +
" \"sonar.editions.jsonUrl\": \"https://foo.bar/editions.json\"," +
" \"sonar.updatecenter.activate\": \"false\"," +
" \"sonar.technicalDebt.ratingGrid\": \"0.05,0.1,0.2,0.5\"" +
" }" +

+ 1
- 37
server/sonar-web/src/main/js/api/marketplace.ts Целия файл

@@ -17,45 +17,17 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { checkStatus, corsRequest, getJSON, parseJSON, post, postJSON } from '../helpers/request';
import { getJSON, postJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export interface Edition {
key: string;
name: string;
textDescription: string;
homeUrl: string;
licenseRequestUrl: string;
downloadUrl: string;
}

export interface EditionsPerVersion {
[version: string]: Edition[];
}

export interface EditionStatus {
currentEditionKey?: string;
nextEditionKey?: string;
installError?: string;
installationStatus:
| 'NONE'
| 'AUTOMATIC_IN_PROGRESS'
| 'MANUAL_IN_PROGRESS'
| 'AUTOMATIC_READY'
| 'UNINSTALL_IN_PROGRESS';
}

export function getEditionStatus(): Promise<EditionStatus> {
return getJSON('/api/editions/status');
}

export function getEditionsList(url: string): Promise<EditionsPerVersion> {
return corsRequest(url)
.submit()
.then(checkStatus)
.then(parseJSON);
}

export function getLicensePreview(data: {
license: string;
}): Promise<{
@@ -72,11 +44,3 @@ export function getFormData(): Promise<{ serverId: string; ncloc: number }> {
export function applyLicense(data: { license: string }): Promise<EditionStatus> {
return postJSON('/api/editions/apply_license', data).catch(throwGlobalError);
}

export function uninstallEdition(): Promise<void | Response> {
return post('/api/editions/uninstall').catch(throwGlobalError);
}

export function dismissErrorMessage(): Promise<void | Response> {
return post('/api/editions/clear_error_message').catch(throwGlobalError);
}

+ 15
- 32
server/sonar-web/src/main/js/app/components/AdminContainer.tsx Целия файл

@@ -22,20 +22,10 @@ import * as PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import SettingsNav from './nav/settings/SettingsNav';
import {
getAppState,
getGlobalSettingValue,
getMarketplaceEditionStatus,
getMarketplacePendingPlugins
} from '../../store/rootReducer';
import { getAppState, getMarketplacePendingPlugins } from '../../store/rootReducer';
import { getSettingsNavigation } from '../../api/nav';
import { EditionStatus, getEditionStatus } from '../../api/marketplace';
import { setAdminPages } from '../../store/appState/duck';
import {
fetchEditions,
setEditionStatus,
fetchPendingPlugins
} from '../../store/marketplace/actions';
import { fetchCurrentEdition, fetchPendingPlugins } from '../../store/marketplace/actions';
import { translate } from '../../helpers/l10n';
import { Extension } from '../types';
import { PluginPendingResult } from '../../api/plugins';
@@ -47,23 +37,22 @@ interface StateProps {
organizationsEnabled: boolean;
version: string;
};
editionStatus?: EditionStatus;
editionsUrl: string;
pendingPlugins: PluginPendingResult;
}

interface DispatchProps {
fetchEditions: (url: string, version: string) => void;
interface DispatchToProps {
fetchPendingPlugins: () => void;
fetchCurrentEdition: () => void;
setAdminPages: (adminPages: Extension[]) => void;
setEditionStatus: (editionStatus: EditionStatus) => void;
}

interface OwnProps {
location: {};
}

class AdminContainer extends React.PureComponent<StateProps & DispatchProps & OwnProps> {
type Props = StateProps & DispatchToProps & OwnProps;

class AdminContainer extends React.PureComponent<Props> {
static contextTypes = {
canAdmin: PropTypes.bool.isRequired
};
@@ -73,17 +62,13 @@ class AdminContainer extends React.PureComponent<StateProps & DispatchProps & Ow
handleRequiredAuthorization();
} else {
this.fetchNavigationSettings();
this.props.fetchEditions(this.props.editionsUrl, this.props.appState.version);
this.fetchEditionStatus();
this.props.fetchCurrentEdition();
}
}

fetchNavigationSettings = () =>
getSettingsNavigation().then(r => this.props.setAdminPages(r.extensions), () => {});

fetchEditionStatus = () =>
getEditionStatus().then(editionStatus => this.props.setEditionStatus(editionStatus), () => {});

render() {
const { adminPages, organizationsEnabled } = this.props.appState;

@@ -98,7 +83,6 @@ class AdminContainer extends React.PureComponent<StateProps & DispatchProps & Ow
<div>
<Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
<SettingsNav
editionStatus={this.props.editionStatus}
extensions={adminPages}
fetchPendingPlugins={this.props.fetchPendingPlugins}
location={this.props.location}
@@ -113,16 +97,15 @@ class AdminContainer extends React.PureComponent<StateProps & DispatchProps & Ow

const mapStateToProps = (state: any): StateProps => ({
appState: getAppState(state),
editionStatus: getMarketplaceEditionStatus(state),
editionsUrl: (getGlobalSettingValue(state, 'sonar.editions.jsonUrl') || {}).value,
pendingPlugins: getMarketplacePendingPlugins(state)
});

const mapDispatchToProps: DispatchProps = {
setAdminPages,
setEditionStatus,
fetchEditions,
fetchPendingPlugins
const mapDispatchToProps: DispatchToProps = {
fetchCurrentEdition,
fetchPendingPlugins,
setAdminPages
};

export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer);
export default connect<StateProps, DispatchToProps, OwnProps>(mapStateToProps, mapDispatchToProps)(
AdminContainer
);

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx Целия файл

@@ -81,7 +81,7 @@ export default class ComponentNav extends React.PureComponent<Props> {
}
return (
<ContextNavBar
height={notifComponent ? theme.contextNavHeightRaw + 20 : theme.contextNavHeightRaw}
height={notifComponent ? theme.contextNavHeightRaw + 30 : theme.contextNavHeightRaw}
id="context-navigation"
notif={notifComponent}>
<div className="navbar-context-justified">

+ 0
- 179
server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx Целия файл

@@ -1,179 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { FormattedMessage } from 'react-intl';
import NavBarNotif from '../../../../components/nav/NavBarNotif';
import RestartForm from '../../../../components/common/RestartForm';
import { Button } from '../../../../components/ui/buttons';
import { dismissErrorMessage, Edition, EditionStatus } from '../../../../api/marketplace';
import { translate, translateWithParameters } from '../../../../helpers/l10n';

interface Props {
editions?: Edition[];
editionStatus: EditionStatus;
preventRestart: boolean;
setEditionStatus: (editionStatus: EditionStatus) => void;
}

interface State {
openRestart: boolean;
}

export default class SettingsEditionsNotif extends React.PureComponent<Props, State> {
state: State = { openRestart: false };

handleOpenRestart = () => this.setState({ openRestart: true });
hanleCloseRestart = () => this.setState({ openRestart: false });

handleDismissError = () =>
dismissErrorMessage().then(
() => this.props.setEditionStatus({ ...this.props.editionStatus, installError: undefined }),
() => {}
);

renderStatusMsg(edition?: Edition) {
const { editionStatus } = this.props;
return (
<NavBarNotif className="alert alert-info">
<i className="spinner spacer-right" />
<span>
{edition
? translateWithParameters(
'marketplace.edition_status_x.' + editionStatus.installationStatus,
edition.name
)
: translate('marketplace.edition_status', editionStatus.installationStatus)}
</span>
</NavBarNotif>
);
}

renderRestartMsg(edition?: Edition) {
const { editionStatus, preventRestart } = this.props;
return (
<NavBarNotif className="alert alert-success">
<span>
{edition
? translateWithParameters(
'marketplace.edition_status_x.' + editionStatus.installationStatus,
edition.name
)
: translate('marketplace.edition_status', editionStatus.installationStatus)}
</span>
{edition &&
edition.key === 'datacenter' && (
<span className="little-spacer-left">
<FormattedMessage
defaultMessage={translate('marketplace.see_documentation_to_enable_cluster')}
id="marketplace.see_documentation_to_enable_cluster"
values={{
url: (
<a
href="https://redirect.sonarsource.com/doc/data-center-edition.html"
rel="noopener noreferrer"
target="_blank">
{edition.name}
</a>
)
}}
/>
</span>
)}
{!preventRestart && (
<Button className="js-restart spacer-left" onClick={this.handleOpenRestart}>
{translate('marketplace.restart')}
</Button>
)}
{!preventRestart &&
this.state.openRestart && <RestartForm onClose={this.hanleCloseRestart} />}
</NavBarNotif>
);
}

renderManualMsg(edition?: Edition) {
const { editionStatus } = this.props;
return (
<NavBarNotif className="alert alert-danger">
{edition
? translateWithParameters(
'marketplace.edition_status_x.' + editionStatus.installationStatus,
edition.name
)
: translate('marketplace.edition_status', editionStatus.installationStatus)}
<a
className="spacer-left"
href={
edition && edition.key === 'datacenter'
? 'https://redirect.sonarsource.com/doc/data-center-edition.html'
: 'https://redirect.sonarsource.com/doc/how-to-install-an-edition.html'
}
target="_blank">
{translate('marketplace.how_to_install')}
</a>
{edition && (
<a
className="button spacer-left"
download={`sonarqube-${edition.name}.zip`}
href={edition.downloadUrl}
target="_blank">
{translate('marketplace.download_package')}
</a>
)}
</NavBarNotif>
);
}

renderStatusAlert() {
const { currentEditionKey, installationStatus, nextEditionKey } = this.props.editionStatus;
const nextEdition =
this.props.editions && this.props.editions.find(edition => edition.key === nextEditionKey);
const currentEdition =
this.props.editions &&
this.props.editions.find(
edition =>
edition.key === currentEditionKey || (!currentEditionKey && edition.key === 'community')
);

switch (installationStatus) {
case 'AUTOMATIC_IN_PROGRESS':
return this.renderStatusMsg(nextEdition);
case 'AUTOMATIC_READY':
return this.renderRestartMsg(nextEdition);
case 'UNINSTALL_IN_PROGRESS':
return this.renderRestartMsg(currentEdition);
case 'MANUAL_IN_PROGRESS':
return this.renderManualMsg(nextEdition);
}
return null;
}

render() {
const { installError } = this.props.editionStatus;
if (installError) {
return (
<NavBarNotif className="alert alert-danger" onCancel={this.handleDismissError}>
{installError}
</NavBarNotif>
);
}

return this.renderStatusAlert();
}
}

+ 0
- 49
server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotifContainer.tsx Целия файл

@@ -1,49 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { connect } from 'react-redux';
import SettingsEditionsNotif from './SettingsEditionsNotif';
import { getAppState, getMarketplaceEditions } from '../../../../store/rootReducer';
import { Edition, EditionStatus } from '../../../../api/marketplace';
import { setEditionStatus } from '../../../../store/marketplace/actions';

interface OwnProps {
editionStatus: EditionStatus;
}

interface StateToProps {
editions?: Edition[];
preventRestart: boolean;
}

interface DispatchToProps {
setEditionStatus: (editionStatus: EditionStatus) => void;
}

const mapStateToProps = (state: any): StateToProps => ({
editions: getMarketplaceEditions(state),
preventRestart: !getAppState(state).standalone
});

const mapDispatchToProps = { setEditionStatus };

export default connect<StateToProps, DispatchToProps, OwnProps>(
mapStateToProps,
mapDispatchToProps
)(SettingsEditionsNotif);

+ 12
- 29
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx Целия файл

@@ -20,20 +20,17 @@
import * as React from 'react';
import * as classNames from 'classnames';
import { IndexLink, Link } from 'react-router';
import SettingsEditionsNotifContainer from './SettingsEditionsNotifContainer';
import PendingPluginsActionNotif from './PendingPluginsActionNotif';
import * as theme from '../../../../app/theme';
import ContextNavBar from '../../../../components/nav/ContextNavBar';
import Dropdown from '../../../../components/controls/Dropdown';
import NavBarTabs from '../../../../components/nav/NavBarTabs';
import { EditionStatus } from '../../../../api/marketplace';
import { Extension } from '../../../types';
import { translate } from '../../../../helpers/l10n';
import Dropdown from '../../../../components/controls/Dropdown';
import { PluginPendingResult } from '../../../../api/plugins';
import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
import { translate } from '../../../../helpers/l10n';

interface Props {
editionStatus?: EditionStatus;
extensions: Extension[];
fetchPendingPlugins: () => void;
location: {};
@@ -232,23 +229,16 @@ export default class SettingsNav extends React.PureComponent<Props> {
}

render() {
const { editionStatus, extensions, pendingPlugins } = this.props;
const { extensions, pendingPlugins } = this.props;
const hasSupportExtension = extensions.find(extension => extension.key === 'license/support');
const totalPendingPlugins =
pendingPlugins.installing.length +
pendingPlugins.removing.length +
pendingPlugins.updating.length;

const notifComponents = [];
if (
editionStatus &&
(editionStatus.installError || editionStatus.installationStatus !== 'NONE')
) {
notifComponents.push(<SettingsEditionsNotifContainer editionStatus={editionStatus} />);
}

if (
pendingPlugins.installing.length > 0 ||
pendingPlugins.removing.length > 0 ||
pendingPlugins.updating.length > 0
) {
notifComponents.push(
let notifComponent;
if (totalPendingPlugins > 0) {
notifComponent = (
<PendingPluginsActionNotif
pending={pendingPlugins}
refreshPending={this.props.fetchPendingPlugins}
@@ -256,18 +246,11 @@ export default class SettingsNav extends React.PureComponent<Props> {
);
}

const notifContainer =
notifComponents.length > 0 ? (
<div className="alert-container">
{notifComponents.map((element, index) => <div key={index}>{element}</div>)}
</div>
) : null;

return (
<ContextNavBar
height={theme.contextNavHeightRaw + 38 * notifComponents.length}
height={notifComponent ? theme.contextNavHeightRaw + 30 : theme.contextNavHeightRaw}
id="context-navigation"
notif={notifContainer}>
notif={notifComponent}>
<header className="navbar-context-header">
<h1>{translate('layout.settings')}</h1>
</header>

+ 0
- 130
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx Целия файл

@@ -1,130 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
/* eslint-disable import/order */
import * as React from 'react';
import { mount, shallow } from 'enzyme';
import { click } from '../../../../../helpers/testUtils';
import SettingsEditionsNotif from '../SettingsEditionsNotif';

jest.mock('../../../../../api/marketplace', () => ({
dismissErrorMessage: jest.fn(() => Promise.resolve())
}));

const dismissMsg = require('../../../../../api/marketplace').dismissErrorMessage as jest.Mock<any>;

beforeEach(() => {
dismissMsg.mockClear();
});

it('should display an in progress notif', () => {
const wrapper = shallow(
<SettingsEditionsNotif
editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }}
preventRestart={false}
setEditionStatus={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});

it('should display a ready notification', () => {
const wrapper = shallow(
<SettingsEditionsNotif
editionStatus={{ installationStatus: 'AUTOMATIC_READY' }}
preventRestart={false}
setEditionStatus={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});

it('should display a manual installation notification', () => {
const wrapper = shallow(
<SettingsEditionsNotif
editionStatus={{ installationStatus: 'MANUAL_IN_PROGRESS', nextEditionKey: 'foo' }}
editions={[
{
key: 'foo',
name: 'Foo',
textDescription: 'Foo desc',
downloadUrl: 'download_url',
homeUrl: 'more_url',
licenseRequestUrl: 'license_url'
}
]}
preventRestart={false}
setEditionStatus={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});

it('should display install errors', () => {
const wrapper = shallow(
<SettingsEditionsNotif
editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS', installError: 'Foo error' }}
preventRestart={false}
setEditionStatus={jest.fn()}
/>
);
expect(wrapper).toMatchSnapshot();
});

it('should allow to dismiss install errors', async () => {
const setEditionStatus = jest.fn();
const wrapper = mount(
<SettingsEditionsNotif
editionStatus={{ installationStatus: 'NONE', installError: 'Foo error' }}
preventRestart={false}
setEditionStatus={setEditionStatus}
/>
);
click(wrapper.find('button'));
expect(dismissMsg).toHaveBeenCalled();
await new Promise(setImmediate);
expect(setEditionStatus).toHaveBeenCalledWith({
installationStatus: 'NONE',
installError: undefined
});
});

it('should not display the restart button', () => {
const wrapper = shallow(
<SettingsEditionsNotif
editionStatus={{ installationStatus: 'AUTOMATIC_READY' }}
preventRestart={true}
setEditionStatus={jest.fn()}
/>
);
expect(wrapper.find('button.js-restart').exists()).toBeFalsy();
});

it('should have a link to cluster documentation for datacenter edition', () => {
const editions = [{ key: 'datacenter' }] as any;
const wrapper = shallow(
<SettingsEditionsNotif
editions={editions}
editionStatus={{ installationStatus: 'AUTOMATIC_READY', nextEditionKey: 'datacenter' }}
preventRestart={false}
setEditionStatus={jest.fn()}
/>
);
expect(wrapper.find('FormattedMessage').exists()).toBeTruthy();
});

+ 25
- 0
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx Целия файл

@@ -35,3 +35,28 @@ it('should work with extensions', () => {
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('Dropdown')).toMatchSnapshot();
});

it('should display a pending plugin notif', () => {
const extensions = [{ key: 'foo', name: 'Foo' }];
const wrapper = shallow(
<SettingsNav
extensions={extensions}
fetchPendingPlugins={() => {}}
location={{}}
organizationsEnabled={false}
pendingPlugins={{
installing: [
{
key: 'foo',
name: 'Foo',
version: '1.0',
implementationBuild: '1'
}
],
removing: [],
updating: []
}}
/>
);
expect(wrapper.find('ContextNavBar').prop('notif')).toMatchSnapshot();
});

+ 0
- 62
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap Целия файл

@@ -1,62 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display a manual installation notification 1`] = `
<NavBarNotif
className="alert alert-danger"
>
marketplace.edition_status_x.MANUAL_IN_PROGRESS.Foo
<a
className="spacer-left"
href="https://redirect.sonarsource.com/doc/how-to-install-an-edition.html"
target="_blank"
>
marketplace.how_to_install
</a>
<a
className="button spacer-left"
download="sonarqube-Foo.zip"
href="download_url"
target="_blank"
>
marketplace.download_package
</a>
</NavBarNotif>
`;

exports[`should display a ready notification 1`] = `
<NavBarNotif
className="alert alert-success"
>
<span>
marketplace.edition_status.AUTOMATIC_READY
</span>
<Button
className="js-restart spacer-left"
onClick={[Function]}
>
marketplace.restart
</Button>
</NavBarNotif>
`;

exports[`should display an in progress notif 1`] = `
<NavBarNotif
className="alert alert-info"
>
<i
className="spinner spacer-right"
/>
<span>
marketplace.edition_status.AUTOMATIC_IN_PROGRESS
</span>
</NavBarNotif>
`;

exports[`should display install errors 1`] = `
<NavBarNotif
className="alert alert-danger"
onCancel={[Function]}
>
Foo error
</NavBarNotif>
`;

+ 20
- 1
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap Целия файл

@@ -1,10 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display a pending plugin notif 1`] = `
<PendingPluginsActionNotif
pending={
Object {
"installing": Array [
Object {
"implementationBuild": "1",
"key": "foo",
"name": "Foo",
"version": "1.0",
},
],
"removing": Array [],
"updating": Array [],
}
}
refreshPending={[Function]}
/>
`;

exports[`should work with extensions 1`] = `
<ContextNavBar
height={72}
id="context-navigation"
notif={null}
>
<header
className="navbar-context-header"

+ 0
- 6
server/sonar-web/src/main/js/app/styles/components/alerts.css Целия файл

@@ -17,12 +17,6 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.alert-container .alert {
margin-bottom: 0px;
padding: 0 8px;
line-height: 38px;
}

.alert {
display: block;
margin-bottom: 8px;

+ 0
- 2
server/sonar-web/src/main/js/app/utils/exposeLibraries.ts Целия файл

@@ -27,7 +27,6 @@ import DateFromNow from '../../components/intl/DateFromNow';
import DateFormatter from '../../components/intl/DateFormatter';
import DateTimeFormatter from '../../components/intl/DateTimeFormatter';
import FavoriteContainer from '../../components/controls/FavoriteContainer';
import LicenseEditionSet from '../../apps/marketplace/components/LicenseEditionSet';
import HomePageSelect from '../../components/controls/HomePageSelect';
import ListFooter from '../../components/controls/ListFooter';
import Modal from '../../components/controls/Modal';
@@ -82,7 +81,6 @@ const exposeLibraries = () => {
HelpTooltip,
HomePageSelect,
Level,
LicenseEditionSet,
ListFooter,
LockIcon,
Modal,

+ 4
- 23
server/sonar-web/src/main/js/apps/marketplace/App.tsx Целия файл

@@ -36,21 +36,16 @@ import {
PluginPendingResult,
getInstalledPlugins
} from '../../api/plugins';
import { Edition, EditionStatus } from '../../api/marketplace';
import { RawQuery } from '../../helpers/query';
import { translate } from '../../helpers/l10n';
import './style.css';

export interface Props {
editions?: Edition[];
editionsReadOnly: boolean;
editionStatus?: EditionStatus;
currentEdition?: string;
fetchPendingPlugins: () => void;
loadingEditions: boolean;
location: { pathname: string; query: RawQuery };
pendingPlugins: PluginPendingResult;
standaloneMode: boolean;
setEditionStatus: (editionStatus: EditionStatus) => void;
updateCenterActive: boolean;
}

@@ -66,13 +61,7 @@ export default class App extends React.PureComponent<Props, State> {
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.state = {
loadingPlugins: true,
plugins: []
};
}
state: State = { loadingPlugins: true, plugins: [] };

componentDidMount() {
this.mounted = true;
@@ -130,7 +119,7 @@ export default class App extends React.PureComponent<Props, State> {
};

render() {
const { editions, editionStatus, standaloneMode, pendingPlugins } = this.props;
const { currentEdition, standaloneMode, pendingPlugins } = this.props;
const { loadingPlugins, plugins } = this.state;
const query = parseQuery(this.props.location.query);
const filteredPlugins = query.search ? filterPlugins(plugins, query.search) : plugins;
@@ -140,15 +129,7 @@ export default class App extends React.PureComponent<Props, State> {
<Suggestions suggestions="marketplace" />
<Helmet title={translate('marketplace.page')} />
<Header />
<EditionBoxes
canInstall={standaloneMode && !this.props.editionsReadOnly}
canUninstall={standaloneMode}
editionStatus={editionStatus}
editions={editions}
loading={this.props.loadingEditions}
updateCenterActive={this.props.updateCenterActive}
updateEditionStatus={this.props.setEditionStatus}
/>
<EditionBoxes currentEdition={currentEdition} />
<Search
query={query}
updateCenterActive={this.props.updateCenterActive}

+ 13
- 21
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx Целия файл

@@ -22,13 +22,10 @@ import App from './App';
import {
getAppState,
getGlobalSettingValue,
getMarketplaceState,
getMarketplaceEditions,
getMarketplaceEditionStatus,
getMarketplaceCurrentEdition,
getMarketplacePendingPlugins
} from '../../store/rootReducer';
import { Edition, EditionStatus } from '../../api/marketplace';
import { setEditionStatus, fetchPendingPlugins } from '../../store/marketplace/actions';
import { fetchPendingPlugins } from '../../store/marketplace/actions';
import { RawQuery } from '../../helpers/query';
import { PluginPendingResult } from '../../api/plugins';

@@ -37,32 +34,27 @@ interface OwnProps {
}

interface StateToProps {
editions?: Edition[];
editionsReadOnly: boolean;
editionStatus?: EditionStatus;
loadingEditions: boolean;
currentEdition?: string;
pendingPlugins: PluginPendingResult;
standaloneMode: boolean;
updateCenterActive: boolean;
}

interface DispatchToProps {
setEditionStatus: (editionStatus: EditionStatus) => void;
fetchPendingPlugins: () => void;
}

const mapStateToProps = (state: any) => ({
editions: getMarketplaceEditions(state),
editionsReadOnly: getMarketplaceState(state).readOnly,
editionStatus: getMarketplaceEditionStatus(state),
loadingEditions: getMarketplaceState(state).loading,
pendingPlugins: getMarketplacePendingPlugins(state),
standaloneMode: getAppState(state).standalone,
updateCenterActive:
(getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value === 'true'
});
const mapStateToProps = (state: any) => {
return {
currentEdition: getMarketplaceCurrentEdition(state),
pendingPlugins: getMarketplacePendingPlugins(state),
standaloneMode: getAppState(state).standalone,
updateCenterActive:
(getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value === 'true'
};
};

const mapDispatchToProps = { setEditionStatus, fetchPendingPlugins };
const mapDispatchToProps = { fetchPendingPlugins };

export default connect<StateToProps, DispatchToProps, OwnProps>(
mapStateToProps,

+ 36
- 121
server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx Целия файл

@@ -17,144 +17,59 @@
* 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 { FormattedMessage } from 'react-intl';
import EditionBox from './components/EditionBox';
import LicenseEditionForm from './components/LicenseEditionForm';
import UninstallEditionForm from './components/UninstallEditionForm';
import { sortEditions } from './utils';
import { Edition, EditionStatus } from '../../api/marketplace';
import { translate } from '../../helpers/l10n';
import { EDITIONS } from './utils';
import { getFormData } from '../../api/marketplace';

export interface Props {
canInstall: boolean;
canUninstall: boolean;
editions?: Edition[];
editionStatus?: EditionStatus;
loading: boolean;
updateCenterActive: boolean;
updateEditionStatus: (editionStatus: EditionStatus) => void;
currentEdition?: string;
}

interface State {
installEdition?: Edition;
openUninstallForm: boolean;
serverId?: string;
ncloc?: number;
}

export default class EditionBoxes extends React.PureComponent<Props, State> {
state: State = { openUninstallForm: false };

handleOpenLicenseForm = (edition: Edition) => this.setState({ installEdition: edition });
handleCloseLicenseForm = () => this.setState({ installEdition: undefined });

handleOpenUninstallForm = () => this.setState({ openUninstallForm: true });
handleCloseUninstallForm = () => this.setState({ openUninstallForm: false });

renderForms(sortedEditions: Edition[], installedIdx?: number) {
const { canInstall, canUninstall, editionStatus } = this.props;
const { installEdition, openUninstallForm } = this.state;
const installEditionIdx =
installEdition && sortedEditions.findIndex(edition => edition.key === installEdition.key);
mounted = false;
state: State = {};

if (canInstall && installEdition) {
return (
<LicenseEditionForm
edition={installEdition}
editions={sortedEditions}
isDowngrade={
installedIdx !== undefined &&
installEditionIdx !== undefined &&
installEditionIdx < installedIdx
}
onClose={this.handleCloseLicenseForm}
updateEditionStatus={this.props.updateEditionStatus}
/>
);
}

if (canUninstall && openUninstallForm && editionStatus && editionStatus.currentEditionKey) {
return (
<UninstallEditionForm
edition={sortedEditions.find(edition => edition.key === editionStatus.currentEditionKey)}
editionStatus={editionStatus}
onClose={this.handleCloseUninstallForm}
updateEditionStatus={this.props.updateEditionStatus}
/>
);
}

return null;
componentDidMount() {
this.mounted = true;
this.fetchFormData();
}

render() {
const { canInstall, canUninstall, editions, loading } = this.props;

if (loading) {
return <i className="big-spacer-bottom spinner" />;
}

if (!editions) {
return (
<div className="spacer-bottom marketplace-editions">
<span className="alert alert-info">
<FormattedMessage
defaultMessage={translate('marketplace.editions_unavailable')}
id="marketplace.editions_unavailable"
values={{
url: (
<a href="https://redirect.sonarsource.com/editions/editions.html" target="_blank">
SonarSource.com
</a>
)
}}
/>
</span>
</div>
);
}
componentWillUnmount() {
this.mounted = false;
}

const sortedEditions = sortEditions(editions);
const status = this.props.editionStatus || { installationStatus: 'NONE' };
const inProgressStatus = [
'AUTOMATIC_IN_PROGRESS',
'AUTOMATIC_READY',
'UNINSTALL_IN_PROGRESS'
].includes(status.installationStatus);
const installedIdx = sortedEditions.findIndex(
edition => edition.key === status.currentEditionKey
fetchFormData = () => {
getFormData().then(
formData => {
if (this.mounted) {
this.setState({ ...formData });
}
},
() => {}
);
const nextIdx = sortedEditions.findIndex(edition => edition.key === status.nextEditionKey);
const currentIdx = inProgressStatus ? nextIdx : installedIdx;
};

render() {
const { currentEdition } = this.props;
const { serverId, ncloc } = this.state;
return (
<div className="spacer-bottom marketplace-editions">
<EditionBox
actionLabel={translate('marketplace.downgrade')}
disableAction={inProgressStatus}
displayAction={canUninstall && currentIdx > 0}
edition={sortedEditions[0]}
editionStatus={status}
key={sortedEditions[0].key}
onAction={this.handleOpenUninstallForm}
/>
{sortedEditions
.slice(1)
.map((edition, idx) => (
<EditionBox
actionLabel={
currentIdx > idx + 1
? translate('marketplace.downgrade')
: translate('marketplace.upgrade')
}
disableAction={inProgressStatus}
displayAction={canInstall && currentIdx !== idx + 1}
edition={edition}
editionStatus={status}
key={edition.key}
onAction={this.handleOpenLicenseForm}
/>
))}

{this.renderForms(sortedEditions, installedIdx)}
{EDITIONS.map(edition => (
<EditionBox
currentEdition={currentEdition || 'community'}
edition={edition}
key={edition.key}
ncloc={ncloc}
serverId={serverId}
/>
))}
</div>
);
}

+ 10
- 98
server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx Целия файл

@@ -20,110 +20,22 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import EditionBoxes from '../EditionBoxes';
import { EditionStatus } from '../../../api/marketplace';

const DEFAULT_STATUS: EditionStatus = {
currentEditionKey: 'developer',
nextEditionKey: '',
installationStatus: 'NONE'
};

const DEFAULT_EDITIONS = [
{
key: 'developer',
name: 'Developer Edition',
textDescription: 'foo',
downloadUrl: 'download_url',
homeUrl: 'more_url',
licenseRequestUrl: 'license_url'
},
{
key: 'comunity',
name: 'Comunity Edition',
textDescription: 'bar',
downloadUrl: 'download_url',
homeUrl: 'more_url',
licenseRequestUrl: 'license_url'
}
];
jest.mock('../utils', () => ({
EDITIONS: [
{ key: 'comunity', homeUrl: 'more_url' },
{ key: 'developer', downloadUrl: 'download_url', homeUrl: 'more_url' }
]
}));

it('should display the edition boxes correctly', () => {
const wrapper = getWrapper({ editions: DEFAULT_EDITIONS, loading: true });
expect(wrapper).toMatchSnapshot();
wrapper.setProps({ loading: false });
expect(wrapper).toMatchSnapshot();
});

it('should display an error message', () => {
const wrapper = getWrapper();
expect(wrapper).toMatchSnapshot();
});

it('should display community without the downgrade button', () => {
const communityBox = getWrapper({
editions: DEFAULT_EDITIONS,
editionStatus: {
currentEditionKey: '',
installationStatus: 'NONE'
},
loading: false
})
.find('EditionBox')
.first();
expect(communityBox.prop('displayAction')).toBeFalsy();
});

it('should not display action buttons', () => {
const wrapper = getWrapper({
editions: DEFAULT_EDITIONS,
editionStatus: {
currentEditionKey: '',
installationStatus: 'NONE'
},
loading: false,
canInstall: false,
canUninstall: false
});
wrapper.find('EditionBox').forEach(box => expect(box.prop('displayAction')).toBeFalsy());
});

it('should display disabled action buttons', () => {
const wrapper = getWrapper({
editions: DEFAULT_EDITIONS,
editionStatus: { installationStatus: 'AUTOMATIC_IN_PROGRESS', nextEditionKey: 'developer' },
loading: false
});

wrapper.find('EditionBox').forEach(box => expect(box.prop('disableAction')).toBeTruthy());
expect(wrapper.find('EditionBox').map(box => box.prop('displayAction'))).toEqual([true, false]);

wrapper.setProps({
editionStatus: { currentEditionKey: 'developer', installationStatus: 'UNINSTALL_IN_PROGRESS' }
});
wrapper.find('EditionBox').forEach(box => expect(box.prop('disableAction')).toBeTruthy());
expect(wrapper.find('EditionBox').map(box => box.prop('displayAction'))).toEqual([false, true]);

wrapper.setProps({ editionStatus: { installationStatus: 'AUTOMATIC_READY' } });
wrapper.find('EditionBox').forEach(box => expect(box.prop('disableAction')).toBeTruthy());
expect(getWrapper()).toMatchSnapshot();
});

it('should open the license form', () => {
const wrapper = getWrapper({ editions: DEFAULT_EDITIONS });
(wrapper.instance() as EditionBoxes).handleOpenLicenseForm(DEFAULT_EDITIONS[0]);
wrapper.update();
expect(wrapper.find('LicenseEditionForm').exists()).toBeTruthy();
it('should display the developer edition as installed', () => {
expect(getWrapper({ currentEdition: 'developer' })).toMatchSnapshot();
});

function getWrapper(props = {}) {
return shallow(
<EditionBoxes
canInstall={true}
canUninstall={true}
loading={false}
editionStatus={DEFAULT_STATUS}
updateCenterActive={true}
updateEditionStatus={jest.fn()}
{...props}
/>
);
return shallow(<EditionBoxes currentEdition={undefined} {...props} />);
}

+ 23
- 53
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap Целия файл

@@ -1,87 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display an error message 1`] = `
exports[`should display the developer edition as installed 1`] = `
<div
className="spacer-bottom marketplace-editions"
>
<span
className="alert alert-info"
>
<FormattedMessage
defaultMessage="marketplace.editions_unavailable"
id="marketplace.editions_unavailable"
values={
Object {
"url": <a
href="https://redirect.sonarsource.com/editions/editions.html"
target="_blank"
>
SonarSource.com
</a>,
}
<EditionBox
currentEdition="developer"
edition={
Object {
"homeUrl": "more_url",
"key": "comunity",
}
/>
</span>
}
key="comunity"
/>
<EditionBox
currentEdition="developer"
edition={
Object {
"downloadUrl": "download_url",
"homeUrl": "more_url",
"key": "developer",
}
}
key="developer"
/>
</div>
`;

exports[`should display the edition boxes correctly 1`] = `
<i
className="big-spacer-bottom spinner"
/>
`;

exports[`should display the edition boxes correctly 2`] = `
<div
className="spacer-bottom marketplace-editions"
>
<EditionBox
actionLabel="marketplace.downgrade"
disableAction={false}
displayAction={true}
currentEdition="community"
edition={
Object {
"downloadUrl": "download_url",
"homeUrl": "more_url",
"key": "comunity",
"licenseRequestUrl": "license_url",
"name": "Comunity Edition",
"textDescription": "bar",
}
}
editionStatus={
Object {
"currentEditionKey": "developer",
"installationStatus": "NONE",
"nextEditionKey": "",
}
}
key="comunity"
onAction={[Function]}
/>
<EditionBox
actionLabel="marketplace.upgrade"
disableAction={false}
displayAction={false}
currentEdition="community"
edition={
Object {
"downloadUrl": "download_url",
"homeUrl": "more_url",
"key": "developer",
"licenseRequestUrl": "license_url",
"name": "Developer Edition",
"textDescription": "foo",
}
}
editionStatus={
Object {
"currentEditionKey": "developer",
"installationStatus": "NONE",
"nextEditionKey": "",
}
}
key="developer"
onAction={[Function]}
/>
</div>
`;

+ 26
- 34
server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx Целия файл

@@ -18,45 +18,37 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import EditionBoxBadge from './EditionBoxBadge';
import { Edition, EditionStatus } from '../../../api/marketplace';
import { Button } from '../../../components/ui/buttons';
import CheckIcon from '../../../components/icons-components/CheckIcon';
import { translate } from '../../../helpers/l10n';
import DocInclude from '../../../components/docs/DocInclude';
import { Edition, getEditionUrl } from '../utils';

interface Props {
actionLabel: string;
disableAction: boolean;
displayAction: boolean;
currentEdition: string;
edition: Edition;
editionStatus?: EditionStatus;
onAction: (edition: Edition) => void;
ncloc?: number;
serverId?: string;
}

export default class EditionBox extends React.PureComponent<Props> {
handleAction = () => {
this.props.onAction(this.props.edition);
};

render() {
const { disableAction, displayAction, edition, editionStatus } = this.props;
return (
<div className="boxed-group boxed-group-inner marketplace-edition">
{editionStatus && <EditionBoxBadge editionKey={edition.key} status={editionStatus} />}
<div>
<h3 className="spacer-bottom">{edition.name}</h3>
<p>{edition.textDescription}</p>
</div>
<div className="marketplace-edition-action spacer-top">
<a href={edition.homeUrl} target="_blank">
{translate('marketplace.learn_more')}
</a>
{displayAction && (
<Button disabled={disableAction} onClick={this.handleAction}>
{this.props.actionLabel}
</Button>
)}
</div>
export default function EditionBox({ currentEdition, edition, ncloc, serverId }: Props) {
const isInstalled = currentEdition === edition.key;
const url = getEditionUrl(edition, { ncloc, serverId, sourceEdition: currentEdition });
return (
<div className="boxed-group boxed-group-inner marketplace-edition">
{isInstalled && (
<span className="marketplace-edition-badge badge badge-normal-size display-flex-center">
<CheckIcon className="little-spacer-right" size={14} />
{translate('marketplace.installed')}
</span>
)}
<div>
<DocInclude path={'/tooltips/editions/' + edition.key} />
</div>
<div className="marketplace-edition-action spacer-top">
<a href={url} target="_blank">
{translate('marketplace.learn_more')}
</a>
</div>
);
}
</div>
);
}

+ 0
- 62
server/sonar-web/src/main/js/apps/marketplace/components/EditionBoxBadge.tsx Целия файл

@@ -1,62 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 CheckIcon from '../../../components/icons-components/CheckIcon';
import { EditionStatus } from '../../../api/marketplace';
import { translate } from '../../../helpers/l10n';

interface Props {
editionKey: string;
status: EditionStatus;
}

export default function EditionBoxBadge({ editionKey, status }: Props) {
const isInstalled =
status.currentEditionKey === editionKey ||
(!status.currentEditionKey && editionKey === 'community');
const isProgressing =
status.nextEditionKey === editionKey || (!status.nextEditionKey && editionKey === 'community');
const inProgressStatus = [
'AUTOMATIC_READY',
'AUTOMATIC_IN_PROGRESS',
'UNINSTALL_IN_PROGRESS'
].includes(status.installationStatus);

if (inProgressStatus) {
if (isProgressing) {
return (
<span className="marketplace-edition-badge badge badge-normal-size">
{status.installationStatus === 'AUTOMATIC_IN_PROGRESS'
? translate('marketplace.installing')
: translate('marketplace.pending')}
</span>
);
}
} else if (isInstalled) {
return (
<span className="marketplace-edition-badge badge badge-normal-size display-flex-center">
<CheckIcon size={14} className="little-spacer-right" />
{translate('marketplace.installed')}
</span>
);
}

return null;
}

+ 0
- 114
server/sonar-web/src/main/js/apps/marketplace/components/LicenseEditionForm.tsx Целия файл

@@ -1,114 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 LicenseEditionSet from './LicenseEditionSet';
import { Edition, EditionStatus, applyLicense } from '../../../api/marketplace';
import Modal from '../../../components/controls/Modal';
import { Button, ResetButtonLink } from '../../../components/ui/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export interface Props {
edition: Edition;
editions: Edition[];
isDowngrade: boolean;
onClose: () => void;
updateEditionStatus: (editionStatus: EditionStatus) => void;
}

interface State {
license: string;
status?: string;
submitting: boolean;
}

export default class LicenseEditionForm extends React.PureComponent<Props, State> {
mounted = false;
state: State = { license: '', submitting: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleLicenseChange = (license: string, status?: string) => {
if (this.mounted) {
this.setState({ license, status });
}
};

handleConfirmClick = () => {
const { license, status } = this.state;
if (license && status) {
this.setState({ submitting: true });
applyLicense({ license }).then(
editionStatus => {
this.props.updateEditionStatus(editionStatus);
this.props.onClose();
},
() => {
if (this.mounted) {
this.setState({ submitting: false });
}
}
);
}
};

render() {
const { edition, isDowngrade } = this.props;
const { license, submitting, status } = this.state;

const header = isDowngrade
? translateWithParameters('marketplace.downgrade_to_x', edition.name)
: translateWithParameters('marketplace.upgrade_to_x', edition.name);
return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>

<LicenseEditionSet
className="modal-body"
edition={edition}
editions={this.props.editions}
updateLicense={this.handleLicenseChange}
/>

<footer className="modal-foot">
{submitting && <i className="spinner spacer-right" />}
{status && (
<Button
className="js-confirm"
disabled={!license || submitting}
onClick={this.handleConfirmClick}>
{status === 'AUTOMATIC_INSTALL'
? translate('marketplace.install')
: translate('save')}
</Button>
)}
<ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
</footer>
</Modal>
);
}
}

+ 0
- 269
server/sonar-web/src/main/js/apps/marketplace/components/LicenseEditionSet.tsx Целия файл

@@ -1,269 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { stringify } from 'querystring';
import * as React from 'react';
import * as classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { debounce } from 'lodash';
import Checkbox from '../../../components/controls/Checkbox';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { omitNil } from '../../../helpers/request';
import { Edition, getFormData, getLicensePreview } from '../../../api/marketplace';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export interface Props {
className?: string;
edition?: Edition;
editions: Edition[];
updateLicense: (license?: string, status?: string) => void;
}

interface State {
acceptTerms: boolean;
formData?: {
serverId?: string;
ncloc?: number;
};
license: string;
licenseEdition?: Edition;
loading: boolean;
previewStatus?: string;
wrongEdition: boolean;
}

export default class LicenseEditionSet extends React.PureComponent<Props, State> {
mounted = false;

constructor(props: Props) {
super(props);
this.state = { acceptTerms: false, license: '', loading: false, wrongEdition: false };
this.fetchLicensePreview = debounce(this.fetchLicensePreview, 100);
}

componentDidMount() {
this.mounted = true;
this.fetchFormData();
}

componentWillUnmount() {
this.mounted = false;
}

fetchLicensePreview = (license: string) => {
this.setState({ loading: true });
getLicensePreview({ license }).then(
({ previewStatus, nextEditionKey }) => {
if (this.mounted) {
const { edition } = this.props;
const licenseEdition = this.props.editions.find(
edition => edition.key === nextEditionKey
);
const wrongEdition = Boolean(
!licenseEdition || (edition && edition.key !== nextEditionKey)
);
this.setLicense({ license, loading: false, licenseEdition, previewStatus, wrongEdition });
}
},
() => {
if (this.mounted) {
this.resetLicense({ license, loading: false });
}
}
);
};

fetchFormData = () => {
getFormData().then(
formData => {
if (this.mounted) {
this.setState({ formData });
}
},
() => {}
);
};

getLicenseFormUrl = (edition: Edition) => {
let url = edition.licenseRequestUrl;
if (this.state.formData) {
const query = stringify(omitNil(this.state.formData));
if (query) {
url += '?' + query;
}
}
return url;
};

handleLicenseChange = (event: React.SyntheticEvent<HTMLTextAreaElement>) => {
const license = event.currentTarget.value;
if (license) {
this.fetchLicensePreview(license);
this.setState({ license });
} else {
this.resetLicense({});
}
};

handleTermsCheck = (checked: boolean) => {
this.setLicense({ acceptTerms: checked });
};

resetLicense<K extends keyof State>(state: Pick<State, K>) {
this.setLicense(
Object.assign(
{
license: '',
licenseEdition: undefined,
previewStatus: undefined,
wrongEdition: false
},
state
)
);
}

setLicense<K extends keyof State>(state: Pick<State, K>) {
this.setState(state, this.updateParentLicense);
}

updateParentLicense = () => {
const { acceptTerms, license, previewStatus, wrongEdition } = this.state;
this.props.updateLicense(
previewStatus !== 'NO_INSTALL' && !acceptTerms ? undefined : license,
wrongEdition ? undefined : previewStatus
);
};

renderAlert() {
const { licenseEdition, previewStatus, wrongEdition } = this.state;
if (!previewStatus || wrongEdition) {
const { edition } = this.props;

return (
<div className="spacer-top">
{wrongEdition && (
<p className="alert alert-danger">
{edition
? translateWithParameters('marketplace.wrong_license_type_x', edition.name)
: translate('marketplace.wrong_license_type')}
</p>
)}
{edition && (
<a href={this.getLicenseFormUrl(edition)} target="_blank">
{translate('marketplace.i_need_a_license')}
</a>
)}
</div>
);
}

return (
<div className="spacer-top">
<p
className={classNames('alert', {
'alert-warning': previewStatus === 'AUTOMATIC_INSTALL',
'alert-success': previewStatus === 'NO_INSTALL',
'alert-danger': previewStatus === 'MANUAL_INSTALL'
})}>
{translateWithParameters(
'marketplace.license_preview_status.' + previewStatus,
licenseEdition ? licenseEdition.name : translate('marketplace.commercial_edition')
)}
{licenseEdition &&
licenseEdition.key === 'datacenter' &&
previewStatus !== 'NO_INSTALL' && (
<span className="little-spacer-left">
<FormattedMessage
defaultMessage={translate('marketplace.how_to_setup_cluster_url')}
id="marketplace.how_to_setup_cluster_url"
values={{
url: (
<a
href="https://redirect.sonarsource.com/doc/data-center-edition.html"
rel="noopener noreferrer"
target="_blank">
{licenseEdition.name}
</a>
)
}}
/>
</span>
)}
</p>
{previewStatus !== 'NO_INSTALL' && (
<span className="js-edition-tos">
<Checkbox
checked={this.state.acceptTerms}
id="edition-terms"
onCheck={this.handleTermsCheck}>
<label className="little-spacer-left" htmlFor="edition-terms">
{translate('marketplace.i_accept_the')}
</label>
</Checkbox>
<a
className="nowrap little-spacer-left"
href="http://dist.sonarsource.com/SonarSource_Terms_And_Conditions.pdf"
rel="noopener noreferrer"
target="_blank">
{translate('marketplace.terms_and_conditions')}
</a>
</span>
)}
</div>
);
}

render() {
const { className, edition } = this.props;
const { license, loading } = this.state;

return (
<div className={className}>
{edition && (
<label className="display-inline-block spacer-bottom" htmlFor="set-license">
{translateWithParameters('marketplace.enter_license_for_x', edition.name)}
<em className="mandatory">*</em>
</label>
)}
<textarea
autoFocus={true}
className="display-block input-super-large"
id="set-license"
onChange={this.handleLicenseChange}
required={true}
rows={8}
style={{ resize: 'none' }}
value={license}
/>

<DeferredSpinner
customSpinner={
<p className="spacer-top">
<i className="spinner spacer-right text-bottom" />
{translate('marketplace.checking_license')}
</p>
}
loading={loading}>
{this.renderAlert()}
</DeferredSpinner>
</div>
);
}
}

+ 0
- 94
server/sonar-web/src/main/js/apps/marketplace/components/UninstallEditionForm.tsx Целия файл

@@ -1,94 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { Edition, EditionStatus, uninstallEdition } from '../../../api/marketplace';
import Modal from '../../../components/controls/Modal';
import { Button, ResetButtonLink } from '../../../components/ui/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';

export interface Props {
edition?: Edition;
editionStatus: EditionStatus;
onClose: () => void;
updateEditionStatus: (editionStatus: EditionStatus) => void;
}

interface State {
loading: boolean;
}

export default class UninstallEditionForm extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleConfirmClick = () => {
this.setState({ loading: true });
uninstallEdition()
.then(() => {
this.props.updateEditionStatus({
...this.props.editionStatus,
currentEditionKey: undefined,
installationStatus: 'UNINSTALL_IN_PROGRESS'
});
this.props.onClose();
})
.catch(() => {
if (this.mounted) {
this.setState({ loading: false });
}
});
};

render() {
const { edition } = this.props;
const { loading } = this.state;
const currentEdition = edition ? edition.name : translate('marketplace.commercial_edition');
const header = translateWithParameters('marketplace.downgrade_to_community_edition');
return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>

<div className="modal-body">
<p>{translateWithParameters('marketplace.uninstall_x_confirmation', currentEdition)}</p>
</div>

<footer className="modal-foot">
{loading && <i className="spinner spacer-right" />}
<Button disabled={loading} onClick={this.handleConfirmClick}>
{translate('marketplace.downgrade')}
</Button>
<ResetButtonLink className="js-modal-close" onClick={this.props.onClose}>
{translate('cancel')}
</ResetButtonLink>
</footer>
</Modal>
);
}
}

+ 6
- 30
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx Целия файл

@@ -19,46 +19,22 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { Edition, EditionStatus } from '../../../../api/marketplace';
import EditionBox from '../EditionBox';

const DEFAULT_STATUS: EditionStatus = {
currentEditionKey: '',
nextEditionKey: '',
installationStatus: 'NONE'
};

const DEFAULT_EDITION: Edition = {
const DEFAULT_EDITION = {
key: 'foo',
name: 'Foo',
textDescription: 'Foo desc',
downloadUrl: 'download_url',
homeUrl: 'more_url',
licenseRequestUrl: 'license_url'
homeUrl: 'more_url'
};

it('should display the edition', () => {
expect(getWrapper()).toMatchSnapshot();
});

it('should disable action button', () => {
expect(getWrapper({ disableAction: true })).toMatchSnapshot();
expect(getWrapper({ ncloc: 1000, serverId: 'serverId' })).toMatchSnapshot();
});

it('should correctly hide action buttons', () => {
expect(getWrapper({ displayAction: false })).toMatchSnapshot();
it('should show insalled badge', () => {
expect(getWrapper({ currentEdition: 'foo' })).toMatchSnapshot();
});

function getWrapper(props = {}) {
return shallow(
<EditionBox
actionLabel="action"
disableAction={false}
displayAction={true}
edition={DEFAULT_EDITION}
editionStatus={DEFAULT_STATUS}
onAction={jest.fn()}
{...props}
/>
);
return shallow(<EditionBox currentEdition="community" edition={DEFAULT_EDITION} {...props} />);
}

+ 0
- 81
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBoxBadge-test.tsx Целия файл

@@ -1,81 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import { EditionStatus } from '../../../../api/marketplace';
import EditionBoxBadge from '../EditionBoxBadge';

const DEFAULT_STATUS: EditionStatus = {
currentEditionKey: '',
nextEditionKey: '',
installationStatus: 'NONE'
};

it('should display installed badge', () => {
expect(
getWrapper({
status: {
currentEditionKey: 'foo',
nextEditionKey: '',
installationStatus: 'NONE'
}
})
).toMatchSnapshot();
});

it('should display installing badge', () => {
expect(
getWrapper({
status: {
currentEditionKey: 'foo',
nextEditionKey: 'foo',
installationStatus: 'AUTOMATIC_IN_PROGRESS'
}
})
).toMatchSnapshot();
});

it('should display pending badge', () => {
expect(
getWrapper({
status: {
currentEditionKey: '',
nextEditionKey: 'foo',
installationStatus: 'AUTOMATIC_READY'
}
})
).toMatchSnapshot();
});

it('should not display a badge', () => {
expect(
getWrapper({
status: {
currentEditionKey: '',
nextEditionKey: 'bar',
installationStatus: 'AUTOMATIC_READY'
}
}).type()
).toBeNull();
});

function getWrapper(props = {}) {
return shallow(<EditionBoxBadge editionKey="foo" status={DEFAULT_STATUS} {...props} />);
}

+ 0
- 98
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/LicenseEditionForm-test.tsx Целия файл

@@ -1,98 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
/* eslint-disable import/order */
import * as React from 'react';
import { shallow } from 'enzyme';
import { click } from '../../../../helpers/testUtils';
import LicenseEditionForm from '../LicenseEditionForm';

jest.mock('../../../../api/marketplace', () => ({
applyLicense: jest.fn(() =>
Promise.resolve({ nextEditionKey: 'foo', installationStatus: 'AUTOMATIC_IN_PROGRESS' })
)
}));

const applyLicense = require('../../../../api/marketplace').applyLicense as jest.Mock<any>;

const DEFAULT_EDITION = {
key: 'foo',
name: 'Foo',
textDescription: 'Foo desc',
downloadUrl: 'download_url',
homeUrl: 'more_url',
licenseRequestUrl: 'license_url'
};

beforeEach(() => {
applyLicense.mockClear();
});

it('should display correctly', () => {
expect(getWrapper()).toMatchSnapshot();
});

it('should correctly change the button based on the status and license', () => {
const wrapper = getWrapper();
let button;
(wrapper.instance() as LicenseEditionForm).mounted = true;

wrapper.setState({ license: 'mylicense', status: 'NO_INSTALL' });
button = wrapper.find('Button');
expect(button).toMatchSnapshot();

wrapper.setState({ license: undefined, status: 'MANUAL_INSTALL' });
button = wrapper.find('Button');
expect(button).toMatchSnapshot();

wrapper.setState({ status: 'AUTOMATIC_INSTALL' });
button = wrapper.find('Button');
expect(button).toMatchSnapshot();

wrapper.setState({ license: 'mylicense' });
expect(wrapper.find('Button').prop('disabled')).toBeFalsy();
});

it('should update the edition status after install', async () => {
const updateEditionStatus = jest.fn();
const wrapper = getWrapper({ updateEditionStatus });
const form = wrapper.instance() as LicenseEditionForm;
form.handleLicenseChange('mylicense', 'AUTOMATIC_INSTALL');
wrapper.update();
click(wrapper.find('Button'));
expect(applyLicense).toHaveBeenCalledWith({ license: 'mylicense' });
await new Promise(setImmediate);
expect(updateEditionStatus).toHaveBeenCalledWith({
nextEditionKey: 'foo',
installationStatus: 'AUTOMATIC_IN_PROGRESS'
});
});

function getWrapper(props = {}) {
return shallow(
<LicenseEditionForm
edition={DEFAULT_EDITION}
editions={[DEFAULT_EDITION]}
isDowngrade={false}
onClose={jest.fn()}
updateEditionStatus={jest.fn()}
{...props}
/>
);
}

+ 0
- 126
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/LicenseEditionSet-test.tsx Целия файл

@@ -1,126 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
/* eslint-disable import/order */
import * as React from 'react';
import { shallow } from 'enzyme';
import { change, waitAndUpdate } from '../../../../helpers/testUtils';
import LicenseEditionSet from '../LicenseEditionSet';

jest.mock('../../../../api/marketplace', () => ({
getLicensePreview: jest.fn(() =>
Promise.resolve({ nextEditionKey: 'foo', previewStatus: 'NO_INSTALL' })
),
getFormData: jest.fn(() => Promise.resolve({ serverId: 'foo', ncloc: 1000 }))
}));

jest.mock('lodash', () => {
const lodash = require.requireActual('lodash');
lodash.debounce = (fn: Function) => (...args: any[]) => fn(...args);
return lodash;
});

const getLicensePreview = require('../../../../api/marketplace').getLicensePreview as jest.Mock<
any
>;

const DEFAULT_EDITION = {
key: 'foo',
name: 'Foo',
textDescription: 'Foo desc',
downloadUrl: 'download_url',
homeUrl: 'more_url',
licenseRequestUrl: 'license_url'
};

beforeEach(() => {
getLicensePreview.mockClear();
});

it('should display correctly', () => {
expect(getWrapper()).toMatchSnapshot();
});

it('should display the get license link with parameters', async () => {
const wrapper = getWrapper();
await waitAndUpdate(wrapper);
expect(wrapper.find('a')).toMatchSnapshot();
});

it('should correctly display status message after checking license', async () => {
let wrapper = await testLicenseStatus('NO_INSTALL');
expect(wrapper.find('p.alert')).toMatchSnapshot();
wrapper = await testLicenseStatus('AUTOMATIC_INSTALL');
expect(wrapper.find('p.alert')).toMatchSnapshot();
wrapper = await testLicenseStatus('MANUAL_INSTALL');
expect(wrapper.find('p.alert')).toMatchSnapshot();
wrapper = await testLicenseStatus('AUTOMATIC_INSTALL', jest.fn(), 'bar');
expect(wrapper.find('p.alert')).toMatchSnapshot();
wrapper = await testLicenseStatus('AUTOMATIC_INSTALL', jest.fn(), 'bar', { edition: undefined });
expect(wrapper.find('p.alert')).toMatchSnapshot();
});

it('should display terms of license checkbox', async () => {
let updateLicense = jest.fn();
let wrapper = await testLicenseStatus('NO_INSTALL', updateLicense);
expect(wrapper.find('.js-edition-tos').exists()).toBeFalsy();
expect(updateLicense).toHaveBeenCalledWith('mylicense', 'NO_INSTALL');

updateLicense = jest.fn();
wrapper = await testLicenseStatus('AUTOMATIC_INSTALL', updateLicense);
const tosCheckbox = wrapper.find('.js-edition-tos');
expect(tosCheckbox.find('a').exists()).toBeTruthy();
expect(updateLicense).toHaveBeenLastCalledWith(undefined, 'AUTOMATIC_INSTALL');
(tosCheckbox.find('Checkbox').prop('onCheck') as Function)(true);
expect(updateLicense).toHaveBeenLastCalledWith('mylicense', 'AUTOMATIC_INSTALL');

updateLicense = jest.fn();
wrapper = await testLicenseStatus('MANUAL_INSTALL', updateLicense);
expect(wrapper.find('.js-edition-tos').exists()).toBeTruthy();
expect(updateLicense).toHaveBeenLastCalledWith(undefined, 'MANUAL_INSTALL');
});

function getWrapper(props = {}) {
return shallow(
<LicenseEditionSet
edition={DEFAULT_EDITION}
editions={[DEFAULT_EDITION]}
updateLicense={jest.fn()}
{...props}
/>
);
}

async function testLicenseStatus(
status: string,
updateLicense = jest.fn(),
nextEditionKey = 'foo',
props = {}
) {
getLicensePreview.mockImplementation(() =>
Promise.resolve({ nextEditionKey, previewStatus: status })
);
const wrapper = getWrapper({ updateLicense, ...props });
change(wrapper.find('textarea'), 'mylicense');
expect(getLicensePreview).toHaveBeenCalled();
await new Promise(setImmediate);
expect(updateLicense).toHaveBeenCalled();
wrapper.update();
return wrapper;
}

+ 0
- 72
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/UninstallEditionForm-test.tsx Целия файл

@@ -1,72 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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.
*/
/* eslint-disable import/order */
import * as React from 'react';
import { shallow } from 'enzyme';
import { click } from '../../../../helpers/testUtils';
import UninstallEditionForm from '../UninstallEditionForm';

jest.mock('../../../../api/marketplace', () => ({
uninstallEdition: jest.fn(() => Promise.resolve())
}));

const uninstallEdition = require('../../../../api/marketplace').uninstallEdition as jest.Mock<any>;

const DEFAULT_EDITION = {
key: 'foo',
name: 'Foo',
textDescription: 'Foo desc',
downloadUrl: 'download_url',
homeUrl: 'more_url',
licenseRequestUrl: 'license_url'
};

beforeEach(() => {
uninstallEdition.mockClear();
});

it('should display correctly', () => {
expect(getWrapper()).toMatchSnapshot();
});

it('should update the edition status after uninstall', async () => {
const updateEditionStatus = jest.fn();
const wrapper = getWrapper({ updateEditionStatus });
(wrapper.instance() as UninstallEditionForm).mounted = true;
click(wrapper.find('Button'));
expect(uninstallEdition).toHaveBeenCalled();
await new Promise(setImmediate);
expect(updateEditionStatus).toHaveBeenCalledWith({
currentEditionKey: undefined,
installationStatus: 'UNINSTALL_IN_PROGRESS'
});
});

function getWrapper(props = {}) {
return shallow(
<UninstallEditionForm
edition={DEFAULT_EDITION}
editionStatus={{ currentEditionKey: 'foo', installationStatus: 'NONE' }}
onClose={jest.fn()}
updateEditionStatus={jest.fn()}
{...props}
/>
);
}

+ 18
- 88
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap Целия файл

@@ -1,34 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should correctly hide action buttons 1`] = `
exports[`should display the edition 1`] = `
<div
className="boxed-group boxed-group-inner marketplace-edition"
>
<EditionBoxBadge
editionKey="foo"
status={
Object {
"currentEditionKey": "",
"installationStatus": "NONE",
"nextEditionKey": "",
}
}
/>
<div>
<h3
className="spacer-bottom"
>
Foo
</h3>
<p>
Foo desc
</p>
<DocInclude
path="/tooltips/editions/foo"
/>
</div>
<div
className="marketplace-edition-action spacer-top"
>
<a
href="more_url"
href="more_url?ncloc=1000&serverId=serverId&sourceEdition=community"
target="_blank"
>
marketplace.learn_more
@@ -37,88 +22,33 @@ exports[`should correctly hide action buttons 1`] = `
</div>
`;

exports[`should disable action button 1`] = `
exports[`should show insalled badge 1`] = `
<div
className="boxed-group boxed-group-inner marketplace-edition"
>
<EditionBoxBadge
editionKey="foo"
status={
Object {
"currentEditionKey": "",
"installationStatus": "NONE",
"nextEditionKey": "",
}
}
/>
<div>
<h3
className="spacer-bottom"
>
Foo
</h3>
<p>
Foo desc
</p>
</div>
<div
className="marketplace-edition-action spacer-top"
<span
className="marketplace-edition-badge badge badge-normal-size display-flex-center"
>
<a
href="more_url"
target="_blank"
>
marketplace.learn_more
</a>
<Button
disabled={true}
onClick={[Function]}
>
action
</Button>
</div>
</div>
`;

exports[`should display the edition 1`] = `
<div
className="boxed-group boxed-group-inner marketplace-edition"
>
<EditionBoxBadge
editionKey="foo"
status={
Object {
"currentEditionKey": "",
"installationStatus": "NONE",
"nextEditionKey": "",
}
}
/>
<CheckIcon
className="little-spacer-right"
size={14}
/>
marketplace.installed
</span>
<div>
<h3
className="spacer-bottom"
>
Foo
</h3>
<p>
Foo desc
</p>
<DocInclude
path="/tooltips/editions/foo"
/>
</div>
<div
className="marketplace-edition-action spacer-top"
>
<a
href="more_url"
href="more_url?sourceEdition=foo"
target="_blank"
>
marketplace.learn_more
</a>
<Button
disabled={false}
onClick={[Function]}
>
action
</Button>
</div>
</div>
`;

+ 0
- 29
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBoxBadge-test.tsx.snap Целия файл

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display installed badge 1`] = `
<span
className="marketplace-edition-badge badge badge-normal-size display-flex-center"
>
<CheckIcon
className="little-spacer-right"
size={14}
/>
marketplace.installed
</span>
`;

exports[`should display installing badge 1`] = `
<span
className="marketplace-edition-badge badge badge-normal-size"
>
marketplace.installing
</span>
`;

exports[`should display pending badge 1`] = `
<span
className="marketplace-edition-badge badge badge-normal-size"
>
marketplace.pending
</span>
`;

+ 0
- 81
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/LicenseEditionForm-test.tsx.snap Целия файл

@@ -1,81 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should correctly change the button based on the status and license 1`] = `
<Button
className="js-confirm"
disabled={false}
onClick={[Function]}
>
save
</Button>
`;

exports[`should correctly change the button based on the status and license 2`] = `
<Button
className="js-confirm"
disabled={true}
onClick={[Function]}
>
save
</Button>
`;

exports[`should correctly change the button based on the status and license 3`] = `
<Button
className="js-confirm"
disabled={true}
onClick={[Function]}
>
marketplace.install
</Button>
`;

exports[`should display correctly 1`] = `
<Modal
contentLabel="marketplace.upgrade_to_x.Foo"
onRequestClose={[MockFunction]}
>
<header
className="modal-head"
>
<h2>
marketplace.upgrade_to_x.Foo
</h2>
</header>
<LicenseEditionSet
className="modal-body"
edition={
Object {
"downloadUrl": "download_url",
"homeUrl": "more_url",
"key": "foo",
"licenseRequestUrl": "license_url",
"name": "Foo",
"textDescription": "Foo desc",
}
}
editions={
Array [
Object {
"downloadUrl": "download_url",
"homeUrl": "more_url",
"key": "foo",
"licenseRequestUrl": "license_url",
"name": "Foo",
"textDescription": "Foo desc",
},
]
}
updateLicense={[Function]}
/>
<footer
className="modal-foot"
>
<ResetButtonLink
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</footer>
</Modal>
`;

+ 0
- 105
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/LicenseEditionSet-test.tsx.snap Целия файл

@@ -1,105 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should correctly display status message after checking license 1`] = `
<p
className="alert alert-success"
>
marketplace.license_preview_status.NO_INSTALL.Foo
</p>
`;

exports[`should correctly display status message after checking license 2`] = `
<p
className="alert alert-warning"
>
marketplace.license_preview_status.AUTOMATIC_INSTALL.Foo
</p>
`;

exports[`should correctly display status message after checking license 3`] = `
<p
className="alert alert-danger"
>
marketplace.license_preview_status.MANUAL_INSTALL.Foo
</p>
`;

exports[`should correctly display status message after checking license 4`] = `
<p
className="alert alert-danger"
>
marketplace.wrong_license_type_x.Foo
</p>
`;

exports[`should correctly display status message after checking license 5`] = `
<p
className="alert alert-danger"
>
marketplace.wrong_license_type
</p>
`;

exports[`should display correctly 1`] = `
<div>
<label
className="display-inline-block spacer-bottom"
htmlFor="set-license"
>
marketplace.enter_license_for_x.Foo
<em
className="mandatory"
>
*
</em>
</label>
<textarea
autoFocus={true}
className="display-block input-super-large"
id="set-license"
onChange={[Function]}
required={true}
rows={8}
style={
Object {
"resize": "none",
}
}
value=""
/>
<DeferredSpinner
customSpinner={
<p
className="spacer-top"
>
<i
className="spinner spacer-right text-bottom"
/>
marketplace.checking_license
</p>
}
loading={false}
timeout={100}
>
<div
className="spacer-top"
>
<a
href="license_url"
target="_blank"
>
marketplace.i_need_a_license
</a>
</div>
</DeferredSpinner>
</div>
`;

exports[`should display the get license link with parameters 1`] = `
<a
href="license_url?serverId=foo&ncloc=1000"
target="_blank"
>
marketplace.i_need_a_license
</a>
`;

+ 0
- 39
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/UninstallEditionForm-test.tsx.snap Целия файл

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display correctly 1`] = `
<Modal
contentLabel="marketplace.downgrade_to_community_edition."
onRequestClose={[MockFunction]}
>
<header
className="modal-head"
>
<h2>
marketplace.downgrade_to_community_edition.
</h2>
</header>
<div
className="modal-body"
>
<p>
marketplace.uninstall_x_confirmation.Foo
</p>
</div>
<footer
className="modal-foot"
>
<Button
disabled={false}
onClick={[Function]}
>
marketplace.downgrade
</Button>
<ResetButtonLink
className="js-modal-close"
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</footer>
</Modal>
`;

+ 5
- 0
server/sonar-web/src/main/js/apps/marketplace/style.css Целия файл

@@ -35,6 +35,11 @@
margin-right: 8px;
}

.marketplace-edition .markdown h3 {
font-size: 14px;
margin-top: 0;
}

.marketplace-edition-badge {
position: absolute;
right: -1px;

+ 53
- 14
server/sonar-web/src/main/js/apps/marketplace/utils.ts Целия файл

@@ -17,26 +17,58 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { memoize, sortBy } from 'lodash';
import { stringify } from 'querystring';
import { memoize } from 'lodash';
import { Plugin, PluginAvailable, PluginInstalled, PluginPending } from '../../api/plugins';
import { cleanQuery, parseAsString, RawQuery, serializeString } from '../../helpers/query';
import { Edition } from '../../api/marketplace';
import { omitNil } from '../../helpers/request';

export interface Edition {
downloadUrl?: string;
homeUrl: string;
key: string;
}

export interface Query {
filter: string;
search?: string;
}

export function isPluginAvailable(plugin: Plugin): plugin is PluginAvailable {
return (plugin as any).release !== undefined;
}
export const EDITIONS: Edition[] = [
{
key: 'community',
homeUrl: 'https://redirect.sonarsource.com/editions/community.html'
},
{
key: 'developer',
homeUrl: 'https://redirect.sonarsource.com/editions/developer.html',
downloadUrl:
'https://sonarsource.bintray.com/CommercialDistribution/editions/developer-edition-7.0.0.717.zip'
},
{
key: 'enterprise',
homeUrl: 'https://redirect.sonarsource.com/editions/enterprise.html',
downloadUrl:
'https://sonarsource.bintray.com/CommercialDistribution/editions/enterprise-edition-7.0.0.717.zip'
},
{
key: 'datacenter',
homeUrl: 'https://redirect.sonarsource.com/editions/datacenter.html',
downloadUrl:
'https://sonarsource.bintray.com/CommercialDistribution/editions/datacenter-edition-7.0.0.717.zip'
}
];

export function isPluginInstalled(plugin: Plugin): plugin is PluginInstalled {
return isPluginPending(plugin) && (plugin as any).updatedAt !== undefined;
}

export function isPluginPending(plugin: Plugin): plugin is PluginPending {
return (plugin as any).version !== undefined;
export function getEditionUrl(
edition: Edition,
data: { serverId?: string; ncloc?: number; sourceEdition: string }
) {
let url = edition.homeUrl;
const query = stringify(omitNil(data));
if (query) {
url += '?' + query;
}
return url;
}

export function filterPlugins(plugins: Plugin[], search: string): Plugin[] {
@@ -50,9 +82,16 @@ export function filterPlugins(plugins: Plugin[], search: string): Plugin[] {
});
}

const EDITIONS_ORDER = ['community', 'developer', 'enterprise', 'datacenter'];
export function sortEditions(editions: Edition[]): Edition[] {
return sortBy(editions, edition => EDITIONS_ORDER.indexOf(edition.key));
export function isPluginAvailable(plugin: Plugin): plugin is PluginAvailable {
return (plugin as any).release !== undefined;
}

export function isPluginInstalled(plugin: Plugin): plugin is PluginInstalled {
return isPluginPending(plugin) && (plugin as any).updatedAt !== undefined;
}

export function isPluginPending(plugin: Plugin): plugin is PluginPending {
return (plugin as any).version !== undefined;
}

export const DEFAULT_FILTER = 'all';

+ 13
- 56
server/sonar-web/src/main/js/store/marketplace/actions.ts Целия файл

@@ -18,62 +18,34 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Dispatch } from 'react-redux';
import { getEditionsForVersion, getEditionsForLastVersion } from './utils';
import { Edition, EditionStatus, getEditionStatus, getEditionsList } from '../../api/marketplace';
import { getEditionStatus } from '../../api/marketplace';
import { getPendingPlugins, PluginPendingResult } from '../../api/plugins';

interface LoadEditionsAction {
type: 'LOAD_EDITIONS';
loading: boolean;
}

interface SetPendingPluginsAction {
type: 'SET_PENDING_PLUGINS';
pending: PluginPendingResult;
}

interface SetEditionsAction {
type: 'SET_EDITIONS';
editions: Edition[];
readOnly: boolean;
}

interface SetEditionStatusAction {
type: 'SET_EDITION_STATUS';
status: EditionStatus;
interface SetCurrentEditionAction {
type: 'SET_CURRENT_EDITION';
currentEdition?: string;
}

export type Action =
| LoadEditionsAction
| SetEditionsAction
| SetEditionStatusAction
| SetPendingPluginsAction;

export function loadEditions(loading = true): LoadEditionsAction {
return { type: 'LOAD_EDITIONS', loading };
}
export type Action = SetCurrentEditionAction | SetPendingPluginsAction;

export function setPendingPlugins(pending: PluginPendingResult): SetPendingPluginsAction {
return { type: 'SET_PENDING_PLUGINS', pending };
}

export function setEditions(editions: Edition[], readOnly?: boolean): SetEditionsAction {
return { type: 'SET_EDITIONS', editions, readOnly: !!readOnly };
}
export const setCurrentEdition = (currentEdition?: string) => (dispatch: Dispatch<Action>) => {
dispatch({ type: 'SET_CURRENT_EDITION', currentEdition });
};

let editionTimer: number | undefined;
export const setEditionStatus = (status: EditionStatus) => (dispatch: Dispatch<Action>) => {
dispatch({ type: 'SET_EDITION_STATUS', status });
if (editionTimer) {
window.clearTimeout(editionTimer);
editionTimer = undefined;
}
if (status.installationStatus === 'AUTOMATIC_IN_PROGRESS') {
editionTimer = window.setTimeout(() => {
getEditionStatus().then(status => setEditionStatus(status)(dispatch), () => {});
editionTimer = undefined;
}, 2000);
}
export const fetchCurrentEdition = () => (dispatch: Dispatch<Action>) => {
getEditionStatus().then(
editionStatus => dispatch(setCurrentEdition(editionStatus.currentEditionKey)),
() => {}
);
};

export const fetchPendingPlugins = () => (dispatch: Dispatch<Action>) => {
@@ -84,18 +56,3 @@ export const fetchPendingPlugins = () => (dispatch: Dispatch<Action>) => {
() => {}
);
};

export const fetchEditions = (url: string, version: string) => (dispatch: Dispatch<Action>) => {
dispatch(loadEditions(true));
getEditionsList(url).then(
editionsPerVersion => {
const editions = getEditionsForVersion(editionsPerVersion, version);
if (editions) {
dispatch(setEditions(editions));
} else {
dispatch(setEditions(getEditionsForLastVersion(editionsPerVersion), true));
}
},
() => dispatch(loadEditions(false))
);
};

+ 8
- 26
server/sonar-web/src/main/js/store/marketplace/reducer.ts Целия файл

@@ -18,48 +18,30 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Action } from './actions';
import { Edition, EditionStatus } from '../../api/marketplace';
import { PluginPendingResult } from '../../api/plugins';

interface State {
editions?: Edition[];
loading: boolean;
status?: EditionStatus;
readOnly: boolean;
currentEdition?: string;
pending: PluginPendingResult;
}

const defaultState: State = {
loading: true,
readOnly: false,
pending: { installing: [], removing: [], updating: [] }
};
const defaultState: State = { pending: { installing: [], removing: [], updating: [] } };

export default function(state: State = defaultState, action: Action): State {
if (action.type === 'SET_EDITIONS') {
return { ...state, editions: action.editions, readOnly: action.readOnly, loading: false };
}
if (action.type === 'LOAD_EDITIONS') {
return { ...state, loading: action.loading };
}
if (action.type === 'SET_PENDING_PLUGINS') {
return {
...state,
pending: action.pending
};
}
if (action.type === 'SET_EDITION_STATUS') {
const hasChanged = Object.keys(action.status).some(
(key: keyof EditionStatus) => !state.status || state.status[key] !== action.status[key]
);
// Prevent from rerendering the whole admin if the status didn't change
if (hasChanged) {
return { ...state, status: action.status };
}
if (action.type === 'SET_CURRENT_EDITION') {
return {
...state,
currentEdition: action.currentEdition
};
}
return state;
}

export const getEditions = (state: State) => state.editions;
export const getEditionStatus = (state: State) => state.status;
export const getCurrentEdition = (state: State) => state.currentEdition;
export const getPendingPlugins = (state: State) => state.pending;

+ 0
- 49
server/sonar-web/src/main/js/store/marketplace/utils.ts Целия файл

@@ -1,49 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { sortBy } from 'lodash';
import { Edition, EditionsPerVersion } from '../../api/marketplace';

export function getEditionsForLastVersion(editions: EditionsPerVersion): Edition[] {
const sortedVersion = sortBy(Object.keys(editions), [
(version: string) => -Number(version.split('.')[0]),
(version: string) => -Number(version.split('.')[1] || 0),
(version: string) => -Number(version.split('.')[2] || 0)
]);
return editions[sortedVersion[0]];
}

export function getEditionsForVersion(
editions: EditionsPerVersion,
version: string
): Edition[] | undefined {
const minorVersion = version.match(/\d+\.\d+.\d+/);
if (minorVersion) {
if (editions[minorVersion[0]]) {
return editions[minorVersion[0]];
}
}
const majorVersion = version.match(/\d+\.\d+/);
if (majorVersion) {
if (editions[majorVersion[0]]) {
return editions[majorVersion[0]];
}
}
return undefined;
}

+ 2
- 4
server/sonar-web/src/main/js/store/rootReducer.js Целия файл

@@ -73,10 +73,8 @@ export const isFavorite = (state, componentKey) =>

export const getMarketplaceState = state => state.marketplace;

export const getMarketplaceEditions = state => fromMarketplace.getEditions(state.marketplace);

export const getMarketplaceEditionStatus = state =>
fromMarketplace.getEditionStatus(state.marketplace);
export const getMarketplaceCurrentEdition = state =>
fromMarketplace.getCurrentEdition(state.marketplace);

export const getMarketplacePendingPlugins = state =>
fromMarketplace.getPendingPlugins(state.marketplace);

+ 0
- 10
sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java Целия файл

@@ -47,7 +47,6 @@ public class CorePropertyDefinitions {
public static final String ORGANIZATIONS_CREATE_PERSONAL_ORG = "sonar.organizations.createPersonalOrg";
public static final String ONBOARDING_TUTORIAL_SHOW_TO_NEW_USERS = "sonar.onboardingTutorial.showToNewUsers";
public static final String DISABLE_NOTIFICATION_ON_BUILT_IN_QPROFILES = "sonar.builtInQualityProfiles.disableNotificationOnUpdate";
public static final String EDITIONS_CONFIG_URL = "sonar.editions.jsonUrl";

private CorePropertyDefinitions() {
// only static stuff
@@ -232,15 +231,6 @@ public class CorePropertyDefinitions {
.category(CATEGORY_ORGANIZATIONS)
.type(BOOLEAN)
.hidden()
.build(),

// EDITIONS
PropertyDefinition.builder(EDITIONS_CONFIG_URL)
.name("Defines URL of JSON file with the definitions of SonarSource editions.")
.defaultValue("https://update.sonarsource.org/editions.json")
.category(CATEGORY_ORGANIZATIONS)
.type(BOOLEAN)
.hidden()
.build()));
return defs;
}

+ 0
- 15
sonar-core/src/main/resources/org/sonar/l10n/core.properties Целия файл

@@ -2150,9 +2150,6 @@ marketplace.installed=Installed
marketplace.installing=Installing...
marketplace.upgrade=Upgrade
marketplace.downgrade=Downgrade
marketplace.downgrade_to_community_edition=Downgrade to Community Edition
marketplace.uninstall_x_confirmation=Are you sure you want to uninstall {0} and get back to the Community Edition?
marketplace.pending=Pending...
marketplace.checking_license=Checking your license...
marketplace._installed=installed
marketplace.available_under_commercial_license=Available under our commercial editions
@@ -2178,24 +2175,12 @@ marketplace.uninstall=Uninstall
marketplace.i_accept_the=I accept the
marketplace.commercial_edition=Commercial Edition
marketplace.terms_and_conditions=Terms and Conditions
marketplace.editions_unavailable=Explore our Commercial Editions on {url}: advanced feature packs brought to you by SonarSource
marketplace.edition_status.AUTOMATIC_IN_PROGRESS=Installing your new Commercial Edition... Please wait...
marketplace.edition_status.AUTOMATIC_READY=Commercial Edition successfully installed. Please restart the server to activate your new edition.
marketplace.edition_status.UNINSTALL_IN_PROGRESS=Commercial Edition successfully downgraded. Please restart the server to remove the features.
marketplace.edition_status.MANUAL_IN_PROGRESS=Commercial Edition can't automatically be installed because of internet access issues. Please manually install the package.
marketplace.edition_status_x.AUTOMATIC_IN_PROGRESS=Installing your new {0}... Please wait...
marketplace.edition_status_x.AUTOMATIC_READY={0} successfully installed. Please restart the server to activate your new edition.
marketplace.edition_status_x.UNINSTALL_IN_PROGRESS=Successfully downgraded to {0}. Please restart the server to remove the features.
marketplace.edition_status_x.MANUAL_IN_PROGRESS={0} can't automatically be installed because of internet access issues. Please manually install the package.
marketplace.release_notes=Release Notes
marketplace.how_to_install=How to install it?
marketplace.see_documentation_to_enable_cluster=See {url} documentation to set up a cluster.
marketplace.how_to_setup_cluster_url=Further configuration is required to set up a cluster. See {url} documentation.
marketplace.enter_license_for_x=Enter your license key for {0}
marketplace.wrong_license_type=Your license is not compatible with any existing editions. Please provide a valid license.
marketplace.wrong_license_type_x=Your license is not compatible with the selected edition. Please provide a valid license for {0}.
marketplace.i_need_a_license=I need a license key
marketplace.download_package=Download package
marketplace.search=Search by features, tags, or categories...



+ 1
- 1
sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java Целия файл

@@ -30,7 +30,7 @@ public class CorePropertyDefinitionsTest {
@Test
public void all() {
List<PropertyDefinition> defs = CorePropertyDefinitions.all();
assertThat(defs).hasSize(59);
assertThat(defs).hasSize(58);
}

@Test

Loading…
Отказ
Запис