]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12514 UI for project-level form - Bitbucket
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 22 Oct 2019 12:29:27 +0000 (14:29 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 6 Nov 2019 09:04:28 +0000 (10:04 +0100)
server/sonar-web/src/main/js/api/almSettings.ts
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecoration/__tests__/__snapshots__/PRDecorationTabs-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBinding.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/PRDecorationBinding-test.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBindingRenderer-test.tsx.snap
server/sonar-web/src/main/js/types/alm-settings.d.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2d20b9f05115b48befa8184b67fa3feaf7d55612..487e12ed19b01f8f45e70373f1d2dce21ff885a1 100644 (file)
@@ -78,6 +78,10 @@ export function setProjectAzureBinding(data: T.AzureProjectAlmBinding) {
   return post('/api/alm_settings/set_azure_binding', data).catch(throwGlobalError);
 }
 
+export function setProjectBitbucketBinding(data: T.BitbucketProjectAlmBinding) {
+  return post('/api/alm_settings/set_bitbucket_binding', data).catch(throwGlobalError);
+}
+
 export function setProjectGithubBinding(data: T.GithubProjectAlmBinding) {
   return post('/api/alm_settings/set_github_binding', data).catch(throwGlobalError);
 }
index d04c62f26d38bc09eaa7b283cf9cbb6fbc470bad..7d8a26c3d7c2910b5b16902029dde4224d9d6c78 100644 (file)
@@ -25,6 +25,10 @@ exports[`should render correctly 1`] = `
           "key": "github",
           "label": "Github Enterprise",
         },
+        Object {
+          "key": "bitbucket",
+          "label": "Bitbucket Server",
+        },
         Object {
           "key": "azure",
           "label": "Azure DevOps Server",
@@ -70,6 +74,10 @@ exports[`should render correctly 2`] = `
           "key": "github",
           "label": "Github Enterprise",
         },
+        Object {
+          "key": "bitbucket",
+          "label": "Bitbucket Server",
+        },
         Object {
           "key": "azure",
           "label": "Azure DevOps Server",
@@ -120,6 +128,10 @@ exports[`should render correctly 3`] = `
           "key": "github",
           "label": "Github Enterprise",
         },
+        Object {
+          "key": "bitbucket",
+          "label": "Bitbucket Server",
+        },
         Object {
           "key": "azure",
           "label": "Azure DevOps Server",
@@ -165,6 +177,10 @@ exports[`should render correctly 4`] = `
           "key": "github",
           "label": "Github Enterprise",
         },
+        Object {
+          "key": "bitbucket",
+          "label": "Bitbucket Server",
+        },
         Object {
           "key": "azure",
           "label": "Azure DevOps Server",
index 6c31725727a229ce9d0a609b6eefa27655880046..70ff66b5a83253483a84dbb3fcc90e26b845c891 100644 (file)
@@ -23,6 +23,7 @@ import {
   getAlmSettings,
   getProjectAlmBinding,
   setProjectAzureBinding,
+  setProjectBitbucketBinding,
   setProjectGithubBinding
 } from '../../../../api/almSettings';
 import throwGlobalError from '../../../../app/utils/throwGlobalError';
@@ -43,8 +44,11 @@ interface State {
   success: boolean;
 }
 
-const FIELDS_BY_ALM: { [almKey: string]: Array<'repository'> } = {
+const FIELDS_BY_ALM: {
+  [almKey: string]: Array<'repository' | 'repositoryKey' | 'repositorySlug'>;
+} = {
   [ALM_KEYS.AZURE]: [],
+  [ALM_KEYS.BITBUCKET]: ['repositoryKey', 'repositorySlug'],
   [ALM_KEYS.GITHUB]: ['repository']
 };
 
@@ -138,7 +142,7 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat
   submitProjectAlmBinding(
     alm: ALM_KEYS,
     key: string,
-    almSpecificFields?: { repository?: string }
+    almSpecificFields?: { repository?: string; repositoryKey?: string; repositorySlug?: string }
   ): Promise<void> {
     const almSetting = key;
     const project = this.props.component.key;
@@ -149,6 +153,18 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat
           almSetting,
           project
         });
+      case ALM_KEYS.BITBUCKET: {
+        if (!almSpecificFields) {
+          return Promise.reject();
+        }
+        const { repositoryKey = '', repositorySlug = '' } = almSpecificFields;
+        return setProjectBitbucketBinding({
+          almSetting,
+          project,
+          repositoryKey,
+          repositorySlug
+        });
+      }
       case ALM_KEYS.GITHUB: {
         const repository = almSpecificFields && almSpecificFields.repository;
         if (!repository) {
index 3fa7892e3208910a293b1f7bf674d77484b052e1..c9a8d349faff5897d3f9388c6d54c523cec90dfa 100644 (file)
@@ -21,6 +21,7 @@ import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { Link } from 'react-router';
 import { Button, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
+import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
 import Select from 'sonar-ui-common/components/controls/Select';
 import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon';
 import { Alert } from 'sonar-ui-common/components/ui/Alert';
@@ -45,9 +46,49 @@ function renderLabel(v: T.AlmSettingsInstance) {
   return v.url ? `${v.key} — ${v.url}` : v.key;
 }
 
+function renderField(props: {
+  help?: boolean;
+  helpParams?: { [key: string]: string | number | boolean | Date | JSX.Element | null | undefined };
+  id: string;
+  onFieldChange: (id: keyof T.ProjectAlmBinding, value: string) => void;
+  propKey: keyof T.ProjectAlmBinding;
+  value: string;
+}) {
+  const { help, helpParams, id, propKey, value, onFieldChange } = props;
+  return (
+    <div className="form-field">
+      <label className="display-flex-center" htmlFor={id}>
+        {translate('settings.pr_decoration.binding.form', id)}
+        <em className="mandatory spacer-right">*</em>
+        {help && (
+          <HelpTooltip
+            overlay={
+              <FormattedMessage
+                defaultMessage={translate('settings.pr_decoration.binding.form', id, 'help')}
+                id={`settings.pr_decoration.binding.form.${id}.help`}
+                values={helpParams}
+              />
+            }
+            placement="right"
+          />
+        )}
+      </label>
+      <input
+        className="input-super-large"
+        id={id}
+        maxLength={256}
+        name={id}
+        onChange={e => onFieldChange(propKey, e.currentTarget.value)}
+        type="text"
+        value={value}
+      />
+    </div>
+  );
+}
+
 export default function PRDecorationBindingRenderer(props: PRDecorationBindingRendererProps) {
   const {
-    formData: { repository, key },
+    formData: { key, repository, repositoryKey, repositorySlug },
     hasBinding,
     instances,
     isValid,
@@ -114,24 +155,53 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe
           />
         </div>
 
-        {alm === ALM_KEYS.GITHUB && (
-          <div className="form-field">
-            <label htmlFor="repository">
-              {translate('settings.pr_decoration.binding.form.repository')}
-              <em className="mandatory spacer-right">*</em>
-            </label>
-            <input
-              className="input-super-large"
-              id="repository"
-              maxLength={256}
-              name="repository"
-              onChange={e => props.onFieldChange('repository', e.currentTarget.value)}
-              type="text"
-              value={repository}
-            />
-          </div>
+        {alm === ALM_KEYS.BITBUCKET && (
+          <>
+            {renderField({
+              help: true,
+              helpParams: {
+                example: (
+                  <>
+                    {'.../projects/'}
+                    <strong>{'{KEY}'}</strong>
+                    {'/repos/{SLUG}/browse'}
+                  </>
+                )
+              },
+              id: 'repository_key',
+              onFieldChange: props.onFieldChange,
+              propKey: 'repositoryKey',
+              value: repositoryKey || ''
+            })}
+            {renderField({
+              help: true,
+              helpParams: {
+                example: (
+                  <>
+                    {'.../projects/{KEY}/repos/'}
+                    <strong>{'{SLUG}'}</strong>
+                    {'/browse'}
+                  </>
+                )
+              },
+              id: 'repository_slug',
+              onFieldChange: props.onFieldChange,
+              propKey: 'repositorySlug',
+              value: repositorySlug || ''
+            })}
+          </>
         )}
 
+        {alm === ALM_KEYS.GITHUB &&
+          renderField({
+            help: true,
+            helpParams: { example: 'SonarSource/sonarqube' },
+            id: 'repository',
+            onFieldChange: props.onFieldChange,
+            propKey: 'repository',
+            value: repository || ''
+          })}
+
         <div className="display-flex-center">
           <DeferredSpinner className="spacer-right" loading={saving} />
           <SubmitButton className="spacer-right" disabled={saving || !isValid}>
index e0e1b899c4acceb3abbb488328022d80a9ee0ab3..b1eaaf1346cde060b3c1de1cba9322cf45281fbf 100644 (file)
@@ -25,6 +25,7 @@ import {
   getAlmSettings,
   getProjectAlmBinding,
   setProjectAzureBinding,
+  setProjectBitbucketBinding,
   setProjectGithubBinding
 } from '../../../../../api/almSettings';
 import { mockComponent } from '../../../../../helpers/testMocks';
@@ -35,6 +36,7 @@ jest.mock('../../../../../api/almSettings', () => ({
   getAlmSettings: jest.fn().mockResolvedValue([]),
   getProjectAlmBinding: jest.fn().mockResolvedValue(undefined),
   setProjectAzureBinding: jest.fn().mockResolvedValue(undefined),
+  setProjectBitbucketBinding: jest.fn().mockResolvedValue(undefined),
   setProjectGithubBinding: jest.fn().mockResolvedValue(undefined),
   deleteProjectAlmBinding: jest.fn().mockResolvedValue(undefined)
 }));
@@ -99,12 +101,13 @@ it('should handle reset', async () => {
   expect(wrapper.state().hasBinding).toBe(false);
 });
 
-it('should handle submit to github or azure', async () => {
+it('should handle submit to github, azure or bitbucket', async () => {
   const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
   const instances = [
     { key: 'github', alm: ALM_KEYS.GITHUB },
-    { key: 'azure', alm: ALM_KEYS.AZURE }
+    { key: 'azure', alm: ALM_KEYS.AZURE },
+    { key: 'bitbucket', alm: ALM_KEYS.BITBUCKET }
   ];
 
   // Github
@@ -134,6 +137,23 @@ it('should handle submit to github or azure', async () => {
   });
   expect(wrapper.state().hasBinding).toBe(true);
   expect(wrapper.state().success).toBe(true);
+
+  // bitbucket
+  const bitbucketKey = 'bitbucket';
+  const repositoryKey = 'repoKey';
+  const repositorySlug = 'repoSlug';
+  wrapper.setState({ formData: { key: bitbucketKey, repositoryKey, repositorySlug } });
+  wrapper.instance().handleSubmit();
+  await waitAndUpdate(wrapper);
+
+  expect(setProjectBitbucketBinding).toBeCalledWith({
+    almSetting: bitbucketKey,
+    project: PROJECT_KEY,
+    repositoryKey,
+    repositorySlug
+  });
+  expect(wrapper.state().hasBinding).toBe(true);
+  expect(wrapper.state().success).toBe(true);
 });
 
 it('should handle failures gracefully', async () => {
@@ -185,11 +205,23 @@ it('should validate form', async () => {
   expect(wrapper.instance().validateForm({ key: '', repository: 'c' })).toBe(false);
 
   wrapper.setState({
-    instances: [{ key: 'azure', alm: ALM_KEYS.AZURE }, { key: 'github', alm: ALM_KEYS.GITHUB }]
+    instances: [
+      { key: 'azure', alm: ALM_KEYS.AZURE },
+      { key: 'bitbucket', alm: ALM_KEYS.BITBUCKET },
+      { key: 'github', alm: ALM_KEYS.GITHUB }
+    ]
   });
   expect(wrapper.instance().validateForm({ key: 'azure' })).toBe(true);
+
   expect(wrapper.instance().validateForm({ key: 'github', repository: '' })).toBe(false);
   expect(wrapper.instance().validateForm({ key: 'github', repository: 'asdf' })).toBe(true);
+
+  expect(wrapper.instance().validateForm({ key: 'bitbucket', repositoryKey: 'key' })).toBe(false);
+  expect(
+    wrapper
+      .instance()
+      .validateForm({ key: 'bitbucket', repositoryKey: 'key', repositorySlug: 'slug' })
+  ).toBe(true);
 });
 
 function shallowRender(props: Partial<PRDecorationBinding['props']> = {}) {
index a57c9bfa8569e31df0fe292ee3358d5e8f110a0f..5fd4b3eca001b4b184742da1d753e96c1a0f95f5 100644 (file)
@@ -380,6 +380,7 @@ exports[`should render multiple instances correctly 2`] = `
       className="form-field"
     >
       <label
+        className="display-flex-center"
         htmlFor="repository"
       >
         settings.pr_decoration.binding.form.repository
@@ -388,6 +389,20 @@ exports[`should render multiple instances correctly 2`] = `
         >
           *
         </em>
+        <HelpTooltip
+          overlay={
+            <FormattedMessage
+              defaultMessage="settings.pr_decoration.binding.form.repository.help"
+              id="settings.pr_decoration.binding.form.repository.help"
+              values={
+                Object {
+                  "example": "SonarSource/sonarqube",
+                }
+              }
+            />
+          }
+          placement="right"
+        />
       </label>
       <input
         className="input-super-large"
index 17d7285bc574cb14c3c78a25c151d7348418c551..0fdb03ce4a4d735628c268d276bb05c59634709a 100644 (file)
@@ -42,18 +42,18 @@ declare namespace T {
     personalAccessToken: string;
     url: string;
   }
-  
+
   export interface GithubBindingDefinition extends AlmSettingsBinding {
     appId: string;
     privateKey: string;
     url: string;
   }
 
-
-
   export interface ProjectAlmBinding {
     key: string;
     repository?: string;
+    repositoryKey?: string;
+    repositorySlug?: string;
   }
 
   export interface AzureProjectAlmBinding {
@@ -61,6 +61,13 @@ declare namespace T {
     project: string;
   }
 
+  export interface BitbucketProjectAlmBinding {
+    almSetting: string;
+    project: string;
+    repositoryKey: string;
+    repositorySlug: string;
+  }
+
   export interface GithubProjectAlmBinding {
     almSetting: string;
     project: string;
index 73113f8f983145a75561b92a79620b8901f2c2cd..3cd73890fa6fed288bc011e4902a90d1d87322a8 100644 (file)
@@ -963,6 +963,10 @@ settings.pr_decoration.binding.form.url=Project location
 settings.pr_decoration.binding.form.name=Configuration name
 settings.pr_decoration.binding.form.repository=Repository identifier
 settings.pr_decoration.binding.form.repository.help=This is the path of your repository. Example: {example}
+settings.pr_decoration.binding.form.repository_key=Project Key
+settings.pr_decoration.binding.form.repository_key.help=You can obtain it from the URL of the Bitbucket Server repository page ({example})
+settings.pr_decoration.binding.form.repository_slug=Repository SLUG
+settings.pr_decoration.binding.form.repository_slug.help=You can obtain it from the URL of the Bitbucket Server repository page ({example})
 
 property.category.general=General
 property.category.general.email=Email