mirror of
https://github.com/wassname/talk.git
synced 2026-07-31 12:50:48 +08:00
[CORL-687] Webhooks (#2738)
* feat: initial webhook impl * feat: added support for key rotation * feat: harmonized fetcher * feat: added expired secrets cleaning * feat: event system refactor * feat: added story event * feat: simplfiied webhook handler * feat: added ref's to locations where user events can be added * feat: added UI to support webhooks * fix: renaming some Webhook -> WebhookEndpoint * fix: review comments to adjuist flow * feat: added localizations * fix: linting, updated snapshots * fix: adapted for new fluent * fix: rearranged folders * fix: linting * feat: added webhooks documentation * feat: improved toc generation * feat: added some tests to webhooks * fix: chain transition hooks * feat: added tests around webhook ui * fix: renamed events * fix: adjusted circle markdown linting * fix: adjusted doctoc script call * review: review fixes * review: review comments * review: adjusted signing secret confirmation * review: adjusted styles to harmonize button usage * fix: updated snapshots and tests * review: move form out of webhooks Moved the form out of the webhooks by relocating the layout used for the route associated with the configure routes. * fix: fixed bugs and snapshots with tests * feat: revised slack message format to use block api * fix: fixed a small text bug Co-authored-by: Vinh <vinh@vinh.tech> Co-authored-by: Kim Gardner <kgardnr@gmail.com>
This commit is contained in:
co-authored by
Vinh
Kim Gardner
parent
34ba2da88d
commit
e42c2b925d
@@ -0,0 +1,5 @@
|
||||
import { urls } from "coral-framework/helpers";
|
||||
|
||||
export default function getEndpointLink(endpointID: string) {
|
||||
return `${urls.admin.configureWebhookEndpoint}/${endpointID}`;
|
||||
}
|
||||
@@ -9,18 +9,22 @@ import { createAuthCheckRoute } from "./routes/AuthCheck";
|
||||
import CommunityRoute from "./routes/Community";
|
||||
import ConfigureRoute from "./routes/Configure";
|
||||
import {
|
||||
AddWebhookEndpointRoute,
|
||||
AdvancedConfigRoute,
|
||||
AuthConfigRoute,
|
||||
ConfigureWebhookEndpointRoute,
|
||||
EmailConfigRoute,
|
||||
GeneralConfigRoute,
|
||||
ModerationConfigRoute,
|
||||
OrganizationConfigRoute,
|
||||
SlackConfigRoute,
|
||||
WebhookEndpointsConfigRoute,
|
||||
WordListConfigRoute,
|
||||
} from "./routes/Configure/sections";
|
||||
import { Sites } from "./routes/Configure/sections/Sites";
|
||||
import AddSiteRoute from "./routes/Configure/sections/Sites/AddSiteRoute";
|
||||
import SiteRoute from "./routes/Configure/sections/Sites/SiteRoute";
|
||||
import WebhookEndpointsLayout from "./routes/Configure/sections/WebhookEndpoints/WebhookEndpointsLayout";
|
||||
import ForgotPasswordRoute from "./routes/ForgotPassword";
|
||||
import InviteRoute from "./routes/Invite";
|
||||
import LoginRoute from "./routes/Login";
|
||||
@@ -113,6 +117,14 @@ export default makeRouteConfig(
|
||||
<Route path="email" {...EmailConfigRoute.routeConfig} />
|
||||
<Route path="slack" {...SlackConfigRoute.routeConfig} />
|
||||
</Route>
|
||||
<Route path="configure/webhooks" Component={WebhookEndpointsLayout}>
|
||||
<Route path="/" {...WebhookEndpointsConfigRoute.routeConfig} />
|
||||
<Route path="add" {...AddWebhookEndpointRoute.routeConfig} />
|
||||
<Route
|
||||
path="endpoint/:webhookEndpointID"
|
||||
{...ConfigureWebhookEndpointRoute.routeConfig}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="configure/organization/sites" Component={Sites}>
|
||||
<Redirect from="/" to="/admin/configure/organization/sites/new" />
|
||||
<Route path="new" {...AddSiteRoute.routeConfig} />
|
||||
|
||||
@@ -28,6 +28,9 @@ const ConfigureLinks: FunctionComponent<{}> = () => {
|
||||
<Localized id="configure-sideBarNavigation-slack">
|
||||
<Link to="/admin/configure/slack">Slack</Link>
|
||||
</Localized>
|
||||
<Localized id="configure-sideBarNavigation-webhooks">
|
||||
<Link to="/admin/configure/webhooks">Webhooks</Link>
|
||||
</Localized>
|
||||
<Localized id="configure-sideBarNavigation-advanced">
|
||||
<Link to="/admin/configure/advanced">Advanced</Link>
|
||||
</Localized>
|
||||
|
||||
@@ -24,7 +24,7 @@ class NavigationWarningContainer extends React.Component<Props> {
|
||||
);
|
||||
|
||||
this.removeTransitionHook = props.router.addTransitionHook(() =>
|
||||
this.props.active ? warningMessage : true
|
||||
this.props.active ? warningMessage : undefined
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,7 @@ const SitesConfig: FunctionComponent<Props> = ({
|
||||
id="configure-organization-sites-add-site"
|
||||
icon={<Icon>add</Icon>}
|
||||
>
|
||||
<Button
|
||||
to="/admin/configure/organization/sites/new"
|
||||
iconLeft
|
||||
size="large"
|
||||
>
|
||||
<Button to="/admin/configure/organization/sites/new" iconLeft>
|
||||
<Icon>add</Icon>
|
||||
Add a site
|
||||
</Button>
|
||||
|
||||
@@ -107,7 +107,7 @@ const SlackConfigContainer: FunctionComponent<Props> = ({ form, settings }) => {
|
||||
on how to create a Slack App see our documentation.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Button color="dark" onClick={onAddChannel}>
|
||||
<Button iconLeft onClick={onAddChannel}>
|
||||
<ButtonIcon size="md" className={styles.icon}>
|
||||
add
|
||||
</ButtonIcon>
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import ConfigBox from "coral-admin/routes/Configure/ConfigBox";
|
||||
import Header from "coral-admin/routes/Configure/Header";
|
||||
import { urls } from "coral-framework/helpers";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { HorizontalGutter } from "coral-ui/components/v2";
|
||||
|
||||
import { AddWebhookEndpointContainer_settings } from "coral-admin/__generated__/AddWebhookEndpointContainer_settings.graphql";
|
||||
|
||||
import { ConfigureWebhookEndpointForm } from "../ConfigureWebhookEndpointForm";
|
||||
|
||||
interface Props {
|
||||
router: Router;
|
||||
match: Match;
|
||||
settings: AddWebhookEndpointContainer_settings;
|
||||
}
|
||||
|
||||
const AddWebhookEndpointContainer: FunctionComponent<Props> = ({
|
||||
settings,
|
||||
router,
|
||||
}) => {
|
||||
const onCancel = useCallback(() => {
|
||||
router.push(urls.admin.webhooks);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<ConfigBox
|
||||
title={
|
||||
<Localized id="configure-webhooks-addEndpoint">
|
||||
<Header>Add a webhook endpoint</Header>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<ConfigureWebhookEndpointForm
|
||||
settings={settings}
|
||||
webhookEndpoint={null}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</ConfigBox>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouter(
|
||||
withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment AddWebhookEndpointContainer_settings on Settings {
|
||||
...ConfigureWebhookEndpointForm_settings
|
||||
}
|
||||
`,
|
||||
})(AddWebhookEndpointContainer)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { Delay, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { AddWebhookEndpointRouteQueryResponse } from "coral-admin/__generated__/AddWebhookEndpointRouteQuery.graphql";
|
||||
|
||||
import AddWebhookEndpointContainer from "./AddWebhookEndpointContainer";
|
||||
|
||||
interface Props {
|
||||
data: AddWebhookEndpointRouteQueryResponse | null;
|
||||
}
|
||||
|
||||
const AddWebhookEndpointRoute: FunctionComponent<Props> = ({ data }) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
);
|
||||
}
|
||||
|
||||
return <AddWebhookEndpointContainer settings={data.settings} />;
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query AddWebhookEndpointRouteQuery {
|
||||
settings {
|
||||
...AddWebhookEndpointContainer_settings
|
||||
}
|
||||
}
|
||||
`,
|
||||
cacheConfig: { force: true },
|
||||
})(AddWebhookEndpointRoute);
|
||||
|
||||
export default enhanced;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as AddWebhookEndpointRoute,
|
||||
} from "./AddWebhookEndpointRoute";
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import ConfigBox from "coral-admin/routes/Configure/ConfigBox";
|
||||
import Header from "coral-admin/routes/Configure/Header";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { HorizontalGutter } from "coral-ui/components/v2";
|
||||
|
||||
import { ConfigureWebhookEndpointContainer_settings } from "coral-admin/__generated__/ConfigureWebhookEndpointContainer_settings.graphql";
|
||||
import { ConfigureWebhookEndpointContainer_webhookEndpoint } from "coral-admin/__generated__/ConfigureWebhookEndpointContainer_webhookEndpoint.graphql";
|
||||
|
||||
import EndpointDangerZone from "./EndpointDangerZone";
|
||||
import EndpointDetails from "./EndpointDetails";
|
||||
import EndpointStatus from "./EndpointStatus";
|
||||
|
||||
interface Props {
|
||||
webhookEndpoint: ConfigureWebhookEndpointContainer_webhookEndpoint;
|
||||
settings: ConfigureWebhookEndpointContainer_settings;
|
||||
}
|
||||
|
||||
const ConfigureWebhookEndpointContainer: FunctionComponent<Props> = ({
|
||||
webhookEndpoint,
|
||||
settings,
|
||||
}) => {
|
||||
return (
|
||||
<HorizontalGutter size="double" data-testid="webhook-endpoint-container">
|
||||
<ConfigBox
|
||||
title={
|
||||
<Localized id="configure-webhooks-configureWebhookEndpoint">
|
||||
<Header htmlFor="configure-webhooks-header.title">
|
||||
Configure webhook endpoint
|
||||
</Header>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<EndpointDetails
|
||||
webhookEndpoint={webhookEndpoint}
|
||||
settings={settings}
|
||||
/>
|
||||
<EndpointStatus webhookEndpoint={webhookEndpoint} />
|
||||
<EndpointDangerZone webhookEndpoint={webhookEndpoint} />
|
||||
</ConfigBox>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
webhookEndpoint: graphql`
|
||||
fragment ConfigureWebhookEndpointContainer_webhookEndpoint on WebhookEndpoint {
|
||||
...EndpointDangerZone_webhookEndpoint
|
||||
...EndpointDetails_webhookEndpoint
|
||||
...EndpointStatus_webhookEndpoint
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ConfigureWebhookEndpointContainer_settings on Settings {
|
||||
...EndpointDetails_settings
|
||||
}
|
||||
`,
|
||||
})(ConfigureWebhookEndpointContainer);
|
||||
|
||||
export default enhanced;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { CallOut, Delay, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { ConfigureWebhookEndpointRouteQueryResponse } from "coral-admin/__generated__/ConfigureWebhookEndpointRouteQuery.graphql";
|
||||
|
||||
import ConfigureWebhookContainer from "./ConfigureWebhookEndpointContainer";
|
||||
|
||||
interface Props {
|
||||
data: ConfigureWebhookEndpointRouteQueryResponse | null;
|
||||
}
|
||||
|
||||
const ConfigureWebhookEndpointRoute: FunctionComponent<Props> = ({ data }) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.webhookEndpoint) {
|
||||
return (
|
||||
<Localized id="configure-webhooks-webhookEndpointNotFound">
|
||||
<CallOut color="error" fullWidth>
|
||||
Webhook endpoint not found
|
||||
</CallOut>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigureWebhookContainer
|
||||
webhookEndpoint={data.webhookEndpoint}
|
||||
settings={data.settings}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query ConfigureWebhookEndpointRouteQuery($webhookEndpointID: ID!) {
|
||||
webhookEndpoint(id: $webhookEndpointID) {
|
||||
...ConfigureWebhookEndpointContainer_webhookEndpoint
|
||||
}
|
||||
settings {
|
||||
...ConfigureWebhookEndpointContainer_settings
|
||||
}
|
||||
}
|
||||
`,
|
||||
cacheConfig: { force: true },
|
||||
prepareVariables: (params, match) => {
|
||||
return {
|
||||
webhookEndpointID: match.params.webhookEndpointID,
|
||||
};
|
||||
},
|
||||
})(ConfigureWebhookEndpointRoute);
|
||||
|
||||
export default enhanced;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { DeleteWebhookEndpointMutation as MutationTypes } from "coral-admin/__generated__/DeleteWebhookEndpointMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const DeleteWebhookEndpointMutation = createMutation(
|
||||
"deleteWebhookEndpoint",
|
||||
(environment: Environment, { id }: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DeleteWebhookEndpointMutation(
|
||||
$input: DeleteWebhookEndpointInput!
|
||||
) {
|
||||
deleteWebhookEndpoint(input: $input) {
|
||||
endpoint {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default DeleteWebhookEndpointMutation;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { DisableWebhookEndpointMutation as MutationTypes } from "coral-admin/__generated__/DisableWebhookEndpointMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const DisableWebhookEndpointMutation = createMutation(
|
||||
"disableWebhookEndpoint",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DisableWebhookEndpointMutation(
|
||||
$input: DisableWebhookEndpointInput!
|
||||
) {
|
||||
disableWebhookEndpoint(input: $input) {
|
||||
endpoint {
|
||||
...ConfigureWebhookEndpointContainer_webhookEndpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default DisableWebhookEndpointMutation;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { EnableWebhookEndpointMutation as MutationTypes } from "coral-admin/__generated__/EnableWebhookEndpointMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const EnableWebhookEndpointMutation = createMutation(
|
||||
"enableWebhookEndpoint",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation EnableWebhookEndpointMutation(
|
||||
$input: EnableWebhookEndpointInput!
|
||||
) {
|
||||
enableWebhookEndpoint(input: $input) {
|
||||
endpoint {
|
||||
...ConfigureWebhookEndpointContainer_webhookEndpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default EnableWebhookEndpointMutation;
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
|
||||
import Subheader from "coral-admin/routes/Configure/Subheader";
|
||||
import { urls } from "coral-framework/helpers";
|
||||
import { useCoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { getMessage } from "coral-framework/lib/i18n";
|
||||
import {
|
||||
graphql,
|
||||
useMutation,
|
||||
withFragmentContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
FormField,
|
||||
FormFieldDescription,
|
||||
Label,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { EndpointDangerZone_webhookEndpoint } from "coral-admin/__generated__/EndpointDangerZone_webhookEndpoint.graphql";
|
||||
|
||||
import DeleteWebhookEndpointMutation from "./DeleteWebhookEndpointMutation";
|
||||
import DisableWebhookEndpointMutation from "./DisableWebhookEndpointMutation";
|
||||
import EnableWebhookEndpointMutation from "./EnableWebhookEndpointMutation";
|
||||
import RotateSigningSecretModal from "./RotateSigningSecretModal";
|
||||
|
||||
interface Props {
|
||||
webhookEndpoint: EndpointDangerZone_webhookEndpoint;
|
||||
router: Router;
|
||||
match: Match;
|
||||
}
|
||||
|
||||
const EndpointDangerZone: FunctionComponent<Props> = ({
|
||||
webhookEndpoint,
|
||||
router,
|
||||
}) => {
|
||||
const { localeBundles } = useCoralContext();
|
||||
const enableWebhookEndpoint = useMutation(EnableWebhookEndpointMutation);
|
||||
const disableWebhookEndpoint = useMutation(DisableWebhookEndpointMutation);
|
||||
const deleteWebhookEndpoint = useMutation(DeleteWebhookEndpointMutation);
|
||||
|
||||
const [rotateSecretOpen, setRotateSecretOpen] = useState<boolean>(false);
|
||||
const onRotateSecret = useCallback(async () => {
|
||||
setRotateSecretOpen(true);
|
||||
}, []);
|
||||
const onHideRotateSecret = useCallback(async () => {
|
||||
setRotateSecretOpen(false);
|
||||
}, [setRotateSecretOpen]);
|
||||
|
||||
const onEnable = useCallback(async () => {
|
||||
const message = getMessage(
|
||||
localeBundles,
|
||||
"configure-webhooks-confirmEnable",
|
||||
"Enabling the webhook endpoint will start to send events to this URL. Are you sure you want to continue?"
|
||||
);
|
||||
|
||||
if (window.confirm(message)) {
|
||||
await enableWebhookEndpoint({ id: webhookEndpoint.id });
|
||||
}
|
||||
}, [webhookEndpoint, enableWebhookEndpoint]);
|
||||
const onDisable = useCallback(async () => {
|
||||
const message = getMessage(
|
||||
localeBundles,
|
||||
"configure-webhooks-confirmDisable",
|
||||
"Disabling this webhook endpoint will stop any new events from being sent to this URL. Are you sure you want to continue?"
|
||||
);
|
||||
|
||||
if (window.confirm(message)) {
|
||||
await disableWebhookEndpoint({ id: webhookEndpoint.id });
|
||||
}
|
||||
}, [webhookEndpoint, disableWebhookEndpoint]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
const message = getMessage(
|
||||
localeBundles,
|
||||
"configure-webhooks-confirmDelete",
|
||||
"Deleting this webhook endpoint will stop any new events from being sent to this URL, and remove all the associated settings with this webhook endpoint. Are you sure you want to continue?"
|
||||
);
|
||||
|
||||
if (window.confirm(message)) {
|
||||
await deleteWebhookEndpoint({ id: webhookEndpoint.id });
|
||||
|
||||
// Send the user back to the webhook endpoints listing.
|
||||
router.push(urls.admin.webhooks);
|
||||
}
|
||||
}, [webhookEndpoint, disableWebhookEndpoint, router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Localized id="configure-webhooks-dangerZone">
|
||||
<Subheader>Danger Zone</Subheader>
|
||||
</Localized>
|
||||
<FormField>
|
||||
<Localized id="configure-webhooks-rotateSigningSecret">
|
||||
<Label>Rotate signing secret</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-rotateSigningSecretDescription">
|
||||
<FormFieldDescription>
|
||||
Rotating the signing secret will allow to you to safely replace a
|
||||
signing secret used in production with a delay.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-rotateSigningSecretButton">
|
||||
<Button color="alert" onClick={onRotateSecret}>
|
||||
Rotate signing secret
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
<RotateSigningSecretModal
|
||||
endpointID={webhookEndpoint.id}
|
||||
onHide={onHideRotateSecret}
|
||||
open={rotateSecretOpen}
|
||||
/>
|
||||
{webhookEndpoint.enabled ? (
|
||||
<FormField>
|
||||
<Localized id="configure-webhooks-disableEndpoint">
|
||||
<Label>Disable endpoint</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-disableEndpointDescription">
|
||||
<FormFieldDescription>
|
||||
This endpoint is current enabled. By disabling this endpoint no
|
||||
new events will be sent to the URL provided.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-disableEndpointButton">
|
||||
<Button color="alert" onClick={onDisable}>
|
||||
Disable endpoint
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
) : (
|
||||
<FormField>
|
||||
<Localized id="configure-webhooks-enableEndpoint">
|
||||
<Label>Enable endpoint</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-enableEndpointDescription">
|
||||
<FormFieldDescription>
|
||||
This endpoint is current disabled. By enabling this endpoint new
|
||||
events will be sent to the URL provided.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-enableEndpointButton">
|
||||
<Button color="regular" onClick={onEnable}>
|
||||
Enable endpoint
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField>
|
||||
<Localized id="configure-webhooks-deleteEndpoint">
|
||||
<Label>Delete endpoint</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-deleteEndpointDescription">
|
||||
<FormFieldDescription>
|
||||
Deleting the endpoint will prevent any new events from being sent to
|
||||
the URL provided.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-deleteEndpointButton">
|
||||
<Button color="alert" onClick={onDelete}>
|
||||
Delete endpoint
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouter(
|
||||
withFragmentContainer<Props>({
|
||||
webhookEndpoint: graphql`
|
||||
fragment EndpointDangerZone_webhookEndpoint on WebhookEndpoint {
|
||||
id
|
||||
enabled
|
||||
}
|
||||
`,
|
||||
})(EndpointDangerZone)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import Subheader from "coral-admin/routes/Configure/Subheader";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
|
||||
import { EndpointDetails_settings } from "coral-admin/__generated__/EndpointDetails_settings.graphql";
|
||||
import { EndpointDetails_webhookEndpoint } from "coral-admin/__generated__/EndpointDetails_webhookEndpoint.graphql";
|
||||
|
||||
import ConfigureWebhookEndpointForm from "../ConfigureWebhookEndpointForm";
|
||||
|
||||
interface Props {
|
||||
webhookEndpoint: EndpointDetails_webhookEndpoint;
|
||||
settings: EndpointDetails_settings;
|
||||
}
|
||||
|
||||
const EndpointDetails: FunctionComponent<Props> = ({
|
||||
webhookEndpoint,
|
||||
settings,
|
||||
}) => (
|
||||
<>
|
||||
<Subheader>Endpoint details</Subheader>
|
||||
<ConfigureWebhookEndpointForm
|
||||
settings={settings}
|
||||
webhookEndpoint={webhookEndpoint}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
webhookEndpoint: graphql`
|
||||
fragment EndpointDetails_webhookEndpoint on WebhookEndpoint {
|
||||
...ConfigureWebhookEndpointForm_webhookEndpoint
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment EndpointDetails_settings on Settings {
|
||||
...ConfigureWebhookEndpointForm_settings
|
||||
}
|
||||
`,
|
||||
})(EndpointDetails);
|
||||
|
||||
export default enhanced;
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import Subheader from "coral-admin/routes/Configure/Subheader";
|
||||
import { CopyButton } from "coral-framework/components";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Flex,
|
||||
FormField,
|
||||
FormFieldDescription,
|
||||
HelperText,
|
||||
Label,
|
||||
PasswordField,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { EndpointStatus_webhookEndpoint } from "coral-admin/__generated__/EndpointStatus_webhookEndpoint.graphql";
|
||||
|
||||
import StatusMarker from "../StatusMarker";
|
||||
|
||||
interface Props {
|
||||
webhookEndpoint: EndpointStatus_webhookEndpoint;
|
||||
}
|
||||
|
||||
const EndpointStatus: FunctionComponent<Props> = ({ webhookEndpoint }) => {
|
||||
return (
|
||||
<>
|
||||
<Localized id="configure-webhooks-endpointStatus">
|
||||
<Subheader>Endpoint status</Subheader>
|
||||
</Localized>
|
||||
<FormField>
|
||||
<Localized id="configure-webhooks-status">
|
||||
<Label>Status</Label>
|
||||
</Localized>
|
||||
<StatusMarker enabled={webhookEndpoint.enabled} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Localized id="configure-webhooks-signingSecret">
|
||||
<Label>Signing secret</Label>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-webhooks-signingSecretDescription"
|
||||
externalLink={
|
||||
<ExternalLink href="https://github.com/coralproject/talk/blob/master/WEBHOOKS.md#webhook-signing" />
|
||||
}
|
||||
>
|
||||
<FormFieldDescription>
|
||||
The following signing secret is used to sign request payloads sent
|
||||
to the URL. To learn more about webhook signing, visit our{" "}
|
||||
<ExternalLink href="https://github.com/coralproject/talk/blob/master/WEBHOOKS.md#webhook-signing">
|
||||
Webhook Guide
|
||||
</ExternalLink>
|
||||
.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Flex direction="row" itemGutter="half" alignItems="center">
|
||||
<PasswordField
|
||||
value={webhookEndpoint.signingSecret.secret}
|
||||
fullWidth
|
||||
readOnly
|
||||
/>
|
||||
<CopyButton text={webhookEndpoint.signingSecret.secret} />
|
||||
</Flex>
|
||||
<HelperText>
|
||||
KEY GENERATED AT: {webhookEndpoint.signingSecret.createdAt}
|
||||
</HelperText>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
webhookEndpoint: graphql`
|
||||
fragment EndpointStatus_webhookEndpoint on WebhookEndpoint {
|
||||
id
|
||||
enabled
|
||||
signingSecret {
|
||||
secret
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(EndpointStatus);
|
||||
|
||||
export default enhanced;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.root {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--v2-font-size-5);
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
line-height: var(--v2-line-height-title);
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { useNotification } from "coral-admin/App/GlobalNotification";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
Card,
|
||||
CardCloseButton,
|
||||
Flex,
|
||||
FormField,
|
||||
HelperText,
|
||||
HorizontalGutter,
|
||||
Label,
|
||||
Modal,
|
||||
Option,
|
||||
SelectField,
|
||||
} from "coral-ui/components/v2";
|
||||
import AppNotification from "coral-ui/components/v2/AppNotification";
|
||||
|
||||
import RotateWebhookEndpointSecretMutation from "./RotateWebhookEndpointSecretMutation";
|
||||
|
||||
import styles from "./RotateSigningSecretModal.css";
|
||||
|
||||
interface Props {
|
||||
endpointID: string;
|
||||
onHide: () => void;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const RotateWebhookEndpointSecretModal: FunctionComponent<Props> = ({
|
||||
onHide,
|
||||
open,
|
||||
endpointID,
|
||||
}) => {
|
||||
const rotateWebhookEndpointSecret = useMutation(
|
||||
RotateWebhookEndpointSecretMutation
|
||||
);
|
||||
const { setMessage, clearMessage } = useNotification();
|
||||
const onRotateSecret = useCallback(
|
||||
async ({ inactiveIn: inactiveInString }) => {
|
||||
try {
|
||||
const inactiveIn = parseInt(inactiveInString, 10);
|
||||
await rotateWebhookEndpointSecret({ id: endpointID, inactiveIn });
|
||||
|
||||
// Post a notification about the successful change.
|
||||
setMessage(
|
||||
<Localized id="configure-webhooks-rotateSigningSecretSuccessUseNewSecret">
|
||||
<AppNotification icon="check_circle_outline" onClose={clearMessage}>
|
||||
Webhook endpoint signing secret has been rotated. Please ensure
|
||||
you update your integrations to use the new secret below.
|
||||
</AppNotification>
|
||||
</Localized>
|
||||
);
|
||||
window.scroll(0, 0);
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidRequestError) {
|
||||
return err.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: err.message };
|
||||
}
|
||||
|
||||
// Dismiss the modal.
|
||||
onHide();
|
||||
|
||||
return;
|
||||
},
|
||||
[endpointID, rotateWebhookEndpointSecret]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal open={open}>
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<Card className={styles.root}>
|
||||
<Flex justifyContent="flex-end">
|
||||
<CardCloseButton onClick={onHide} ref={firstFocusableRef} />
|
||||
</Flex>
|
||||
<Form onSubmit={onRotateSecret} initialValues={{ inactiveIn: 0 }}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="configure-webhooks-rotateSigningSecret">
|
||||
<h2 className={styles.title}>Rotate signing secret</h2>
|
||||
</Localized>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<Localized id="configure-webhooks-rotateSigningSecretHelper">
|
||||
<HelperText>
|
||||
After it expires, signatures will no longer be generated
|
||||
with the old secret.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
<Field name="inactiveIn">
|
||||
{({ input }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-webhooks-expiresOldSecret">
|
||||
<Label>Expire the old secret</Label>
|
||||
</Localized>
|
||||
<SelectField {...input} fullWidth>
|
||||
<Localized id="configure-webhooks-expiresOldSecretImmediately">
|
||||
<Option value="0">Immediately</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-webhooks-expiresOldSecretHoursFromNow"
|
||||
$hours={1}
|
||||
>
|
||||
<Option value="3600">1 hour from now</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-webhooks-expiresOldSecretHoursFromNow"
|
||||
$hours={2}
|
||||
>
|
||||
<Option value="7200">2 hours from now</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-webhooks-expiresOldSecretHoursFromNow"
|
||||
$hours={12}
|
||||
>
|
||||
<Option value="43200">12 hours from now</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-webhooks-expiresOldSecretHoursFromNow"
|
||||
$hours={24}
|
||||
>
|
||||
<Option value="86400">24 hours from now</Option>
|
||||
</Localized>
|
||||
</SelectField>
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Flex direction="row" justifyContent="flex-end" itemGutter>
|
||||
<Localized id="configure-webhooks-cancelButton">
|
||||
<Button color="regular" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-rotateSigningSecretButton">
|
||||
<Button
|
||||
type="submit"
|
||||
color="alert"
|
||||
disabled={submitting}
|
||||
ref={lastFocusableRef}
|
||||
>
|
||||
Rotate signing secret
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RotateWebhookEndpointSecretModal;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { RotateWebhookEndpointSecretMutation as MutationTypes } from "coral-admin/__generated__/RotateWebhookEndpointSecretMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const RotateWebhookEndpointSecretMutation = createMutation(
|
||||
"rotateWebhookEndpointSecret",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RotateWebhookEndpointSecretMutation(
|
||||
$input: RotateWebhookEndpointSecretInput!
|
||||
) {
|
||||
rotateWebhookEndpointSecret(input: $input) {
|
||||
endpoint {
|
||||
...ConfigureWebhookEndpointContainer_webhookEndpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default RotateWebhookEndpointSecretMutation;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as ConfigureWebhookEndpointRoute,
|
||||
} from "./ConfigureWebhookEndpointRoute";
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import getEndpointLink from "coral-admin/helpers/getEndpointLink";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { colorFromMeta, ValidationMessage } from "coral-framework/lib/form";
|
||||
import {
|
||||
graphql,
|
||||
useMutation,
|
||||
withFragmentContainer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateURL,
|
||||
} from "coral-framework/lib/validation";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
Flex,
|
||||
FormField,
|
||||
HorizontalGutter,
|
||||
Label,
|
||||
TextField,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ConfigureWebhookEndpointForm_settings } from "coral-admin/__generated__/ConfigureWebhookEndpointForm_settings.graphql";
|
||||
import { ConfigureWebhookEndpointForm_webhookEndpoint } from "coral-admin/__generated__/ConfigureWebhookEndpointForm_webhookEndpoint.graphql";
|
||||
|
||||
import CreateWebhookEndpointMutation from "./CreateWebhookEndpointMutation";
|
||||
import EventsSelectField from "./EventsSelectField";
|
||||
import UpdateWebhookEndpointMutation from "./UpdateWebhookEndpointMutation";
|
||||
|
||||
interface Props {
|
||||
onCancel?: () => void;
|
||||
router: Router;
|
||||
match: Match;
|
||||
webhookEndpoint: ConfigureWebhookEndpointForm_webhookEndpoint | null;
|
||||
settings: ConfigureWebhookEndpointForm_settings;
|
||||
}
|
||||
|
||||
const ConfigureWebhookEndpointForm: FunctionComponent<Props> = ({
|
||||
onCancel,
|
||||
settings,
|
||||
webhookEndpoint,
|
||||
router,
|
||||
}) => {
|
||||
const create = useMutation(CreateWebhookEndpointMutation);
|
||||
const update = useMutation(UpdateWebhookEndpointMutation);
|
||||
const onSubmit = useCallback(
|
||||
async values => {
|
||||
try {
|
||||
if (webhookEndpoint) {
|
||||
// The webhook endpoint was defined, update it.
|
||||
await update(values);
|
||||
} else {
|
||||
// The webhook endpoint wasn't defined, created it.
|
||||
const result = await create(values);
|
||||
|
||||
// Redirect the user to the new webhook endpoint page.
|
||||
router.push(getEndpointLink(result.endpoint.id));
|
||||
|
||||
// We don't need to close this modal because we are navigating...
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidRequestError) {
|
||||
return err.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: err.message };
|
||||
}
|
||||
},
|
||||
[webhookEndpoint, create, update, router]
|
||||
);
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={onSubmit}
|
||||
initialValues={
|
||||
webhookEndpoint ? webhookEndpoint : { events: [], all: false, url: "" }
|
||||
}
|
||||
>
|
||||
{({ handleSubmit, submitting, submitError, pristine }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<Field
|
||||
name="url"
|
||||
validate={composeValidators(required, validateURL)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-webhooks-endpointURL">
|
||||
<Label>Endpoint URL</Label>
|
||||
</Localized>
|
||||
<TextField
|
||||
{...input}
|
||||
placeholder="https://"
|
||||
color={colorFromMeta(meta)}
|
||||
fullWidth
|
||||
/>
|
||||
<ValidationMessage meta={meta} fullWidth />
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<EventsSelectField settings={settings} />
|
||||
<Flex direction="row" justifyContent="flex-end" itemGutter>
|
||||
{onCancel && (
|
||||
<Localized id="configure-webhooks-cancelButton">
|
||||
<Button type="button" color="mono" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
{webhookEndpoint ? (
|
||||
<Localized id="configure-webhooks-updateWebhookEndpointButton">
|
||||
<Button type="submit" disabled={submitting || pristine}>
|
||||
Update details
|
||||
</Button>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="configure-webhooks-addEndpointButton">
|
||||
<Button type="submit" disabled={submitting}>
|
||||
Add webhook endpoint
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouter(
|
||||
withFragmentContainer<Props>({
|
||||
webhookEndpoint: graphql`
|
||||
fragment ConfigureWebhookEndpointForm_webhookEndpoint on WebhookEndpoint {
|
||||
id
|
||||
url
|
||||
events
|
||||
all
|
||||
}
|
||||
`,
|
||||
settings: graphql`
|
||||
fragment ConfigureWebhookEndpointForm_settings on Settings {
|
||||
...EventsSelectField_settings
|
||||
}
|
||||
`,
|
||||
})(ConfigureWebhookEndpointForm)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { CreateWebhookEndpointMutation as MutationTypes } from "coral-admin/__generated__/CreateWebhookEndpointMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const CreateWebhookEndpointMutation = createMutation(
|
||||
"createWebhookEndpoint",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation CreateWebhookEndpointMutation(
|
||||
$input: CreateWebhookEndpointInput!
|
||||
) {
|
||||
createWebhookEndpoint(input: $input) {
|
||||
endpoint {
|
||||
id
|
||||
}
|
||||
settings {
|
||||
...WebhookEndpointsConfigContainer_settings
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default CreateWebhookEndpointMutation;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
.list {
|
||||
max-height: 295px;
|
||||
}
|
||||
|
||||
.event {
|
||||
font-family: monospace;
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { useField } from "react-final-form";
|
||||
|
||||
import { ValidationMessage } from "coral-framework/lib/form";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { validateWebhookEventSelection } from "coral-framework/lib/validation";
|
||||
import { Typography } from "coral-ui/components";
|
||||
import {
|
||||
Button,
|
||||
CheckBox,
|
||||
Flex,
|
||||
FormField,
|
||||
FormFieldDescription,
|
||||
HelperText,
|
||||
Label,
|
||||
ListGroup,
|
||||
ListGroupRow,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import {
|
||||
EventsSelectField_settings,
|
||||
WEBHOOK_EVENT_NAME,
|
||||
} from "coral-admin/__generated__/EventsSelectField_settings.graphql";
|
||||
|
||||
import styles from "./EventsSelectField.css";
|
||||
|
||||
interface Props {
|
||||
settings: EventsSelectField_settings;
|
||||
}
|
||||
|
||||
const EventsSelectField: FunctionComponent<Props> = ({ settings }) => {
|
||||
const { input: all } = useField<boolean>("all");
|
||||
const { input: events, meta } = useField<WEBHOOK_EVENT_NAME[]>("events", {
|
||||
validate: validateWebhookEventSelection,
|
||||
});
|
||||
|
||||
const onClear = useCallback(() => {
|
||||
if (all.value) {
|
||||
all.onChange(false);
|
||||
} else {
|
||||
events.onChange([]);
|
||||
}
|
||||
}, [all, events]);
|
||||
|
||||
const onCheckChange = useCallback(
|
||||
(event: WEBHOOK_EVENT_NAME, selectedIndex: number) => () => {
|
||||
const changed = [...events.value];
|
||||
if (selectedIndex >= 0) {
|
||||
changed.splice(selectedIndex, 1);
|
||||
} else {
|
||||
changed.push(event);
|
||||
}
|
||||
|
||||
events.onChange(changed);
|
||||
},
|
||||
[events]
|
||||
);
|
||||
|
||||
const onRecieveAll = useCallback(() => {
|
||||
all.onChange(true);
|
||||
}, [all]);
|
||||
|
||||
return (
|
||||
<FormField>
|
||||
<Flex justifyContent="space-between">
|
||||
<Localized id="configure-webhooks-eventsToSend">
|
||||
<Label>Events to send</Label>
|
||||
</Localized>
|
||||
{(all.value || events.value.length > 0) && (
|
||||
<Localized id="configure-webhooks-clearEventsToSend">
|
||||
<Button variant="text" onClick={onClear}>
|
||||
Clear
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
<Localized
|
||||
id="configure-webhooks-eventsToSendDescription"
|
||||
externalLink={
|
||||
<ExternalLink href="https://github.com/coralproject/talk/blob/master/WEBHOOKS.md#events-listing" />
|
||||
}
|
||||
>
|
||||
<FormFieldDescription>
|
||||
These are the events that are registered to this particular endpoint.
|
||||
Visit our{" "}
|
||||
<ExternalLink href="https://github.com/coralproject/talk/blob/master/WEBHOOKS.md#events-listing">
|
||||
Webhook Guide
|
||||
</ExternalLink>{" "}
|
||||
for the schema of these events. Any event matching the following will
|
||||
be sent to the endpoint if it is enabled:
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<ListGroup className={styles.list}>
|
||||
{settings.webhookEvents.map(event => {
|
||||
const selectedIndex = events.value.indexOf(event);
|
||||
return (
|
||||
<ListGroupRow key={event}>
|
||||
<CheckBox
|
||||
disabled={all.value}
|
||||
checked={all.value || selectedIndex >= 0}
|
||||
onChange={onCheckChange(event, selectedIndex)}
|
||||
>
|
||||
<Typography className={styles.event}>{event}</Typography>
|
||||
</CheckBox>
|
||||
</ListGroupRow>
|
||||
);
|
||||
})}
|
||||
</ListGroup>
|
||||
{all.value ? (
|
||||
<Localized id="configure-webhooks-allEvents">
|
||||
<HelperText>
|
||||
The endpoint will receive all events, including any added in the
|
||||
future.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
) : events.value.length > 0 ? (
|
||||
<Localized
|
||||
id="configure-webhooks-selectedEvents"
|
||||
$count={events.value.length}
|
||||
>
|
||||
<HelperText>{events.value.length} event selected.</HelperText>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized
|
||||
id="configure-webhooks-selectAnEvent"
|
||||
button={<Button variant="text" onClick={onRecieveAll} />}
|
||||
>
|
||||
<HelperText>
|
||||
Select events above or{" "}
|
||||
<Button variant="text" onClick={onRecieveAll}>
|
||||
receive all events
|
||||
</Button>
|
||||
.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
)}
|
||||
<ValidationMessage meta={meta} fullWidth />
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment EventsSelectField_settings on Settings {
|
||||
webhookEvents
|
||||
}
|
||||
`,
|
||||
})(EventsSelectField);
|
||||
|
||||
export default enhanced;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { UpdateWebhookEndpointMutation as MutationTypes } from "coral-admin/__generated__/UpdateWebhookEndpointMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const UpdateWebhookEndpointMutation = createMutation(
|
||||
"updateWebhookEndpoint",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdateWebhookEndpointMutation(
|
||||
$input: UpdateWebhookEndpointInput!
|
||||
) {
|
||||
updateWebhookEndpoint(input: $input) {
|
||||
endpoint {
|
||||
...ConfigureWebhookEndpointContainer_webhookEndpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default UpdateWebhookEndpointMutation;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as ConfigureWebhookEndpointForm,
|
||||
} from "./ConfigureWebhookEndpointForm";
|
||||
@@ -0,0 +1,12 @@
|
||||
.success {
|
||||
background-color: var(--v2-palette-success-main);
|
||||
border-color: var(--v2-palette-success-main);
|
||||
color: var(--v2-colors-pure-white);
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--v2-palette-error-darkest);
|
||||
border-color: var(--v2-palette-error-darkest);
|
||||
color: var(--v2-colors-pure-white);
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Marker } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./StatusMarker.css";
|
||||
|
||||
interface Props {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const StatusMarker: FunctionComponent<Props> = ({ enabled }) =>
|
||||
enabled ? (
|
||||
<Localized id="configure-webhooks-enabledWebhookEndpoint">
|
||||
<Marker className={styles.success}>Enabled</Marker>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="configure-webhooks-disabledWebhookEndpoint">
|
||||
<Marker className={styles.error}>Disabled</Marker>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default StatusMarker;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.urlColumn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detailsButton {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
line-height: var(--v2-line-height-reset);
|
||||
font-size: var(--v2-font-size-2);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import getEndpointLink from "coral-admin/helpers/getEndpointLink";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Icon,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { WebhookEndpointRow_webhookEndpoint } from "coral-admin/__generated__/WebhookEndpointRow_webhookEndpoint.graphql";
|
||||
|
||||
import StatusMarker from "./StatusMarker";
|
||||
|
||||
import styles from "./WebhookEndpointRow.css";
|
||||
|
||||
interface Props {
|
||||
endpoint: WebhookEndpointRow_webhookEndpoint;
|
||||
}
|
||||
|
||||
const WebhookEndpointRow: FunctionComponent<Props> = ({ endpoint }) => (
|
||||
<TableRow data-testid={`webhook-endpoint-${endpoint.id}`}>
|
||||
<TableCell className={styles.urlColumn}>{endpoint.url}</TableCell>
|
||||
<TableCell>
|
||||
<StatusMarker enabled={endpoint.enabled} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Flex justifyContent="flex-end">
|
||||
<Localized
|
||||
id="configure-webhooks-detailsButton"
|
||||
icon={<Icon>keyboard_arrow_right</Icon>}
|
||||
>
|
||||
<Button variant="text" to={getEndpointLink(endpoint.id)} iconRight>
|
||||
Details
|
||||
<Icon>keyboard_arrow_right</Icon>
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
endpoint: graphql`
|
||||
fragment WebhookEndpointRow_webhookEndpoint on WebhookEndpoint {
|
||||
id
|
||||
enabled
|
||||
url
|
||||
}
|
||||
`,
|
||||
})(WebhookEndpointRow);
|
||||
|
||||
export default enhanced;
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { urls } from "coral-framework/helpers";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
FormFieldDescription,
|
||||
HorizontalGutter,
|
||||
Icon,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { WebhookEndpointsConfigContainer_settings } from "coral-admin/__generated__/WebhookEndpointsConfigContainer_settings.graphql";
|
||||
|
||||
import ConfigBox from "../../ConfigBox";
|
||||
import Header from "../../Header";
|
||||
import Subheader from "../../Subheader";
|
||||
import WebhookEndpointRow from "./WebhookEndpointRow";
|
||||
|
||||
interface Props {
|
||||
settings: WebhookEndpointsConfigContainer_settings;
|
||||
}
|
||||
|
||||
const WebhookEndpointsConfigContainer: FunctionComponent<Props> = ({
|
||||
settings,
|
||||
}) => {
|
||||
return (
|
||||
<HorizontalGutter size="double" data-testid="webhooks-container">
|
||||
<ConfigBox
|
||||
title={
|
||||
<Localized id="configure-webhooks-header-title">
|
||||
<Header htmlFor="configure-webhooks-header.title">Webhooks</Header>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<Localized
|
||||
id="configure-webhooks-description"
|
||||
externalLink={
|
||||
<ExternalLink href="https://docs.coralproject.net/coral/v5/integrating/webhooks/" />
|
||||
}
|
||||
>
|
||||
<FormFieldDescription>
|
||||
Configure an endpoint to send events to when events occur within
|
||||
Coral. These events will be JSON encoded and signed. To learn more
|
||||
about webhook signing, visit our{" "}
|
||||
<ExternalLink href="https://docs.coralproject.net/coral/v5/integrating/webhooks/">
|
||||
our docs
|
||||
</ExternalLink>
|
||||
.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Button
|
||||
to={urls.admin.addWebhookEndpoint}
|
||||
iconLeft
|
||||
data-testid="add-webhook-endpoint"
|
||||
>
|
||||
<Icon size="md">add</Icon>
|
||||
<Localized id="configure-webhooks-addEndpointButton">
|
||||
Add webhook endpoint
|
||||
</Localized>
|
||||
</Button>
|
||||
<Localized id="configure-webhooks-endpoints">
|
||||
<Subheader>Endpoints</Subheader>
|
||||
</Localized>
|
||||
{settings.webhooks.endpoints.length > 0 ? (
|
||||
<Table fullWidth>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<Localized id="configure-webhooks-url">
|
||||
<TableCell>URL</TableCell>
|
||||
</Localized>
|
||||
<Localized id="configure-webhooks-status">
|
||||
<TableCell>Status</TableCell>
|
||||
</Localized>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{settings.webhooks.endpoints.map((endpoint, idx) => (
|
||||
<WebhookEndpointRow key={idx} endpoint={endpoint} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<Localized id="configure-webhooks-noEndpoints">
|
||||
<CallOut color="regular" fullWidth>
|
||||
There are no webhook endpoints configured, add one above.
|
||||
</CallOut>
|
||||
</Localized>
|
||||
)}
|
||||
</ConfigBox>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment WebhookEndpointsConfigContainer_settings on Settings {
|
||||
webhooks {
|
||||
endpoints {
|
||||
...WebhookEndpointRow_webhookEndpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(WebhookEndpointsConfigContainer);
|
||||
|
||||
export default enhanced;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { Delay, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { WebhookEndpointsConfigRouteQueryResponse } from "coral-admin/__generated__/WebhookEndpointsConfigRouteQuery.graphql";
|
||||
|
||||
import WebhookEndpointsConfigContainer from "./WebhookEndpointsConfigContainer";
|
||||
|
||||
interface Props {
|
||||
data: WebhookEndpointsConfigRouteQueryResponse | null;
|
||||
}
|
||||
|
||||
const WebhookEndpointsConfigRoute: FunctionComponent<Props> = ({ data }) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
);
|
||||
}
|
||||
|
||||
return <WebhookEndpointsConfigContainer settings={data.settings} />;
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query WebhookEndpointsConfigRouteQuery {
|
||||
settings {
|
||||
...WebhookEndpointsConfigContainer_settings
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(WebhookEndpointsConfigRoute);
|
||||
|
||||
export default enhanced;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import MainLayout from "coral-admin/components/MainLayout";
|
||||
|
||||
import ConfigureLinks from "../../ConfigureLinks";
|
||||
import Layout from "../../Layout";
|
||||
import Main from "../../Main";
|
||||
import SideBar from "../../SideBar";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
const WebhookEndpointsLayout: FunctionComponent<Props> = props => {
|
||||
return (
|
||||
<MainLayout>
|
||||
<Layout>
|
||||
<SideBar>
|
||||
<ConfigureLinks />
|
||||
</SideBar>
|
||||
<Main>{props.children}</Main>
|
||||
</Layout>
|
||||
</MainLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebhookEndpointsLayout;
|
||||
@@ -0,0 +1,8 @@
|
||||
export {
|
||||
default,
|
||||
default as WebhookEndpointsConfigRoute,
|
||||
} from "./WebhookEndpointsConfigRoute";
|
||||
export { default as AddWebhookEndpointRoute } from "./AddWebhookEndpoint";
|
||||
export {
|
||||
default as ConfigureWebhookEndpointRoute,
|
||||
} from "./ConfigureWebhookEndpoint";
|
||||
@@ -6,3 +6,8 @@ export { ModerationConfigRoute } from "./Moderation";
|
||||
export { OrganizationConfigRoute } from "./Organization";
|
||||
export { WordListConfigRoute } from "./WordList";
|
||||
export { SlackConfigRoute } from "./Slack";
|
||||
export {
|
||||
WebhookEndpointsConfigRoute,
|
||||
ConfigureWebhookEndpointRoute,
|
||||
AddWebhookEndpointRoute,
|
||||
} from "./WebhookEndpoints";
|
||||
|
||||
@@ -88,6 +88,15 @@ exports[`renders configure advanced 1`] = `
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/webhooks"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Webhooks
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link Link-linkActive"
|
||||
|
||||
@@ -88,6 +88,15 @@ exports[`renders configure auth 1`] = `
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/webhooks"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Webhooks
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -88,6 +88,15 @@ exports[`renders configure general 1`] = `
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/webhooks"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Webhooks
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -88,6 +88,15 @@ exports[`renders configure moderation 1`] = `
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/webhooks"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Webhooks
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -88,6 +88,15 @@ exports[`renders configure organization 1`] = `
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/webhooks"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Webhooks
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
@@ -320,7 +329,7 @@ moderation questions.
|
||||
Add a new site to your organization or edit an existing site's details.
|
||||
</p>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeLarge Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
|
||||
data-color="regular"
|
||||
data-variant="regular"
|
||||
href="/admin/configure/organization/sites/new"
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`displays a list of webhook endpoints that have been configured 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
data-testid="webhooks-container"
|
||||
>
|
||||
<div
|
||||
className="Box-root ConfigBox-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="Header-root"
|
||||
htmlFor="configure-webhooks-header.title"
|
||||
>
|
||||
Configure webhook endpoint
|
||||
</label>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
className="ConfigBox-content"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
|
||||
>
|
||||
<p
|
||||
className="FormFieldDescription-root"
|
||||
>
|
||||
Configure an endpoint to send events to when events occur within
|
||||
Coral. These events will be JSON encoded and signed. To learn more
|
||||
about webhook signing, visit our
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://docs.coralproject.net/coral/v5/integrating/webhooks/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Webhook Guide
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
|
||||
data-color="regular"
|
||||
data-testid="add-webhook-endpoint"
|
||||
data-variant="regular"
|
||||
href="/admin/configure/webhooks/add"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
add
|
||||
</i>
|
||||
Add webhook endpoint
|
||||
</a>
|
||||
<h3
|
||||
className="Subheader-root"
|
||||
>
|
||||
Endpoints
|
||||
</h3>
|
||||
<table
|
||||
className="Table-root Table-fullWidth"
|
||||
>
|
||||
<thead
|
||||
className="TableHead-root"
|
||||
>
|
||||
<tr
|
||||
className="TableRow-root"
|
||||
>
|
||||
<th
|
||||
className="TableCell-root TableCell-header"
|
||||
>
|
||||
URL
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root TableCell-header"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root TableCell-header"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
className="TableBody-root"
|
||||
>
|
||||
<tr
|
||||
className="TableRow-root TableRow-body"
|
||||
data-testid="webhook-endpoint-webhook-endpoint-1"
|
||||
>
|
||||
<td
|
||||
className="TableCell-root WebhookEndpointRow-urlColumn TableCell-body"
|
||||
>
|
||||
http://example.com/webhook-endpoint-1
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<span
|
||||
className="Marker-root StatusMarker-success Marker-colorPending Marker-variantRegular"
|
||||
>
|
||||
Enabled
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
|
||||
>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantText Button-uppercase Button-iconRight"
|
||||
data-color="regular"
|
||||
data-variant="text"
|
||||
href="/admin/configure/webhooks/endpoint/webhook-endpoint-1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Details
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
keyboard_arrow_right
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
className="TableRow-root TableRow-body"
|
||||
data-testid="webhook-endpoint-webhook-endpoint-2"
|
||||
>
|
||||
<td
|
||||
className="TableCell-root WebhookEndpointRow-urlColumn TableCell-body"
|
||||
>
|
||||
http://example.com/webhook-endpoint-2
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<span
|
||||
className="Marker-root StatusMarker-error Marker-colorPending Marker-variantRegular"
|
||||
>
|
||||
Disabled
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
|
||||
>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantText Button-uppercase Button-iconRight"
|
||||
data-color="regular"
|
||||
data-variant="text"
|
||||
href="/admin/configure/webhooks/endpoint/webhook-endpoint-2"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Details
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
keyboard_arrow_right
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`goes to add new webhook endpoint when clicking add 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
data-testid="webhooks-container"
|
||||
>
|
||||
<div
|
||||
className="Box-root ConfigBox-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="Header-root"
|
||||
htmlFor="configure-webhooks-header.title"
|
||||
>
|
||||
Configure webhook endpoint
|
||||
</label>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
className="ConfigBox-content"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
|
||||
>
|
||||
<p
|
||||
className="FormFieldDescription-root"
|
||||
>
|
||||
Configure an endpoint to send events to when events occur within
|
||||
Coral. These events will be JSON encoded and signed. To learn more
|
||||
about webhook signing, visit our
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://docs.coralproject.net/coral/v5/integrating/webhooks/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Webhook Guide
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
|
||||
data-color="regular"
|
||||
data-testid="add-webhook-endpoint"
|
||||
data-variant="regular"
|
||||
href="/admin/configure/webhooks/add"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
add
|
||||
</i>
|
||||
Add webhook endpoint
|
||||
</a>
|
||||
<h3
|
||||
className="Subheader-root"
|
||||
>
|
||||
Endpoints
|
||||
</h3>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorRegular CallOut-fullWidth"
|
||||
>
|
||||
<div
|
||||
className="CallOut-inner"
|
||||
>
|
||||
There are no webhook endpoints configured, add one above.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`goes to the webhook endpoint configuration page when selected 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
data-testid="webhooks-container"
|
||||
>
|
||||
<div
|
||||
className="Box-root ConfigBox-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="Header-root"
|
||||
htmlFor="configure-webhooks-header.title"
|
||||
>
|
||||
Configure webhook endpoint
|
||||
</label>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
className="ConfigBox-content"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
|
||||
>
|
||||
<p
|
||||
className="FormFieldDescription-root"
|
||||
>
|
||||
Configure an endpoint to send events to when events occur within
|
||||
Coral. These events will be JSON encoded and signed. To learn more
|
||||
about webhook signing, visit our
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://docs.coralproject.net/coral/v5/integrating/webhooks/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Webhook Guide
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
|
||||
data-color="regular"
|
||||
data-testid="add-webhook-endpoint"
|
||||
data-variant="regular"
|
||||
href="/admin/configure/webhooks/add"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
add
|
||||
</i>
|
||||
Add webhook endpoint
|
||||
</a>
|
||||
<h3
|
||||
className="Subheader-root"
|
||||
>
|
||||
Endpoints
|
||||
</h3>
|
||||
<table
|
||||
className="Table-root Table-fullWidth"
|
||||
>
|
||||
<thead
|
||||
className="TableHead-root"
|
||||
>
|
||||
<tr
|
||||
className="TableRow-root"
|
||||
>
|
||||
<th
|
||||
className="TableCell-root TableCell-header"
|
||||
>
|
||||
URL
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root TableCell-header"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
className="TableCell-root TableCell-header"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
className="TableBody-root"
|
||||
>
|
||||
<tr
|
||||
className="TableRow-root TableRow-body"
|
||||
data-testid="webhook-endpoint-webhook-endpoint-1"
|
||||
>
|
||||
<td
|
||||
className="TableCell-root WebhookEndpointRow-urlColumn TableCell-body"
|
||||
>
|
||||
http://example.com/webhook-endpoint-1
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<span
|
||||
className="Marker-root StatusMarker-success Marker-colorPending Marker-variantRegular"
|
||||
>
|
||||
Enabled
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="TableCell-root TableCell-body"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
|
||||
>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantText Button-uppercase Button-iconRight"
|
||||
data-color="regular"
|
||||
data-variant="text"
|
||||
href="/admin/configure/webhooks/endpoint/webhook-endpoint-1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Details
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
keyboard_arrow_right
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders webhooks 1`] = `
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-double"
|
||||
data-testid="webhooks-container"
|
||||
>
|
||||
<div
|
||||
className="Box-root ConfigBox-root"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root ConfigBox-title Flex-flex Flex-justifySpaceBetween"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="Header-root"
|
||||
htmlFor="configure-webhooks-header.title"
|
||||
>
|
||||
Configure webhook endpoint
|
||||
</label>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
className="ConfigBox-content"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
|
||||
>
|
||||
<p
|
||||
className="FormFieldDescription-root"
|
||||
>
|
||||
Configure an endpoint to send events to when events occur within
|
||||
Coral. These events will be JSON encoded and signed. To learn more
|
||||
about webhook signing, visit our
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://docs.coralproject.net/coral/v5/integrating/webhooks/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Webhook Guide
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<a
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-iconLeft"
|
||||
data-color="regular"
|
||||
data-testid="add-webhook-endpoint"
|
||||
data-variant="regular"
|
||||
href="/admin/configure/webhooks/add"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
add
|
||||
</i>
|
||||
Add webhook endpoint
|
||||
</a>
|
||||
<h3
|
||||
className="Subheader-root"
|
||||
>
|
||||
Endpoints
|
||||
</h3>
|
||||
<div
|
||||
className="CallOut-root CallOut-colorRegular CallOut-fullWidth"
|
||||
>
|
||||
<div
|
||||
className="CallOut-inner"
|
||||
>
|
||||
There are no webhook endpoints configured, add one above.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -88,6 +88,15 @@ exports[`renders configure wordList 1`] = `
|
||||
Slack
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/webhooks"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Webhooks
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
act,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
replaceHistoryLocation,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import { settings, users } from "../fixtures";
|
||||
|
||||
beforeEach(async () => {
|
||||
replaceHistoryLocation("http://localhost/admin/configure/webhooks");
|
||||
});
|
||||
|
||||
const viewer = users.admins[0];
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
viewer: () => viewer,
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return await act(async () => {
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("webhooks-container")
|
||||
);
|
||||
|
||||
return { testRenderer, container, context };
|
||||
});
|
||||
}
|
||||
|
||||
it("renders webhooks", async () => {
|
||||
const { container } = await createTestRenderer();
|
||||
await act(async () => {
|
||||
await wait(() => {
|
||||
expect(within(container).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("goes to add new webhook endpoint when clicking add", async () => {
|
||||
const {
|
||||
container,
|
||||
context: { transitionControl },
|
||||
} = await createTestRenderer();
|
||||
|
||||
// Prevent router transitions.
|
||||
transitionControl.allowTransition = false;
|
||||
|
||||
act(() => {
|
||||
within(container)
|
||||
.getByText(/Add webhook endpoint/)
|
||||
.props.onClick({ button: 0, preventDefault: noop });
|
||||
});
|
||||
|
||||
// Expect a routing request was made to the right url.
|
||||
await act(async () => {
|
||||
await wait(() => {
|
||||
expect(transitionControl.history[0].pathname).toBe(
|
||||
"/admin/configure/webhooks/add"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await wait(() => {
|
||||
expect(within(container).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("displays a list of webhook endpoints that have been configured", async () => {
|
||||
const resolvers = createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () =>
|
||||
pureMerge<typeof settings>(settings, {
|
||||
webhooks: {
|
||||
endpoints: [
|
||||
{
|
||||
id: "webhook-endpoint-1",
|
||||
enabled: true,
|
||||
url: "http://example.com/webhook-endpoint-1",
|
||||
all: true,
|
||||
events: [],
|
||||
},
|
||||
{
|
||||
id: "webhook-endpoint-2",
|
||||
enabled: false,
|
||||
url: "http://example.com/webhook-endpoint-2",
|
||||
all: true,
|
||||
events: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
const { container } = await createTestRenderer({ resolvers });
|
||||
|
||||
await act(async () => {
|
||||
await wait(() => {
|
||||
expect(within(container).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("goes to the webhook endpoint configuration page when selected", async () => {
|
||||
const resolvers = createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () =>
|
||||
pureMerge<typeof settings>(settings, {
|
||||
webhooks: {
|
||||
endpoints: [
|
||||
{
|
||||
id: "webhook-endpoint-1",
|
||||
enabled: true,
|
||||
url: "http://example.com/webhook-endpoint-1",
|
||||
all: true,
|
||||
events: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
const {
|
||||
container,
|
||||
context: { transitionControl },
|
||||
} = await createTestRenderer({ resolvers });
|
||||
|
||||
// Prevent router transitions.
|
||||
transitionControl.allowTransition = false;
|
||||
|
||||
act(() => {
|
||||
const row = within(container).getByTestID(
|
||||
"webhook-endpoint-webhook-endpoint-1"
|
||||
);
|
||||
|
||||
within(row)
|
||||
.getByText(/Details/, {
|
||||
selector: "a",
|
||||
})
|
||||
.props.onClick({ button: 0, preventDefault: noop });
|
||||
});
|
||||
|
||||
// Expect a routing request was made to the right url.
|
||||
await act(async () => {
|
||||
await wait(() => {
|
||||
expect(transitionControl.history[0].pathname).toBe(
|
||||
"/admin/configure/webhooks/endpoint/webhook-endpoint-1"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await wait(() => {
|
||||
expect(within(container).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
GQLUSER_ROLE,
|
||||
GQLUSER_STATUS,
|
||||
GQLUsersConnection,
|
||||
GQLWEBHOOK_EVENT_NAME,
|
||||
} from "coral-framework/schema";
|
||||
import { createFixture, createFixtures } from "coral-framework/testHelpers";
|
||||
|
||||
@@ -152,6 +153,10 @@ export const settings = createFixture<GQLSettings>({
|
||||
},
|
||||
},
|
||||
},
|
||||
webhooks: {
|
||||
endpoints: [],
|
||||
},
|
||||
webhookEvents: [GQLWEBHOOK_EVENT_NAME.STORY_CREATED],
|
||||
stories: {
|
||||
scraping: {
|
||||
enabled: true,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export default {
|
||||
admin: {
|
||||
moderate: "/admin/moderate",
|
||||
configureWebhooks: "/admin/configure/webhooks",
|
||||
webhooks: "/admin/configure/webhooks",
|
||||
addWebhookEndpoint: "/admin/configure/webhooks/add",
|
||||
configureWebhookEndpoint: "/admin/configure/webhooks/endpoint",
|
||||
},
|
||||
embed: {
|
||||
stream: "/embed/stream",
|
||||
|
||||
@@ -14,13 +14,13 @@ export const VALIDATION_REQUIRED = () => (
|
||||
|
||||
export const VALIDATION_TOO_SHORT = (minLength: number) => (
|
||||
<Localized id="framework-validation-tooShort" $minLength={minLength}>
|
||||
<span>{"Please enter at least {$minLength} characters."}</span>
|
||||
<span>Please enter at least {minLength} characters.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const VALIDATION_TOO_LONG = (maxLength: number) => (
|
||||
<Localized id="framework-validation-tooLong" $maxLength={maxLength}>
|
||||
<span>{"Please enter at max {$maxLength} characters."}</span>
|
||||
<span>Please enter at max {maxLength} characters.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
@@ -38,19 +38,19 @@ export const INVALID_CHARACTERS = () => (
|
||||
|
||||
export const USERNAME_TOO_SHORT = (minLength: number) => (
|
||||
<Localized id="framework-validation-usernameTooShort" $minLength={minLength}>
|
||||
<span>{"Usernames must contain at least {$minLength} characters."}</span>
|
||||
<span>Usernames must contain at least {minLength} characters.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const USERNAME_TOO_LONG = (maxLength: number) => (
|
||||
<Localized id="framework-validation-usernameTooLong" $maxLength={maxLength}>
|
||||
<span>{"Usernames cannot be longer than {$maxLength} characters."}</span>
|
||||
<span>Usernames cannot be longer than {maxLength} characters.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const PASSWORD_TOO_SHORT = (minLength: number) => (
|
||||
<Localized id="framework-validation-passwordTooShort" $minLength={minLength}>
|
||||
<span>{"Password must contain at least {$minLength} characters."}</span>
|
||||
<span>Password must contain at least {minLength} characters.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
@@ -60,6 +60,12 @@ export const PASSWORDS_DO_NOT_MATCH = () => (
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const INVALID_WEBHOOK_ENDPOINT_EVENT_SELECTION = () => (
|
||||
<Localized id="framework-validation-invalidWebhookEndpointEventSelection">
|
||||
<span>Select at least one event to receive.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const USERNAMES_DO_NOT_MATCH = () => (
|
||||
<Localized id="framework-validation-usernamesDoNotMatch">
|
||||
<span>Usernames do not match. Try again.</span>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
INVALID_CHARACTERS,
|
||||
INVALID_EMAIL,
|
||||
INVALID_URL,
|
||||
INVALID_WEBHOOK_ENDPOINT_EVENT_SELECTION,
|
||||
NOT_A_WHOLE_NUMBER,
|
||||
NOT_A_WHOLE_NUMBER_BETWEEN,
|
||||
NOT_A_WHOLE_NUMBER_GREATER_THAN,
|
||||
@@ -155,6 +156,15 @@ export const validateEqualPasswords = createValidator(
|
||||
PASSWORDS_DO_NOT_MATCH()
|
||||
);
|
||||
|
||||
/**
|
||||
* validateWebhookEventSelection is a Validator that checks for a valid
|
||||
* combination of event selections for webhook endpoints.
|
||||
*/
|
||||
export const validateWebhookEventSelection = createValidator(
|
||||
(v, values) => values.all || (values.events && values.events.length > 0),
|
||||
INVALID_WEBHOOK_ENDPOINT_EVENT_SELECTION()
|
||||
);
|
||||
|
||||
/**
|
||||
* validateEqualEmails is a Validator that checks for correct email confirmation.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.root {
|
||||
border: 1px solid var(--v2-colors-grey-300);
|
||||
border-radius: var(--round-corners);
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Flex } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./ListGroup.css";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ListGroup: FunctionComponent<Props> = ({ className, children }) => {
|
||||
return (
|
||||
<Flex direction="column" className={cn(styles.root, className)}>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListGroup;
|
||||
@@ -0,0 +1,8 @@
|
||||
.root {
|
||||
border-bottom: 1px solid var(--v2-colors-grey-200);
|
||||
padding: var(--v2-spacing-2);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import styles from "./ListGroupRow.css";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ListGroupRow: FunctionComponent<Props> = ({ className, children }) => {
|
||||
return <div className={cn(styles.root, className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export default ListGroupRow;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ListGroup } from "./ListGroup";
|
||||
export { default as ListGroupRow } from "./ListGroupRow";
|
||||
@@ -36,6 +36,7 @@ export { default as HelperText } from "./HelperText";
|
||||
export { default as HorizontalGutter } from "./HorizontalGutter";
|
||||
export { default as Icon } from "./Icon";
|
||||
export { default as Label } from "./Label";
|
||||
export { ListGroup, ListGroupRow } from "./ListGroup";
|
||||
export { Marker, Count as MarkerCount } from "./Marker";
|
||||
export { default as Message, MessageIcon } from "./Message";
|
||||
export { default as Modal, ModalProps } from "./Modal";
|
||||
|
||||
Reference in New Issue
Block a user