]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9936 Add Editions pack in the marketplace
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 16 Oct 2017 08:01:37 +0000 (10:01 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 23 Oct 2017 15:01:13 +0000 (08:01 -0700)
28 files changed:
server/sonar-web/src/main/js/api/marketplace.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/AdminContainer.js [deleted file]
server/sonar-web/src/main/js/app/components/AdminContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap [deleted file]
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/marketplace/App.tsx
server/sonar-web/src/main/js/apps/marketplace/AppContainer.tsx
server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/__tests__/PendingActions-test.tsx
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/PendingActions-test.tsx.snap
server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/marketplace/style.css [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/request.ts
server/sonar-web/src/main/js/store/appState/duck.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/api/marketplace.ts b/server/sonar-web/src/main/js/api/marketplace.ts
new file mode 100644 (file)
index 0000000..282be5b
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { checkStatus, corsRequest, getJSON, parseJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
+
+export interface Edition {
+  name: string;
+  desc: string;
+  more_link: string;
+  request_license_link: string;
+  download_link: string;
+}
+
+export interface Editions {
+  [key: string]: Edition;
+}
+
+export interface EditionStatus {
+  currentEditionKey?: string;
+  nextEditionKey?: string;
+  installationStatus:
+    | 'NONE'
+    | 'AUTOMATIC_IN_PROGRESS'
+    | 'MANUAL_IN_PROGRESS'
+    | 'AUTOMATIC_READY'
+    | 'AUTOMATIC_FAILURE';
+}
+
+export function getEditionStatus(): Promise<EditionStatus> {
+  return getJSON('/api/editions/status').catch(throwGlobalError);
+}
+
+export function getEditionsList(): Promise<Editions> {
+  // TODO Replace with real url
+  const url =
+    'https://gist.githubusercontent.com/gregaubert/e34535494f8a94bec7cbc4d750ae7d06/raw/ba8670a28d4bc6fbac18f92e450ec42029cc5dcb/editions.json';
+  return corsRequest(url)
+    .submit()
+    .then(checkStatus)
+    .then(parseJSON);
+}
diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.js b/server/sonar-web/src/main/js/app/components/AdminContainer.js
deleted file mode 100644 (file)
index 0794e8f..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import SettingsNav from './nav/settings/SettingsNav';
-import { getAppState } from '../../store/rootReducer';
-import { onFail } from '../../store/rootActions';
-import { getSettingsNavigation } from '../../api/nav';
-import { setAdminPages } from '../../store/appState/duck';
-import { translate } from '../../helpers/l10n';
-
-class AdminContainer extends React.PureComponent {
-  componentDidMount() {
-    if (!this.props.appState.canAdmin) {
-      // workaround cyclic dependencies
-      const handleRequiredAuthorization = require('../utils/handleRequiredAuthorization').default;
-      handleRequiredAuthorization();
-    }
-    this.loadData();
-  }
-
-  loadData() {
-    getSettingsNavigation().then(
-      r => this.props.setAdminPages(r.extensions),
-      onFail(this.props.dispatch)
-    );
-  }
-
-  render() {
-    const { adminPages } = this.props.appState;
-
-    // Check that the adminPages are loaded
-    if (!adminPages) {
-      return null;
-    }
-
-    const defaultTitle = translate('layout.settings');
-
-    return (
-      <div>
-        <Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
-        <SettingsNav location={this.props.location} extensions={adminPages} />
-        {this.props.children}
-      </div>
-    );
-  }
-}
-
-const mapStateToProps = state => ({
-  appState: getAppState(state)
-});
-
-const mapDispatchToProps = { setAdminPages };
-
-export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer);
diff --git a/server/sonar-web/src/main/js/app/components/AdminContainer.tsx b/server/sonar-web/src/main/js/app/components/AdminContainer.tsx
new file mode 100644 (file)
index 0000000..fec085b
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 * as PropTypes from 'prop-types';
+import Helmet from 'react-helmet';
+import { connect } from 'react-redux';
+import SettingsNav from './nav/settings/SettingsNav';
+import { getAppState } from '../../store/rootReducer';
+import { getSettingsNavigation } from '../../api/nav';
+import { EditionStatus, getEditionStatus } from '../../api/marketplace';
+import { setAdminPages, setEditionStatus } from '../../store/appState/duck';
+import { translate } from '../../helpers/l10n';
+import { Extension } from '../types';
+
+interface Props {
+  appState: {
+    adminPages: Extension[];
+    editionStatus?: EditionStatus;
+    organizationsEnabled: boolean;
+  };
+  location: {};
+  setAdminPages: (adminPages: Extension[]) => void;
+  setEditionStatus: (editionStatus: EditionStatus) => void;
+}
+
+class AdminContainer extends React.PureComponent<Props> {
+  static contextTypes = {
+    canAdmin: PropTypes.bool.isRequired
+  };
+
+  componentDidMount() {
+    if (!this.context.canAdmin) {
+      // workaround cyclic dependencies
+      const handleRequiredAuthorization = require('../utils/handleRequiredAuthorization').default;
+      handleRequiredAuthorization();
+    } else {
+      this.loadData();
+    }
+  }
+
+  loadData() {
+    Promise.all([getSettingsNavigation(), getEditionStatus()]).then(
+      ([r, editionStatus]) => {
+        this.props.setAdminPages(r.extensions);
+        this.props.setEditionStatus(editionStatus);
+      },
+      () => {}
+    );
+  }
+
+  render() {
+    const { adminPages, editionStatus, organizationsEnabled } = this.props.appState;
+
+    // Check that the adminPages are loaded
+    if (!adminPages) {
+      return null;
+    }
+
+    const defaultTitle = translate('layout.settings');
+
+    return (
+      <div>
+        <Helmet defaultTitle={defaultTitle} titleTemplate={'%s - ' + defaultTitle} />
+        <SettingsNav
+          customOrganizations={organizationsEnabled}
+          editionStatus={editionStatus}
+          extensions={adminPages}
+          location={this.props.location}
+        />
+        {this.props.children}
+      </div>
+    );
+  }
+}
+
+const mapStateToProps = (state: any) => ({
+  appState: getAppState(state)
+});
+
+const mapDispatchToProps = { setAdminPages, setEditionStatus };
+
+export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer as any);
index ac54d018716ce375eba63f51ea5f67c9dd69d289..5f770db9ed4327ee758c4109f9add38d4ec99875 100644 (file)
@@ -21,7 +21,7 @@ import * as React from 'react';
 import { Link } from 'react-router';
 import * as classNames from 'classnames';
 import * as PropTypes from 'prop-types';
-import { Branch, Component, ComponentExtension } from '../../../types';
+import { Branch, Component, Extension } from '../../../types';
 import NavBarTabs from '../../../../components/nav/NavBarTabs';
 import {
   isShortLivingBranch,
@@ -419,7 +419,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
     );
   }
 
-  renderExtension = ({ key, name }: ComponentExtension, isAdmin: boolean) => {
+  renderExtension = ({ key, name }: Extension, isAdmin: boolean) => {
     const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`;
     return (
       <li key={key}>
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsEditionsNotif.tsx
new file mode 100644 (file)
index 0000000..5df886e
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 NavBarNotif from '../../../../components/nav/NavBarNotif';
+import { EditionStatus } from '../../../../api/marketplace';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  editionStatus: EditionStatus;
+}
+
+export default class SettingsEditionsNotif extends React.PureComponent<Props> {
+  render() {
+    const { editionStatus } = this.props;
+
+    if (editionStatus.installationStatus === 'AUTOMATIC_IN_PROGRESS') {
+      return (
+        <NavBarNotif className="alert alert-info">
+          <i className="spinner spacer-right text-bottom" />
+          <span>{translate('marketplace.status.AUTOMATIC_IN_PROGRESS')}</span>
+        </NavBarNotif>
+      );
+    } else if (editionStatus.installationStatus === 'AUTOMATIC_READY') {
+      return (
+        <NavBarNotif className="alert alert-success">
+          <span>{translate('marketplace.status.AUTOMATIC_READY')}</span>
+        </NavBarNotif>
+      );
+    } else if (
+      ['MANUAL_IN_PROGRESS', 'AUTOMATIC_FAILURE'].includes(editionStatus.installationStatus)
+    ) {
+      return (
+        <NavBarNotif className="alert alert-danger">
+          {translate('marketplace.status', editionStatus.installationStatus)}
+          <a className="little-spacer-left" href="https://www.sonarsource.com" target="_blank">
+            {translate('marketplace.how_to_install')}
+          </a>
+        </NavBarNotif>
+      );
+    }
+    return null;
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.js
deleted file mode 100644 (file)
index e307778..0000000
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import classNames from 'classnames';
-import { IndexLink, Link } from 'react-router';
-import { connect } from 'react-redux';
-import ContextNavBar from '../../../../components/nav/ContextNavBar';
-import NavBarTabs from '../../../../components/nav/NavBarTabs';
-import { translate } from '../../../../helpers/l10n';
-import { areThereCustomOrganizations } from '../../../../store/rootReducer';
-
-class SettingsNav extends React.PureComponent {
-  static defaultProps = {
-    extensions: []
-  };
-
-  isSomethingActive(urls) {
-    const path = window.location.pathname;
-    return urls.some(url => path.indexOf(window.baseUrl + url) === 0);
-  }
-
-  isSecurityActive() {
-    const urls = [
-      '/admin/users',
-      '/admin/groups',
-      '/admin/permissions',
-      '/admin/permission_templates'
-    ];
-    return this.isSomethingActive(urls);
-  }
-
-  isProjectsActive() {
-    const urls = ['/admin/projects_management', '/admin/background_tasks'];
-    return this.isSomethingActive(urls);
-  }
-
-  isSystemActive() {
-    const urls = ['/admin/update_center', '/admin/system'];
-    return this.isSomethingActive(urls);
-  }
-
-  renderExtension = ({ key, name }) => {
-    return (
-      <li key={key}>
-        <Link to={`/admin/extension/${key}`} activeClassName="active">
-          {name}
-        </Link>
-      </li>
-    );
-  };
-
-  render() {
-    const isSecurity = this.isSecurityActive();
-    const isProjects = this.isProjectsActive();
-    const isSystem = this.isSystemActive();
-    const isSupport = this.isSomethingActive(['/admin/extension/license/support']);
-
-    const securityClassName = classNames('dropdown-toggle', { active: isSecurity });
-    const projectsClassName = classNames('dropdown-toggle', { active: isProjects });
-    const systemClassName = classNames('dropdown-toggle', { active: isSystem });
-    const configurationClassNames = classNames('dropdown-toggle', {
-      active: !isSecurity && !isProjects && !isSystem && !isSupport
-    });
-
-    const extensionsWithoutSupport = this.props.extensions.filter(
-      extension => extension.key !== 'license/support'
-    );
-
-    const hasSupportExtension = extensionsWithoutSupport.length < this.props.extensions.length;
-
-    return (
-      <ContextNavBar id="context-navigation" height={65}>
-        <h1 className="navbar-context-header">
-          <strong>{translate('layout.settings')}</strong>
-        </h1>
-
-        <NavBarTabs>
-          <li className="dropdown">
-            <a
-              className={configurationClassNames}
-              data-toggle="dropdown"
-              id="settings-navigation-configuration"
-              href="#">
-              {translate('sidebar.project_settings')} <i className="icon-dropdown" />
-            </a>
-            <ul className="dropdown-menu">
-              <li>
-                <IndexLink to="/admin/settings" activeClassName="active">
-                  {translate('settings.page')}
-                </IndexLink>
-              </li>
-              <li>
-                <IndexLink to="/admin/settings/encryption" activeClassName="active">
-                  {translate('property.category.security.encryption')}
-                </IndexLink>
-              </li>
-              <li>
-                <IndexLink to="/admin/custom_metrics" activeClassName="active">
-                  {translate('custom_metrics.page')}
-                </IndexLink>
-              </li>
-              {extensionsWithoutSupport.map(this.renderExtension)}
-            </ul>
-          </li>
-
-          <li className="dropdown">
-            <a className={securityClassName} data-toggle="dropdown" href="#">
-              {translate('sidebar.security')} <i className="icon-dropdown" />
-            </a>
-            <ul className="dropdown-menu">
-              <li>
-                <IndexLink to="/admin/users" activeClassName="active">
-                  {translate('users.page')}
-                </IndexLink>
-              </li>
-              {!this.props.customOrganizations && (
-                <li>
-                  <IndexLink to="/admin/groups" activeClassName="active">
-                    {translate('user_groups.page')}
-                  </IndexLink>
-                </li>
-              )}
-              {!this.props.customOrganizations && (
-                <li>
-                  <IndexLink to="/admin/permissions" activeClassName="active">
-                    {translate('global_permissions.page')}
-                  </IndexLink>
-                </li>
-              )}
-              {!this.props.customOrganizations && (
-                <li>
-                  <IndexLink to="/admin/permission_templates" activeClassName="active">
-                    {translate('permission_templates')}
-                  </IndexLink>
-                </li>
-              )}
-            </ul>
-          </li>
-
-          <li className="dropdown">
-            <a className={projectsClassName} data-toggle="dropdown" href="#">
-              {translate('sidebar.projects')} <i className="icon-dropdown" />
-            </a>
-            <ul className="dropdown-menu">
-              {!this.props.customOrganizations && (
-                <li>
-                  <IndexLink to="/admin/projects_management" activeClassName="active">
-                    {translate('management')}
-                  </IndexLink>
-                </li>
-              )}
-              <li>
-                <IndexLink to="/admin/background_tasks" activeClassName="active">
-                  {translate('background_tasks.page')}
-                </IndexLink>
-              </li>
-            </ul>
-          </li>
-
-          <li className="dropdown">
-            <a className={systemClassName} data-toggle="dropdown" href="#">
-              {translate('sidebar.system')} <i className="icon-dropdown" />
-            </a>
-            <ul className="dropdown-menu">
-              <li>
-                <IndexLink to="/admin/update_center" activeClassName="active">
-                  {translate('update_center.page')}
-                </IndexLink>
-              </li>
-              <li>
-                <IndexLink to="/admin/system" activeClassName="active">
-                  {translate('system_info.page')}
-                </IndexLink>
-              </li>
-            </ul>
-          </li>
-
-          <li>
-            <IndexLink to="/admin/marketplace" activeClassName="active">
-              {translate('marketplace.page')}
-            </IndexLink>
-          </li>
-
-          {hasSupportExtension && (
-            <li>
-              <IndexLink to="/admin/extension/license/support" activeClassName="active">
-                {translate('support')}
-              </IndexLink>
-            </li>
-          )}
-        </NavBarTabs>
-      </ContextNavBar>
-    );
-  }
-}
-
-const mapStateToProps = state => ({
-  customOrganizations: areThereCustomOrganizations(state)
-});
-
-export default connect(mapStateToProps)(SettingsNav);
-
-export const UnconnectedSettingsNav = SettingsNav;
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
new file mode 100644 (file)
index 0000000..b677146
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 * as classNames from 'classnames';
+import { IndexLink, Link } from 'react-router';
+import ContextNavBar from '../../../../components/nav/ContextNavBar';
+import SettingsEditionsNotif from './SettingsEditionsNotif';
+import NavBarTabs from '../../../../components/nav/NavBarTabs';
+import { EditionStatus } from '../../../../api/marketplace';
+import { Extension } from '../../../types';
+import { translate } from '../../../../helpers/l10n';
+
+interface Props {
+  editionStatus?: EditionStatus;
+  extensions: Extension[];
+  customOrganizations: boolean;
+  location: {};
+}
+
+export default class SettingsNav extends React.PureComponent<Props> {
+  static defaultProps = {
+    extensions: []
+  };
+
+  isSomethingActive(urls: string[]): boolean {
+    const path = window.location.pathname;
+    return urls.some((url: string) => path.indexOf((window as any).baseUrl + url) === 0);
+  }
+
+  isSecurityActive() {
+    const urls = [
+      '/admin/users',
+      '/admin/groups',
+      '/admin/permissions',
+      '/admin/permission_templates'
+    ];
+    return this.isSomethingActive(urls);
+  }
+
+  isProjectsActive() {
+    const urls = ['/admin/projects_management', '/admin/background_tasks'];
+    return this.isSomethingActive(urls);
+  }
+
+  isSystemActive() {
+    const urls = ['/admin/update_center', '/admin/system'];
+    return this.isSomethingActive(urls);
+  }
+
+  renderExtension = ({ key, name }: Extension) => {
+    return (
+      <li key={key}>
+        <Link to={`/admin/extension/${key}`} activeClassName="active">
+          {name}
+        </Link>
+      </li>
+    );
+  };
+
+  render() {
+    const { customOrganizations, editionStatus, extensions } = this.props;
+    const isSecurity = this.isSecurityActive();
+    const isProjects = this.isProjectsActive();
+    const isSystem = this.isSystemActive();
+    const isSupport = this.isSomethingActive(['/admin/extension/license/support']);
+
+    const securityClassName = classNames('dropdown-toggle', { active: isSecurity });
+    const projectsClassName = classNames('dropdown-toggle', { active: isProjects });
+    const systemClassName = classNames('dropdown-toggle', { active: isSystem });
+    const configurationClassNames = classNames('dropdown-toggle', {
+      active: !isSecurity && !isProjects && !isSystem && !isSupport
+    });
+
+    const extensionsWithoutSupport = extensions.filter(
+      extension => extension.key !== 'license/support'
+    );
+
+    const hasSupportExtension = extensionsWithoutSupport.length < extensions.length;
+
+    let notifComponent;
+    if (editionStatus && editionStatus.installationStatus !== 'NONE') {
+      notifComponent = <SettingsEditionsNotif editionStatus={editionStatus} />;
+    }
+    return (
+      <ContextNavBar
+        id="context-navigation"
+        height={notifComponent ? 95 : 65}
+        notif={notifComponent}>
+        <h1 className="navbar-context-header">
+          <strong>{translate('layout.settings')}</strong>
+        </h1>
+
+        <NavBarTabs>
+          <li className="dropdown">
+            <a
+              className={configurationClassNames}
+              data-toggle="dropdown"
+              id="settings-navigation-configuration"
+              href="#">
+              {translate('sidebar.project_settings')} <i className="icon-dropdown" />
+            </a>
+            <ul className="dropdown-menu">
+              <li>
+                <IndexLink to="/admin/settings" activeClassName="active">
+                  {translate('settings.page')}
+                </IndexLink>
+              </li>
+              <li>
+                <IndexLink to="/admin/settings/encryption" activeClassName="active">
+                  {translate('property.category.security.encryption')}
+                </IndexLink>
+              </li>
+              <li>
+                <IndexLink to="/admin/custom_metrics" activeClassName="active">
+                  {translate('custom_metrics.page')}
+                </IndexLink>
+              </li>
+              {extensionsWithoutSupport.map(this.renderExtension)}
+            </ul>
+          </li>
+
+          <li className="dropdown">
+            <a className={securityClassName} data-toggle="dropdown" href="#">
+              {translate('sidebar.security')} <i className="icon-dropdown" />
+            </a>
+            <ul className="dropdown-menu">
+              <li>
+                <IndexLink to="/admin/users" activeClassName="active">
+                  {translate('users.page')}
+                </IndexLink>
+              </li>
+              {!customOrganizations && (
+                <li>
+                  <IndexLink to="/admin/groups" activeClassName="active">
+                    {translate('user_groups.page')}
+                  </IndexLink>
+                </li>
+              )}
+              {!customOrganizations && (
+                <li>
+                  <IndexLink to="/admin/permissions" activeClassName="active">
+                    {translate('global_permissions.page')}
+                  </IndexLink>
+                </li>
+              )}
+              {!customOrganizations && (
+                <li>
+                  <IndexLink to="/admin/permission_templates" activeClassName="active">
+                    {translate('permission_templates')}
+                  </IndexLink>
+                </li>
+              )}
+            </ul>
+          </li>
+
+          <li className="dropdown">
+            <a className={projectsClassName} data-toggle="dropdown" href="#">
+              {translate('sidebar.projects')} <i className="icon-dropdown" />
+            </a>
+            <ul className="dropdown-menu">
+              {!customOrganizations && (
+                <li>
+                  <IndexLink to="/admin/projects_management" activeClassName="active">
+                    {translate('management')}
+                  </IndexLink>
+                </li>
+              )}
+              <li>
+                <IndexLink to="/admin/background_tasks" activeClassName="active">
+                  {translate('background_tasks.page')}
+                </IndexLink>
+              </li>
+            </ul>
+          </li>
+
+          <li className="dropdown">
+            <a className={systemClassName} data-toggle="dropdown" href="#">
+              {translate('sidebar.system')} <i className="icon-dropdown" />
+            </a>
+            <ul className="dropdown-menu">
+              <li>
+                <IndexLink to="/admin/update_center" activeClassName="active">
+                  {translate('update_center.page')}
+                </IndexLink>
+              </li>
+              <li>
+                <IndexLink to="/admin/system" activeClassName="active">
+                  {translate('system_info.page')}
+                </IndexLink>
+              </li>
+            </ul>
+          </li>
+
+          <li>
+            <IndexLink to="/admin/marketplace" activeClassName="active">
+              {translate('marketplace.page')}
+            </IndexLink>
+          </li>
+
+          {hasSupportExtension && (
+            <li>
+              <IndexLink to="/admin/extension/license/support" activeClassName="active">
+                {translate('support')}
+              </IndexLink>
+            </li>
+          )}
+        </NavBarTabs>
+      </ContextNavBar>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsEditionsNotif-test.tsx
new file mode 100644 (file)
index 0000000..55612c0
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 SettingsEditionsNotif from '../SettingsEditionsNotif';
+
+it('should display an in progress notif', () => {
+  const wrapper = shallow(
+    <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should display an error notification', () => {
+  const wrapper = shallow(
+    <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_FAILURE' }} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should display a ready notification', () => {
+  const wrapper = shallow(
+    <SettingsEditionsNotif editionStatus={{ installationStatus: 'AUTOMATIC_READY' }} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.js b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.js
deleted file mode 100644 (file)
index 48f8539..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 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 React from 'react';
-import { shallow } from 'enzyme';
-import { UnconnectedSettingsNav } from '../SettingsNav';
-
-it('should work with extensions', () => {
-  const extensions = [{ key: 'foo', name: 'Foo' }];
-  const wrapper = shallow(<UnconnectedSettingsNav extensions={extensions} />);
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/SettingsNav-test.tsx
new file mode 100644 (file)
index 0000000..f819af9
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 SettingsNav from '../SettingsNav';
+
+it('should work with extensions', () => {
+  const extensions = [{ key: 'foo', name: 'Foo' }];
+  const wrapper = shallow(
+    <SettingsNav
+      customOrganizations={false}
+      editionStatus={{ installationStatus: 'NONE' }}
+      extensions={extensions}
+      location={{}}
+    />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should display an edition notification', () => {
+  const wrapper = shallow(
+    <SettingsNav
+      customOrganizations={false}
+      editionStatus={{ installationStatus: 'AUTOMATIC_IN_PROGRESS' }}
+      extensions={[]}
+      location={{}}
+    />
+  );
+  expect({ ...wrapper.find('ContextNavBar').props(), children: [] }).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsEditionsNotif-test.tsx.snap
new file mode 100644 (file)
index 0000000..030ff10
--- /dev/null
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display a ready notification 1`] = `
+<NavBarNotif
+  className="alert alert-success"
+>
+  <span>
+    marketplace.status.AUTOMATIC_READY
+  </span>
+</NavBarNotif>
+`;
+
+exports[`should display an error notification 1`] = `
+<NavBarNotif
+  className="alert alert-danger"
+>
+  marketplace.status.AUTOMATIC_FAILURE
+  <a
+    className="little-spacer-left"
+    href="https://www.sonarsource.com"
+    target="_blank"
+  >
+    marketplace.how_to_install
+  </a>
+</NavBarNotif>
+`;
+
+exports[`should display an in progress notif 1`] = `
+<NavBarNotif
+  className="alert alert-info"
+>
+  <i
+    className="spinner spacer-right text-bottom"
+  />
+  <span>
+    marketplace.status.AUTOMATIC_IN_PROGRESS
+  </span>
+</NavBarNotif>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.js.snap
deleted file mode 100644 (file)
index 62c113f..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should work with extensions 1`] = `
-<ContextNavBar
-  height={65}
-  id="context-navigation"
->
-  <h1
-    className="navbar-context-header"
-  >
-    <strong>
-      layout.settings
-    </strong>
-  </h1>
-  <NavBarTabs>
-    <li
-      className="dropdown"
-    >
-      <a
-        className="dropdown-toggle active"
-        data-toggle="dropdown"
-        href="#"
-        id="settings-navigation-configuration"
-      >
-        sidebar.project_settings
-         
-        <i
-          className="icon-dropdown"
-        />
-      </a>
-      <ul
-        className="dropdown-menu"
-      >
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/settings"
-          >
-            settings.page
-          </IndexLink>
-        </li>
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/settings/encryption"
-          >
-            property.category.security.encryption
-          </IndexLink>
-        </li>
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/custom_metrics"
-          >
-            custom_metrics.page
-          </IndexLink>
-        </li>
-        <li>
-          <Link
-            activeClassName="active"
-            onlyActiveOnIndex={false}
-            style={Object {}}
-            to="/admin/extension/foo"
-          >
-            Foo
-          </Link>
-        </li>
-      </ul>
-    </li>
-    <li
-      className="dropdown"
-    >
-      <a
-        className="dropdown-toggle"
-        data-toggle="dropdown"
-        href="#"
-      >
-        sidebar.security
-         
-        <i
-          className="icon-dropdown"
-        />
-      </a>
-      <ul
-        className="dropdown-menu"
-      >
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/users"
-          >
-            users.page
-          </IndexLink>
-        </li>
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/groups"
-          >
-            user_groups.page
-          </IndexLink>
-        </li>
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/permissions"
-          >
-            global_permissions.page
-          </IndexLink>
-        </li>
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/permission_templates"
-          >
-            permission_templates
-          </IndexLink>
-        </li>
-      </ul>
-    </li>
-    <li
-      className="dropdown"
-    >
-      <a
-        className="dropdown-toggle"
-        data-toggle="dropdown"
-        href="#"
-      >
-        sidebar.projects
-         
-        <i
-          className="icon-dropdown"
-        />
-      </a>
-      <ul
-        className="dropdown-menu"
-      >
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/projects_management"
-          >
-            management
-          </IndexLink>
-        </li>
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/background_tasks"
-          >
-            background_tasks.page
-          </IndexLink>
-        </li>
-      </ul>
-    </li>
-    <li
-      className="dropdown"
-    >
-      <a
-        className="dropdown-toggle"
-        data-toggle="dropdown"
-        href="#"
-      >
-        sidebar.system
-         
-        <i
-          className="icon-dropdown"
-        />
-      </a>
-      <ul
-        className="dropdown-menu"
-      >
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/update_center"
-          >
-            update_center.page
-          </IndexLink>
-        </li>
-        <li>
-          <IndexLink
-            activeClassName="active"
-            to="/admin/system"
-          >
-            system_info.page
-          </IndexLink>
-        </li>
-      </ul>
-    </li>
-    <li>
-      <IndexLink
-        activeClassName="active"
-        to="/admin/marketplace"
-      >
-        marketplace.page
-      </IndexLink>
-    </li>
-  </NavBarTabs>
-</ContextNavBar>
-`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
new file mode 100644 (file)
index 0000000..9dd9e56
--- /dev/null
@@ -0,0 +1,216 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display an edition notification 1`] = `
+Object {
+  "children": Array [],
+  "height": 95,
+  "id": "context-navigation",
+  "notif": <SettingsEditionsNotif
+    editionStatus={
+        Object {
+            "installationStatus": "AUTOMATIC_IN_PROGRESS",
+          }
+    }
+/>,
+}
+`;
+
+exports[`should work with extensions 1`] = `
+<ContextNavBar
+  height={65}
+  id="context-navigation"
+>
+  <h1
+    className="navbar-context-header"
+  >
+    <strong>
+      layout.settings
+    </strong>
+  </h1>
+  <NavBarTabs>
+    <li
+      className="dropdown"
+    >
+      <a
+        className="dropdown-toggle active"
+        data-toggle="dropdown"
+        href="#"
+        id="settings-navigation-configuration"
+      >
+        sidebar.project_settings
+         
+        <i
+          className="icon-dropdown"
+        />
+      </a>
+      <ul
+        className="dropdown-menu"
+      >
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/settings"
+          >
+            settings.page
+          </IndexLink>
+        </li>
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/settings/encryption"
+          >
+            property.category.security.encryption
+          </IndexLink>
+        </li>
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/custom_metrics"
+          >
+            custom_metrics.page
+          </IndexLink>
+        </li>
+        <li>
+          <Link
+            activeClassName="active"
+            onlyActiveOnIndex={false}
+            style={Object {}}
+            to="/admin/extension/foo"
+          >
+            Foo
+          </Link>
+        </li>
+      </ul>
+    </li>
+    <li
+      className="dropdown"
+    >
+      <a
+        className="dropdown-toggle"
+        data-toggle="dropdown"
+        href="#"
+      >
+        sidebar.security
+         
+        <i
+          className="icon-dropdown"
+        />
+      </a>
+      <ul
+        className="dropdown-menu"
+      >
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/users"
+          >
+            users.page
+          </IndexLink>
+        </li>
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/groups"
+          >
+            user_groups.page
+          </IndexLink>
+        </li>
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/permissions"
+          >
+            global_permissions.page
+          </IndexLink>
+        </li>
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/permission_templates"
+          >
+            permission_templates
+          </IndexLink>
+        </li>
+      </ul>
+    </li>
+    <li
+      className="dropdown"
+    >
+      <a
+        className="dropdown-toggle"
+        data-toggle="dropdown"
+        href="#"
+      >
+        sidebar.projects
+         
+        <i
+          className="icon-dropdown"
+        />
+      </a>
+      <ul
+        className="dropdown-menu"
+      >
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/projects_management"
+          >
+            management
+          </IndexLink>
+        </li>
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/background_tasks"
+          >
+            background_tasks.page
+          </IndexLink>
+        </li>
+      </ul>
+    </li>
+    <li
+      className="dropdown"
+    >
+      <a
+        className="dropdown-toggle"
+        data-toggle="dropdown"
+        href="#"
+      >
+        sidebar.system
+         
+        <i
+          className="icon-dropdown"
+        />
+      </a>
+      <ul
+        className="dropdown-menu"
+      >
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/update_center"
+          >
+            update_center.page
+          </IndexLink>
+        </li>
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/system"
+          >
+            system_info.page
+          </IndexLink>
+        </li>
+      </ul>
+    </li>
+    <li>
+      <IndexLink
+        activeClassName="active"
+        to="/admin/marketplace"
+      >
+        marketplace.page
+      </IndexLink>
+    </li>
+  </NavBarTabs>
+</ContextNavBar>
+`;
index 8bf75ae6e04d31990c1edff3d806466944042f30..73239aeb4305ceac53d211877653d8c47b915982 100644 (file)
@@ -57,7 +57,7 @@ export interface ShortLivingBranch {
 
 export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch;
 
-export interface ComponentExtension {
+export interface Extension {
   key: string;
   name: string;
 }
@@ -71,7 +71,7 @@ export interface Component {
   }>;
   configuration?: ComponentConfiguration;
   description?: string;
-  extensions?: ComponentExtension[];
+  extensions?: Extension[];
   isFavorite?: boolean;
   key: string;
   name: string;
@@ -83,7 +83,7 @@ export interface Component {
 }
 
 interface ComponentConfiguration {
-  extensions?: ComponentExtension[];
+  extensions?: Extension[];
   showBackgroundTasks?: boolean;
   showLinks?: boolean;
   showManualMeasures?: boolean;
index 0cb9096adc3266812a1cb850f64467fbc8c0ef5f..6ee3d1bce8f2b35b1d57b3c38fd150238ef81ba9 100644 (file)
@@ -22,6 +22,7 @@ import * as PropTypes from 'prop-types';
 import { sortBy, uniqBy } from 'lodash';
 import Helmet from 'react-helmet';
 import Header from './Header';
+import EditionBoxes from './EditionBoxes';
 import Footer from './Footer';
 import PendingActions from './PendingActions';
 import PluginsList from './PluginsList';
@@ -34,11 +35,13 @@ import {
   Plugin,
   PluginPending
 } from '../../api/plugins';
+import { EditionStatus } from '../../api/marketplace';
 import { RawQuery } from '../../helpers/query';
 import { translate } from '../../helpers/l10n';
 import { filterPlugins, parseQuery, Query, serializeQuery } from './utils';
 
 export interface Props {
+  editionStatus?: EditionStatus;
   location: { pathname: string; query: RawQuery };
   updateCenterActive: boolean;
 }
@@ -109,7 +112,11 @@ export default class App extends React.PureComponent<Props, State> {
           });
         }
       },
-      () => {}
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
     );
   };
 
@@ -121,7 +128,11 @@ export default class App extends React.PureComponent<Props, State> {
           this.setState({ loading: false, plugins });
         }
       },
-      () => {}
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
     );
   };
 
@@ -154,6 +165,10 @@ export default class App extends React.PureComponent<Props, State> {
       <div className="page page-limited" id="marketplace-page">
         <Helmet title={translate('marketplace.page')} />
         <Header />
+        <EditionBoxes
+          editionStatus={this.props.editionStatus}
+          updateCenterActive={this.props.updateCenterActive}
+        />
         <PendingActions refreshPending={this.fetchPendingPlugins} pending={pending} />
         <Search
           query={query}
index 4319d2b1890cf5ce176a6f2554551ad2f22981e2..418219fd22a1d0e274ae3183f56d0927fda2d64f 100644 (file)
  */
 import { connect } from 'react-redux';
 import App from './App';
-import { getGlobalSettingValue } from '../../store/rootReducer';
+import { getAppState, getGlobalSettingValue } from '../../store/rootReducer';
+import './style.css';
 
 const mapStateToProps = (state: any) => ({
+  editionStatus: getAppState(state).editionStatus,
   updateCenterActive: (getGlobalSettingValue(state, 'sonar.updatecenter.activate') || {}).value
 });
 
diff --git a/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx b/server/sonar-web/src/main/js/apps/marketplace/EditionBoxes.tsx
new file mode 100644 (file)
index 0000000..5c74a1e
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 EditionBox from './components/EditionBox';
+import { Editions, EditionStatus, getEditionsList } from '../../api/marketplace';
+import { translate } from '../../helpers/l10n';
+
+export interface Props {
+  editionStatus?: EditionStatus;
+  updateCenterActive: boolean;
+}
+
+interface State {
+  editions: Editions;
+  editionsError: boolean;
+  loading: boolean;
+}
+
+export default class EditionBoxes extends React.PureComponent<Props, State> {
+  mounted: boolean;
+  state: State = { editions: {}, editionsError: false, loading: true };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchEditions();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchEditions = () => {
+    this.setState({ loading: true });
+    getEditionsList().then(
+      editions => {
+        if (this.mounted) {
+          this.setState({
+            loading: false,
+            editions,
+            editionsError: false
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ editionsError: true, loading: false });
+        }
+      }
+    );
+  };
+
+  render() {
+    const { editions, loading } = this.state;
+    if (loading) {
+      return null;
+    }
+    return (
+      <div className="spacer-bottom marketplace-editions">
+        {this.state.editionsError ? (
+          <span className="alert alert-info">
+            <FormattedMessage
+              defaultMessage={translate('marketplace.editions_unavailable')}
+              id="marketplace.editions_unavailable"
+              values={{
+                url: (
+                  <a href="https://www.sonarsource.com" target="_blank">
+                    SonarSource.com
+                  </a>
+                )
+              }}
+            />
+          </span>
+        ) : (
+          Object.keys(editions).map(key => (
+            <EditionBox
+              edition={editions[key]}
+              editionKey={key}
+              editionStatus={this.props.editionStatus}
+              key={key}
+            />
+          ))
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/__tests__/EditionBoxes-test.tsx
new file mode 100644 (file)
index 0000000..49ee1ab
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 EditionBoxes from '../EditionBoxes';
+import { EditionStatus } from '../../../api/marketplace';
+
+const DEFAULT_STATUS: EditionStatus = {
+  currentEditionKey: 'foo',
+  nextEditionKey: '',
+  installationStatus: 'NONE'
+};
+
+it('should display the edition boxes', () => {
+  const wrapper = getWrapper();
+  expect(wrapper).toMatchSnapshot();
+  wrapper.setState({
+    editions: {
+      foo: {
+        name: 'Foo',
+        desc: 'Foo desc',
+        download_link: 'download_url',
+        more_link: 'more_url',
+        request_license_link: 'license_url'
+      },
+      bar: {
+        name: 'Bar',
+        desc: 'Bar desc',
+        download_link: 'download_url',
+        more_link: 'more_url',
+        request_license_link: 'license_url'
+      }
+    },
+    loading: false
+  });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should display an error message', () => {
+  const wrapper = getWrapper();
+  wrapper.setState({ loading: false, editionsError: true });
+  expect(wrapper).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <EditionBoxes editionStatus={DEFAULT_STATUS} updateCenterActive={true} {...props} />
+  );
+}
index 9d9d15c3bf823a463a7d2e3c1382c29291ea2b6c..4ebc2b4709a66dd52905e70d9c6663e7f7c4c7fd 100644 (file)
@@ -36,14 +36,14 @@ it('should display pending actions', () => {
   expect(getWrapper()).toMatchSnapshot();
 });
 
-it('should not display nothing', () => {
+it('should not display anything', () => {
   expect(getWrapper({ pending: { installing: [], updating: [], removing: [] } })).toMatchSnapshot();
 });
 
 it('should open the restart form', () => {
   const wrapper = getWrapper();
   click(wrapper.find('.js-restart'));
-  expect(wrapper.find('RestartForm')).toHaveLength(1);
+  expect(wrapper.find('RestartForm').exists()).toBeTruthy();
 });
 
 it('should cancel all pending and refresh them', async () => {
diff --git a/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/__tests__/__snapshots__/EditionBoxes-test.tsx.snap
new file mode 100644 (file)
index 0000000..7ee4e6b
--- /dev/null
@@ -0,0 +1,73 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display an error message 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://www.sonarsource.com"
+            target="_blank"
+        >
+            SonarSource.com
+        </a>,
+        }
+      }
+    />
+  </span>
+</div>
+`;
+
+exports[`should display the edition boxes 1`] = `null`;
+
+exports[`should display the edition boxes 2`] = `
+<div
+  className="spacer-bottom marketplace-editions"
+>
+  <EditionBox
+    edition={
+      Object {
+        "desc": "Foo desc",
+        "download_link": "download_url",
+        "more_link": "more_url",
+        "name": "Foo",
+        "request_license_link": "license_url",
+      }
+    }
+    editionKey="foo"
+    editionStatus={
+      Object {
+        "currentEditionKey": "foo",
+        "installationStatus": "NONE",
+        "nextEditionKey": "",
+      }
+    }
+  />
+  <EditionBox
+    edition={
+      Object {
+        "desc": "Bar desc",
+        "download_link": "download_url",
+        "more_link": "more_url",
+        "name": "Bar",
+        "request_license_link": "license_url",
+      }
+    }
+    editionKey="bar"
+    editionStatus={
+      Object {
+        "currentEditionKey": "foo",
+        "installationStatus": "NONE",
+        "nextEditionKey": "",
+      }
+    }
+  />
+</div>
+`;
index 36604e80f9847aff803f50dce3ef964c40e5981a..3afbde17ba65efb200f8b7ad59ec5d86040ade9d 100644 (file)
@@ -64,4 +64,4 @@ exports[`should display pending actions 1`] = `
 </div>
 `;
 
-exports[`should not display nothing 1`] = `null`;
+exports[`should not display anything 1`] = `null`;
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/EditionBox.tsx
new file mode 100644 (file)
index 0000000..3299504
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { Edition, EditionStatus } from '../../../api/marketplace';
+import { translate } from '../../../helpers/l10n';
+
+export interface Props {
+  edition: Edition;
+  editionKey: string;
+  editionStatus?: EditionStatus;
+}
+
+export default class EditionBox extends React.PureComponent<Props> {
+  render() {
+    const { edition, editionKey, editionStatus } = this.props;
+    const isInstalled = editionStatus && editionStatus.currentEditionKey === editionKey;
+    const isInstalling = editionStatus && editionStatus.nextEditionKey === editionKey;
+    const installInProgress =
+      editionStatus && editionStatus.installationStatus === 'AUTOMATIC_IN_PROGRESS';
+    return (
+      <div className="boxed-group boxed-group-inner marketplace-edition">
+        {isInstalled &&
+        !isInstalling && (
+          <span className="marketplace-edition-badge badge badge-normal-size">
+            <CheckIcon size={14} className="little-spacer-right text-text-top" />
+            {translate('marketplace.installed')}
+          </span>
+        )}
+        {isInstalling && (
+          <span className="marketplace-edition-badge badge badge-normal-size">
+            {translate('marketplace.installing')}
+          </span>
+        )}
+        <div>
+          <h3 className="spacer-bottom">{edition.name}</h3>
+          <p>{edition.desc}</p>
+        </div>
+        <div className="marketplace-edition-action spacer-top">
+          <a href={edition.more_link} target="_blank">
+            {translate('marketplace.learn_more')}
+          </a>
+          {!isInstalled && (
+            <button disabled={installInProgress}>{translate('marketplace.install')}</button>
+          )}
+          {isInstalled && (
+            <button className="button-red" disabled={installInProgress}>
+              {translate('marketplace.uninstall')}
+            </button>
+          )}
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/EditionBox-test.tsx
new file mode 100644 (file)
index 0000000..ebb42f2
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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 { Edition, EditionStatus } from '../../../../api/marketplace';
+import EditionBox from '../EditionBox';
+
+const DEFAULT_STATUS: EditionStatus = {
+  currentEditionKey: '',
+  nextEditionKey: '',
+  installationStatus: 'NONE'
+};
+
+const DEFAULT_EDITION: Edition = {
+  name: 'Foo',
+  desc: 'Foo desc',
+  download_link: 'download_url',
+  more_link: 'more_url',
+  request_license_link: 'license_url'
+};
+
+it('should display the edition', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should display installed badge', () => {
+  expect(
+    getWrapper({
+      editionStatus: {
+        currentEditionKey: 'foo',
+        nextEditionKey: '',
+        installationStatus: 'NONE'
+      }
+    })
+  ).toMatchSnapshot();
+});
+
+it('should display installing badge', () => {
+  expect(
+    getWrapper({
+      editionStatus: {
+        currentEditionKey: 'foo',
+        nextEditionKey: 'foo',
+        installationStatus: 'NONE'
+      }
+    })
+  ).toMatchSnapshot();
+});
+
+it('should disable install button', () => {
+  expect(
+    getWrapper({
+      editionStatus: {
+        currentEditionKey: 'foo',
+        nextEditionKey: '',
+        installationStatus: 'AUTOMATIC_IN_PROGRESS'
+      }
+    })
+  ).toMatchSnapshot();
+});
+
+it('should disable uninstall button', () => {
+  expect(
+    getWrapper({
+      editionStatus: {
+        currentEditionKey: '',
+        nextEditionKey: 'foo',
+        installationStatus: 'AUTOMATIC_IN_PROGRESS'
+      }
+    })
+  ).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+  return shallow(
+    <EditionBox
+      edition={DEFAULT_EDITION}
+      editionKey="foo"
+      editionStatus={DEFAULT_STATUS}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap b/server/sonar-web/src/main/js/apps/marketplace/components/__tests__/__snapshots__/EditionBox-test.tsx.snap
new file mode 100644 (file)
index 0000000..6814875
--- /dev/null
@@ -0,0 +1,192 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should disable install button 1`] = `
+<div
+  className="boxed-group boxed-group-inner marketplace-edition"
+>
+  <span
+    className="marketplace-edition-badge badge badge-normal-size"
+  >
+    <CheckIcon
+      className="little-spacer-right text-text-top"
+      size={14}
+    />
+    marketplace.installed
+  </span>
+  <div>
+    <h3
+      className="spacer-bottom"
+    >
+      Foo
+    </h3>
+    <p>
+      Foo desc
+    </p>
+  </div>
+  <div
+    className="marketplace-edition-action spacer-top"
+  >
+    <a
+      href="more_url"
+      target="_blank"
+    >
+      marketplace.learn_more
+    </a>
+    <button
+      className="button-red"
+      disabled={true}
+    >
+      marketplace.uninstall
+    </button>
+  </div>
+</div>
+`;
+
+exports[`should disable uninstall button 1`] = `
+<div
+  className="boxed-group boxed-group-inner marketplace-edition"
+>
+  <span
+    className="marketplace-edition-badge badge badge-normal-size"
+  >
+    marketplace.installing
+  </span>
+  <div>
+    <h3
+      className="spacer-bottom"
+    >
+      Foo
+    </h3>
+    <p>
+      Foo desc
+    </p>
+  </div>
+  <div
+    className="marketplace-edition-action spacer-top"
+  >
+    <a
+      href="more_url"
+      target="_blank"
+    >
+      marketplace.learn_more
+    </a>
+    <button
+      disabled={true}
+    >
+      marketplace.install
+    </button>
+  </div>
+</div>
+`;
+
+exports[`should display installed badge 1`] = `
+<div
+  className="boxed-group boxed-group-inner marketplace-edition"
+>
+  <span
+    className="marketplace-edition-badge badge badge-normal-size"
+  >
+    <CheckIcon
+      className="little-spacer-right text-text-top"
+      size={14}
+    />
+    marketplace.installed
+  </span>
+  <div>
+    <h3
+      className="spacer-bottom"
+    >
+      Foo
+    </h3>
+    <p>
+      Foo desc
+    </p>
+  </div>
+  <div
+    className="marketplace-edition-action spacer-top"
+  >
+    <a
+      href="more_url"
+      target="_blank"
+    >
+      marketplace.learn_more
+    </a>
+    <button
+      className="button-red"
+      disabled={false}
+    >
+      marketplace.uninstall
+    </button>
+  </div>
+</div>
+`;
+
+exports[`should display installing badge 1`] = `
+<div
+  className="boxed-group boxed-group-inner marketplace-edition"
+>
+  <span
+    className="marketplace-edition-badge badge badge-normal-size"
+  >
+    marketplace.installing
+  </span>
+  <div>
+    <h3
+      className="spacer-bottom"
+    >
+      Foo
+    </h3>
+    <p>
+      Foo desc
+    </p>
+  </div>
+  <div
+    className="marketplace-edition-action spacer-top"
+  >
+    <a
+      href="more_url"
+      target="_blank"
+    >
+      marketplace.learn_more
+    </a>
+    <button
+      className="button-red"
+      disabled={false}
+    >
+      marketplace.uninstall
+    </button>
+  </div>
+</div>
+`;
+
+exports[`should display the edition 1`] = `
+<div
+  className="boxed-group boxed-group-inner marketplace-edition"
+>
+  <div>
+    <h3
+      className="spacer-bottom"
+    >
+      Foo
+    </h3>
+    <p>
+      Foo desc
+    </p>
+  </div>
+  <div
+    className="marketplace-edition-action spacer-top"
+  >
+    <a
+      href="more_url"
+      target="_blank"
+    >
+      marketplace.learn_more
+    </a>
+    <button
+      disabled={false}
+    >
+      marketplace.install
+    </button>
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/marketplace/style.css b/server/sonar-web/src/main/js/apps/marketplace/style.css
new file mode 100644 (file)
index 0000000..037a0c1
--- /dev/null
@@ -0,0 +1,32 @@
+.marketplace-editions {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  margin-left: -8px;
+  margin-right: -8px;
+}
+
+.marketplace-edition {
+  position: relative;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  background-color: #f3f3f3;
+  margin-left: 8px;
+  margin-right: 8px;
+}
+
+.marketplace-edition-badge {
+  position: absolute;
+  right: -1px;
+  top: 16px;
+  padding: 4px 8px;
+  border-radius: 2px 0 0 2px;
+}
+
+.marketplace-edition-action {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+}
index 1abef7a83993cd69eae8efea8c071ff56e64f421..6e955baeeac4ea008f41e0e34c279cf1ea67bbfc 100644 (file)
@@ -78,11 +78,9 @@ class Request {
 
   constructor(private url: string, private options: { method?: string } = {}) {}
 
-  submit(): Promise<Response> {
+  getSubmitData(customHeaders: any = {}): { url: string; options: RequestInit } {
     let url = this.url;
-
     const options: RequestInit = { ...DEFAULT_OPTIONS, ...this.options };
-    const customHeaders: any = {};
 
     if (this.data) {
       if (this.data instanceof FormData) {
@@ -100,10 +98,13 @@ class Request {
 
     options.headers = {
       ...DEFAULT_HEADERS,
-      ...customHeaders,
-      ...getCSRFToken()
+      ...customHeaders
     };
+    return { url, options };
+  }
 
+  submit(): Promise<Response> {
+    const { url, options } = this.getSubmitData({ ...getCSRFToken() });
     return window.fetch((window as any).baseUrl + url, options);
   }
 
@@ -127,6 +128,19 @@ export function request(url: string): Request {
   return new Request(url);
 }
 
+/**
+ * Make a cors request
+ */
+export function corsRequest(url: string, mode: RequestMode = 'cors'): Request {
+  const options: RequestInit = { mode };
+  const request = new Request(url, options);
+  request.submit = function() {
+    const { url, options } = this.getSubmitData();
+    return window.fetch(url, options);
+  };
+  return request;
+}
+
 /**
  * Check that response status is ok
  */
index ed005f2888f28f69d73431f15f4582f1babd0595..d783242272fc3bc70c0c4a3328f48bf08720fc89 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
+import { Extension } from '../../app/types';
+import { EditionStatus } from '../../api/marketplace';
+
 interface AppState {
-  adminPages?: any[];
+  adminPages?: Extension[];
   authenticationError: boolean;
   authorizationError: boolean;
+  editionStatus?: EditionStatus;
   organizationsEnabled: boolean;
   qualifiers?: string[];
 }
@@ -32,14 +37,23 @@ interface SetAppStateAction {
 
 interface SetAdminPagesAction {
   type: 'SET_ADMIN_PAGES';
-  adminPages: any[];
+  adminPages: Extension[];
+}
+
+interface SetEditionStatusAction {
+  type: 'SET_EDITION_STATUS';
+  editionStatus: EditionStatus;
 }
 
 interface RequireAuthorizationAction {
   type: 'REQUIRE_AUTHORIZATION';
 }
 
-export type Action = SetAppStateAction | SetAdminPagesAction | RequireAuthorizationAction;
+export type Action =
+  | SetAppStateAction
+  | SetAdminPagesAction
+  | SetEditionStatusAction
+  | RequireAuthorizationAction;
 
 export function setAppState(appState: AppState): SetAppStateAction {
   return {
@@ -48,7 +62,7 @@ export function setAppState(appState: AppState): SetAppStateAction {
   };
 }
 
-export function setAdminPages(adminPages: any[]): SetAdminPagesAction {
+export function setAdminPages(adminPages: Extension[]): SetAdminPagesAction {
   return { type: 'SET_ADMIN_PAGES', adminPages };
 }
 
@@ -56,6 +70,10 @@ export function requireAuthorization(): RequireAuthorizationAction {
   return { type: 'REQUIRE_AUTHORIZATION' };
 }
 
+export function setEditionStatus(editionStatus: EditionStatus): SetEditionStatusAction {
+  return { type: 'SET_EDITION_STATUS', editionStatus };
+}
+
 const defaultValue: AppState = {
   authenticationError: false,
   authorizationError: false,
@@ -71,6 +89,10 @@ export default function(state: AppState = defaultValue, action: Action): AppStat
     return { ...state, adminPages: action.adminPages };
   }
 
+  if (action.type === 'SET_EDITION_STATUS') {
+    return { ...state, editionStatus: action.editionStatus };
+  }
+
   if (action.type === 'REQUIRE_AUTHORIZATION') {
     return { ...state, authorizationError: true };
   }
index ead056362e4af5fcd2cfa1837ca0e13cdc4539d2..a91bb089669d44b26a370b3db4b5912599875c01 100644 (file)
@@ -2069,6 +2069,7 @@ marketplace.revert=Revert
 marketplace.system_upgrades=System Upgrades
 marketplace.install=Install
 marketplace.installed=Installed
+marketplace.installing=Installing...
 marketplace._installed=installed
 marketplace.available_under_commercial_license=Available under our commercial editions
 marketplace.learn_more=Learn more
@@ -2089,6 +2090,12 @@ marketplace.update_to_x=Update to {0}
 marketplace.uninstall=Uninstall
 marketplace.i_accept_the=I accept the
 marketplace.terms_and_conditions=Terms and Conditions
+marketplace.editions_unavailable=Explore our Editions: advanced feature packs brought to you by SonarSource on {url}
+marketplace.status.AUTOMATIC_IN_PROGRESS=Updating your installation... Please wait...
+marketplace.status.AUTOMATIC_READY=New installation complete. Please restart Server to benefit from it.
+marketplace.status.MANUAL_IN_PROGRESS=Can't install Developer Edition because of internet access issue. Please manually install the package in your SonarQube's plugins folder.
+marketplace.status.AUTOMATIC_FAILURE=Can't install Developer Edition. Please manually install the package in your SonarQube's plugins folder.
+marketplace.how_to_install=How to install it?
 
 #------------------------------------------------------------------------------
 #