]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13127: Adding front config for summary analysis comment for GH.
authorMathieu Suen <mathieu.suen@sonarsource.com>
Fri, 28 Feb 2020 10:33:45 +0000 (11:33 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 6 Mar 2020 20:04:32 +0000 (20:04 +0000)
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__/PRDecorationBindingRenderer-test.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/__tests__/__snapshots__/PRDecorationBinding-test.tsx.snap
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.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index f43c1f2201b39c3ca3c12236388121be8f798c6a..a0fe97c4c29083b1d169985a25d955454b980a68 100644 (file)
@@ -38,9 +38,11 @@ interface Props {
 interface State {
   formData: ProjectAlmBinding;
   instances: AlmSettingsInstance[];
+  isChanged: boolean;
+  isConfigured: boolean;
   isValid: boolean;
   loading: boolean;
-  originalData?: ProjectAlmBinding;
+  orignalData?: ProjectAlmBinding;
   saving: boolean;
   success: boolean;
 }
@@ -59,6 +61,8 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat
   state: State = {
     formData: { key: '' },
     instances: [],
+    isChanged: false,
+    isConfigured: false,
     isValid: false,
     loading: true,
     saving: false,
@@ -84,9 +88,11 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat
             return {
               formData: newFormData,
               instances: instances || [],
+              isChanged: false,
+              isConfigured: !!originalData,
               isValid: this.validateForm(newFormData),
               loading: false,
-              originalData
+              orignalData: newFormData
             };
           });
         }
@@ -125,7 +131,8 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat
               repository: '',
               slug: ''
             },
-            originalData: undefined,
+            isChanged: false,
+            isConfigured: false,
             saving: false,
             success: true
           });
@@ -161,14 +168,20 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat
         });
       }
       case AlmKeys.GitHub: {
-        const repository = almSpecificFields && almSpecificFields.repository;
+        const repository = almSpecificFields?.repository;
+        // By default it must remain true.
+        const summaryCommentEnabled =
+          almSpecificFields?.summaryCommentEnabled === undefined
+            ? true
+            : almSpecificFields?.summaryCommentEnabled;
         if (!repository) {
           return Promise.reject();
         }
         return setProjectGithubBinding({
           almSetting,
           project,
-          repository
+          repository,
+          summaryCommentEnabled
         });
       }
 
@@ -198,23 +211,38 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat
       return;
     }
 
-    if (key) {
-      this.submitProjectAlmBinding(selected.alm, key, additionalFields)
-        .then(() => {
-          if (this.mounted) {
-            this.setState({
-              saving: false,
-              success: true
-            });
-          }
-        })
-        .then(this.fetchDefinitions)
-        .catch(this.catchError);
-    }
+    this.submitProjectAlmBinding(selected.alm, key, additionalFields)
+      .then(() => {
+        if (this.mounted) {
+          this.setState({
+            saving: false,
+            success: true
+          });
+        }
+      })
+      .then(this.fetchDefinitions)
+      .catch(this.catchError);
   };
 
-  handleFieldChange = (id: keyof ProjectAlmBinding, value: string) => {
-    this.setState(({ formData }) => {
+  isDataSame(
+    { key, repository = '', slug = '', summaryCommentEnabled = false }: ProjectAlmBinding,
+    {
+      key: oKey = '',
+      repository: oRepository = '',
+      slug: oSlug = '',
+      summaryCommentEnabled: osummaryCommentEnabled = false
+    }: ProjectAlmBinding
+  ) {
+    return (
+      key === oKey &&
+      repository === oRepository &&
+      slug === oSlug &&
+      summaryCommentEnabled === osummaryCommentEnabled
+    );
+  }
+
+  handleFieldChange = (id: keyof ProjectAlmBinding, value: string | boolean) => {
+    this.setState(({ formData, orignalData }) => {
       const newFormData = {
         ...formData,
         [id]: value
@@ -222,6 +250,7 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat
       return {
         formData: newFormData,
         isValid: this.validateForm(newFormData),
+        isChanged: !this.isDataSame(newFormData, orignalData || { key: '' }),
         success: false
       };
     });
index 8ba7492f05ffb237550b08fe8cfe23f4fe9cd590..91b8a78af1f9ed5c8978b6b6f5986d1eed1fb7d5 100644 (file)
@@ -28,20 +28,34 @@ import { Alert } from 'sonar-ui-common/components/ui/Alert';
 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { AlmKeys, AlmSettingsInstance, ProjectAlmBinding } from '../../../../types/alm-settings';
+import InputForBoolean from '../inputs/InputForBoolean';
 
 export interface PRDecorationBindingRendererProps {
   formData: ProjectAlmBinding;
   instances: AlmSettingsInstance[];
+  isChanged: boolean;
+  isConfigured: boolean;
   isValid: boolean;
   loading: boolean;
-  onFieldChange: (id: keyof ProjectAlmBinding, value: string) => void;
+  onFieldChange: (id: keyof ProjectAlmBinding, value: string | boolean) => void;
   onReset: () => void;
   onSubmit: () => void;
-  originalData?: ProjectAlmBinding;
   saving: boolean;
   success: boolean;
 }
 
+interface LabelProps {
+  help?: boolean;
+  helpParams?: T.Dict<string | JSX.Element>;
+  id: string;
+  optional?: boolean;
+}
+
+interface CommonFieldProps extends LabelProps {
+  onFieldChange: (id: keyof ProjectAlmBinding, value: string | boolean) => void;
+  propKey: keyof ProjectAlmBinding;
+}
+
 function optionRenderer(instance: AlmSettingsInstance) {
   return instance.url ? (
     <>
@@ -53,35 +67,57 @@ function optionRenderer(instance: AlmSettingsInstance) {
   );
 }
 
-function renderField(props: {
-  help?: boolean;
-  helpParams?: { [key: string]: string | JSX.Element };
-  id: string;
-  onFieldChange: (id: keyof ProjectAlmBinding, value: string) => void;
-  optional?: boolean;
-  propKey: keyof ProjectAlmBinding;
-  value: string;
-}) {
-  const { help, helpParams, id, propKey, optional, value, onFieldChange } = props;
+function renderLabel(props: LabelProps) {
+  const { help, helpParams, optional, id } = props;
+  return (
+    <label className="display-flex-center" htmlFor={id}>
+      {translate('settings.pr_decoration.binding.form', id)}
+      {!optional && <em className="mandatory">*</em>}
+      {help && (
+        <HelpTooltip
+          className="spacer-left"
+          overlay={
+            <FormattedMessage
+              defaultMessage={translate('settings.pr_decoration.binding.form', id, 'help')}
+              id={`settings.pr_decoration.binding.form.${id}.help`}
+              values={helpParams}
+            />
+          }
+          placement="right"
+        />
+      )}
+    </label>
+  );
+}
+
+function renderBooleanField(
+  props: Omit<CommonFieldProps, 'optional'> & {
+    value: boolean;
+  }
+) {
+  const { id, value, onFieldChange, propKey } = props;
   return (
     <div className="form-field">
-      <label className="display-flex-center" htmlFor={id}>
-        {translate('settings.pr_decoration.binding.form', id)}
-        {!optional && <em className="mandatory">*</em>}
-        {help && (
-          <HelpTooltip
-            className="spacer-left"
-            overlay={
-              <FormattedMessage
-                defaultMessage={translate('settings.pr_decoration.binding.form', id, 'help')}
-                id={`settings.pr_decoration.binding.form.${id}.help`}
-                values={helpParams}
-              />
-            }
-            placement="right"
-          />
-        )}
-      </label>
+      {renderLabel({ ...props, optional: true })}
+      <InputForBoolean
+        isDefault={true}
+        name={id}
+        onChange={v => onFieldChange(propKey, v)}
+        value={value}
+      />
+    </div>
+  );
+}
+
+function renderField(
+  props: CommonFieldProps & {
+    value: string;
+  }
+) {
+  const { id, propKey, value, onFieldChange } = props;
+  return (
+    <div className="form-field">
+      {renderLabel(props)}
       <input
         className="input-super-large"
         id={id}
@@ -95,20 +131,14 @@ function renderField(props: {
   );
 }
 
-function isDataSame(
-  { key, repository = '', slug = '' }: ProjectAlmBinding,
-  { key: oKey = '', repository: oRepository = '', slug: oSlug = '' }: ProjectAlmBinding
-) {
-  return key === oKey && repository === oRepository && slug === oSlug;
-}
-
 export default function PRDecorationBindingRenderer(props: PRDecorationBindingRendererProps) {
   const {
-    formData: { key, repository, slug },
+    formData: { key, repository, slug, summaryCommentEnabled },
     instances,
+    isChanged,
+    isConfigured,
     isValid,
     loading,
-    originalData,
     saving,
     success
   } = props;
@@ -140,8 +170,6 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe
   const selected = key && instances.find(i => i.key === key);
   const alm = selected && selected.alm;
 
-  const isChanged = !isDataSame({ key, repository, slug }, originalData || { key: '' });
-
   return (
     <div>
       <header className="page-header">
@@ -218,15 +246,25 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe
           </>
         )}
 
-        {alm === AlmKeys.GitHub &&
-          renderField({
-            help: true,
-            helpParams: { example: 'SonarSource/sonarqube' },
-            id: 'github.repository',
-            onFieldChange: props.onFieldChange,
-            propKey: 'repository',
-            value: repository || ''
-          })}
+        {alm === AlmKeys.GitHub && (
+          <>
+            {renderField({
+              help: true,
+              helpParams: { example: 'SonarSource/sonarqube' },
+              id: 'github.repository',
+              onFieldChange: props.onFieldChange,
+              propKey: 'repository',
+              value: repository || ''
+            })}
+            {renderBooleanField({
+              help: true,
+              id: 'github.summary_comment_setting',
+              onFieldChange: props.onFieldChange,
+              propKey: 'summaryCommentEnabled',
+              value: summaryCommentEnabled === undefined ? true : summaryCommentEnabled
+            })}
+          </>
+        )}
 
         {alm === AlmKeys.GitLab &&
           renderField({
@@ -245,7 +283,7 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe
               <span data-test="project-settings__alm-save">{translate('save')}</span>
             </SubmitButton>
           )}
-          {originalData && (
+          {isConfigured && (
             <Button className="spacer-right" onClick={props.onReset}>
               <span data-test="project-settings__alm-reset">{translate('reset_verb')}</span>
             </Button>
index 0049df40d7ba11ce7fbde28a3fda8ee0c86fddda..ed59ce74aff760dfd30c4b181a81549ee731c9e0 100644 (file)
@@ -66,7 +66,7 @@ it('should fill selects and fill formdata', async () => {
 
   expect(wrapper.state().loading).toBe(false);
   expect(wrapper.state().formData).toEqual(formdata);
-  expect(wrapper.state().originalData).toEqual(formdata);
+  expect(wrapper.state().isChanged).toBe(false);
 });
 
 it('should handle reset', async () => {
@@ -84,7 +84,7 @@ it('should handle reset', async () => {
 
   expect(deleteProjectAlmBinding).toBeCalledWith(PROJECT_KEY);
   expect(wrapper.state().formData).toEqual({ key: '', repository: '', slug: '' });
-  expect(wrapper.state().originalData).toBeUndefined();
+  expect(wrapper.state().isChanged).toBe(false);
 });
 
 describe('handleSubmit', () => {
@@ -99,14 +99,19 @@ describe('handleSubmit', () => {
     await waitAndUpdate(wrapper);
     const githubKey = 'github';
     const repository = 'repo/path';
-    wrapper.setState({ formData: { key: githubKey, repository }, instances });
+    const summaryCommentEnabled = true;
+    wrapper.setState({
+      formData: { key: githubKey, repository, summaryCommentEnabled },
+      instances
+    });
     wrapper.instance().handleSubmit();
     await waitAndUpdate(wrapper);
 
     expect(setProjectGithubBinding).toBeCalledWith({
       almSetting: githubKey,
       project: PROJECT_KEY,
-      repository
+      repository,
+      summaryCommentEnabled
     });
     expect(wrapper.state().success).toBe(true);
   });
@@ -146,23 +151,31 @@ describe('handleSubmit', () => {
   });
 });
 
-it('should handle failures gracefully', async () => {
-  (getProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status: 500 });
-  (setProjectGithubBinding as jest.Mock).mockRejectedValueOnce({ status: 500 });
-  (deleteProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status: 500 });
-
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  wrapper.setState({
-    formData: {
+describe.each([[500], [404]])('For status %i', status => {
+  it('should handle failures gracefully', async () => {
+    const newFormData = {
       key: 'whatever',
       repository: 'something/else'
-    }
-  });
+    };
 
-  wrapper.instance().handleSubmit();
-  await waitAndUpdate(wrapper);
-  wrapper.instance().handleReset();
+    (getProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status });
+    (setProjectGithubBinding as jest.Mock).mockRejectedValueOnce({ status });
+    (deleteProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status });
+
+    const wrapper = shallowRender();
+    await waitAndUpdate(wrapper);
+    wrapper.setState({
+      formData: newFormData,
+      orignalData: undefined
+    });
+
+    wrapper.instance().handleSubmit();
+    await waitAndUpdate(wrapper);
+    expect(wrapper.instance().state.orignalData).toBeUndefined();
+    wrapper.instance().handleReset();
+    await waitAndUpdate(wrapper);
+    expect(wrapper.instance().state.formData).toEqual(newFormData);
+  });
 });
 
 it('should handle field changes', async () => {
@@ -189,6 +202,48 @@ it('should handle field changes', async () => {
     key: 'instance2',
     repository
   });
+
+  wrapper.instance().handleFieldChange('summaryCommentEnabled', true);
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().formData).toEqual({
+    key: 'instance2',
+    repository,
+    summaryCommentEnabled: true
+  });
+});
+
+it('should reject submit github settings', async () => {
+  const wrapper = shallowRender();
+
+  expect.assertions(1);
+  await expect(
+    wrapper.instance().submitProjectAlmBinding(AlmKeys.GitHub, 'github-binding', {})
+  ).rejects.toBe(undefined);
+});
+
+it('should accept submit github settings', async () => {
+  (setProjectGithubBinding as jest.Mock).mockRestore();
+  const wrapper = shallowRender();
+  await wrapper
+    .instance()
+    .submitProjectAlmBinding(AlmKeys.GitHub, 'github-binding', { repository: 'foo' });
+  expect(setProjectGithubBinding).toHaveBeenCalledWith({
+    almSetting: 'github-binding',
+    project: PROJECT_KEY,
+    repository: 'foo',
+    summaryCommentEnabled: true
+  });
+
+  await wrapper.instance().submitProjectAlmBinding(AlmKeys.GitHub, 'github-binding', {
+    repository: 'foo',
+    summaryCommentEnabled: true
+  });
+  expect(setProjectGithubBinding).toHaveBeenCalledWith({
+    almSetting: 'github-binding',
+    project: PROJECT_KEY,
+    repository: 'foo',
+    summaryCommentEnabled: true
+  });
 });
 
 it('should validate form', async () => {
index 019f6e4210309308c86442cc5ad2c33bd2f8d0bd..a9940e730e25f3571d8285df044276331dfad398 100644 (file)
@@ -84,12 +84,10 @@ it('should render multiple instances correctly', () => {
         key: 'i1',
         repository: 'account/repo'
       },
+      isChanged: false,
+      isConfigured: true,
       instances,
-      loading: false,
-      originalData: {
-        key: 'i1',
-        repository: 'account/repo'
-      }
+      loading: false
     })
   ).toMatchSnapshot();
 });
@@ -137,6 +135,8 @@ it('should render optional fields correctly', () => {
       formData: {
         key: 'key'
       },
+      isChanged: true,
+      isConfigured: false,
       instances: [{ key: 'key', url: 'http://example.com', alm: AlmKeys.GitLab }],
       loading: false
     })
@@ -151,12 +151,13 @@ function shallowRender(props: Partial<PRDecorationBindingRendererProps> = {}) {
         repository: ''
       }}
       instances={[]}
+      isChanged={false}
+      isConfigured={false}
       isValid={false}
       loading={true}
       onFieldChange={jest.fn()}
       onReset={jest.fn()}
       onSubmit={jest.fn()}
-      originalData={undefined}
       saving={false}
       success={false}
       {...props}
index 66d782641f1b1c6c003622f9a6fb2dd956caaac2..9b9f4ddfb4968a62bf17cdcfa02c394b75926ab2 100644 (file)
@@ -458,6 +458,33 @@ exports[`should render multiple instances correctly 2`] = `
         value="account/repo"
       />
     </div>
+    <div
+      className="form-field"
+    >
+      <label
+        className="display-flex-center"
+        htmlFor="github.summary_comment_setting"
+      >
+        settings.pr_decoration.binding.form.github.summary_comment_setting
+        <HelpTooltip
+          className="spacer-left"
+          overlay={
+            <FormattedMessage
+              defaultMessage="settings.pr_decoration.binding.form.github.summary_comment_setting.help"
+              id="settings.pr_decoration.binding.form.github.summary_comment_setting.help"
+              values={Object {}}
+            />
+          }
+          placement="right"
+        />
+      </label>
+      <InputForBoolean
+        isDefault={true}
+        name="github.summary_comment_setting"
+        onChange={[Function]}
+        value={true}
+      />
+    </div>
     <div
       className="display-flex-center"
     >
index 6f138d94e4df2b6084b2b3b25762ffb20675cfa8..a25148c6fb2f5ffe3d2a31e4018376590cc03d91 100644 (file)
@@ -52,6 +52,7 @@ export interface ProjectAlmBinding {
   key: string;
   repository?: string;
   slug?: string;
+  summaryCommentEnabled?: boolean;
 }
 
 export interface AzureProjectAlmBinding {
@@ -70,6 +71,7 @@ export interface GithubProjectAlmBinding {
   almSetting: string;
   project: string;
   repository: string;
+  summaryCommentEnabled: boolean;
 }
 
 export interface GitlabProjectAlmBinding {
index aa865428be5fa3b91025e7861aec19b8fe9917a0..0ed6ae378b65c717b7c77b3b663d6b19d51844ca 100644 (file)
@@ -1076,6 +1076,8 @@ settings.pr_decoration.binding.form.url=Project location
 settings.pr_decoration.binding.form.name=Configuration name
 settings.pr_decoration.binding.form.github.repository=Repository identifier
 settings.pr_decoration.binding.form.github.repository.help=The path of your repository URL. Example: {example}
+settings.pr_decoration.binding.form.github.summary_comment_setting=Enable analysis summary under the GitHub Conversation tab
+settings.pr_decoration.binding.form.github.summary_comment_setting.help=When enabled, Pull Request analysis summary is displayed under the GitHub Conversation tab. Notifications may be sent by GitHub depending on your settings.
 settings.pr_decoration.binding.form.bitbucket.repository=Project Key
 settings.pr_decoration.binding.form.bitbucket.repository.help=The project key is part of your Bitbucket Server repository URL. Example: ({example})
 settings.pr_decoration.binding.form.bitbucket.slug=Repository SLUG