]> source.dussan.org Git - sonarqube.git/blob
bebbde1d0202a0edd8bcad862d539d9ebdb4fae5
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2020 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 import * as React from 'react';
21 import { FormattedMessage } from 'react-intl';
22 import { Link } from 'react-router';
23 import { Button, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
24 import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
25 import Select from 'sonar-ui-common/components/controls/Select';
26 import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon';
27 import { Alert } from 'sonar-ui-common/components/ui/Alert';
28 import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
29 import { translate } from 'sonar-ui-common/helpers/l10n';
30 import {
31   AlmKeys,
32   AlmSettingsInstance,
33   ProjectAlmBindingResponse
34 } from '../../../../types/alm-settings';
35 import InputForBoolean from '../inputs/InputForBoolean';
36
37 export interface PRDecorationBindingRendererProps {
38   formData: T.Omit<ProjectAlmBindingResponse, 'alm'>;
39   instances: AlmSettingsInstance[];
40   isChanged: boolean;
41   isConfigured: boolean;
42   isValid: boolean;
43   loading: boolean;
44   onFieldChange: (id: keyof ProjectAlmBindingResponse, value: string | boolean) => void;
45   onReset: () => void;
46   onSubmit: () => void;
47   saving: boolean;
48   success: boolean;
49 }
50
51 interface LabelProps {
52   help?: boolean;
53   helpParams?: T.Dict<string | JSX.Element>;
54   id: string;
55   optional?: boolean;
56 }
57
58 interface CommonFieldProps extends LabelProps {
59   onFieldChange: (id: keyof ProjectAlmBindingResponse, value: string | boolean) => void;
60   propKey: keyof ProjectAlmBindingResponse;
61 }
62
63 function optionRenderer(instance: AlmSettingsInstance) {
64   return instance.url ? (
65     <>
66       <span>{instance.key} — </span>
67       <span className="text-muted">{instance.url}</span>
68     </>
69   ) : (
70     <span>{instance.key}</span>
71   );
72 }
73
74 function renderLabel(props: LabelProps) {
75   const { help, helpParams, optional, id } = props;
76   return (
77     <label className="display-flex-center" htmlFor={id}>
78       {translate('settings.pr_decoration.binding.form', id)}
79       {!optional && <em className="mandatory">*</em>}
80       {help && (
81         <HelpTooltip
82           className="spacer-left"
83           overlay={
84             <FormattedMessage
85               defaultMessage={translate('settings.pr_decoration.binding.form', id, 'help')}
86               id={`settings.pr_decoration.binding.form.${id}.help`}
87               values={helpParams}
88             />
89           }
90           placement="right"
91         />
92       )}
93     </label>
94   );
95 }
96
97 function renderBooleanField(
98   props: Omit<CommonFieldProps, 'optional'> & {
99     value: boolean;
100   }
101 ) {
102   const { id, value, onFieldChange, propKey } = props;
103   return (
104     <div className="form-field">
105       {renderLabel({ ...props, optional: true })}
106       <InputForBoolean
107         isDefault={true}
108         name={id}
109         onChange={v => onFieldChange(propKey, v)}
110         value={value}
111       />
112     </div>
113   );
114 }
115
116 function renderField(
117   props: CommonFieldProps & {
118     value: string;
119   }
120 ) {
121   const { id, propKey, value, onFieldChange } = props;
122   return (
123     <div className="form-field">
124       {renderLabel(props)}
125       <input
126         className="input-super-large"
127         id={id}
128         maxLength={256}
129         name={id}
130         onChange={e => onFieldChange(propKey, e.currentTarget.value)}
131         type="text"
132         value={value}
133       />
134     </div>
135   );
136 }
137
138 export default function PRDecorationBindingRenderer(props: PRDecorationBindingRendererProps) {
139   const {
140     formData: { key, repository, slug, summaryCommentEnabled },
141     instances,
142     isChanged,
143     isConfigured,
144     isValid,
145     loading,
146     saving,
147     success
148   } = props;
149
150   if (loading) {
151     return <DeferredSpinner />;
152   }
153
154   if (instances.length < 1) {
155     return (
156       <div>
157         <Alert className="spacer-top huge-spacer-bottom" variant="info">
158           <FormattedMessage
159             defaultMessage={translate('settings.pr_decoration.binding.no_bindings')}
160             id="settings.pr_decoration.binding.no_bindings"
161             values={{
162               link: (
163                 <Link to="/documentation/analysis/pull-request/#pr-decoration">
164                   {translate('learn_more')}
165                 </Link>
166               )
167             }}
168           />
169         </Alert>
170       </div>
171     );
172   }
173
174   const selected = key && instances.find(i => i.key === key);
175   const alm = selected && selected.alm;
176
177   return (
178     <div>
179       <header className="page-header">
180         <h1 className="page-title">{translate('settings.pr_decoration.binding.title')}</h1>
181       </header>
182
183       <div className="markdown small spacer-top big-spacer-bottom">
184         {translate('settings.pr_decoration.binding.description')}
185       </div>
186
187       <form
188         onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
189           event.preventDefault();
190           props.onSubmit();
191         }}>
192         <div className="form-field">
193           <label htmlFor="name">
194             {translate('settings.pr_decoration.binding.form.name')}
195             <em className="mandatory spacer-right">*</em>
196           </label>
197           <Select
198             autosize={true}
199             className="abs-width-400"
200             clearable={false}
201             id="name"
202             menuContainerStyle={{
203               maxWidth: '210%' /* Allow double the width of the select */,
204               width: 'auto'
205             }}
206             onChange={(instance: AlmSettingsInstance) => props.onFieldChange('key', instance.key)}
207             optionRenderer={optionRenderer}
208             options={instances}
209             searchable={false}
210             value={key}
211             valueKey="key"
212             valueRenderer={optionRenderer}
213           />
214         </div>
215
216         {alm === AlmKeys.Bitbucket && (
217           <>
218             {renderField({
219               help: true,
220               helpParams: {
221                 example: (
222                   <>
223                     {'.../projects/'}
224                     <strong>{'{KEY}'}</strong>
225                     {'/repos/{SLUG}/browse'}
226                   </>
227                 )
228               },
229               id: 'bitbucket.repository',
230               onFieldChange: props.onFieldChange,
231               propKey: 'repository',
232               value: repository || ''
233             })}
234             {renderField({
235               help: true,
236               helpParams: {
237                 example: (
238                   <>
239                     {'.../projects/{KEY}/repos/'}
240                     <strong>{'{SLUG}'}</strong>
241                     {'/browse'}
242                   </>
243                 )
244               },
245               id: 'bitbucket.slug',
246               onFieldChange: props.onFieldChange,
247               propKey: 'slug',
248               value: slug || ''
249             })}
250           </>
251         )}
252
253         {alm === AlmKeys.GitHub && (
254           <>
255             {renderField({
256               help: true,
257               helpParams: { example: 'SonarSource/sonarqube' },
258               id: 'github.repository',
259               onFieldChange: props.onFieldChange,
260               propKey: 'repository',
261               value: repository || ''
262             })}
263             {renderBooleanField({
264               help: true,
265               id: 'github.summary_comment_setting',
266               onFieldChange: props.onFieldChange,
267               propKey: 'summaryCommentEnabled',
268               value: summaryCommentEnabled === undefined ? true : summaryCommentEnabled
269             })}
270           </>
271         )}
272
273         {alm === AlmKeys.GitLab &&
274           renderField({
275             help: true,
276             id: 'gitlab.repository',
277             onFieldChange: props.onFieldChange,
278             optional: true,
279             propKey: 'repository',
280             value: repository || ''
281           })}
282
283         <div className="display-flex-center">
284           <DeferredSpinner className="spacer-right" loading={saving} />
285           {isChanged && (
286             <SubmitButton className="spacer-right button-success" disabled={saving || !isValid}>
287               <span data-test="project-settings__alm-save">{translate('save')}</span>
288             </SubmitButton>
289           )}
290           {isConfigured && (
291             <Button className="spacer-right" onClick={props.onReset}>
292               <span data-test="project-settings__alm-reset">{translate('reset_verb')}</span>
293             </Button>
294           )}
295           {!saving && success && (
296             <span className="text-success">
297               <AlertSuccessIcon className="spacer-right" />
298               {translate('settings.state.saved')}
299             </span>
300           )}
301         </div>
302       </form>
303     </div>
304   );
305 }