*/
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;
}): 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);
+}
'/project/history',
'background_tasks',
'/project/key',
- '/project/deletion'
+ '/project/deletion',
+ '/project/webhooks'
];
interface Props {
'delete',
'permissions',
'permission_templates',
- 'projects_management'
+ 'projects_management',
+ 'webhooks'
];
export default function OrganizationNavigationAdministration({ location, organization }: Props) {
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>
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
)
}));
}
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>
+ </>
);
}
}
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 {
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;
};
/>
<InputValidationField
description={translate('webhooks.url.description')}
- disabled={isSubmitting}
dirty={dirty}
+ disabled={isSubmitting}
error={errors.url}
id="webhook-url"
label={
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
<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 && (
*/
import * as React from 'react';
import CreateWebhookForm from './CreateWebhookForm';
+import DeliveriesForm from './DeliveriesForm';
import ActionsDropdown, {
ActionsDropdownItem,
ActionsDropdownDivider
}
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;
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 });
};
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')}
)}
</ConfirmButton>
</ActionsDropdown>
-
+ {this.state.deliveries && (
+ <DeliveriesForm onClose={this.handleDeliveriesStop} webhook={webhook} />
+ )}
{this.state.updating && (
<CreateWebhookForm
onClose={this.handleUpdatingStop}
* 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';
<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}
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', () => {
});
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({
});
});
});
+
+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' }
+ ]);
+});
--- /dev/null
+/*
+ * 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} />);
+}
--- /dev/null
+/*
+ * 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} />);
+}
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
// 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>
`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
>
<button
className="js-webhook-create disabled"
+ type="button"
>
create
</button>
<button
className="js-webhook-create"
onClick={[Function]}
+ type="button"
>
create
</button>
>
update_verb
</ActionsDropdownItem>
+ <ActionsDropdownItem
+ className="js-webhook-deliveries"
+ onClick={[Function]}
+ >
+ webhooks.deliveries.show
+ </ActionsDropdownItem>
<ActionsDropdownDivider />
<ConfirmButton
confirmButtonText="delete"
</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",
}
}
/>
getPathUrlAsString,
getProjectUrl,
getQualityGatesUrl,
- getQualityGateUrl
+ getQualityGateUrl,
+ isUrl
} from '../urls';
const SIMPLE_COMPONENT_KEY = 'sonarqube';
});
});
});
+
+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();
+ });
+});
// 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;
+ }
+}
webhooks.delete=Delete Webhook
webhooks.delete.confirm=Are you sure you want to delete the webhook "{0}"?
webhooks.description=Webhooks are used to notify external services when a project analysis is done. An HTTP POST request including a JSON payload is sent to each of the provided URLs. Learn more in the {url}.
+webhooks.deliveries.show=Show recent deliveries
+webhooks.deliveries_for_x=Recent deliveries for {0}
+webhooks.delivery.duration_x=Duration: {0}
+webhooks.delivery.payload=Payload:
+webhooks.delivery.response_x=Response: {0}
webhooks.documentation_link=Webhooks documentation
webhooks.maximum_reached=You reached your maximum number of {0} webhooks. You can still update or delete an existing one.
webhooks.name=Name
webhooks.no_result=No webhook defined.
webhooks.update=Update Webhook
webhooks.url=URL
-webhooks.url.bad_auth=Bad format of URL authentication.
+webhooks.url.bad_format=Bad format of URL.
webhooks.url.bad_protocol=URL must start with "http://" or "https://".
webhooks.url.description=Server endpoint that will receive the webhook payload, for example: "http://my_server/foo". If HTTP Basic authentication is used, HTTPS is recommended to avoid man in the middle attacks. Example: "https://myLogin:myPassword@my_server/foo"
webhooks.url.required=URL is required.
\ No newline at end of file