aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/package.json1
-rw-r--r--server/sonar-web/src/main/js/api/webhooks.ts25
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/modals.css43
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/App.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx131
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx89
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx105
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageActions-test.tsx51
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx66
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap35
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap33
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap24
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap5
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx4
-rw-r--r--server/sonar-web/src/main/js/components/controls/InputValidationField.tsx52
-rw-r--r--server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx49
-rw-r--r--server/sonar-web/src/main/js/components/controls/ValidationModal.tsx108
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/InputValidationField-test.tsx44
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/ModalValidationField-test.tsx48
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx67
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap11
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap73
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap68
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/AlertSuccessIcon.tsx40
-rw-r--r--server/sonar-web/yarn.lock22
35 files changed, 1363 insertions, 26 deletions
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index 56658e361e8..ac465a81a0b 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -18,6 +18,7 @@
"d3-shape": "1.2.0",
"date-fns": "1.29.0",
"escape-html": "1.0.3",
+ "formik": "0.11.7",
"handlebars": "2.0.0",
"history": "3.3.0",
"intl-relativeformat": "2.1.0",
diff --git a/server/sonar-web/src/main/js/api/webhooks.ts b/server/sonar-web/src/main/js/api/webhooks.ts
index 547702464b1..f23cb2f079b 100644
--- a/server/sonar-web/src/main/js/api/webhooks.ts
+++ b/server/sonar-web/src/main/js/api/webhooks.ts
@@ -17,13 +17,34 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { getJSON } from '../helpers/request';
+import { getJSON, post, postJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
import { Webhook } from '../app/types';
+export function createWebhook(data: {
+ name: string;
+ organization: string | undefined;
+ project?: string;
+ url: string;
+}): Promise<{ webhook: Webhook }> {
+ return postJSON('/api/webhooks/create', data).catch(throwGlobalError);
+}
+
+export function deleteWebhook(data: { webhook: string }): Promise<void | Response> {
+ return post('/api/webhooks/delete', data).catch(throwGlobalError);
+}
+
export function searchWebhooks(data: {
organization: string | undefined;
project?: string;
}): Promise<{ webhooks: Webhook[] }> {
- return getJSON('/api/webhooks/search', data).catch(throwGlobalError);
+ return getJSON('/api/webhooks/list', data).catch(throwGlobalError);
+}
+
+export function updateWebhook(data: {
+ webhook: string;
+ name: string;
+ url: string;
+}): Promise<void | Response> {
+ return post('/api/webhooks/update', data).catch(throwGlobalError);
}
diff --git a/server/sonar-web/src/main/js/app/styles/components/modals.css b/server/sonar-web/src/main/js/app/styles/components/modals.css
index 3bf2f396559..dbe8a64cb98 100644
--- a/server/sonar-web/src/main/js/app/styles/components/modals.css
+++ b/server/sonar-web/src/main/js/app/styles/components/modals.css
@@ -121,19 +121,24 @@ ul.modal-head-metadata li {
height: auto;
}
-.modal-field {
+.modal-field,
+.modal-large-field,
+.modal-validation-field {
clear: both;
display: block;
padding: 5px 0 5px 130px;
}
.modal-large-field {
- clear: both;
- display: block;
padding: 20px 40px;
}
-.modal-field label {
+.modal-validation-field {
+ padding: 3px 0 3px 130px;
+}
+
+.modal-field label,
+.modal-validation-field label {
position: relative;
left: -140px;
display: block;
@@ -149,7 +154,8 @@ ul.modal-head-metadata li {
text-overflow: ellipsis;
}
-.modal-field label.simple-label {
+.modal-field label.simple-label,
+.modal-validation-field label.simple-label {
display: inline-block;
vertical-align: middle;
float: none;
@@ -215,6 +221,29 @@ ul.modal-head-metadata li {
width: 100%;
}
+.modal-validation-field input,
+.modal-validation-field textarea,
+.modal-validation-field .Select {
+ margin-right: 5px;
+ margin-bottom: 2px;
+ width: 250px;
+}
+
+.modal-validation-field input:not(.has-error),
+.modal-validation-field .Select:not(.has-error) {
+ margin-bottom: 18px;
+}
+
+.modal-validation-field .has-error,
+.modal-validation-field .has-error > .Select-control {
+ border-color: var(--red);
+}
+
+.modal-validation-field .is-valid,
+.modal-validation-field .is-valid > .Select-control {
+ border-color: var(--green);
+}
+
.modal-field-description {
padding-bottom: 4px;
line-height: 1.4;
@@ -224,6 +253,10 @@ ul.modal-head-metadata li {
text-overflow: ellipsis;
}
+.modal-validation-field .modal-field-description {
+ margin-top: 2px;
+}
+
.modal-foot {
line-height: var(--controlHeight);
padding: 10px;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
index e1d559ef307..d48a3690400 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
@@ -19,9 +19,10 @@
*/
import * as React from 'react';
import { Helmet } from 'react-helmet';
+import PageActions from './PageActions';
import PageHeader from './PageHeader';
import WebhooksList from './WebhooksList';
-import { searchWebhooks } from '../../../api/webhooks';
+import { createWebhook, deleteWebhook, searchWebhooks, updateWebhook } from '../../../api/webhooks';
import { LightComponent, Organization, Webhook } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
@@ -48,12 +49,8 @@ export default class App extends React.PureComponent<Props, State> {
this.mounted = false;
}
- fetchWebhooks = ({ organization, component } = this.props) => {
- this.setState({ loading: true });
- searchWebhooks({
- organization: organization && organization.key,
- project: component && component.key
- }).then(
+ fetchWebhooks = () => {
+ return searchWebhooks(this.getScopeParams()).then(
({ webhooks }) => {
if (this.mounted) {
this.setState({ loading: false, webhooks });
@@ -67,15 +64,65 @@ export default class App extends React.PureComponent<Props, State> {
);
};
+ getScopeParams = ({ organization, component } = this.props) => {
+ return { organization: organization && organization.key, project: component && component.key };
+ };
+
+ handleCreate = (data: { name: string; url: string }) => {
+ return createWebhook({
+ ...data,
+ ...this.getScopeParams()
+ }).then(({ webhook }) => {
+ if (this.mounted) {
+ this.setState(({ webhooks }) => ({ webhooks: [...webhooks, webhook] }));
+ }
+ });
+ };
+
+ handleDelete = (webhook: string) => {
+ return deleteWebhook({ webhook }).then(() => {
+ if (this.mounted) {
+ this.setState(({ webhooks }) => ({
+ webhooks: webhooks.filter(item => item.key !== webhook)
+ }));
+ }
+ });
+ };
+
+ handleUpdate = (data: { webhook: string; name: string; url: string }) => {
+ return updateWebhook(data).then(() => {
+ if (this.mounted) {
+ this.setState(({ webhooks }) => ({
+ webhooks: webhooks.map(
+ webhook => (webhook.key === data.webhook ? { ...webhook, ...data } : webhook)
+ )
+ }));
+ }
+ });
+ };
+
render() {
const { loading, webhooks } = this.state;
+
return (
<div className="page page-limited">
<Helmet title={translate('webhooks.page')} />
- <PageHeader loading={loading} />
+
+ <PageHeader loading={loading}>
+ <PageActions
+ loading={loading}
+ onCreate={this.handleCreate}
+ webhooksCount={webhooks.length}
+ />
+ </PageHeader>
+
{!loading && (
<div className="boxed-group boxed-group-inner">
- <WebhooksList webhooks={webhooks} />
+ <WebhooksList
+ onDelete={this.handleDelete}
+ onUpdate={this.handleUpdate}
+ webhooks={webhooks}
+ />
</div>
)}
</div>
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx
new file mode 100644
index 00000000000..91a12c2ba7c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { FormikProps } from 'formik';
+import ValidationModal from '../../../components/controls/ValidationModal';
+import InputValidationField from '../../../components/controls/InputValidationField';
+import { Webhook } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ onClose: () => void;
+ onDone: (data: { name: string; url: string }) => Promise<void>;
+ webhook?: Webhook;
+}
+
+interface Values {
+ name: string;
+ url: string;
+}
+
+export default class CreateWebhookForm extends React.PureComponent<Props> {
+ handleCancelClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onClose();
+ };
+
+ handleValidate = (data: Values) => {
+ const { name, url } = data;
+ const errors: { name?: string; url?: string } = {};
+ if (!name.trim()) {
+ errors.name = translate('webhooks.name.required');
+ }
+ if (!url.trim()) {
+ errors.url = translate('webhooks.url.required');
+ } else if (!url.startsWith('http://') && !url.startsWith('https://')) {
+ errors.url = translate('webhooks.url.bad_protocol');
+ } else if (url.indexOf(':', 6) > 0 && url.indexOf('@') <= 0) {
+ errors.url = translate('webhooks.url.bad_auth');
+ }
+ return errors;
+ };
+
+ render() {
+ const { webhook } = this.props;
+ const isUpdate = !!webhook;
+ const modalHeader = isUpdate ? translate('webhooks.update') : translate('webhooks.create');
+ const confirmButtonText = isUpdate ? translate('update_verb') : translate('create');
+ return (
+ <ValidationModal
+ confirmButtonText={confirmButtonText}
+ header={modalHeader}
+ initialValues={{
+ name: webhook ? webhook.name : '',
+ url: webhook ? webhook.url : ''
+ }}
+ isInitialValid={isUpdate}
+ onClose={this.props.onClose}
+ onSubmit={this.props.onDone}
+ validate={this.handleValidate}>
+ {({
+ dirty,
+ errors,
+ handleBlur,
+ handleChange,
+ isSubmitting,
+ touched,
+ values
+ }: FormikProps<Values>) => (
+ <>
+ <InputValidationField
+ autoFocus={true}
+ dirty={dirty}
+ disabled={isSubmitting}
+ error={errors.name}
+ id="webhook-name"
+ label={
+ <label htmlFor="webhook-name">
+ {translate('webhooks.name')}
+ <em className="mandatory">*</em>
+ </label>
+ }
+ name="name"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ touched={touched.name}
+ type="text"
+ value={values.name}
+ />
+ <InputValidationField
+ description={translate('webhooks.url.description')}
+ disabled={isSubmitting}
+ dirty={dirty}
+ error={errors.url}
+ id="webhook-url"
+ label={
+ <label htmlFor="webhook-url">
+ {translate('webhooks.url')}
+ <em className="mandatory">*</em>
+ </label>
+ }
+ name="url"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ touched={touched.url}
+ type="text"
+ value={values.url}
+ />
+ </>
+ )}
+ </ValidationModal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx
new file mode 100644
index 00000000000..92df63c688b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import CreateWebhookForm from './CreateWebhookForm';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+ loading: boolean;
+ onCreate: (data: { name: string; url: string }) => Promise<void>;
+ webhooksCount: number;
+}
+
+interface State {
+ openCreate: boolean;
+}
+
+const WEBHOOKS_LIMIT = 10;
+
+export default class PageActions extends React.PureComponent<Props, State> {
+ mounted: boolean = false;
+ state: State = { openCreate: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleCreateClose = () => {
+ if (this.mounted) {
+ this.setState({ openCreate: false });
+ }
+ };
+
+ handleCreateOpen = () => {
+ this.setState({ openCreate: true });
+ };
+
+ renderCreate = () => {
+ if (this.props.webhooksCount >= WEBHOOKS_LIMIT) {
+ return (
+ <Tooltip
+ overlay={translateWithParameters('webhooks.maximum_reached', WEBHOOKS_LIMIT)}
+ placement="left">
+ <button className="js-webhook-create disabled">{translate('create')}</button>
+ </Tooltip>
+ );
+ }
+
+ return (
+ <>
+ <button className="js-webhook-create" onClick={this.handleCreateOpen}>
+ {translate('create')}
+ </button>
+ {this.state.openCreate && (
+ <CreateWebhookForm onClose={this.handleCreateClose} onDone={this.props.onCreate} />
+ )}
+ </>
+ );
+ };
+
+ render() {
+ if (this.props.loading) {
+ return null;
+ }
+
+ return <div className="page-actions">{this.renderCreate()}</div>;
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx
index 229b835e5e9..2a23425df06 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx
@@ -22,15 +22,18 @@ import { FormattedMessage } from 'react-intl';
import { translate } from '../../../helpers/l10n';
interface Props {
+ children?: React.ReactNode;
loading: boolean;
}
-export default function PageHeader({ loading }: Props) {
+export default function PageHeader({ children, loading }: Props) {
return (
<header className="page-header">
<h1 className="page-title">{translate('webhooks.page')}</h1>
{loading && <i className="spinner" />}
+ {children}
+
<p className="page-description">
<FormattedMessage
defaultMessage={translate('webhooks.description')}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
new file mode 100644
index 00000000000..db7c401f832
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
@@ -0,0 +1,105 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import CreateWebhookForm from './CreateWebhookForm';
+import ActionsDropdown, {
+ ActionsDropdownItem,
+ ActionsDropdownDivider
+} from '../../../components/controls/ActionsDropdown';
+import ConfirmButton from '../../../components/controls/ConfirmButton';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { Webhook } from '../../../app/types';
+
+interface Props {
+ onDelete: (webhook: string) => Promise<void>;
+ onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>;
+ webhook: Webhook;
+}
+
+interface State {
+ updating: boolean;
+}
+
+export default class WebhookActions extends React.PureComponent<Props, State> {
+ mounted: boolean = false;
+ state: State = { updating: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleDelete = () => {
+ return this.props.onDelete(this.props.webhook.key);
+ };
+
+ handleUpdate = (data: { name: string; url: string }) => {
+ return this.props.onUpdate({ ...data, webhook: this.props.webhook.key });
+ };
+
+ handleUpdateClick = () => {
+ this.setState({ updating: true });
+ };
+
+ handleUpdatingStop = () => {
+ this.setState({ updating: false });
+ };
+
+ render() {
+ const { webhook } = this.props;
+
+ return (
+ <>
+ <ActionsDropdown className="big-spacer-left">
+ <ActionsDropdownItem className="js-webhook-update" onClick={this.handleUpdateClick}>
+ {translate('update_verb')}
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ConfirmButton
+ confirmButtonText={translate('delete')}
+ isDestructive={true}
+ modalBody={translateWithParameters('webhooks.delete.confirm', webhook.name)}
+ modalHeader={translate('webhooks.delete')}
+ onConfirm={this.handleDelete}>
+ {({ onClick }) => (
+ <ActionsDropdownItem
+ className="js-webhook-delete"
+ destructive={true}
+ onClick={onClick}>
+ {translate('delete')}
+ </ActionsDropdownItem>
+ )}
+ </ConfirmButton>
+ </ActionsDropdown>
+
+ {this.state.updating && (
+ <CreateWebhookForm
+ onClose={this.handleUpdatingStop}
+ onDone={this.handleUpdate}
+ webhook={webhook}
+ />
+ )}
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx
index f156900d581..b51ceb085af 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx
@@ -18,17 +18,23 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import WebhookActions from './WebhookActions';
import { Webhook } from '../../../app/types';
interface Props {
+ onDelete: (webhook: string) => Promise<void>;
+ onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>;
webhook: Webhook;
}
-export default function WebhookItem({ webhook }: Props) {
+export default function WebhookItem({ onDelete, onUpdate, webhook }: Props) {
return (
<tr>
<td>{webhook.name}</td>
<td>{webhook.url}</td>
+ <td className="thin nowrap text-right">
+ <WebhookActions onDelete={onDelete} onUpdate={onUpdate} webhook={webhook} />
+ </td>
</tr>
);
}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx
index 600fad4e8d3..6bfaea6527b 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx
@@ -23,6 +23,8 @@ import { Webhook } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
interface Props {
+ onDelete: (webhook: string) => Promise<void>;
+ onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>;
webhooks: Webhook[];
}
@@ -32,6 +34,7 @@ export default class WebhooksList extends React.PureComponent<Props> {
<tr>
<th>{translate('name')}</th>
<th>{translate('webhooks.url')}</th>
+ <th />
</tr>
</thead>
);
@@ -45,7 +48,14 @@ export default class WebhooksList extends React.PureComponent<Props> {
<table className="data zebra">
{this.renderHeader()}
<tbody>
- {webhooks.map(webhook => <WebhookItem key={webhook.key} webhook={webhook} />)}
+ {webhooks.map(webhook => (
+ <WebhookItem
+ key={webhook.key}
+ onDelete={this.props.onDelete}
+ onUpdate={this.props.onUpdate}
+ webhook={webhook}
+ />
+ ))}
</tbody>
</table>
);
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx
new file mode 100644
index 00000000000..a5e20a63282
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import CreateWebhookForm from '../CreateWebhookForm';
+
+const webhook = { key: '1', name: 'foo', 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();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <CreateWebhookForm onClose={jest.fn()} onDone={jest.fn(() => Promise.resolve())} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageActions-test.tsx
new file mode 100644
index 00000000000..f50a9b062a4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageActions-test.tsx
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import PageActions from '../PageActions';
+import { click } from '../../../../helpers/testUtils';
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should not render', () => {
+ expect(getWrapper({ loading: true }).type()).toBeNull();
+});
+
+it('should not allow to create a new webhook', () => {
+ expect(getWrapper({ webhooksCount: 10 })).toMatchSnapshot();
+});
+
+it('should display the create form', () => {
+ const onCreate = jest.fn();
+ const wrapper = getWrapper({ onCreate });
+ click(wrapper.find('.js-webhook-create'));
+ expect(wrapper.find('CreateWebhookForm').exists()).toBeTruthy();
+ wrapper.find('CreateWebhookForm').prop<Function>('onDone')({
+ name: 'foo',
+ url: 'http://foo.bar'
+ });
+ expect(onCreate).lastCalledWith({ name: 'foo', url: 'http://foo.bar' });
+});
+
+function getWrapper(props = {}) {
+ return shallow(<PageActions onCreate={jest.fn()} loading={false} webhooksCount={5} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx
index 148d1570cd4..055228c98a2 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx
@@ -22,5 +22,11 @@ import { shallow } from 'enzyme';
import PageHeader from '../PageHeader';
it('should render correctly', () => {
- expect(shallow(<PageHeader loading={true} />)).toMatchSnapshot();
+ expect(
+ shallow(
+ <PageHeader loading={true}>
+ <div />
+ </PageHeader>
+ )
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx
new file mode 100644
index 00000000000..51aa702e132
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import WebhookActions from '../WebhookActions';
+import { click } from '../../../../helpers/testUtils';
+
+const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' };
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+it('should display the update webhook form', () => {
+ const onUpdate = jest.fn(() => Promise.resolve());
+ const wrapper = getWrapper({ onUpdate });
+ click(wrapper.find('.js-webhook-update'));
+ expect(wrapper.find('CreateWebhookForm').exists()).toBeTruthy();
+ wrapper.find('CreateWebhookForm').prop<Function>('onDone')({
+ name: webhook.name,
+ url: webhook.url
+ });
+ expect(onUpdate).lastCalledWith({ webhook: webhook.key, name: webhook.name, url: webhook.url });
+});
+
+it('should display the delete webhook form', () => {
+ const onDelete = jest.fn(() => Promise.resolve());
+ const wrapper = getWrapper({ onDelete });
+ click(
+ wrapper
+ .find('ConfirmButton')
+ .dive()
+ .find('.js-webhook-delete')
+ );
+ expect(wrapper.find('ConfirmButton').exists()).toBeTruthy();
+ wrapper.find('ConfirmButton').prop<Function>('onConfirm')();
+ expect(onDelete).lastCalledWith(webhook.key);
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <WebhookActions
+ onDelete={jest.fn(() => Promise.resolve())}
+ onUpdate={jest.fn(() => Promise.resolve())}
+ webhook={webhook}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx
index ab0a1a5d0e4..9322cf58e0a 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx
@@ -24,5 +24,13 @@ import WebhookItem from '../WebhookItem';
const webhook = { key: '1', name: 'my webhook', url: 'http://webhook.target' };
it('should render correctly', () => {
- expect(shallow(<WebhookItem webhook={webhook} />)).toMatchSnapshot();
+ expect(
+ shallow(
+ <WebhookItem
+ onDelete={jest.fn(() => Promise.resolve())}
+ onUpdate={jest.fn(() => Promise.resolve())}
+ webhook={webhook}
+ />
+ )
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx
index e0337cc8f5c..531eaeded8f 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx
@@ -27,9 +27,20 @@ const webhooks = [
];
it('should correctly render empty webhook list', () => {
- expect(shallow(<WebhooksList webhooks={[]} />)).toMatchSnapshot();
+ expect(getWrapper({ webhooks: [] })).toMatchSnapshot();
});
it('should correctly render the webhooks', () => {
- expect(shallow(<WebhooksList webhooks={webhooks} />)).toMatchSnapshot();
+ expect(getWrapper()).toMatchSnapshot();
});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <WebhooksList
+ onDelete={jest.fn(() => Promise.resolve())}
+ onUpdate={jest.fn(() => Promise.resolve())}
+ webhooks={webhooks}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
index 717e2e76bb0..3490f184a15 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
@@ -11,7 +11,13 @@ exports[`should be in loading status 1`] = `
/>
<PageHeader
loading={true}
- />
+ >
+ <PageActions
+ loading={true}
+ onCreate={[Function]}
+ webhooksCount={0}
+ />
+ </PageHeader>
</div>
`;
@@ -26,11 +32,19 @@ exports[`should fetch webhooks and display them 1`] = `
/>
<PageHeader
loading={false}
- />
+ >
+ <PageActions
+ loading={false}
+ onCreate={[Function]}
+ webhooksCount={0}
+ />
+ </PageHeader>
<div
className="boxed-group boxed-group-inner"
>
<WebhooksList
+ onDelete={[Function]}
+ onUpdate={[Function]}
webhooks={Array []}
/>
</div>
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap
new file mode 100644
index 00000000000..8ad194ad454
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly when creating a new webhook 1`] = `
+<ValidationModal
+ confirmButtonText="create"
+ header="webhooks.create"
+ initialValues={
+ Object {
+ "name": "",
+ "url": "",
+ }
+ }
+ isInitialValid={false}
+ onClose={[MockFunction]}
+ onSubmit={[MockFunction]}
+ validate={[Function]}
+/>
+`;
+
+exports[`should render correctly when updating a webhook 1`] = `
+<ValidationModal
+ confirmButtonText="update_verb"
+ header="webhooks.update"
+ initialValues={
+ Object {
+ "name": "foo",
+ "url": "http://foo.bar",
+ }
+ }
+ isInitialValid={true}
+ onClose={[MockFunction]}
+ onSubmit={[MockFunction]}
+ validate={[Function]}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap
new file mode 100644
index 00000000000..abbb9ca3fb9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not allow to create a new webhook 1`] = `
+<div
+ className="page-actions"
+>
+ <Tooltip
+ overlay="webhooks.maximum_reached.10"
+ placement="left"
+ >
+ <button
+ className="js-webhook-create disabled"
+ >
+ create
+ </button>
+ </Tooltip>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ className="page-actions"
+>
+ <React.Fragment>
+ <button
+ className="js-webhook-create"
+ onClick={[Function]}
+ >
+ create
+ </button>
+ </React.Fragment>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
index e40da7ba2d3..e44b8019fca 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
@@ -12,6 +12,7 @@ exports[`should render correctly 1`] = `
<i
className="spinner"
/>
+ <div />
<p
className="page-description"
>
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap
new file mode 100644
index 00000000000..a82a5f3ed35
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<React.Fragment>
+ <ActionsDropdown
+ className="big-spacer-left"
+ >
+ <ActionsDropdownItem
+ className="js-webhook-update"
+ onClick={[Function]}
+ >
+ update_verb
+ </ActionsDropdownItem>
+ <ActionsDropdownDivider />
+ <ConfirmButton
+ confirmButtonText="delete"
+ isDestructive={true}
+ modalBody="webhooks.delete.confirm.foo"
+ modalHeader="webhooks.delete"
+ onConfirm={[Function]}
+ />
+ </ActionsDropdown>
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap
index b6968c76bed..7f45d646e0a 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap
@@ -8,5 +8,20 @@ exports[`should render correctly 1`] = `
<td>
http://webhook.target
</td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <WebhookActions
+ onDelete={[MockFunction]}
+ onUpdate={[MockFunction]}
+ webhook={
+ Object {
+ "key": "1",
+ "name": "my webhook",
+ "url": "http://webhook.target",
+ }
+ }
+ />
+ </td>
</tr>
`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap
index 01bcb34685a..ddf82de6e99 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap
@@ -18,11 +18,14 @@ exports[`should correctly render the webhooks 1`] = `
<th>
webhooks.url
</th>
+ <th />
</tr>
</thead>
<tbody>
<WebhookItem
key="1"
+ onDelete={[MockFunction]}
+ onUpdate={[MockFunction]}
webhook={
Object {
"key": "1",
@@ -33,6 +36,8 @@ exports[`should correctly render the webhooks 1`] = `
/>
<WebhookItem
key="2"
+ onDelete={[MockFunction]}
+ onUpdate={[MockFunction]}
webhook={
Object {
"key": "2",
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
index ab766a9eed6..354e11add9c 100644
--- a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
@@ -110,7 +110,9 @@ export default class ConfirmButton extends React.PureComponent<Props, State> {
disabled={submitting}>
{confirmButtonText}
</SubmitButton>
- <ResetButtonLink onClick={onCloseClick}>{translate('cancel')}</ResetButtonLink>
+ <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
+ {translate('cancel')}
+ </ResetButtonLink>
</footer>
</form>
)}
diff --git a/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx b/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx
new file mode 100644
index 00000000000..aae2cde6025
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import ModalValidationField from './ModalValidationField';
+
+interface Props {
+ autoFocus?: boolean;
+ className?: string;
+ description?: string;
+ dirty: boolean;
+ disabled: boolean;
+ error: string | undefined;
+ id?: string;
+ label?: React.ReactNode;
+ name: string;
+ onBlur: (event: React.FocusEvent<any>) => void;
+ onChange: (event: React.ChangeEvent<any>) => void;
+ placeholder?: string;
+ touched: boolean;
+ type?: string;
+ value: string;
+}
+
+export default function InputValidationField({ className, ...props }: Props) {
+ const { description, dirty, error, label, touched, ...inputProps } = props;
+ const modalValidationProps = { description, dirty, error, label, touched };
+ return (
+ <ModalValidationField {...modalValidationProps}>
+ {({ className: validationClassName }) => (
+ <input className={classNames(className, validationClassName)} {...inputProps} />
+ )}
+ </ModalValidationField>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx b/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx
new file mode 100644
index 00000000000..503b58952e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import AlertErrorIcon from '../icons-components/AlertErrorIcon';
+import AlertSuccessIcon from '../icons-components/AlertSuccessIcon';
+
+interface Props {
+ children: (props: { className?: string }) => React.ReactNode;
+ description?: string;
+ dirty: boolean;
+ error: string | undefined;
+ label?: React.ReactNode;
+ touched: boolean;
+}
+
+export default function ModalValidationField(props: Props) {
+ const { description, dirty, error } = props;
+
+ const isValid = dirty && props.touched && error === undefined;
+ const showError = dirty && props.touched && error !== undefined;
+ return (
+ <div className="modal-validation-field">
+ {props.label}
+ {props.children({ className: classNames({ 'has-error': showError, 'is-valid': isValid }) })}
+ {showError && <AlertErrorIcon className="little-spacer-top" />}
+ {isValid && <AlertSuccessIcon className="little-spacer-top" />}
+ {showError && <p className="text-danger">{error}</p>}
+ {description && <div className="modal-field-description">{description}</div>}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx b/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx
new file mode 100644
index 00000000000..d52a0b31af8
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { withFormik, Form, FormikActions, FormikProps } from 'formik';
+import Modal from './Modal';
+import DeferredSpinner from '../common/DeferredSpinner';
+import { translate } from '../../helpers/l10n';
+
+interface InnerFormProps<Values> {
+ children: (props: FormikProps<Values>) => React.ReactNode;
+ confirmButtonText: string;
+ header: string;
+ initialValues: Values;
+}
+
+interface Props<Values> extends InnerFormProps<Values> {
+ isInitialValid?: boolean;
+ onClose: () => void;
+ validate: (data: Values) => void | object | Promise<object>;
+ onSubmit: (data: Values) => void | Promise<void>;
+}
+
+export default class ValidationModal<Values> extends React.PureComponent<Props<Values>> {
+ handleCancelClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onClose();
+ };
+
+ handleSubmit = (data: Values, { setSubmitting }: FormikActions<Values>) => {
+ const result = this.props.onSubmit(data);
+ if (result) {
+ result.then(
+ () => {
+ setSubmitting(false);
+ this.props.onClose();
+ },
+ () => {
+ setSubmitting(false);
+ }
+ );
+ } else {
+ setSubmitting(false);
+ this.props.onClose();
+ }
+ };
+
+ render() {
+ const { header } = this.props;
+
+ const InnerForm = withFormik<InnerFormProps<Values>, Values>({
+ handleSubmit: this.handleSubmit,
+ isInitialValid: this.props.isInitialValid,
+ mapPropsToValues: props => props.initialValues,
+ validate: this.props.validate
+ })(props => (
+ <Form>
+ <div className="modal-head">
+ <h2>{props.header}</h2>
+ </div>
+
+ <div className="modal-body">{props.children(props)}</div>
+
+ <footer className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={props.isSubmitting} />
+ <button disabled={props.isSubmitting || !props.isValid || !props.dirty} type="submit">
+ {props.confirmButtonText}
+ </button>
+ <button
+ className="button-link"
+ disabled={props.isSubmitting}
+ onClick={this.handleCancelClick}
+ type="reset">
+ {translate('cancel')}
+ </button>
+ </footer>
+ </Form>
+ ));
+
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <InnerForm
+ confirmButtonText={this.props.confirmButtonText}
+ header={header}
+ initialValues={this.props.initialValues}>
+ {this.props.children}
+ </InnerForm>
+ </Modal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/InputValidationField-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/InputValidationField-test.tsx
new file mode 100644
index 00000000000..e0864a1b8fc
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/InputValidationField-test.tsx
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import InputValidationField from '../InputValidationField';
+
+it('should render correctly', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <InputValidationField
+ description="Field description"
+ dirty={true}
+ disabled={false}
+ error="Bad formatting"
+ label="Foo field"
+ name="field"
+ onBlur={jest.fn()}
+ onChange={jest.fn()}
+ touched={true}
+ value="foo"
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ModalValidationField-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ModalValidationField-test.tsx
new file mode 100644
index 00000000000..ae9a2f74118
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/ModalValidationField-test.tsx
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ModalValidationField from '../ModalValidationField';
+
+it('should display the field without any error/validation', () => {
+ expect(getWrapper({ description: 'Describe Foo.', touched: false })).toMatchSnapshot();
+ expect(getWrapper({ dirty: false })).toMatchSnapshot();
+});
+
+it('should display the field as valid', () => {
+ expect(getWrapper({ error: undefined })).toMatchSnapshot();
+});
+
+it('should display the field with an error', () => {
+ expect(getWrapper()).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+ return shallow(
+ <ModalValidationField
+ dirty={true}
+ error="Is required"
+ label={<label>Foo</label>}
+ touched={true}
+ {...props}>
+ {({ className }) => <input className={className} type="text" />}
+ </ModalValidationField>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx
new file mode 100644
index 00000000000..f3048542aa1
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/ValidationModal-test.tsx
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { FormikProps } from 'formik';
+import ValidationModal from '../ValidationModal';
+
+it('should render correctly', () => {
+ const { wrapper, inner } = getWrapper();
+ expect(wrapper).toMatchSnapshot();
+ expect(inner).toMatchSnapshot();
+});
+
+interface Values {
+ field: string;
+}
+
+function getWrapper(props = {}) {
+ const wrapper = shallow(
+ <ValidationModal
+ confirmButtonText="confirm"
+ header="title"
+ initialValues={{ field: 'foo' }}
+ isInitialValid={true}
+ onClose={jest.fn()}
+ validate={(values: Values) => ({ field: values.field.length < 2 && 'Too small' })}
+ onSubmit={jest.fn(() => Promise.resolve())}
+ {...props}>
+ {(props: FormikProps<Values>) => (
+ <form onSubmit={props.handleSubmit}>
+ <input
+ onChange={props.handleChange}
+ onBlur={props.handleBlur}
+ name="field"
+ type="text"
+ value={props.values.field}
+ />
+ </form>
+ )}
+ </ValidationModal>
+ );
+ return {
+ wrapper,
+ inner: wrapper
+ .childAt(0)
+ .dive()
+ .dive()
+ .dive()
+ };
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap
new file mode 100644
index 00000000000..8afa4d9aa5a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/InputValidationField-test.tsx.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<ModalValidationField
+ description="Field description"
+ dirty={true}
+ error="Bad formatting"
+ label="Foo field"
+ touched={true}
+/>
+`;
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap
new file mode 100644
index 00000000000..dc901ce06f2
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ModalValidationField-test.tsx.snap
@@ -0,0 +1,73 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should display the field as valid 1`] = `
+<div
+ className="modal-validation-field"
+>
+ <label>
+ Foo
+ </label>
+ <input
+ className="is-valid"
+ type="text"
+ />
+ <AlertSuccessIcon
+ className="little-spacer-top"
+ />
+</div>
+`;
+
+exports[`should display the field with an error 1`] = `
+<div
+ className="modal-validation-field"
+>
+ <label>
+ Foo
+ </label>
+ <input
+ className="has-error"
+ type="text"
+ />
+ <AlertErrorIcon
+ className="little-spacer-top"
+ />
+ <p
+ className="text-danger"
+ >
+ Is required
+ </p>
+</div>
+`;
+
+exports[`should display the field without any error/validation 1`] = `
+<div
+ className="modal-validation-field"
+>
+ <label>
+ Foo
+ </label>
+ <input
+ className=""
+ type="text"
+ />
+ <div
+ className="modal-field-description"
+ >
+ Describe Foo.
+ </div>
+</div>
+`;
+
+exports[`should display the field without any error/validation 2`] = `
+<div
+ className="modal-validation-field"
+>
+ <label>
+ Foo
+ </label>
+ <input
+ className=""
+ type="text"
+ />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap
new file mode 100644
index 00000000000..898438ec774
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ValidationModal-test.tsx.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Modal
+ contentLabel="title"
+ onRequestClose={[MockFunction]}
+>
+ <C
+ confirmButtonText="confirm"
+ header="title"
+ initialValues={
+ Object {
+ "field": "foo",
+ }
+ }
+ />
+</Modal>
+`;
+
+exports[`should render correctly 2`] = `
+<Form>
+ <div
+ className="modal-head"
+ >
+ <h2>
+ title
+ </h2>
+ </div>
+ <div
+ className="modal-body"
+ >
+ <form
+ onSubmit={[Function]}
+ >
+ <input
+ name="field"
+ onBlur={[Function]}
+ onChange={[Function]}
+ type="text"
+ value="foo"
+ />
+ </form>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <button
+ disabled={true}
+ type="submit"
+ >
+ confirm
+ </button>
+ <button
+ className="button-link"
+ disabled={false}
+ onClick={[Function]}
+ type="reset"
+ >
+ cancel
+ </button>
+ </footer>
+</Form>
+`;
diff --git a/server/sonar-web/src/main/js/components/icons-components/AlertSuccessIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/AlertSuccessIcon.tsx
new file mode 100644
index 00000000000..432461ad528
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/AlertSuccessIcon.tsx
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as theme from '../../app/theme';
+import { IconProps } from './types';
+
+export default function AlertSuccessIcon({ className, fill = theme.green, size = 16 }: IconProps) {
+ return (
+ <svg
+ className={className}
+ width={size}
+ height={size}
+ viewBox="0 0 16 16"
+ version="1.1"
+ xmlnsXlink="http://www.w3.org/1999/xlink"
+ xmlSpace="preserve">
+ <path
+ style={{ fill }}
+ d="M12.607 6.554q0-0.25-0.161-0.411l-0.813-0.804q-0.17-0.17-0.402-0.17t-0.402 0.17l-3.643 3.634-2.018-2.018q-0.17-0.17-0.402-0.17t-0.402 0.17l-0.813 0.804q-0.161 0.161-0.161 0.411 0 0.241 0.161 0.402l3.232 3.232q0.17 0.17 0.402 0.17 0.241 0 0.411-0.17l4.848-4.848q0.161-0.161 0.161-0.402zM14.857 8q0 1.866-0.92 3.442t-2.496 2.496-3.442 0.92-3.442-0.92-2.496-2.496-0.92-3.442 0.92-3.442 2.496-2.496 3.442-0.92 3.442 0.92 2.496 2.496 0.92 3.442z"
+ />
+ </svg>
+ );
+}
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index 7be3c394b57..4f6610a2e05 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -3173,6 +3173,16 @@ form-data@~2.3.1:
combined-stream "^1.0.5"
mime-types "^2.1.12"
+formik@0.11.7:
+ version "0.11.7"
+ resolved "https://registry.yarnpkg.com/formik/-/formik-0.11.7.tgz#7b2c66a5546c793dfb07b39b965aef69dcd39326"
+ dependencies:
+ lodash.clonedeep "^4.5.0"
+ lodash.isequal "4.5.0"
+ lodash.topath "4.5.2"
+ prop-types "^15.5.10"
+ warning "^3.0.0"
+
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -4807,6 +4817,10 @@ lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+lodash.clonedeep@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+
lodash.cond@^4.3.0:
version "4.5.2"
resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
@@ -4823,6 +4837,10 @@ lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+lodash.isequal@4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+
lodash.keys@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -4839,6 +4857,10 @@ lodash.sortby@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+lodash.topath@4.5.2:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/lodash.topath/-/lodash.topath-4.5.2.tgz#3616351f3bba61994a0931989660bd03254fd009"
+
lodash.unescape@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"