mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #728 from omnivore-app/webhook-ui
Create webhook settings ui
This commit is contained in:
commit
f18542f865
10 changed files with 692 additions and 29 deletions
|
|
@ -19,17 +19,21 @@ const CheckboxIndicator = styled(CheckboxPrimitive.Indicator, {
|
|||
color: '#FFFFFF',
|
||||
})
|
||||
|
||||
export const CheckboxComponent:React.FC<{
|
||||
checked: boolean;
|
||||
setChecked:(arg: boolean) => void;
|
||||
}> = ({checked, setChecked}) => {
|
||||
const toggleChecked = () => setChecked(!checked);
|
||||
export const CheckboxComponent: React.FC<{
|
||||
checked: boolean
|
||||
setChecked: (arg: boolean) => void
|
||||
}> = ({ checked, setChecked }) => {
|
||||
const toggleChecked = () => setChecked(!checked)
|
||||
|
||||
return (
|
||||
<Checkbox css={{
|
||||
border: checked ? '2px solid #F9D354': '2px solid #3F3E3C4D',
|
||||
backgroundColor: checked ? '#F9D354' : '#FFFFFF'
|
||||
}} checked={checked} onCheckedChange={toggleChecked}>
|
||||
<Checkbox
|
||||
css={{
|
||||
border: checked ? '2px solid #F9D354' : '2px solid #3F3E3C4D',
|
||||
backgroundColor: checked ? '#F9D354' : '#FFFFFF',
|
||||
}}
|
||||
checked={checked}
|
||||
onCheckedChange={toggleChecked}
|
||||
>
|
||||
<CheckboxIndicator>
|
||||
<CheckIcon />
|
||||
</CheckboxIndicator>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,22 @@
|
|||
import { styled } from '../tokens/stitches.config'
|
||||
import { useState } from 'react'
|
||||
import Checkbox from './Checkbox'
|
||||
import { Box, HStack, VStack } from './LayoutPrimitives'
|
||||
import { StyledText } from './StyledText'
|
||||
|
||||
export interface FormInputProps {
|
||||
name: string
|
||||
label: string
|
||||
value?: any
|
||||
onChange?: (value: any) => void
|
||||
type?: string
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
hidden?: boolean
|
||||
required?: boolean
|
||||
css?: any
|
||||
options?: string[]
|
||||
}
|
||||
|
||||
export const FormInput = styled('input', {
|
||||
border: 'none',
|
||||
|
|
@ -19,3 +37,66 @@ export const BorderedFormInput = styled(FormInput, {
|
|||
border: `1px solid $grayBorder`,
|
||||
p: '$3',
|
||||
})
|
||||
|
||||
export function GeneralFormInput(props: FormInputProps): JSX.Element {
|
||||
const [input, setInput] = useState<FormInputProps>(props)
|
||||
|
||||
if (props.type === 'checkbox') {
|
||||
return (
|
||||
<VStack>
|
||||
{input.options?.map((label, index) => (
|
||||
<HStack key={index}>
|
||||
<StyledText>{label}</StyledText>
|
||||
<Box css={{ padding: '10px 0 0 10px' }}>
|
||||
<Checkbox
|
||||
key={index}
|
||||
checked={input.value[index]}
|
||||
setChecked={(arg) => {
|
||||
input.value[index] = arg
|
||||
setInput(input)
|
||||
props.onChange &&
|
||||
props.onChange(
|
||||
input.options?.filter((_, i) => input.value[i])
|
||||
)
|
||||
}}
|
||||
></Checkbox>
|
||||
</Box>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<FormInput
|
||||
key={input.name}
|
||||
type={input.type || 'text'}
|
||||
value={input.value}
|
||||
placeholder={input.placeholder}
|
||||
onChange={(event) => {
|
||||
if (input.onChange) {
|
||||
setInput({ ...input, value: event.target.value })
|
||||
input.onChange(event.target.value)
|
||||
}
|
||||
}}
|
||||
disabled={input.disabled}
|
||||
hidden={input.hidden}
|
||||
required={input.required}
|
||||
css={{
|
||||
border: '1px solid $grayBorder',
|
||||
borderRadius: '8px',
|
||||
width: '100%',
|
||||
bg: 'transparent',
|
||||
fontSize: '16px',
|
||||
textIndent: '8px',
|
||||
marginBottom: '2px',
|
||||
color: '$grayTextContrast',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
|
||||
},
|
||||
}}
|
||||
name={input.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { styled } from '../tokens/stitches.config'
|
|||
import { StyledText } from './StyledText'
|
||||
import { InfoLink } from './InfoLink'
|
||||
import { Button } from './Button'
|
||||
import { Plus, Trash } from 'phosphor-react'
|
||||
import { PencilSimple, Plus, Trash } from 'phosphor-react'
|
||||
import { isDarkTheme } from '../../lib/themeUpdater'
|
||||
|
||||
interface TableProps {
|
||||
|
|
@ -11,8 +11,9 @@ interface TableProps {
|
|||
infoLink?: string
|
||||
onAdd?: () => void
|
||||
headers: string[]
|
||||
rows: string[][]
|
||||
rows: Map<string, Record<string, any>>
|
||||
onDelete?: (id: string) => void
|
||||
onEdit?: (obj: any) => void
|
||||
}
|
||||
|
||||
const HeaderWrapper = styled(Box, {
|
||||
|
|
@ -157,14 +158,20 @@ export function Table(props: TableProps): JSX.Element {
|
|||
color: '$grayTextContrast',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
width: '240px',
|
||||
}}
|
||||
>
|
||||
{header}
|
||||
</StyledText>
|
||||
</Box>
|
||||
))}
|
||||
<Box
|
||||
css={{
|
||||
width: '120px',
|
||||
}}
|
||||
></Box>
|
||||
</TableHeading>
|
||||
{props.rows.map((row, index) => (
|
||||
{Array.from(props.rows.keys()).map((key, index) => (
|
||||
<TableCard
|
||||
key={index}
|
||||
css={{
|
||||
|
|
@ -175,9 +182,8 @@ export function Table(props: TableProps): JSX.Element {
|
|||
borderTopLeftRadius: index === 0 ? '5px' : '',
|
||||
borderTopRightRadius: index === 0 ? '5px' : '',
|
||||
},
|
||||
borderBottomLeftRadius: index == props.rows.length - 1 ? '5px' : '',
|
||||
borderBottomRightRadius:
|
||||
index == props.rows.length - 1 ? '5px' : '',
|
||||
borderBottomLeftRadius: index == props.rows.size - 1 ? '5px' : '',
|
||||
borderBottomRightRadius: index == props.rows.size - 1 ? '5px' : '',
|
||||
padding: '10px 20px 10px 40px',
|
||||
}}
|
||||
>
|
||||
|
|
@ -191,7 +197,7 @@ export function Table(props: TableProps): JSX.Element {
|
|||
},
|
||||
}}
|
||||
>
|
||||
{row.map((cell, index) => (
|
||||
{Object.values(props.rows.get(key) || {}).map((cell, index) => (
|
||||
<HStack
|
||||
key={index}
|
||||
css={{
|
||||
|
|
@ -210,12 +216,24 @@ export function Table(props: TableProps): JSX.Element {
|
|||
></Input>
|
||||
</HStack>
|
||||
))}
|
||||
{props.onEdit && (
|
||||
<IconButton
|
||||
style="ctaWhite"
|
||||
css={{ mr: '$1', background: '$labelButtonsBg' }}
|
||||
onClick={() => {
|
||||
props.onEdit &&
|
||||
props.onEdit({ ...props.rows.get(key), id: key })
|
||||
}}
|
||||
>
|
||||
<PencilSimple size={24} color={iconColor} />
|
||||
</IconButton>
|
||||
)}
|
||||
{props.onDelete && (
|
||||
<IconButton
|
||||
style="ctaWhite"
|
||||
css={{ mr: '$1', background: '$labelButtonsBg' }}
|
||||
onClick={() => {
|
||||
props.onDelete && props.onDelete(row[0])
|
||||
props.onDelete && props.onDelete(key)
|
||||
}}
|
||||
>
|
||||
<Trash size={16} color={iconColor} />
|
||||
|
|
|
|||
108
packages/web/components/patterns/FormModal.tsx
Normal file
108
packages/web/components/patterns/FormModal.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import {
|
||||
ModalContent,
|
||||
ModalOverlay,
|
||||
ModalRoot,
|
||||
} from '../elements/ModalPrimitives'
|
||||
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
|
||||
import { Button } from '../elements/Button'
|
||||
import { StyledText } from '../elements/StyledText'
|
||||
import { useState } from 'react'
|
||||
import { FormInputProps, GeneralFormInput } from '../elements/FormElements'
|
||||
import { CrossIcon } from '../elements/images/CrossIcon'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
|
||||
export interface FormModalProps {
|
||||
inputs?: FormInputProps[]
|
||||
title: string
|
||||
acceptButtonLabel?: string
|
||||
onSubmit: () => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function FormModal(props: FormModalProps): JSX.Element {
|
||||
const [inputs, setInputs] = useState<FormInputProps[]>(props.inputs || [])
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault()
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
css={{ overflow: 'auto', p: '0' }}
|
||||
>
|
||||
<VStack>
|
||||
<HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<StyledText style="modalHeadline" css={{ p: '16px' }}>
|
||||
{props.title}
|
||||
</StyledText>
|
||||
<Button
|
||||
css={{ pt: '16px', pr: '16px' }}
|
||||
style="ghost"
|
||||
onClick={() => {
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<CrossIcon
|
||||
size={20}
|
||||
strokeColor={theme.colors.grayText.toString()}
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
<Box css={{ width: '100%' }}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
props.onSubmit()
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
{inputs.map((input, index) => (
|
||||
<HStack key={index} css={{ padding: '10px 0 0 10px' }}>
|
||||
<Box
|
||||
css={{
|
||||
p: '0',
|
||||
width: '25%',
|
||||
paddingLeft: '16px',
|
||||
paddingTop: '5px',
|
||||
}}
|
||||
>
|
||||
<StyledText style={'highlightTitle'}>
|
||||
{input.label}
|
||||
</StyledText>
|
||||
</Box>
|
||||
<Box css={{ width: '100%' }}>
|
||||
<GeneralFormInput {...input} />
|
||||
</Box>
|
||||
</HStack>
|
||||
))}
|
||||
<HStack
|
||||
alignment={'center'}
|
||||
distribution="center"
|
||||
css={{
|
||||
padding: '20px 0 20px 0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={'ctaPill'}
|
||||
onClick={() => props.onOpenChange(false)}
|
||||
css={{ marginRight: '20px' }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button style={'ctaDarkYellow'}>
|
||||
{props.acceptButtonLabel || 'Submit'}
|
||||
</Button>
|
||||
</HStack>
|
||||
</form>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { Webhook } from '../queries/useGetWebhooksQuery'
|
||||
|
||||
interface DeleteWebhookResult {
|
||||
deleteWebhook: DeleteWebhook
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type DeleteWebhook = {
|
||||
webhook: Webhook
|
||||
}
|
||||
|
||||
export async function deleteWebhookMutation(
|
||||
id: string
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation {
|
||||
deleteWebhook(id: "${id}") {
|
||||
... on DeleteWebhookSuccess {
|
||||
webhook {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on DeleteWebhookError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation)) as DeleteWebhookResult
|
||||
return data.errorCodes ? undefined : data.deleteWebhook.webhook.id
|
||||
} catch (error) {
|
||||
console.log('deleteWebhookMutation error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
50
packages/web/lib/networking/mutations/setWebhookMutation.ts
Normal file
50
packages/web/lib/networking/mutations/setWebhookMutation.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { Webhook, WebhookEvent } from '../queries/useGetWebhooksQuery'
|
||||
|
||||
export interface SetWebhookInput {
|
||||
contentType?: string[]
|
||||
enabled?: boolean
|
||||
eventTypes: WebhookEvent[]
|
||||
id?: string
|
||||
method?: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface SetWebhookResult {
|
||||
setWebhook: SetWebhook
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type SetWebhook = {
|
||||
webhook: Webhook
|
||||
}
|
||||
|
||||
export async function setWebhookMutation(
|
||||
input: SetWebhookInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation SetWebhook($input: SetWebhookInput!) {
|
||||
setWebhook(input: $input) {
|
||||
... on SetWebhookSuccess {
|
||||
webhook {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on SetWebhookError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation, {
|
||||
input,
|
||||
})) as SetWebhookResult
|
||||
return data.errorCodes ? undefined : data.setWebhook.webhook.id
|
||||
} catch (error) {
|
||||
console.log('setWebhookMutation error', error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
62
packages/web/lib/networking/queries/useGetWebhookQuery.tsx
Normal file
62
packages/web/lib/networking/queries/useGetWebhookQuery.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { makeGqlFetcher } from '../networkHelpers'
|
||||
import { Webhook } from './useGetWebhooksQuery'
|
||||
|
||||
interface WebhookQueryResponse {
|
||||
isValidating?: boolean
|
||||
webhook?: Webhook
|
||||
revalidate?: () => void
|
||||
}
|
||||
|
||||
interface WebhookQueryResponseData {
|
||||
webhook: WebhookData
|
||||
}
|
||||
|
||||
interface WebhookData {
|
||||
webhook: unknown
|
||||
}
|
||||
|
||||
export function useGetWebhookQuery(id: string): WebhookQueryResponse {
|
||||
const query = gql`
|
||||
query GetWebhook($id: ID!) {
|
||||
webhook(id: $id) {
|
||||
... on WebhookSuccess {
|
||||
webhook {
|
||||
id
|
||||
url
|
||||
eventTypes
|
||||
contentType
|
||||
method
|
||||
enabled
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
... on WebhookError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const { data, mutate, isValidating } = useSWR(query, makeGqlFetcher({ id }))
|
||||
console.log('webhook data', data)
|
||||
|
||||
try {
|
||||
if (data) {
|
||||
const result = data as WebhookQueryResponseData
|
||||
const webhook = result.webhook.webhook as Webhook
|
||||
return {
|
||||
isValidating,
|
||||
webhook,
|
||||
revalidate: () => {
|
||||
mutate()
|
||||
},
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
85
packages/web/lib/networking/queries/useGetWebhooksQuery.tsx
Normal file
85
packages/web/lib/networking/queries/useGetWebhooksQuery.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
export type WebhookEvent =
|
||||
| 'PAGE_CREATED'
|
||||
| 'PAGE_UPDATED'
|
||||
| 'PAGE_DELETED'
|
||||
| 'HIGHLIGHT_CREATED'
|
||||
| 'HIGHLIGHT_UPDATED'
|
||||
| 'HIGHLIGHT_DELETED'
|
||||
|
||||
export interface Webhook {
|
||||
id: string
|
||||
url: string
|
||||
eventTypes: WebhookEvent[]
|
||||
contentType: string
|
||||
method: string
|
||||
enabled: boolean
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
interface WebhooksQueryResponse {
|
||||
isValidating: boolean
|
||||
webhooks: Webhook[]
|
||||
revalidate: () => void
|
||||
}
|
||||
|
||||
interface WebhooksQueryResponseData {
|
||||
webhooks: WebhooksData
|
||||
}
|
||||
|
||||
interface WebhooksData {
|
||||
webhooks: unknown
|
||||
}
|
||||
|
||||
export function useGetWebhooksQuery(): WebhooksQueryResponse {
|
||||
const query = gql`
|
||||
query GetWebhooks {
|
||||
webhooks {
|
||||
... on WebhooksSuccess {
|
||||
webhooks {
|
||||
id
|
||||
url
|
||||
eventTypes
|
||||
contentType
|
||||
method
|
||||
enabled
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
... on WebhooksError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher)
|
||||
console.log('webhooks data', data)
|
||||
|
||||
try {
|
||||
if (data) {
|
||||
const result = data as WebhooksQueryResponseData
|
||||
const webhooks = result.webhooks.webhooks as Webhook[]
|
||||
return {
|
||||
isValidating,
|
||||
webhooks,
|
||||
revalidate: () => {
|
||||
mutate()
|
||||
},
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
return {
|
||||
isValidating: false,
|
||||
webhooks: [],
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
revalidate: () => {},
|
||||
}
|
||||
}
|
||||
|
|
@ -27,11 +27,14 @@ export default function SubscriptionsPage(): JSX.Element {
|
|||
}
|
||||
|
||||
const headers = ['Name', 'Email', 'Updated Time']
|
||||
const rows = subscriptions.map((subscription) => [
|
||||
subscription.name,
|
||||
subscription.newsletterEmail,
|
||||
subscription.updatedAt.toString(),
|
||||
])
|
||||
const rows = new Map<string, string[]>()
|
||||
subscriptions.forEach((subscription) =>
|
||||
rows.set(subscription.name, [
|
||||
subscription.name,
|
||||
subscription.newsletterEmail,
|
||||
subscription.updatedAt.toString(),
|
||||
])
|
||||
)
|
||||
|
||||
return (
|
||||
<PrimaryLayout pageTestId="settings-subscriptions-tag">
|
||||
|
|
@ -64,13 +67,18 @@ export default function SubscriptionsPage(): JSX.Element {
|
|||
<Table
|
||||
heading={'Popular Newsletters'}
|
||||
headers={['Substack', 'Axios', 'Bloomberg']}
|
||||
rows={[
|
||||
[
|
||||
'https://substack.com/',
|
||||
'https://www.axios.com/newsletters',
|
||||
'https://www.bloomberg.com/account/newsletters',
|
||||
],
|
||||
]}
|
||||
rows={
|
||||
new Map([
|
||||
[
|
||||
'0',
|
||||
[
|
||||
'https://substack.com/',
|
||||
'https://www.axios.com/newsletters',
|
||||
'https://www.bloomberg.com/account/newsletters',
|
||||
],
|
||||
],
|
||||
])
|
||||
}
|
||||
/>
|
||||
</PrimaryLayout>
|
||||
)
|
||||
|
|
|
|||
208
packages/web/pages/settings/webhooks.tsx
Normal file
208
packages/web/pages/settings/webhooks.tsx
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { Table } from '../../components/elements/Table'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
import {
|
||||
useGetWebhooksQuery,
|
||||
WebhookEvent,
|
||||
} from '../../lib/networking/queries/useGetWebhooksQuery'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
|
||||
import { deleteWebhookMutation } from '../../lib/networking/mutations/deleteWebhookMutation'
|
||||
import { FormModal } from '../../components/patterns/FormModal'
|
||||
import { setWebhookMutation } from '../../lib/networking/mutations/setWebhookMutation'
|
||||
import { FormInputProps } from '../../components/elements/FormElements'
|
||||
|
||||
interface Webhook {
|
||||
id?: string
|
||||
url: string
|
||||
eventTypes: string
|
||||
contentType?: string
|
||||
method?: string
|
||||
enabled?: string
|
||||
createdAt?: Date
|
||||
updatedAt?: Date
|
||||
}
|
||||
|
||||
export default function Webhooks(): JSX.Element {
|
||||
const { webhooks, revalidate } = useGetWebhooksQuery()
|
||||
const [onDeleteId, setOnDeleteId] = useState<string | null>(null)
|
||||
const [addModelOpen, setAddModelOpen] = useState(false)
|
||||
const [onEditWebhook, setOnEditWebhook] = useState<Webhook | null>(null)
|
||||
const [url, setUrl] = useState('')
|
||||
const eventTypeOptions = ['PAGE_CREATED', 'HIGHLIGHT_CREATED']
|
||||
const [eventTypes, setEventTypes] = useState<WebhookEvent[]>([])
|
||||
const [contentType, setContentType] = useState('application/json')
|
||||
const [method, setMethod] = useState('POST')
|
||||
const [formInputs, setFormInputs] = useState<FormInputProps[]>([])
|
||||
|
||||
const headers = ['URL', 'Event Types', 'Method', 'Content Type']
|
||||
const rows = useMemo(() => {
|
||||
const rows = new Map<string, Webhook>()
|
||||
webhooks.forEach((webhook) =>
|
||||
rows.set(webhook.id, {
|
||||
url: webhook.url,
|
||||
eventTypes: webhook.eventTypes.join(', '),
|
||||
method: webhook.method,
|
||||
contentType: webhook.contentType,
|
||||
})
|
||||
)
|
||||
return rows
|
||||
}, [webhooks])
|
||||
|
||||
applyStoredTheme(false)
|
||||
|
||||
async function onDelete(id: string): Promise<void> {
|
||||
const result = await deleteWebhookMutation(id)
|
||||
if (result) {
|
||||
showSuccessToast('Webhook deleted', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Failed to delete', { position: 'bottom-right' })
|
||||
}
|
||||
revalidate()
|
||||
}
|
||||
|
||||
async function onCreate(): Promise<void> {
|
||||
const result = await setWebhookMutation({ url, eventTypes })
|
||||
if (result) {
|
||||
showSuccessToast('Webhook created', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Failed to add', { position: 'bottom-right' })
|
||||
}
|
||||
revalidate()
|
||||
}
|
||||
|
||||
async function onUpdate(): Promise<void> {
|
||||
const result = await setWebhookMutation({
|
||||
id: onEditWebhook?.id,
|
||||
url,
|
||||
eventTypes,
|
||||
})
|
||||
if (result) {
|
||||
showSuccessToast('Webhook updated', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Failed to update', { position: 'bottom-right' })
|
||||
}
|
||||
revalidate()
|
||||
}
|
||||
|
||||
return (
|
||||
<PrimaryLayout pageTestId={'webhooks'}>
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
|
||||
{addModelOpen && (
|
||||
<FormModal
|
||||
title={'Add webhook'}
|
||||
onSubmit={onCreate}
|
||||
onOpenChange={setAddModelOpen}
|
||||
inputs={formInputs}
|
||||
acceptButtonLabel={'Add'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{onEditWebhook && (
|
||||
<FormModal
|
||||
title={'Edit webhook'}
|
||||
onSubmit={onUpdate}
|
||||
onOpenChange={() => setOnEditWebhook(null)}
|
||||
inputs={formInputs}
|
||||
acceptButtonLabel={'Update'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{onDeleteId && (
|
||||
<ConfirmationModal
|
||||
message={
|
||||
'Future events will no longer be delivered to this webhook. This action cannot be undone.'
|
||||
}
|
||||
onAccept={async () => {
|
||||
await onDelete(onDeleteId)
|
||||
setOnDeleteId(null)
|
||||
}}
|
||||
onOpenChange={() => setOnDeleteId(null)}
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
heading={'Webhooks'}
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
onDelete={setOnDeleteId}
|
||||
onAdd={() => {
|
||||
setFormInputs([
|
||||
{
|
||||
label: 'URL',
|
||||
onChange: setUrl,
|
||||
name: 'url',
|
||||
placeholder: 'https://example.com/webhook',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Event Types',
|
||||
name: 'eventTypes',
|
||||
value: [true, true],
|
||||
onChange: setEventTypes,
|
||||
options: eventTypeOptions,
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Method',
|
||||
name: 'method',
|
||||
value: method,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: 'Content Type',
|
||||
name: 'contentType',
|
||||
value: contentType,
|
||||
disabled: true,
|
||||
},
|
||||
])
|
||||
setUrl('')
|
||||
setEventTypes(eventTypeOptions as WebhookEvent[])
|
||||
setAddModelOpen(true)
|
||||
}}
|
||||
onEdit={(webhook) => {
|
||||
setFormInputs([
|
||||
{
|
||||
label: 'URL',
|
||||
onChange: setUrl,
|
||||
name: 'url',
|
||||
value: webhook?.url,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'Event Types',
|
||||
name: 'eventTypes',
|
||||
value: eventTypeOptions.map((option) =>
|
||||
webhook?.eventTypes.includes(option)
|
||||
),
|
||||
onChange: setEventTypes,
|
||||
options: eventTypeOptions,
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Method',
|
||||
name: 'method',
|
||||
value: method,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: 'Content Type',
|
||||
name: 'contentType',
|
||||
value: contentType,
|
||||
disabled: true,
|
||||
},
|
||||
])
|
||||
setUrl(webhook?.url)
|
||||
setEventTypes(webhook?.eventTypes)
|
||||
setOnEditWebhook(webhook)
|
||||
}}
|
||||
/>
|
||||
</PrimaryLayout>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue