--- /dev/null
+/*
+ * 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);
+}
+++ /dev/null
-/*
- * 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);
--- /dev/null
+/*
+ * 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);
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,
);
}
- 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}>
--- /dev/null
+/*
+ * 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;
+ }
+}
+++ /dev/null
-/*
- * 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;
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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();
+});
+++ /dev/null
-/*
- * 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();
-});
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+// 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>
+`;
+++ /dev/null
-// 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>
-`;
--- /dev/null
+// 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>
+`;
export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch;
-export interface ComponentExtension {
+export interface Extension {
key: string;
name: string;
}
}>;
configuration?: ComponentConfiguration;
description?: string;
- extensions?: ComponentExtension[];
+ extensions?: Extension[];
isFavorite?: boolean;
key: string;
name: string;
}
interface ComponentConfiguration {
- extensions?: ComponentExtension[];
+ extensions?: Extension[];
showBackgroundTasks?: boolean;
showLinks?: boolean;
showManualMeasures?: boolean;
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';
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;
}
});
}
},
- () => {}
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
);
};
this.setState({ loading: false, plugins });
}
},
- () => {}
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
);
};
<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}
*/
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
});
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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} />
+ );
+}
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 () => {
--- /dev/null
+// 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>
+`;
</div>
`;
-exports[`should not display nothing 1`] = `null`;
+exports[`should not display anything 1`] = `null`;
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+// 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>
+`;
--- /dev/null
+.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;
+}
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) {
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);
}
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
*/
* 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[];
}
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 {
};
}
-export function setAdminPages(adminPages: any[]): SetAdminPagesAction {
+export function setAdminPages(adminPages: Extension[]): SetAdminPagesAction {
return { type: 'SET_ADMIN_PAGES', adminPages };
}
return { type: 'REQUIRE_AUTHORIZATION' };
}
+export function setEditionStatus(editionStatus: EditionStatus): SetEditionStatusAction {
+ return { type: 'SET_EDITION_STATUS', editionStatus };
+}
+
const defaultValue: AppState = {
authenticationError: false,
authorizationError: false,
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 };
}
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
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?
#------------------------------------------------------------------------------
#