aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-02-09 15:44:52 +0100
committerGuillaume Jambet <guillaume.jambet@gmail.com>2018-03-01 15:21:05 +0100
commit4bf292bac1d4dae9b8338c464e88e9dac6ca4b03 (patch)
treecd3b55c1a527196333e8cf7e9d6bf207679a0c18 /server/sonar-web/src
parent91fe807305e89ecd9df3b6f4f221540fd451659e (diff)
downloadsonarqube-4bf292bac1d4dae9b8338c464e88e9dac6ca4b03.tar.gz
sonarqube-4bf292bac1d4dae9b8338c464e88e9dac6ca4b03.zip
SONAR-10347 Add ability to browse webhook deliveries payloads
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/api/webhooks.ts21
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/App.tsx43
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx120
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx119
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx23
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx73
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx74
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap77
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap107
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap84
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap2
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap6
-rw-r--r--server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap16
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts55
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts14
23 files changed, 852 insertions, 82 deletions
diff --git a/server/sonar-web/src/main/js/api/webhooks.ts b/server/sonar-web/src/main/js/api/webhooks.ts
index f23cb2f079b..a51a01c0948 100644
--- a/server/sonar-web/src/main/js/api/webhooks.ts
+++ b/server/sonar-web/src/main/js/api/webhooks.ts
@@ -19,7 +19,7 @@
*/
import { getJSON, post, postJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
-import { Webhook } from '../app/types';
+import { Webhook, WebhookDelivery, Paging } from '../app/types';
export function createWebhook(data: {
name: string;
@@ -48,3 +48,22 @@ export function updateWebhook(data: {
}): Promise<void | Response> {
return post('/api/webhooks/update', data).catch(throwGlobalError);
}
+
+export function searchDeliveries(data: {
+ ceTaskId?: string;
+ componentKey?: string;
+ webhook?: string;
+ p?: number;
+ ps?: number;
+}): Promise<{
+ deliveries: WebhookDelivery[];
+ paging: Paging;
+}> {
+ return getJSON('/api/webhooks/deliveries', data).catch(throwGlobalError);
+}
+
+export function getDelivery(data: {
+ deliveryId: string;
+}): Promise<{ delivery: WebhookDelivery & { payload: string } }> {
+ return getJSON('/api/webhooks/delivery', data).catch(throwGlobalError);
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
index bb56f7751a8..7771dce09bb 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
@@ -42,7 +42,8 @@ const SETTINGS_URLS = [
'/project/history',
'background_tasks',
'/project/key',
- '/project/deletion'
+ '/project/deletion',
+ '/project/webhooks'
];
interface Props {
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
index cc2b9631ee8..338494e7604 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
@@ -36,7 +36,8 @@ const ADMIN_PATHS = [
'delete',
'permissions',
'permission_templates',
- 'projects_management'
+ 'projects_management',
+ 'webhooks'
];
export default function OrganizationNavigationAdministration({ location, organization }: Props) {
diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx
index 2c7b4f8b863..bbce6164647 100644
--- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx
+++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx
@@ -35,37 +35,37 @@ export default function OrganizationNavigationMenu({ location, organization }: P
return (
<NavBarTabs className="navbar-context-tabs">
<li>
- <Link to={`/organizations/${organization.key}/projects`} activeClassName="active">
+ <Link activeClassName="active" to={`/organizations/${organization.key}/projects`}>
{translate('projects.page')}
</Link>
</li>
<li>
<Link
+ activeClassName="active"
to={{
pathname: `/organizations/${organization.key}/issues`,
query: { resolved: 'false' }
- }}
- activeClassName="active">
+ }}>
{translate('issues.page')}
</Link>
</li>
<li>
- <Link to={`/organizations/${organization.key}/quality_profiles`} activeClassName="active">
+ <Link activeClassName="active" to={`/organizations/${organization.key}/quality_profiles`}>
{translate('quality_profiles.page')}
</Link>
</li>
<li>
- <Link to={`/organizations/${organization.key}/rules`} activeClassName="active">
+ <Link activeClassName="active" to={`/organizations/${organization.key}/rules`}>
{translate('coding_rules.page')}
</Link>
</li>
<li>
- <Link to={getQualityGatesUrl(organization.key)} activeClassName="active">
+ <Link activeClassName="active" to={getQualityGatesUrl(organization.key)}>
{translate('quality_gates.page')}
</Link>
</li>
<li>
- <Link to={`/organizations/${organization.key}/members`} activeClassName="active">
+ <Link activeClassName="active" to={`/organizations/${organization.key}/members`}>
{translate('organization.members.page')}
</Link>
</li>
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 d48a3690400..68a8f9d42bd 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
@@ -94,7 +94,10 @@ export default class App extends React.PureComponent<Props, State> {
if (this.mounted) {
this.setState(({ webhooks }) => ({
webhooks: webhooks.map(
- webhook => (webhook.key === data.webhook ? { ...webhook, ...data } : webhook)
+ webhook =>
+ webhook.key === data.webhook
+ ? { ...webhook, name: data.name, url: data.url }
+ : webhook
)
}));
}
@@ -105,27 +108,29 @@ export default class App extends React.PureComponent<Props, State> {
const { loading, webhooks } = this.state;
return (
- <div className="page page-limited">
+ <>
<Helmet title={translate('webhooks.page')} />
- <PageHeader loading={loading}>
- <PageActions
- loading={loading}
- onCreate={this.handleCreate}
- webhooksCount={webhooks.length}
- />
- </PageHeader>
-
- {!loading && (
- <div className="boxed-group boxed-group-inner">
- <WebhooksList
- onDelete={this.handleDelete}
- onUpdate={this.handleUpdate}
- webhooks={webhooks}
+ <div className="page page-limited">
+ <PageHeader loading={loading}>
+ <PageActions
+ loading={loading}
+ onCreate={this.handleCreate}
+ webhooksCount={webhooks.length}
/>
- </div>
- )}
- </div>
+ </PageHeader>
+
+ {!loading && (
+ <div className="boxed-group boxed-group-inner">
+ <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
index 91a12c2ba7c..6a76095990f 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx
@@ -22,6 +22,7 @@ import { FormikProps } from 'formik';
import ValidationModal from '../../../components/controls/ValidationModal';
import InputValidationField from '../../../components/controls/InputValidationField';
import { Webhook } from '../../../app/types';
+import { isUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';
interface Props {
@@ -52,8 +53,8 @@ export default class CreateWebhookForm extends React.PureComponent<Props> {
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');
+ } else if (!isUrl(url)) {
+ errors.url = translate('webhooks.url.bad_format');
}
return errors;
};
@@ -106,8 +107,8 @@ export default class CreateWebhookForm extends React.PureComponent<Props> {
/>
<InputValidationField
description={translate('webhooks.url.description')}
- disabled={isSubmitting}
dirty={dirty}
+ disabled={isSubmitting}
error={errors.url}
id="webhook-url"
label={
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx
new file mode 100644
index 00000000000..33c0439d38e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx
@@ -0,0 +1,120 @@
+/*
+ * 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 DeliveryItem from './DeliveryItem';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import ListFooter from '../../../components/controls/ListFooter';
+import Modal from '../../../components/controls/Modal';
+import { Webhook, WebhookDelivery, Paging } from '../../../app/types';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
+import { searchDeliveries } from '../../../api/webhooks';
+
+interface Props {
+ onClose: () => void;
+ webhook: Webhook;
+}
+
+interface State {
+ deliveries: WebhookDelivery[];
+ loading: boolean;
+ paging?: Paging;
+}
+
+const PAGE_SIZE = 10;
+
+export default class DeliveriesForm extends React.PureComponent<Props, State> {
+ mounted: boolean = false;
+ state: State = { deliveries: [], loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchDeliveries();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchDeliveries = ({ webhook } = this.props) => {
+ searchDeliveries({ webhook: webhook.key, ps: PAGE_SIZE }).then(({ deliveries, paging }) => {
+ if (this.mounted) {
+ this.setState({ deliveries, loading: false, paging });
+ }
+ }, this.stopLoading);
+ };
+
+ fetchMoreDeliveries = ({ webhook } = this.props) => {
+ const { paging } = this.state;
+ if (paging) {
+ this.setState({ loading: true });
+ searchDeliveries({ webhook: webhook.key, p: paging.pageIndex + 1, ps: PAGE_SIZE }).then(
+ ({ deliveries, paging }) => {
+ if (this.mounted) {
+ this.setState((state: State) => ({
+ deliveries: [...state.deliveries, ...deliveries],
+ loading: false,
+ paging
+ }));
+ }
+ },
+ this.stopLoading
+ );
+ }
+ };
+
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
+ render() {
+ const { webhook } = this.props;
+ const { deliveries, loading, paging } = this.state;
+ const header = translateWithParameters('webhooks.deliveries_for_x', webhook.name);
+
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <header className="modal-head">
+ <h2>{header}</h2>
+ </header>
+ <div className="modal-body modal-container">
+ {deliveries.map(delivery => <DeliveryItem delivery={delivery} key={delivery.id} />)}
+ <div className="text-center">
+ <DeferredSpinner loading={loading} />
+ </div>
+ {paging !== undefined && (
+ <ListFooter
+ count={deliveries.length}
+ loadMore={this.fetchMoreDeliveries}
+ ready={!loading}
+ total={paging.total}
+ />
+ )}
+ </div>
+ <footer className="modal-foot">
+ <button className="button-link js-modal-close" onClick={this.props.onClose} type="button">
+ {translate('close')}
+ </button>
+ </footer>
+ </Modal>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx
new file mode 100644
index 00000000000..61cf8bbbcd0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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 CheckIcon from '../../../components/icons-components/CheckIcon';
+import ClearIcon from '../../../components/icons-components/ClearIcon';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
+import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion';
+import CodeSnippet from '../../../components/common/CodeSnippet';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { getDelivery } from '../../../api/webhooks';
+import { formatMeasure } from '../../../helpers/measures';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
+import { WebhookDelivery } from '../../../app/types';
+
+interface Props {
+ delivery: WebhookDelivery;
+}
+
+interface State {
+ loading: boolean;
+ open: boolean;
+ payload?: string;
+}
+
+export default class DeliveryItem extends React.PureComponent<Props, State> {
+ mounted: boolean = false;
+ state: State = { loading: false, open: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchPayload = ({ delivery } = this.props) => {
+ this.setState({ loading: true });
+ return getDelivery({ deliveryId: delivery.id }).then(
+ ({ delivery }) => {
+ if (this.mounted) {
+ this.setState({ payload: delivery.payload, loading: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ formatPayload = (payload: string) => {
+ try {
+ return JSON.stringify(JSON.parse(payload), undefined, 2);
+ } catch (error) {
+ return payload;
+ }
+ };
+
+ handleClick = () => {
+ if (!this.state.payload) {
+ this.fetchPayload();
+ }
+ this.setState(({ open }) => ({ open: !open }));
+ };
+
+ render() {
+ const { delivery } = this.props;
+ const { loading, open, payload } = this.state;
+
+ return (
+ <BoxedGroupAccordion
+ onClick={this.handleClick}
+ open={open}
+ renderHeader={() =>
+ delivery.success ? (
+ <AlertSuccessIcon className="pull-right js-success" />
+ ) : (
+ <AlertErrorIcon className="pull-right js-error" />
+ )
+ }
+ title={<DateTimeFormatter date={delivery.at} />}>
+ <div className="big-spacer-left">
+ <p className="spacer-bottom">
+ {translateWithParameters('webhooks.delivery.response_x', delivery.httpStatus)}
+ </p>
+ <p className="spacer-bottom">
+ {translateWithParameters(
+ 'webhooks.delivery.duration_x',
+ formatMeasure(delivery.durationMs, 'MILLISEC')
+ )}
+ </p>
+ <p className="spacer-bottom">{translate('webhooks.delivery.payload')}</p>
+ <DeferredSpinner className="spacer-left spacer-top" loading={loading}>
+ {payload && <CodeSnippet noCopy={true} snippet={this.formatPayload(payload)} />}
+ </DeferredSpinner>
+ </div>
+ </BoxedGroupAccordion>
+ );
+ }
+}
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
index 92df63c688b..3968f8bdfbe 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx
@@ -62,14 +62,16 @@ export default class PageActions extends React.PureComponent<Props, State> {
<Tooltip
overlay={translateWithParameters('webhooks.maximum_reached', WEBHOOKS_LIMIT)}
placement="left">
- <button className="js-webhook-create disabled">{translate('create')}</button>
+ <button className="js-webhook-create disabled" type="button">
+ {translate('create')}
+ </button>
</Tooltip>
);
}
return (
<>
- <button className="js-webhook-create" onClick={this.handleCreateOpen}>
+ <button className="js-webhook-create" onClick={this.handleCreateOpen} type="button">
{translate('create')}
</button>
{this.state.openCreate && (
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
index db7c401f832..42351e01a1f 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import CreateWebhookForm from './CreateWebhookForm';
+import DeliveriesForm from './DeliveriesForm';
import ActionsDropdown, {
ActionsDropdownItem,
ActionsDropdownDivider
@@ -34,12 +35,13 @@ interface Props {
}
interface State {
+ deliveries: boolean;
updating: boolean;
}
export default class WebhookActions extends React.PureComponent<Props, State> {
mounted: boolean = false;
- state: State = { updating: false };
+ state: State = { deliveries: false, updating: false };
componentDidMount() {
this.mounted = true;
@@ -53,6 +55,14 @@ export default class WebhookActions extends React.PureComponent<Props, State> {
return this.props.onDelete(this.props.webhook.key);
};
+ handleDeliveriesClick = () => {
+ this.setState({ deliveries: true });
+ };
+
+ handleDeliveriesStop = () => {
+ this.setState({ deliveries: false });
+ };
+
handleUpdate = (data: { name: string; url: string }) => {
return this.props.onUpdate({ ...data, webhook: this.props.webhook.key });
};
@@ -67,13 +77,18 @@ export default class WebhookActions extends React.PureComponent<Props, State> {
render() {
const { webhook } = this.props;
-
+ // TODO Disable "Show deliveries" if there is no lastDelivery
return (
<>
<ActionsDropdown className="big-spacer-left">
<ActionsDropdownItem className="js-webhook-update" onClick={this.handleUpdateClick}>
{translate('update_verb')}
</ActionsDropdownItem>
+ <ActionsDropdownItem
+ className="js-webhook-deliveries"
+ onClick={this.handleDeliveriesClick}>
+ {translate('webhooks.deliveries.show')}
+ </ActionsDropdownItem>
<ActionsDropdownDivider />
<ConfirmButton
confirmButtonText={translate('delete')}
@@ -91,7 +106,9 @@ export default class WebhookActions extends React.PureComponent<Props, State> {
)}
</ConfirmButton>
</ActionsDropdown>
-
+ {this.state.deliveries && (
+ <DeliveriesForm onClose={this.handleDeliveriesStop} webhook={webhook} />
+ )}
{this.state.updating && (
<CreateWebhookForm
onClose={this.handleUpdatingStop}
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 6bfaea6527b..57f33ffe151 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
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { sortBy } from 'lodash';
import WebhookItem from './WebhookItem';
import { Webhook } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
@@ -48,7 +49,7 @@ export default class WebhooksList extends React.PureComponent<Props> {
<table className="data zebra">
{this.renderHeader()}
<tbody>
- {webhooks.map(webhook => (
+ {sortBy(webhooks, webhook => webhook.name.toLowerCase()).map(webhook => (
<WebhookItem
key={webhook.key}
onDelete={this.props.onDelete}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx
index 085def84f20..62d45726d81 100644
--- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx
@@ -20,18 +20,38 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import App from '../App';
-import { searchWebhooks } from '../../../../api/webhooks';
+import {
+ createWebhook,
+ deleteWebhook,
+ searchWebhooks,
+ updateWebhook
+} from '../../../../api/webhooks';
import { Visibility } from '../../../../app/types';
jest.mock('../../../../api/webhooks', () => ({
- searchWebhooks: jest.fn(() => Promise.resolve({ webhooks: [] }))
+ createWebhook: jest.fn(() =>
+ Promise.resolve({ webhook: { key: '3', name: 'baz', url: 'http://baz' } })
+ ),
+ deleteWebhook: jest.fn(() => Promise.resolve()),
+ searchWebhooks: jest.fn(() =>
+ Promise.resolve({
+ webhooks: [
+ { key: '1', name: 'foo', url: 'http://foo' },
+ { key: '2', name: 'bar', url: 'http://bar' }
+ ]
+ })
+ ),
+ updateWebhook: jest.fn(() => Promise.resolve())
}));
const organization = { key: 'foo', name: 'Foo', projectVisibility: Visibility.Private };
const component = { key: 'bar', organization: 'foo', qualifier: 'TRK' };
beforeEach(() => {
+ (createWebhook as jest.Mock<any>).mockClear();
+ (deleteWebhook as jest.Mock<any>).mockClear();
(searchWebhooks as jest.Mock<any>).mockClear();
+ (updateWebhook as jest.Mock<any>).mockClear();
});
it('should be in loading status', () => {
@@ -58,21 +78,21 @@ describe('should correctly fetch webhooks when', () => {
});
it('on project scope', async () => {
- shallow(<App organization={undefined} component={component} />);
+ shallow(<App component={component} organization={undefined} />);
await new Promise(setImmediate);
expect(searchWebhooks).toHaveBeenCalledWith({ project: component.key });
});
it('on organization scope', async () => {
- shallow(<App organization={organization} component={undefined} />);
+ shallow(<App component={undefined} organization={organization} />);
await new Promise(setImmediate);
expect(searchWebhooks).toHaveBeenCalledWith({ organization: organization.key });
});
it('on project scope within an organization', async () => {
- shallow(<App organization={organization} component={component} />);
+ shallow(<App component={component} organization={organization} />);
await new Promise(setImmediate);
expect(searchWebhooks).toHaveBeenCalledWith({
@@ -81,3 +101,46 @@ describe('should correctly fetch webhooks when', () => {
});
});
});
+
+it('should correctly handle webhook creation', async () => {
+ const webhook = { name: 'baz', url: 'http://baz' };
+ const wrapper = shallow(<App organization={organization} />);
+ (wrapper.instance() as App).handleCreate({ ...webhook });
+ expect(createWebhook).lastCalledWith({
+ ...webhook,
+ organization: organization.key,
+ project: undefined
+ });
+
+ await new Promise(setImmediate);
+ wrapper.update();
+ expect(wrapper.state('webhooks')).toEqual([
+ { key: '1', name: 'foo', url: 'http://foo' },
+ { key: '2', name: 'bar', url: 'http://bar' },
+ { key: '3', name: 'baz', url: 'http://baz' }
+ ]);
+});
+
+it('should correctly handle webhook deletion', async () => {
+ const wrapper = shallow(<App organization={undefined} />);
+ (wrapper.instance() as App).handleDelete('2');
+ expect(deleteWebhook).lastCalledWith({ webhook: '2' });
+
+ await new Promise(setImmediate);
+ wrapper.update();
+ expect(wrapper.state('webhooks')).toEqual([{ key: '1', name: 'foo', url: 'http://foo' }]);
+});
+
+it('should correctly handle webhook update', async () => {
+ const newValues = { webhook: '1', name: 'Cfoo', url: 'http://cfoo' };
+ const wrapper = shallow(<App organization={undefined} />);
+ (wrapper.instance() as App).handleUpdate(newValues);
+ expect(updateWebhook).lastCalledWith(newValues);
+
+ await new Promise(setImmediate);
+ wrapper.update();
+ expect(wrapper.state('webhooks')).toEqual([
+ { key: '1', name: 'Cfoo', url: 'http://cfoo' },
+ { key: '2', name: 'bar', url: 'http://bar' }
+ ]);
+});
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx
new file mode 100644
index 00000000000..cf7cc332f8f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 DeliveriesForm from '../DeliveriesForm';
+import { searchDeliveries } from '../../../../api/webhooks';
+
+jest.mock('../../../../api/webhooks', () => ({
+ searchDeliveries: jest.fn(() =>
+ Promise.resolve({
+ deliveries: [
+ {
+ at: '12.02.2018',
+ durationMs: 20,
+ httpStatus: 200,
+ id: '2',
+ success: true
+ },
+ {
+ at: '11.02.2018',
+ durationMs: 122,
+ httpStatus: 500,
+ id: '1',
+ success: false
+ }
+ ],
+ paging: {
+ pageIndex: 1,
+ pageSize: 10,
+ total: 15
+ }
+ })
+ )
+}));
+
+const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' };
+
+beforeEach(() => {
+ (searchDeliveries as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', async () => {
+ const wrapper = getWrapper();
+ expect(wrapper).toMatchSnapshot();
+
+ await new Promise(setImmediate);
+ expect(searchDeliveries as jest.Mock<any>).lastCalledWith({ webhook: webhook.key, ps: 10 });
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('ListFooter').prop<Function>('loadMore')();
+ expect(searchDeliveries).lastCalledWith({ webhook: webhook.key, p: 2, ps: 10 });
+});
+
+function getWrapper(props = {}) {
+ return shallow(<DeliveriesForm onClose={jest.fn()} webhook={webhook} {...props} />);
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx
new file mode 100644
index 00000000000..585df65cab5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 DeliveryItem from '../DeliveryItem';
+import { getDelivery } from '../../../../api/webhooks';
+
+jest.mock('../../../../api/webhooks', () => ({
+ getDelivery: jest.fn(() =>
+ Promise.resolve({
+ delivery: { payload: '{ "success": true }' }
+ })
+ )
+}));
+
+const delivery = {
+ at: '12.02.2018',
+ durationMs: 20,
+ httpStatus: 200,
+ id: '2',
+ success: true
+};
+
+beforeEach(() => {
+ (getDelivery as jest.Mock<any>).mockClear();
+});
+
+it('should render correctly', async () => {
+ const wrapper = getWrapper();
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('BoxedGroupAccordion').prop<Function>('onClick')();
+ await new Promise(setImmediate);
+ expect(getDelivery).lastCalledWith({ deliveryId: delivery.id });
+ wrapper.update();
+ expect(wrapper).toMatchSnapshot();
+});
+
+function getWrapper(props = {}) {
+ return shallow(<DeliveryItem delivery={delivery} {...props} />);
+}
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
index 51aa702e132..b4331ce8ce8 100644
--- 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
@@ -54,6 +54,12 @@ it('should display the delete webhook form', () => {
expect(onDelete).lastCalledWith(webhook.key);
});
+it('should display the deliveries form', () => {
+ const wrapper = getWrapper();
+ click(wrapper.find('.js-webhook-deliveries'));
+ expect(wrapper.find('DeliveriesForm').exists()).toBeTruthy();
+});
+
function getWrapper(props = {}) {
return shallow(
<WebhookActions
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 3490f184a15..43c7785c6e0 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
@@ -1,52 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should be in loading status 1`] = `
-<div
- className="page page-limited"
->
+<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="webhooks.page"
/>
- <PageHeader
- loading={true}
+ <div
+ className="page page-limited"
>
- <PageActions
+ <PageHeader
loading={true}
- onCreate={[Function]}
- webhooksCount={0}
- />
- </PageHeader>
-</div>
+ >
+ <PageActions
+ loading={true}
+ onCreate={[Function]}
+ webhooksCount={0}
+ />
+ </PageHeader>
+ </div>
+</React.Fragment>
`;
exports[`should fetch webhooks and display them 1`] = `
-<div
- className="page page-limited"
->
+<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="webhooks.page"
/>
- <PageHeader
- loading={false}
- >
- <PageActions
- loading={false}
- onCreate={[Function]}
- webhooksCount={0}
- />
- </PageHeader>
<div
- className="boxed-group boxed-group-inner"
+ className="page page-limited"
>
- <WebhooksList
- onDelete={[Function]}
- onUpdate={[Function]}
- webhooks={Array []}
- />
+ <PageHeader
+ loading={false}
+ >
+ <PageActions
+ loading={false}
+ onCreate={[Function]}
+ webhooksCount={2}
+ />
+ </PageHeader>
+ <div
+ className="boxed-group boxed-group-inner"
+ >
+ <WebhooksList
+ onDelete={[Function]}
+ onUpdate={[Function]}
+ webhooks={
+ Array [
+ Object {
+ "key": "1",
+ "name": "foo",
+ "url": "http://foo",
+ },
+ Object {
+ "key": "2",
+ "name": "bar",
+ "url": "http://bar",
+ },
+ ]
+ }
+ />
+ </div>
</div>
-</div>
+</React.Fragment>
`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap
new file mode 100644
index 00000000000..42cff706441
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap
@@ -0,0 +1,107 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Modal
+ contentLabel="webhooks.deliveries_for_x.foo"
+ onRequestClose={[MockFunction]}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ webhooks.deliveries_for_x.foo
+ </h2>
+ </header>
+ <div
+ className="modal-body modal-container"
+ >
+ <div
+ className="text-center"
+ >
+ <DeferredSpinner
+ loading={true}
+ timeout={100}
+ />
+ </div>
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ className="button-link js-modal-close"
+ onClick={[MockFunction]}
+ type="button"
+ >
+ close
+ </button>
+ </footer>
+</Modal>
+`;
+
+exports[`should render correctly 2`] = `
+<Modal
+ contentLabel="webhooks.deliveries_for_x.foo"
+ onRequestClose={[MockFunction]}
+>
+ <header
+ className="modal-head"
+ >
+ <h2>
+ webhooks.deliveries_for_x.foo
+ </h2>
+ </header>
+ <div
+ className="modal-body modal-container"
+ >
+ <DeliveryItem
+ delivery={
+ Object {
+ "at": "12.02.2018",
+ "durationMs": 20,
+ "httpStatus": 200,
+ "id": "2",
+ "success": true,
+ }
+ }
+ key="2"
+ />
+ <DeliveryItem
+ delivery={
+ Object {
+ "at": "11.02.2018",
+ "durationMs": 122,
+ "httpStatus": 500,
+ "id": "1",
+ "success": false,
+ }
+ }
+ key="1"
+ />
+ <div
+ className="text-center"
+ >
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ />
+ </div>
+ <ListFooter
+ count={2}
+ loadMore={[Function]}
+ ready={true}
+ total={15}
+ />
+ </div>
+ <footer
+ className="modal-foot"
+ >
+ <button
+ className="button-link js-modal-close"
+ onClick={[MockFunction]}
+ type="button"
+ >
+ close
+ </button>
+ </footer>
+</Modal>
+`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap
new file mode 100644
index 00000000000..350ab5714c6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<BoxedGroupAccordion
+ onClick={[Function]}
+ open={false}
+ renderHeader={[Function]}
+ title={
+ <DateTimeFormatter
+ date="12.02.2018"
+ />
+ }
+>
+ <div
+ className="big-spacer-left"
+ >
+ <p
+ className="spacer-bottom"
+ >
+ webhooks.delivery.response_x.200
+ </p>
+ <p
+ className="spacer-bottom"
+ >
+ webhooks.delivery.duration_x.20ms
+ </p>
+ <p
+ className="spacer-bottom"
+ >
+ webhooks.delivery.payload
+ </p>
+ <DeferredSpinner
+ className="spacer-left spacer-top"
+ loading={false}
+ timeout={100}
+ />
+ </div>
+</BoxedGroupAccordion>
+`;
+
+exports[`should render correctly 2`] = `
+<BoxedGroupAccordion
+ onClick={[Function]}
+ open={true}
+ renderHeader={[Function]}
+ title={
+ <DateTimeFormatter
+ date="12.02.2018"
+ />
+ }
+>
+ <div
+ className="big-spacer-left"
+ >
+ <p
+ className="spacer-bottom"
+ >
+ webhooks.delivery.response_x.200
+ </p>
+ <p
+ className="spacer-bottom"
+ >
+ webhooks.delivery.duration_x.20ms
+ </p>
+ <p
+ className="spacer-bottom"
+ >
+ webhooks.delivery.payload
+ </p>
+ <DeferredSpinner
+ className="spacer-left spacer-top"
+ loading={false}
+ timeout={100}
+ >
+ <CodeSnippet
+ noCopy={true}
+ snippet="{
+ \\"success\\": true
+}"
+ />
+ </DeferredSpinner>
+ </div>
+</BoxedGroupAccordion>
+`;
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
index abbb9ca3fb9..7ba2be6a55f 100644
--- 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
@@ -10,6 +10,7 @@ exports[`should not allow to create a new webhook 1`] = `
>
<button
className="js-webhook-create disabled"
+ type="button"
>
create
</button>
@@ -25,6 +26,7 @@ exports[`should render correctly 1`] = `
<button
className="js-webhook-create"
onClick={[Function]}
+ type="button"
>
create
</button>
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
index a82a5f3ed35..e3d35952ba5 100644
--- 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
@@ -11,6 +11,12 @@ exports[`should render correctly 1`] = `
>
update_verb
</ActionsDropdownItem>
+ <ActionsDropdownItem
+ className="js-webhook-deliveries"
+ onClick={[Function]}
+ >
+ webhooks.deliveries.show
+ </ActionsDropdownItem>
<ActionsDropdownDivider />
<ConfirmButton
confirmButtonText="delete"
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 ddf82de6e99..ff8401ea461 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
@@ -23,26 +23,26 @@ exports[`should correctly render the webhooks 1`] = `
</thead>
<tbody>
<WebhookItem
- key="1"
+ key="2"
onDelete={[MockFunction]}
onUpdate={[MockFunction]}
webhook={
Object {
- "key": "1",
- "name": "my webhook",
- "url": "http://webhook.target",
+ "key": "2",
+ "name": "jenkins webhook",
+ "url": "http://jenkins.target",
}
}
/>
<WebhookItem
- key="2"
+ key="1"
onDelete={[MockFunction]}
onUpdate={[MockFunction]}
webhook={
Object {
- "key": "2",
- "name": "jenkins webhook",
- "url": "http://jenkins.target",
+ "key": "1",
+ "name": "my webhook",
+ "url": "http://webhook.target",
}
}
/>
diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
index 5b457e11305..ecd54ab1adb 100644
--- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
+++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
@@ -23,7 +23,8 @@ import {
getPathUrlAsString,
getProjectUrl,
getQualityGatesUrl,
- getQualityGateUrl
+ getQualityGateUrl,
+ isUrl
} from '../urls';
const SIMPLE_COMPONENT_KEY = 'sonarqube';
@@ -113,3 +114,55 @@ describe('#getQualityGate(s)Url', () => {
});
});
});
+
+describe('#isUrl', () => {
+ it('should be valid', () => {
+ expect(isUrl('https://localhost')).toBeTruthy();
+ expect(isUrl('https://localhost/')).toBeTruthy();
+ expect(isUrl('https://localhost:9000')).toBeTruthy();
+ expect(isUrl('https://localhost:9000/')).toBeTruthy();
+ expect(isUrl('https://foo:bar@localhost:9000')).toBeTruthy();
+ expect(isUrl('https://foo@localhost')).toBeTruthy();
+ expect(isUrl('http://foo.com/blah_blah')).toBeTruthy();
+ expect(isUrl('http://foo.com/blah_blah/')).toBeTruthy();
+ expect(isUrl('http://www.example.com/wpstyle/?p=364')).toBeTruthy();
+ expect(isUrl('https://www.example.com/foo/?bar=baz&inga=42&quux')).toBeTruthy();
+ expect(isUrl('http://userid@example.com')).toBeTruthy();
+ expect(isUrl('http://userid@example.com/')).toBeTruthy();
+ expect(isUrl('http://userid:password@example.com:8080')).toBeTruthy();
+ expect(isUrl('http://userid:password@example.com:8080/')).toBeTruthy();
+ expect(isUrl('http://userid@example.com:8080')).toBeTruthy();
+ expect(isUrl('http://userid@example.com:8080/')).toBeTruthy();
+ expect(isUrl('http://userid:password@example.com')).toBeTruthy();
+ expect(isUrl('http://userid:password@example.com/')).toBeTruthy();
+ expect(isUrl('http://142.42.1.1/')).toBeTruthy();
+ expect(isUrl('http://142.42.1.1:8080/')).toBeTruthy();
+ expect(isUrl('http://foo.com/blah_(wikipedia)#cite-1')).toBeTruthy();
+ expect(isUrl('http://foo.com/blah_(wikipedia)_blah#cite-1')).toBeTruthy();
+ expect(isUrl('http://foo.com/(something)?after=parens')).toBeTruthy();
+ expect(isUrl('http://code.google.com/events/#&product=browser')).toBeTruthy();
+ expect(isUrl('http://j.mp')).toBeTruthy();
+ expect(isUrl('http://foo.bar/?q=Test%20URL-encoded%20stuff')).toBeTruthy();
+ expect(isUrl('http://1337.net')).toBeTruthy();
+ expect(isUrl('http://a.b-c.de')).toBeTruthy();
+ expect(isUrl('http://223.255.255.254')).toBeTruthy();
+ expect(isUrl('https://foo_bar.example.com/')).toBeTruthy();
+ });
+
+ it('should not be valid', () => {
+ expect(isUrl('http://')).toBeFalsy();
+ expect(isUrl('http://?')).toBeFalsy();
+ expect(isUrl('http://??')).toBeFalsy();
+ expect(isUrl('http://??/')).toBeFalsy();
+ expect(isUrl('http://#')).toBeFalsy();
+ expect(isUrl('http://##')).toBeFalsy();
+ expect(isUrl('http://##/')).toBeFalsy();
+ expect(isUrl('//')).toBeFalsy();
+ expect(isUrl('//a')).toBeFalsy();
+ expect(isUrl('///a')).toBeFalsy();
+ expect(isUrl('///')).toBeFalsy();
+ expect(isUrl('foo.com')).toBeFalsy();
+ expect(isUrl('http:// shouldfail.com')).toBeFalsy();
+ expect(isUrl(':// should fail')).toBeFalsy();
+ });
+});
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index 732c0b6a6be..2bde3a1184e 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -191,3 +191,17 @@ export function getHomePageUrl(homepage: HomePage) {
// should never happen, but just in case...
return '/projects';
}
+
+export function isUrl(url: string) {
+ if (!URL) {
+ const elem = document.createElement('a');
+ elem.href = url;
+ return !!(elem.host && elem.hostname && elem.protocol);
+ }
+ try {
+ const parsedUrl = new URL(url);
+ return url.includes(parsedUrl.host);
+ } catch (error) {
+ return false;
+ }
+}