]> source.dussan.org Git - sonarqube.git/commitdiff
SQBILLING-93 SonarCloud notifies Muppet when an organization is deleted
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Wed, 2 May 2018 10:00:07 +0000 (12:00 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 3 May 2018 18:20:49 +0000 (20:20 +0200)
13 files changed:
server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidations.java
server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidationsProxyImpl.java
server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java
server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java
server/sonar-web/src/main/js/api/organizations.ts
server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.js [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties
tests/plugins/fake-billing-plugin/src/main/java/FakeBillingValidations.java

index baba8d3af8cd675d8a7566aab0395a96265f09cd..797211a328be91da577fc555c8ffc9d1cd9a8fb6 100644 (file)
@@ -48,6 +48,11 @@ public interface BillingValidations {
    */
   boolean canUpdateProjectVisibilityToPrivate(Organization organization);
 
+  /**
+   * Actions to do on an organization deletion
+   */
+  void onDelete(Organization organization);
+
   class Organization {
     private final String key;
     private final String uuid;
index f2494087d3a318785ee8a3d293beaa989e67a0f0..717c323dd97d1a831a17ade416b9f7326729889d 100644 (file)
@@ -55,4 +55,12 @@ public class BillingValidationsProxyImpl implements BillingValidationsProxy {
   public boolean canUpdateProjectVisibilityToPrivate(Organization organization) {
     return billingValidationsExtension == null || billingValidationsExtension.canUpdateProjectVisibilityToPrivate(organization);
   }
+
+  @Override
+  public void onDelete(Organization organization) {
+    if (billingValidationsExtension == null) {
+      return;
+    }
+    billingValidationsExtension.onDelete(organization);
+  }
 }
index 3a486a15bd2b512ed7350ea766ae7c65055579c7..f7c8158dfcca4d8d2cdcd1cdfe1130055af32df5 100644 (file)
@@ -35,6 +35,8 @@ import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.qualitygate.QualityGateDto;
 import org.sonar.db.qualityprofile.QProfileDto;
 import org.sonar.server.component.ComponentCleanerService;
+import org.sonar.server.organization.BillingValidations;
+import org.sonar.server.organization.BillingValidationsProxy;
 import org.sonar.server.organization.DefaultOrganization;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.organization.OrganizationFlags;
@@ -62,10 +64,11 @@ public class DeleteAction implements OrganizationsWsAction {
   private final UserIndexer userIndexer;
   private final QProfileFactory qProfileFactory;
   private final ProjectLifeCycleListeners projectLifeCycleListeners;
+  private final BillingValidationsProxy billingValidations;
 
-  public DeleteAction(UserSession userSession, DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider,
-    ComponentCleanerService componentCleanerService, OrganizationFlags organizationFlags, UserIndexer userIndexer,
-    QProfileFactory qProfileFactory, ProjectLifeCycleListeners projectLifeCycleListeners) {
+  public DeleteAction(UserSession userSession, DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider, ComponentCleanerService componentCleanerService,
+    OrganizationFlags organizationFlags, UserIndexer userIndexer, QProfileFactory qProfileFactory, ProjectLifeCycleListeners projectLifeCycleListeners,
+    BillingValidationsProxy billingValidations) {
     this.userSession = userSession;
     this.dbClient = dbClient;
     this.defaultOrganizationProvider = defaultOrganizationProvider;
@@ -74,6 +77,7 @@ public class DeleteAction implements OrganizationsWsAction {
     this.userIndexer = userIndexer;
     this.qProfileFactory = qProfileFactory;
     this.projectLifeCycleListeners = projectLifeCycleListeners;
+    this.billingValidations = billingValidations;
   }
 
   @Override
@@ -116,6 +120,7 @@ public class DeleteAction implements OrganizationsWsAction {
       deleteQualityProfiles(dbSession, organization);
       deleteQualityGates(dbSession, organization);
       deleteOrganization(dbSession, organization);
+      billingValidations.onDelete(new BillingValidations.Organization(organization.getKey(), organization.getUuid()));
 
       response.noContent();
     }
index 78f7909f20d5cabfa0b8ea1ead42e595252b6dcf..5790c4e44f9ea326b9e669a461bdefc09c762f2c 100644 (file)
@@ -59,6 +59,8 @@ import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.organization.BillingValidations;
+import org.sonar.server.organization.BillingValidationsProxy;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.organization.TestOrganizationFlags;
 import org.sonar.server.project.Project;
@@ -119,11 +121,13 @@ public class DeleteActionTest {
   private final WebhookDeliveryDao deliveryDao = dbClient.webhookDeliveryDao();
   private final WebhookDeliveryDbTester webhookDeliveryDbTester = db.webhookDelivery();
   private ProjectLifeCycleListeners projectLifeCycleListeners = mock(ProjectLifeCycleListeners.class);
+  private BillingValidationsProxy billingValidationsProxy = mock(BillingValidationsProxy.class);
   private WsActionTester wsTester = new WsActionTester(
-    new DeleteAction(userSession, dbClient, defaultOrganizationProvider, spiedComponentCleanerService, organizationFlags, userIndexer, qProfileFactory, projectLifeCycleListeners));
+    new DeleteAction(userSession, dbClient, defaultOrganizationProvider, spiedComponentCleanerService, organizationFlags, userIndexer, qProfileFactory, projectLifeCycleListeners,
+      billingValidationsProxy));
 
   @Test
-  public void test_definition() {
+  public void definition() {
     WebService.Action action = wsTester.getDef();
     assertThat(action.key()).isEqualTo("delete");
     assertThat(action.isPost()).isTrue();
@@ -542,6 +546,16 @@ public class DeleteActionTest {
     }
   }
 
+  @Test
+  public void call_billing_validation_on_delete() {
+    OrganizationDto organization = db.organizations().insert();
+    logInAsAdministrator(organization);
+
+    sendRequest(organization);
+
+    verify(billingValidationsProxy).onDelete(any(BillingValidations.Organization.class));
+  }
+
   @DataProvider
   public static Object[][] indexOfFailingProjectDeletion() {
     return new Object[][]{
index 943249011cd1f0cdd3d50090ef4e0270792fcad4..cc321f31678c9b7f59e500d288ca4f37b6daa2cc 100644 (file)
@@ -104,3 +104,20 @@ export function changeProjectVisibility(
 ): Promise<void> {
   return post('/api/organizations/update_project_visibility', { organization, projectVisibility });
 }
+
+export interface OrganizationBilling {
+  nclocCount: number;
+  subscription: {
+    plan?: {
+      maxNcloc: number;
+      price: number;
+    };
+    nextBillingDate?: string;
+    status: 'active' | 'inactive' | 'suspended';
+    trial: boolean;
+  };
+}
+
+export function getOrganizationBilling(organization: string): Promise<OrganizationBilling> {
+  return getJSON('/api/billing/show', { organization, p: 1, ps: 1 });
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.js
deleted file mode 100644 (file)
index df8a833..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router';
-import Modal from '../../../components/controls/Modal';
-import { translate } from '../../../helpers/l10n';
-import { getOrganizationByKey } from '../../../store/rootReducer';
-import { deleteOrganization } from '../actions';
-
-class OrganizationDelete extends React.PureComponent {
-  /*:: props: {
-    organization: {
-      key: string,
-      name: string
-    },
-    router: {
-      replace: string => void
-    },
-    deleteOrganization: string => Promise<*>
-  };
-*/
-
-  state = {
-    deleting: false,
-    loading: false
-  };
-
-  handleSubmit = (e /*: Object */) => {
-    e.preventDefault();
-    this.setState({ loading: true });
-    this.props.deleteOrganization(this.props.organization.key).then(() => {
-      this.props.router.replace('/');
-    });
-  };
-
-  handleOpenModal = () => {
-    this.setState({ deleting: true });
-  };
-
-  handleCloseModal = () => {
-    this.setState({ deleting: false });
-  };
-
-  renderModal() {
-    return (
-      <Modal contentLabel="modal form" onRequestClose={this.handleCloseModal}>
-        <header className="modal-head">
-          <h2>{translate('organization.delete')}</h2>
-        </header>
-
-        <form onSubmit={this.handleSubmit}>
-          <div className="modal-body">{translate('organization.delete.question')}</div>
-
-          <footer className="modal-foot">
-            {this.state.loading ? (
-              <i className="spinner" />
-            ) : (
-              <div>
-                <button type="submit" className="button-red">
-                  {translate('delete')}
-                </button>
-                <button type="reset" className="button-link" onClick={this.handleCloseModal}>
-                  {translate('cancel')}
-                </button>
-              </div>
-            )}
-          </footer>
-        </form>
-      </Modal>
-    );
-  }
-
-  render() {
-    const title = translate('organization.delete');
-    return (
-      <div className="page page-limited">
-        <Helmet title={title} />
-
-        <header className="page-header">
-          <h1 className="page-title">{title}</h1>
-          <div className="page-description">{translate('organization.delete.description')}</div>
-        </header>
-
-        <div>
-          <button
-            className="button-red"
-            disabled={this.state.loading || this.state.deleting}
-            onClick={this.handleOpenModal}>
-            {translate('delete')}
-          </button>
-          {this.state.deleting && this.renderModal()}
-        </div>
-      </div>
-    );
-  }
-}
-
-const mapDispatchToProps = { deleteOrganization };
-
-export default connect(null, mapDispatchToProps)(withRouter(OrganizationDelete));
-
-export const UnconnectedOrganizationDelete = OrganizationDelete;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx
new file mode 100644 (file)
index 0000000..e68bb78
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import * as PropTypes from 'prop-types';
+import Helmet from 'react-helmet';
+import { connect } from 'react-redux';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { translate } from '../../../helpers/l10n';
+import { deleteOrganization } from '../actions';
+import { Organization } from '../../../app/types';
+import { Button } from '../../../components/ui/buttons';
+import { getOrganizationBilling } from '../../../api/organizations';
+
+interface DispatchToProps {
+  deleteOrganization: (key: string) => Promise<void>;
+}
+
+interface OwnProps {
+  organization: Pick<Organization, 'key' | 'name'>;
+}
+
+type Props = OwnProps & DispatchToProps;
+
+interface State {
+  hasPaidPlan?: boolean;
+}
+
+export class OrganizationDelete extends React.PureComponent<Props, State> {
+  mounted = false;
+  static contextTypes = {
+    router: PropTypes.object,
+    onSonarCloud: PropTypes.bool
+  };
+
+  state: State = {};
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchOrganizationPlanInfo();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchOrganizationPlanInfo = () => {
+    if (this.context.onSonarCloud) {
+      getOrganizationBilling(this.props.organization.key).then(
+        billingInfo => {
+          if (this.mounted) {
+            this.setState({
+              hasPaidPlan: billingInfo.subscription.status !== 'inactive'
+            });
+          }
+        },
+        () => {
+          if (this.mounted) {
+            this.setState({ hasPaidPlan: false });
+          }
+        }
+      );
+    }
+  };
+
+  onDelete = () => {
+    return this.props.deleteOrganization(this.props.organization.key).then(() => {
+      this.context.router.replace('/');
+    });
+  };
+
+  render() {
+    const { hasPaidPlan } = this.state;
+    const { onSonarCloud } = this.context;
+    const title = translate('organization.delete');
+    return (
+      <>
+        <Helmet title={title} />
+        <div className="page page-limited">
+          <header className="page-header">
+            <h1 className="page-title">{title}</h1>
+            <div className="page-description">
+              {onSonarCloud
+                ? translate('organization.delete.description.sonarcloud')
+                : translate('organization.delete.description')}
+            </div>
+          </header>
+          <ConfirmButton
+            confirmButtonText={translate('delete')}
+            isDestructive={true}
+            modalBody={
+              <div>
+                {translate('organization.delete.question')}
+                {hasPaidPlan && (
+                  <p className="alert alert-warn big-spacer-top">
+                    {translate('organization.delete.sonarcloud.paid_plan_info')}
+                  </p>
+                )}
+              </div>
+            }
+            modalHeader={translate('organization.delete')}
+            onConfirm={this.onDelete}>
+            {({ onClick }) => (
+              <Button className="js-custom-measure-delete button-red" onClick={onClick}>
+                {translate('delete')}
+              </Button>
+            )}
+          </ConfirmButton>
+        </div>
+      </>
+    );
+  }
+}
+
+const mapDispatchToProps: DispatchToProps = { deleteOrganization: deleteOrganization as any };
+
+export default connect<null, DispatchToProps, OwnProps>(null, mapDispatchToProps)(
+  OrganizationDelete
+);
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.js
deleted file mode 100644 (file)
index 1be39fb..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import { UnconnectedOrganizationDelete } from '../OrganizationDelete';
-
-it('smoke test', () => {
-  const organization = { key: 'foo', name: 'Foo' };
-  const wrapper = shallow(<UnconnectedOrganizationDelete organization={organization} />);
-  expect(wrapper).toMatchSnapshot();
-
-  wrapper.setState({ deleting: true });
-  expect(wrapper).toMatchSnapshot();
-
-  wrapper.setState({ loading: true });
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx
new file mode 100644 (file)
index 0000000..fff071d
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { OrganizationDelete } from '../OrganizationDelete';
+import { getOrganizationBilling } from '../../../../api/organizations';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/organizations', () => ({
+  getOrganizationBilling: jest.fn(() =>
+    Promise.resolve({ nclocCount: 1000, subscription: { status: 'active', trial: true } })
+  )
+}));
+
+beforeEach(() => {
+  (getOrganizationBilling as jest.Mock<any>).mockClear();
+});
+
+it('smoke test', () => {
+  expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should redirect the page', async () => {
+  const deleteOrganization = jest.fn(() => Promise.resolve());
+  const replace = jest.fn();
+  const wrapper = getWrapper({ deleteOrganization }, { router: { replace } });
+  (wrapper.instance() as OrganizationDelete).onDelete();
+  await waitAndUpdate(wrapper);
+  expect(deleteOrganization).toHaveBeenCalledWith('foo');
+  expect(replace).toHaveBeenCalledWith('/');
+});
+
+it('should show a info message for paying organization', async () => {
+  const wrapper = getWrapper({}, { onSonarCloud: true });
+  await waitAndUpdate(wrapper);
+  expect(getOrganizationBilling).toHaveBeenCalledWith('foo');
+  expect(wrapper).toMatchSnapshot();
+});
+
+function getWrapper(props = {}, context = {}) {
+  return shallow(
+    <OrganizationDelete
+      deleteOrganization={jest.fn(() => Promise.resolve())}
+      organization={{ key: 'foo', name: 'Foo' }}
+      {...props}
+    />,
+
+    { context: { router: { replace: jest.fn() }, ...context } }
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.js.snap
deleted file mode 100644 (file)
index d8593ee..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`smoke test 1`] = `
-<div
-  className="page page-limited"
->
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="organization.delete"
-  />
-  <header
-    className="page-header"
-  >
-    <h1
-      className="page-title"
-    >
-      organization.delete
-    </h1>
-    <div
-      className="page-description"
-    >
-      organization.delete.description
-    </div>
-  </header>
-  <div>
-    <button
-      className="button-red"
-      disabled={false}
-      onClick={[Function]}
-    >
-      delete
-    </button>
-  </div>
-</div>
-`;
-
-exports[`smoke test 2`] = `
-<div
-  className="page page-limited"
->
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="organization.delete"
-  />
-  <header
-    className="page-header"
-  >
-    <h1
-      className="page-title"
-    >
-      organization.delete
-    </h1>
-    <div
-      className="page-description"
-    >
-      organization.delete.description
-    </div>
-  </header>
-  <div>
-    <button
-      className="button-red"
-      disabled={true}
-      onClick={[Function]}
-    >
-      delete
-    </button>
-    <Modal
-      contentLabel="modal form"
-      onRequestClose={[Function]}
-    >
-      <header
-        className="modal-head"
-      >
-        <h2>
-          organization.delete
-        </h2>
-      </header>
-      <form
-        onSubmit={[Function]}
-      >
-        <div
-          className="modal-body"
-        >
-          organization.delete.question
-        </div>
-        <footer
-          className="modal-foot"
-        >
-          <div>
-            <button
-              className="button-red"
-              type="submit"
-            >
-              delete
-            </button>
-            <button
-              className="button-link"
-              onClick={[Function]}
-              type="reset"
-            >
-              cancel
-            </button>
-          </div>
-        </footer>
-      </form>
-    </Modal>
-  </div>
-</div>
-`;
-
-exports[`smoke test 3`] = `
-<div
-  className="page page-limited"
->
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="organization.delete"
-  />
-  <header
-    className="page-header"
-  >
-    <h1
-      className="page-title"
-    >
-      organization.delete
-    </h1>
-    <div
-      className="page-description"
-    >
-      organization.delete.description
-    </div>
-  </header>
-  <div>
-    <button
-      className="button-red"
-      disabled={true}
-      onClick={[Function]}
-    >
-      delete
-    </button>
-    <Modal
-      contentLabel="modal form"
-      onRequestClose={[Function]}
-    >
-      <header
-        className="modal-head"
-      >
-        <h2>
-          organization.delete
-        </h2>
-      </header>
-      <form
-        onSubmit={[Function]}
-      >
-        <div
-          className="modal-body"
-        >
-          organization.delete.question
-        </div>
-        <footer
-          className="modal-foot"
-        >
-          <i
-            className="spinner"
-          />
-        </footer>
-      </form>
-    </Modal>
-  </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap
new file mode 100644 (file)
index 0000000..0f7e0df
--- /dev/null
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should show a info message for paying organization 1`] = `
+<React.Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="organization.delete"
+  />
+  <div
+    className="page page-limited"
+  >
+    <header
+      className="page-header"
+    >
+      <h1
+        className="page-title"
+      >
+        organization.delete
+      </h1>
+      <div
+        className="page-description"
+      >
+        organization.delete.description.sonarcloud
+      </div>
+    </header>
+    <ConfirmButton
+      confirmButtonText="delete"
+      isDestructive={true}
+      modalBody={
+        <div>
+          organization.delete.question
+          <p
+            className="alert alert-warn big-spacer-top"
+          >
+            organization.delete.sonarcloud.paid_plan_info
+          </p>
+        </div>
+      }
+      modalHeader="organization.delete"
+      onConfirm={[Function]}
+    />
+  </div>
+</React.Fragment>
+`;
+
+exports[`smoke test 1`] = `
+<React.Fragment>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="organization.delete"
+  />
+  <div
+    className="page page-limited"
+  >
+    <header
+      className="page-header"
+    >
+      <h1
+        className="page-title"
+      >
+        organization.delete
+      </h1>
+      <div
+        className="page-description"
+      >
+        organization.delete.description
+      </div>
+    </header>
+    <ConfirmButton
+      confirmButtonText="delete"
+      isDestructive={true}
+      modalBody={
+        <div>
+          organization.delete.question
+        </div>
+      }
+      modalHeader="organization.delete"
+      onConfirm={[Function]}
+    />
+  </div>
+</React.Fragment>
+`;
index 622cfbdde75f111de2f12a2aad9bea89c2de949f..d75b929ec5456e1c775eef6c5fc5c295d65c6a5c 100644 (file)
@@ -2517,6 +2517,8 @@ organization.avatar.preview=Preview
 organization.created=Organization "{0}" has been created.
 organization.delete=Delete Organization
 organization.delete.description=Delete this organization from SonarQube. All projects belonging to the organization will be deleted as well. The operation cannot be undone.
+organization.delete.description.sonarcloud=Delete this organization from SonarCloud. All projects belonging to the organization will be deleted as well. The operation cannot be undone.
+organization.delete.sonarcloud.paid_plan_info=Your current paid plan subscription will stop and you won't be charged anymore.
 organization.delete.question=Are you sure you want to delete this organization?
 organization.deleted=Organization has been deleted.
 organization.description=Description
index bfd4315f6d2fb081d6e00416a0ae879714abc073..16c178c4233a6d2b02b0f66b24fdbf473156fa7d 100644 (file)
@@ -77,4 +77,9 @@ public class FakeBillingValidations implements BillingValidationsExtension {
     }
     return !settings.getBoolean(PREVENT_UPDATING_PROJECTS_VISIBILITY_TO_PRIVATE_SETTING);
   }
+
+  @Override
+  public void onDelete(Organization organization) {
+    // do nothing
+  }
 }