]> source.dussan.org Git - sonarqube.git/commitdiff
SONARCLOUD-108 Adapt the Bitbucket Cloud app to favor the new binding
authorJulien HENRY <julien.henry@sonarsource.com>
Tue, 31 Jul 2018 07:23:10 +0000 (09:23 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 10 Aug 2018 18:21:29 +0000 (20:21 +0200)
12 files changed:
server/sonar-bitbucketcloud/public/index.html
server/sonar-bitbucketcloud/src/main/ts/components/Config.tsx
server/sonar-bitbucketcloud/src/main/ts/components/RepoWidgetNotConfigured.tsx
server/sonar-bitbucketcloud/src/main/ts/components/__tests__/Config-test.tsx
server/sonar-bitbucketcloud/src/main/ts/components/__tests__/RepoWidgetNotConfigured-test.tsx
server/sonar-bitbucketcloud/src/main/ts/components/__tests__/__snapshots__/Config-test.tsx.snap
server/sonar-bitbucketcloud/src/main/ts/components/__tests__/__snapshots__/RepoWidgetNotConfigured-test.tsx.snap
server/sonar-bitbucketcloud/src/main/ts/utils.ts
server/sonar-db-dao/src/main/java/org/sonar/db/alm/ProjectAlmBindingsDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/alm/ProjectAlmBindingsMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/alm/ProjectAlmBindingsMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/alm/ProjectAlmBindingsDaoTest.java

index c9cd601e6ee2fcd14924b72c66688d630ec5d596..097f9b472aaa0facdff3d469dea075baf774a55f 100644 (file)
@@ -20,6 +20,7 @@
     window.bbcJWT = '%BBC_JWT%';
     window.bbcWidgetKey = '%BBC_WIDGET_KEY%';
     window.projectKey = '%BBC_PROJECT_KEY%';
+    window.allowManualBinding = '%BBC_ALLOW_MANUAL_BINDING%';
   </script>
   <% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
     <script src="%WEB_CONTEXT%<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
index 9983088ea7367644a5f588de2d0652f0778058d3..d8818f58ac39f329d1f001fad93064aab5cfd785 100644 (file)
@@ -28,7 +28,7 @@ import LoginForm from './LoginForm';
 import SonarCloudIcon from './SonarCloudIcon';
 import { AppContext } from '../types';
 import { bindProject, displayWSError, getMyProjects, putStoredProperty } from '../api';
-import { displayMessage } from '../utils';
+import { displayMessage, isManualBindingAllowed } from '../utils';
 
 interface ProjectOption {
   label: string;
@@ -70,14 +70,16 @@ export default class Config extends React.PureComponent<Props, State> {
       authenticated: false,
       saving: false,
       disabled: props.disabled,
-      loading: true,
+      loading: isManualBindingAllowed(),
       projects: []
     };
   }
 
   componentDidMount() {
     this.mounted = true;
-    this.fetchMyProjects();
+    if (isManualBindingAllowed()) {
+      this.fetchMyProjects();
+    }
   }
 
   componentWillUnmount() {
@@ -141,7 +143,11 @@ export default class Config extends React.PureComponent<Props, State> {
     }
 
     const { selectedProject } = this;
-    if (selectedProject && selectedProject.value !== this.props.projectKey) {
+    if (
+      isManualBindingAllowed() &&
+      selectedProject &&
+      selectedProject.value !== this.props.projectKey
+    ) {
       updateBinding = true;
       promises.push(
         bindProject({ ...this.props.context, projectKey: selectedProject.value }).then(() => {
@@ -149,6 +155,7 @@ export default class Config extends React.PureComponent<Props, State> {
         })
       );
     }
+
     if (promises.length > 0) {
       (document.activeElement as HTMLElement).blur();
       this.setState({ saving: true });
@@ -175,6 +182,29 @@ export default class Config extends React.PureComponent<Props, State> {
   };
 
   renderContainer = (children: React.ReactNode) => {
+    const { projectKey } = this.props;
+    let descriptionComponent: React.ReactNode = (
+      <>
+        To display the quality of your repository, you have to{' '}
+        <a href={getBaseUrl() + '/projects/create'}>provision</a> a project on SonarCloud and
+        trigger an analysis.
+      </>
+    );
+
+    if (isManualBindingAllowed()) {
+      descriptionComponent =
+        'To display the quality of your repository, you have to link it with a project analyzed on SonarCloud.';
+    } else if (projectKey) {
+      descriptionComponent = (
+        <>
+          This repository is already bound to a{' '}
+          <a href={getBaseUrl() + '/dashboard?id=' + encodeURIComponent(projectKey)}>
+            SonarCloud project
+          </a>
+        </>
+      );
+    }
+
     return (
       <>
         <h2>SonarCloud Settings</h2>
@@ -182,10 +212,7 @@ export default class Config extends React.PureComponent<Props, State> {
           <div className="settings-logo">
             <SonarCloudIcon size={128} />
           </div>
-          <p className="settings-description">
-            To display the quality of your repository, you have to link it with a project analyzed
-            on SonarCloud.
-          </p>
+          <p className="settings-description">{descriptionComponent}</p>
           {children}
         </div>
       </>
@@ -231,7 +258,6 @@ export default class Config extends React.PureComponent<Props, State> {
 
   render() {
     const { authenticated, disabled, loading } = this.state;
-
     if (loading) {
       return this.renderContainer(
         <div className="huge-spacer-top">
@@ -247,9 +273,9 @@ export default class Config extends React.PureComponent<Props, State> {
 
     return this.renderContainer(
       <>
-        {!authenticated && <LoginForm onReload={this.handleReload} />}
+        {isManualBindingAllowed() && !authenticated && <LoginForm onReload={this.handleReload} />}
         <form className="settings-form" onSubmit={this.handleSubmit}>
-          {authenticated && this.renderProjectsSelect()}
+          {isManualBindingAllowed() && authenticated && this.renderProjectsSelect()}
           <div className="display-flex-justify-center">
             <CheckboxStateless
               isChecked={!disabled}
index 774f38a6125412bf0812b30ab1e8146c3088407d..64648a8efca919f5a6a31790ab94f3613690ecc6 100644 (file)
@@ -18,8 +18,9 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import { getBaseUrl } from '@sqcore/helpers/urls';
 import SonarCloudIcon from './SonarCloudIcon';
-import { getRepoSettingsUrl, isRepoAdmin } from '../utils';
+import { getRepoSettingsUrl, isRepoAdmin, isManualBindingAllowed } from '../utils';
 
 interface State {
   settingsUrl?: string;
@@ -47,13 +48,32 @@ export default class RepoWidgetNotConfigured extends React.PureComponent<{}, Sta
 
   render() {
     const { settingsUrl } = this.state;
-    const settingsLink = settingsUrl ? (
-      <a href={settingsUrl} rel="noopener noreferrer" target="_parent">
-        repository settings
-      </a>
-    ) : (
-      'repository settings'
-    );
+    let msgComponent: React.ReactNode =
+      'Contact a repository administrator to link the repository with a SonarCloud project.';
+    if (isRepoAdmin()) {
+      if (isManualBindingAllowed()) {
+        msgComponent = (
+          <>
+            You have to link your repository with an existing SonarCloud project. Go to your{' '}
+            {settingsUrl ? (
+              <a href={settingsUrl} rel="noopener noreferrer" target="_parent">
+                repository settings
+              </a>
+            ) : (
+              'repository settings'
+            )}.
+          </>
+        );
+      } else {
+        msgComponent = (
+          <>
+            You have to <a href={getBaseUrl() + '/projects/create'}>provision</a> a project in
+            SonarCloud for this repository.
+          </>
+        );
+      }
+    }
+
     return (
       <>
         <div className="project-card-header">
@@ -62,16 +82,7 @@ export default class RepoWidgetNotConfigured extends React.PureComponent<{}, Sta
             <h4 className="spacer-left">Code Quality</h4>
           </div>
         </div>
-        <div className="spacer-top">
-          {isRepoAdmin() ? (
-            <>
-              You have to link your repository with an existing SonarCloud project. Go to your{' '}
-              {settingsLink}.
-            </>
-          ) : (
-            'Contact a repository administrator to link the repository with a SonarCloud project.'
-          )}
-        </div>
+        <div className="spacer-top">{msgComponent}</div>
       </>
     );
   }
index e3f8cd28b594566e7241e4d10bd722345c400682..0dee0434d1eedb2f3da0d638fbd86c712637270f 100644 (file)
@@ -21,9 +21,11 @@ import * as React from 'react';
 import { shallow } from 'enzyme';
 import Config from '../Config';
 import { bindProject, getMyProjects } from '../../api';
+import { isManualBindingAllowed } from '../../utils';
 
 jest.mock('../../utils', () => ({
-  displayMessage: jest.fn()
+  displayMessage: jest.fn(),
+  isManualBindingAllowed: jest.fn(() => true)
 }));
 
 jest.mock('../../api', () => ({
@@ -58,6 +60,18 @@ it('should display correctly', async () => {
   expect(wrapper).toMatchSnapshot();
 });
 
+it('should display correctly for auto binding', () => {
+  (isManualBindingAllowed as jest.Mock<any>).mockReturnValue(false);
+  expect(getWrapper()).toMatchSnapshot();
+  (isManualBindingAllowed as jest.Mock<any>).mockReturnValue(true);
+});
+
+it('should display correctly for auto binding with already a projectKey', () => {
+  (isManualBindingAllowed as jest.Mock<any>).mockReturnValue(false);
+  expect(getWrapper()).toMatchSnapshot();
+  (isManualBindingAllowed as jest.Mock<any>).mockReturnValue(true);
+});
+
 it('should display the authentication component and the display checkbox', async () => {
   (getMyProjects as jest.Mock<any>).mockImplementationOnce(() =>
     Promise.reject({ response: { status: 401 } })
index 6b9aac193615d0c8c3ce0a1a56a4c2127ac467b9..89f4b27f68f338ab32210b0e196d8798582ba46f 100644 (file)
 import * as React from 'react';
 import { shallow } from 'enzyme';
 import RepoWidgetNotConfigured from '../RepoWidgetNotConfigured';
+import { isManualBindingAllowed, isRepoAdmin } from '../../utils';
 
 jest.mock('../../utils', () => ({
-  getRepoSettingsUrl: jest.fn(() =>
-    Promise.resolve(
+  getRepoSettingsUrl: jest
+    .fn()
+    .mockResolvedValue(
       'https://bitbucketcloud.org/{}/{repo_uuid}/admin/addon/admin/app-key/repository-config'
-    )
-  ),
-  isRepoAdmin: jest.fn(() => true)
+    ),
+  isRepoAdmin: jest.fn().mockReturnValue(true),
+  isManualBindingAllowed: jest.fn().mockReturnValue(true)
 }));
 
-it('should display correctly', async () => {
+it('should display correctly for admin', async () => {
   const wrapper = shallow(<RepoWidgetNotConfigured />);
   expect(wrapper).toMatchSnapshot();
 
@@ -39,3 +41,13 @@ it('should display correctly', async () => {
   wrapper.update();
   expect(wrapper).toMatchSnapshot();
 });
+
+it('should display correctly for non admin', () => {
+  (isRepoAdmin as jest.Mock<any>).mockReturnValueOnce(false);
+  expect(shallow(<RepoWidgetNotConfigured />)).toMatchSnapshot();
+});
+
+it('should display a link to SonarCloud for auto provisioning', () => {
+  (isManualBindingAllowed as jest.Mock<any>).mockReturnValueOnce(false);
+  expect(shallow(<RepoWidgetNotConfigured />)).toMatchSnapshot();
+});
index 4d8d3d87ed3c9afcecf44b03fea9704500fc850e..1667e404154a4496e38e2fa4c79304beac2947cd 100644 (file)
@@ -235,6 +235,132 @@ exports[`should display correctly 2`] = `
 </React.Fragment>
 `;
 
+exports[`should display correctly for auto binding 1`] = `
+<React.Fragment>
+  <h2>
+    SonarCloud Settings
+  </h2>
+  <div
+    className="settings-content"
+  >
+    <div
+      className="settings-logo"
+    >
+      <SonarCloudIcon
+        size={128}
+      />
+    </div>
+    <p
+      className="settings-description"
+    >
+      <React.Fragment>
+        This repository is already bound to a
+         
+        <a
+          href="/dashboard?id=foo"
+        >
+          SonarCloud project
+        </a>
+      </React.Fragment>
+    </p>
+    <React.Fragment>
+      <form
+        className="settings-form"
+        onSubmit={[Function]}
+      >
+        <div
+          className="display-flex-justify-center"
+        >
+          <CheckboxStateless
+            isChecked={true}
+            label="Show repository overview widget"
+            name="show-widget"
+            onChange={[Function]}
+            value="show-widget"
+          />
+        </div>
+        <div
+          className="ak-field-group"
+        >
+          <WithAnalyticsContext(WithAnalyticsEvents(WithDeprecationWarnings(Button)))
+            analyticsContext={Object {}}
+            appearance="primary"
+            isDisabled={true}
+            type="submit"
+          >
+            Save
+          </WithAnalyticsContext(WithAnalyticsEvents(WithDeprecationWarnings(Button)))>
+        </div>
+      </form>
+      <HelpLink />
+    </React.Fragment>
+  </div>
+</React.Fragment>
+`;
+
+exports[`should display correctly for auto binding with already a projectKey 1`] = `
+<React.Fragment>
+  <h2>
+    SonarCloud Settings
+  </h2>
+  <div
+    className="settings-content"
+  >
+    <div
+      className="settings-logo"
+    >
+      <SonarCloudIcon
+        size={128}
+      />
+    </div>
+    <p
+      className="settings-description"
+    >
+      <React.Fragment>
+        This repository is already bound to a
+         
+        <a
+          href="/dashboard?id=foo"
+        >
+          SonarCloud project
+        </a>
+      </React.Fragment>
+    </p>
+    <React.Fragment>
+      <form
+        className="settings-form"
+        onSubmit={[Function]}
+      >
+        <div
+          className="display-flex-justify-center"
+        >
+          <CheckboxStateless
+            isChecked={true}
+            label="Show repository overview widget"
+            name="show-widget"
+            onChange={[Function]}
+            value="show-widget"
+          />
+        </div>
+        <div
+          className="ak-field-group"
+        >
+          <WithAnalyticsContext(WithAnalyticsEvents(WithDeprecationWarnings(Button)))
+            analyticsContext={Object {}}
+            appearance="primary"
+            isDisabled={true}
+            type="submit"
+          >
+            Save
+          </WithAnalyticsContext(WithAnalyticsEvents(WithDeprecationWarnings(Button)))>
+        </div>
+      </form>
+      <HelpLink />
+    </React.Fragment>
+  </div>
+</React.Fragment>
+`;
+
 exports[`should display the authentication component and the display checkbox 1`] = `
 <form
   className="settings-form"
index ea576d96013301c475e0dfd30d18b0a4c40e9ea4..72d80fcae83d8f682000ecf8554332bf456d57bd 100644 (file)
@@ -1,6 +1,40 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`should display correctly 1`] = `
+exports[`should display a link to SonarCloud for auto provisioning 1`] = `
+<React.Fragment>
+  <div
+    className="project-card-header"
+  >
+    <div
+      className="project-card-header-inner"
+    >
+      <SonarCloudIcon
+        size={24}
+      />
+      <h4
+        className="spacer-left"
+      >
+        Code Quality
+      </h4>
+    </div>
+  </div>
+  <div
+    className="spacer-top"
+  >
+    <React.Fragment>
+      You have to 
+      <a
+        href="/projects/create"
+      >
+        provision
+      </a>
+       a project in SonarCloud for this repository.
+    </React.Fragment>
+  </div>
+</React.Fragment>
+`;
+
+exports[`should display correctly for admin 1`] = `
 <React.Fragment>
   <div
     className="project-card-header"
@@ -31,7 +65,7 @@ exports[`should display correctly 1`] = `
 </React.Fragment>
 `;
 
-exports[`should display correctly 2`] = `
+exports[`should display correctly for admin 2`] = `
 <React.Fragment>
   <div
     className="project-card-header"
@@ -67,3 +101,29 @@ exports[`should display correctly 2`] = `
   </div>
 </React.Fragment>
 `;
+
+exports[`should display correctly for non admin 1`] = `
+<React.Fragment>
+  <div
+    className="project-card-header"
+  >
+    <div
+      className="project-card-header-inner"
+    >
+      <SonarCloudIcon
+        size={24}
+      />
+      <h4
+        className="spacer-left"
+      >
+        Code Quality
+      </h4>
+    </div>
+  </div>
+  <div
+    className="spacer-top"
+  >
+    Contact a repository administrator to link the repository with a SonarCloud project.
+  </div>
+</React.Fragment>
+`;
index 237f4a260f4a9054930b4870d4efc3f39fe29ce0..0108b6669de292fb31cc5fb5f4c053f000340392 100644 (file)
@@ -49,6 +49,10 @@ export function getProjectKey(): string | undefined {
   return (window as any).projectKey;
 }
 
+export function isManualBindingAllowed(): boolean {
+  return (window as any).allowManualBinding !== 'false';
+}
+
 export function getPullRequestContext(): PullRequestContext {
   return {
     prId: (window as any).prId || query.prId,
index f2d6cc18428ba664262737c97c68cb7c801fe82d..e4a77591ab0d81d91817d084870fa5bc3c347685 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.db.alm;
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import javax.annotation.Nullable;
 import org.sonar.api.utils.System2;
 import org.sonar.core.util.UuidFactory;
@@ -72,6 +73,10 @@ public class ProjectAlmBindingsDao implements Dao {
     return executeLargeInputs(repoIds, partionnedIds -> getMapper(session).selectByRepoIds(alm.getId(), partionnedIds));
   }
 
+  public Optional<ProjectAlmBindingDto> selectByRepoId(final DbSession session, ALM alm, String repoId) {
+    return Optional.ofNullable(getMapper(session).selectByRepoId(alm.getId(), repoId));
+  }
+
   private static void checkAlm(@Nullable ALM alm) {
     Objects.requireNonNull(alm, "alm can't be null");
   }
index 9fa3f788f3961d1c779981884f7fdbbc70aac6f7..b8c5464a254f6ecaeb040579b8e0773df60bf294 100644 (file)
@@ -34,4 +34,6 @@ public interface ProjectAlmBindingsMapper {
     @Nullable @Param("githubSlug") String githubSlug, @Param("url") String url, @Param("now") long now);
 
   List<ProjectAlmBindingDto> selectByRepoIds(@Param("almId") String almId, @Param("repoIds") List<String> repoIds);
+
+  ProjectAlmBindingDto selectByRepoId(@Param("almId") String almId, @Param("repoId") String repoId);
 }
index b31e45e6481abcd0720dde7fbea6defb4bdc7ab4..9b6b65d2ae2ac0639d70685b853049db0991fa45 100644 (file)
       url
     from project_alm_bindings
     where
-      alm_id =#{almId, jdbcType=VARCHAR}
+      alm_id = #{almId, jdbcType=VARCHAR}
       and repo_id in
     <foreach collection="repoIds" open="(" close=")" item="repoId" separator=",">
       #{repoId,jdbcType=VARCHAR}
     </foreach>
   </select>
 
+  <select id="selectByRepoId" parameterType="map" resultType="ProjectAlmBinding">
+    select
+    uuid,
+    alm_id as almId,
+    repo_id as repoId,
+    project_uuid as projectUuid,
+    github_slug as githubSlug,
+    url
+    from project_alm_bindings
+    where
+    alm_id = #{almId, jdbcType=VARCHAR}
+    and repo_id = #{repoId, jdbcType=VARCHAR}
+  </select>
+
 </mapper>
index c0b5150d0b6f08f122713fa635bf1881b373944e..287582125c013ed46efd0cfe0c541b581ae1e36f 100644 (file)
@@ -23,6 +23,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import javax.annotation.Nullable;
 import org.assertj.core.api.AbstractAssert;
 import org.junit.Rule;
@@ -207,6 +208,29 @@ public class ProjectAlmBindingsDaoTest {
     assertThat(underTest.bindingExists(dbSession, GITHUB, A_REPO)).isTrue();
   }
 
+  @Test
+  public void select_by_repo_id() {
+    when(system2.now()).thenReturn(DATE);
+    when(uuidFactory.create())
+      .thenReturn("uuid1")
+      .thenReturn("uuid2")
+      .thenReturn("uuid3");
+    underTest.insertOrUpdate(dbSession, GITHUB, A_REPO, A_UUID, A_GITHUB_SLUG, A_URL);
+    underTest.insertOrUpdate(dbSession, GITHUB, ANOTHER_REPO, ANOTHER_UUID, null, ANOTHER_URL);
+    underTest.insertOrUpdate(dbSession, BITBUCKETCLOUD, ANOTHER_REPO, "foo", null, "http://foo");
+
+    assertThat(underTest.selectByRepoId(dbSession, GITHUB, "foo")).isNotPresent();
+
+    Optional<ProjectAlmBindingDto> dto = underTest.selectByRepoId(dbSession, GITHUB, A_REPO);
+    assertThat(dto).isPresent();
+    assertThat(dto.get().getUuid()).isEqualTo("uuid1");
+    assertThat(dto.get().getAlmId()).isEqualTo(GITHUB.getId());
+    assertThat(dto.get().getRepoId()).isEqualTo(A_REPO);
+    assertThat(dto.get().getProjectUuid()).isEqualTo(A_UUID);
+    assertThat(dto.get().getUrl()).isEqualTo(A_URL);
+    assertThat(dto.get().getGithubSlug()).isEqualTo(A_GITHUB_SLUG);
+  }
+
   @Test
   public void select_by_repo_ids() {
     when(system2.now()).thenReturn(DATE);