@@ -24,6 +24,7 @@ export function createWebhook(data: { | |||
name: string; | |||
organization: string | undefined; | |||
project?: string; | |||
secret?: string; | |||
url: string; | |||
}): Promise<{ webhook: T.Webhook }> { | |||
return postJSON('/api/webhooks/create', data).catch(throwGlobalError); | |||
@@ -43,6 +44,7 @@ export function searchWebhooks(data: { | |||
export function updateWebhook(data: { | |||
webhook: string; | |||
name: string; | |||
secret?: string; | |||
url: string; | |||
}): Promise<void | Response> { | |||
return post('/api/webhooks/update', data).catch(throwGlobalError); |
@@ -923,6 +923,7 @@ declare namespace T { | |||
key: string; | |||
latestDelivery?: WebhookDelivery; | |||
name: string; | |||
secret?: string; | |||
url: string; | |||
} | |||
@@ -72,11 +72,15 @@ export default class App extends React.PureComponent<Props, State> { | |||
}; | |||
}; | |||
handleCreate = (data: { name: string; url: string }) => { | |||
return createWebhook({ | |||
...data, | |||
handleCreate = (data: { name: string; secret?: string; url: string }) => { | |||
const createData = { | |||
name: data.name, | |||
url: data.url, | |||
...(data.secret && { secret: data.secret }), | |||
...this.getScopeParams() | |||
}).then(({ webhook }) => { | |||
}; | |||
return createWebhook(createData).then(({ webhook }) => { | |||
if (this.mounted) { | |||
this.setState(({ webhooks }) => ({ webhooks: [...webhooks, webhook] })); | |||
} | |||
@@ -93,12 +97,21 @@ export default class App extends React.PureComponent<Props, State> { | |||
}); | |||
}; | |||
handleUpdate = (data: { webhook: string; name: string; url: string }) => { | |||
return updateWebhook(data).then(() => { | |||
handleUpdate = (data: { webhook: string; name: string; secret?: string; url: string }) => { | |||
const udpateData = { | |||
webhook: data.webhook, | |||
name: data.name, | |||
url: data.url, | |||
...(data.secret && { secret: data.secret }) | |||
}; | |||
return updateWebhook(udpateData).then(() => { | |||
if (this.mounted) { | |||
this.setState(({ webhooks }) => ({ | |||
webhooks: webhooks.map(webhook => | |||
webhook.key === data.webhook ? { ...webhook, name: data.name, url: data.url } : webhook | |||
webhook.key === data.webhook | |||
? { ...webhook, name: data.name, secret: data.secret, url: data.url } | |||
: webhook | |||
) | |||
})); | |||
} |
@@ -32,6 +32,7 @@ interface Props { | |||
interface Values { | |||
name: string; | |||
secret: string; | |||
url: string; | |||
} | |||
@@ -43,8 +44,8 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { | |||
}; | |||
handleValidate = (data: Values) => { | |||
const { name, url } = data; | |||
const errors: { name?: string; url?: string } = {}; | |||
const { name, secret, url } = data; | |||
const errors: { name?: string; secret?: string; url?: string } = {}; | |||
if (!name.trim()) { | |||
errors.name = translate('webhooks.name.required'); | |||
} | |||
@@ -55,6 +56,9 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { | |||
} else if (!isWebUri(url)) { | |||
errors.url = translate('webhooks.url.bad_format'); | |||
} | |||
if (secret && secret.length > 200) { | |||
errors.secret = translate('webhooks.secret.bad_format'); | |||
} | |||
return errors; | |||
}; | |||
@@ -69,6 +73,7 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { | |||
header={modalHeader} | |||
initialValues={{ | |||
name: webhook ? webhook.name : '', | |||
secret: webhook ? webhook.secret : '', | |||
url: webhook ? webhook.url : '' | |||
}} | |||
isInitialValid={isUpdate} | |||
@@ -124,6 +129,20 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { | |||
type="text" | |||
value={values.url} | |||
/> | |||
<InputValidationField | |||
description={translate('webhooks.secret.description')} | |||
dirty={dirty} | |||
disabled={isSubmitting} | |||
error={errors.secret} | |||
id="webhook-secret" | |||
label={<label htmlFor="webhook-secret">{translate('webhooks.secret')}</label>} | |||
name="secret" | |||
onBlur={handleBlur} | |||
onChange={handleChange} | |||
touched={touched.secret} | |||
type="password" | |||
value={values.secret} | |||
/> | |||
</> | |||
)} | |||
</ValidationModal> |
@@ -25,7 +25,7 @@ import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
interface Props { | |||
loading: boolean; | |||
onCreate: (data: { name: string; url: string }) => Promise<void>; | |||
onCreate: (data: { name: string; secret?: string; url: string }) => Promise<void>; | |||
webhooksCount: number; | |||
} | |||
@@ -20,6 +20,7 @@ | |||
import * as React from 'react'; | |||
import WebhookItemLatestDelivery from './WebhookItemLatestDelivery'; | |||
import WebhookActions from './WebhookActions'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
onDelete: (webhook: string) => Promise<void>; | |||
@@ -32,6 +33,7 @@ export default function WebhookItem({ onDelete, onUpdate, webhook }: Props) { | |||
<tr> | |||
<td>{webhook.name}</td> | |||
<td>{webhook.url}</td> | |||
<td>{webhook.secret ? translate('yes') : translate('no')}</td> | |||
<td> | |||
<WebhookItemLatestDelivery webhook={webhook} /> | |||
</td> |
@@ -34,6 +34,7 @@ export default class WebhooksList extends React.PureComponent<Props> { | |||
<tr> | |||
<th>{translate('name')}</th> | |||
<th>{translate('webhooks.url')}</th> | |||
<th>{translate('webhooks.secret_header')}</th> | |||
<th>{translate('webhooks.last_execution')}</th> | |||
<th /> | |||
</tr> |
@@ -21,14 +21,19 @@ import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import CreateWebhookForm from '../CreateWebhookForm'; | |||
const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' }; | |||
const webhookWithoutSecret = { key: '1', name: 'foo', url: 'http://foo.bar' }; | |||
const webhookWithSecret = { key: '2', name: 'bar', secret: 'sonar', url: 'http://foo.bar' }; | |||
it('should render correctly when creating a new webhook', () => { | |||
expect(getWrapper()).toMatchSnapshot(); | |||
}); | |||
it('should render correctly when updating a webhook', () => { | |||
expect(getWrapper({ webhook })).toMatchSnapshot(); | |||
it('should render correctly when updating a webhook without secret', () => { | |||
expect(getWrapper({ webhook: webhookWithoutSecret })).toMatchSnapshot(); | |||
}); | |||
it('should render correctly when updating a webhook with a secret', () => { | |||
expect(getWrapper({ webhook: webhookWithSecret })).toMatchSnapshot(); | |||
}); | |||
function getWrapper(props = {}) { |
@@ -7,6 +7,7 @@ exports[`should render correctly when creating a new webhook 1`] = ` | |||
initialValues={ | |||
Object { | |||
"name": "", | |||
"secret": "", | |||
"url": "", | |||
} | |||
} | |||
@@ -20,13 +21,35 @@ exports[`should render correctly when creating a new webhook 1`] = ` | |||
</ValidationModal> | |||
`; | |||
exports[`should render correctly when updating a webhook 1`] = ` | |||
exports[`should render correctly when updating a webhook with a secret 1`] = ` | |||
<ValidationModal | |||
confirmButtonText="update_verb" | |||
header="webhooks.update" | |||
initialValues={ | |||
Object { | |||
"name": "bar", | |||
"secret": "sonar", | |||
"url": "http://foo.bar", | |||
} | |||
} | |||
isInitialValid={true} | |||
onClose={[MockFunction]} | |||
onSubmit={[MockFunction]} | |||
size="small" | |||
validate={[Function]} | |||
> | |||
<Component /> | |||
</ValidationModal> | |||
`; | |||
exports[`should render correctly when updating a webhook without secret 1`] = ` | |||
<ValidationModal | |||
confirmButtonText="update_verb" | |||
header="webhooks.update" | |||
initialValues={ | |||
Object { | |||
"name": "foo", | |||
"secret": undefined, | |||
"url": "http://foo.bar", | |||
} | |||
} |
@@ -8,6 +8,9 @@ exports[`should render correctly 1`] = ` | |||
<td> | |||
http://webhook.target | |||
</td> | |||
<td> | |||
no | |||
</td> | |||
<td> | |||
<WebhookItemLatestDelivery | |||
webhook={ |
@@ -18,6 +18,9 @@ exports[`should correctly render the webhooks 1`] = ` | |||
<th> | |||
webhooks.url | |||
</th> | |||
<th> | |||
webhooks.secret_header | |||
</th> | |||
<th> | |||
webhooks.last_execution | |||
</th> |
@@ -3187,6 +3187,10 @@ webhooks.name=Name | |||
webhooks.name.required=Name is required. | |||
webhooks.no_result=No webhook defined. | |||
webhooks.update=Update Webhook | |||
webhooks.secret=Secret | |||
webhooks.secret_header=Secret? | |||
webhooks.secret.bad_format=Secret must have a maximum length of 200 characters | |||
webhooks.secret.description=If provided, secret will be used as the key to generate the HMAC hex (lowercase) digest value in the 'X-Sonar-Webhook-HMAC-SHA256' header | |||
webhooks.url=URL | |||
webhooks.url.bad_format=Bad format of URL. | |||
webhooks.url.bad_protocol=URL must start with "http://" or "https://". |