2
0

feat(integration): 🚸 Easy webhook config

This commit is contained in:
Baptiste Arnaud
2022-03-07 08:12:05 +01:00
parent 380eae545b
commit fd9c19a4c2
8 changed files with 234 additions and 110 deletions

View File

@ -6,10 +6,11 @@ import {
AccordionItem,
AccordionPanel,
Button,
Flex,
HStack,
Spinner,
Stack,
useToast,
Text,
} from '@chakra-ui/react'
import { InputWithVariableButton } from 'components/shared/TextboxWithVariableButton/InputWithVariableButton'
import { useTypebot } from 'contexts/TypebotContext'
@ -36,6 +37,7 @@ import { VariableForTestInputs } from './VariableForTestInputs'
import { DataVariableInputs } from './ResponseMappingInputs'
import { byId } from 'utils'
import { deepEqual } from 'fast-equals'
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
type Props = {
step: WebhookStep
@ -111,6 +113,12 @@ export const WebhookSettings = ({
responseVariableMapping: ResponseVariableMapping[]
) => onOptionsChange({ ...options, responseVariableMapping })
const handleAdvancedConfigChange = (isAdvancedConfig: boolean) =>
onOptionsChange({ ...options, isAdvancedConfig })
const handleBodyFormStateChange = (isCustomBody: boolean) =>
onOptionsChange({ ...options, isCustomBody })
const handleTestRequestClick = async () => {
if (!typebot || !localWebhook) return
setIsTestResponseLoading(true)
@ -139,107 +147,126 @@ export const WebhookSettings = ({
if (!localWebhook) return <Spinner />
return (
<Stack spacing={4}>
<InputWithVariableButton
placeholder="Your Webhook URL..."
initialValue={localWebhook.url ?? ''}
onChange={handleUrlChange}
/>
<SwitchWithLabel
id={'easy-config'}
label="Advanced configuration"
initialValue={options.isAdvancedConfig ?? true}
onCheckChange={handleAdvancedConfigChange}
/>
{(options.isAdvancedConfig ?? true) && (
<Stack>
<HStack justify="space-between">
<Text>Method:</Text>
<DropdownList<HttpMethod>
currentItem={localWebhook.method as HttpMethod}
onItemSelect={handleMethodChange}
items={Object.values(HttpMethod)}
/>
</HStack>
<Accordion allowToggle allowMultiple>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Query params
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<KeyValue>
initialItems={localWebhook.queryParams}
onItemsChange={handleQueryParamsChange}
Item={QueryParamsInputs}
addLabel="Add a param"
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Headers
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<KeyValue>
initialItems={localWebhook.headers}
onItemsChange={handleHeadersChange}
Item={HeadersInputs}
addLabel="Add a value"
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Body
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<SwitchWithLabel
id={'custom-body'}
label="Custom body"
initialValue={options.isCustomBody ?? true}
onCheckChange={handleBodyFormStateChange}
/>
{(options.isCustomBody ?? true) && (
<CodeEditor
value={localWebhook.body ?? ''}
lang="json"
onChange={handleBodyChange}
/>
)}
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Variable values for test
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<VariableForTest>
initialItems={
options?.variablesForTest ?? { byId: {}, allIds: [] }
}
onItemsChange={handleVariablesChange}
Item={VariableForTestInputs}
addLabel="Add an entry"
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)}
<Stack>
<Flex>
<DropdownList<HttpMethod>
currentItem={localWebhook.method as HttpMethod}
onItemSelect={handleMethodChange}
items={Object.values(HttpMethod)}
/>
</Flex>
<InputWithVariableButton
placeholder="Your Webhook URL..."
initialValue={localWebhook.url ?? ''}
onChange={handleUrlChange}
/>
<Button
onClick={handleTestRequestClick}
colorScheme="blue"
isLoading={isTestResponseLoading}
>
Test the request
</Button>
{testResponse && (
<CodeEditor isReadOnly lang="json" value={testResponse} />
)}
{(testResponse || options?.responseVariableMapping) && (
<Accordion allowToggle allowMultiple>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Save in variables
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<ResponseVariableMapping>
initialItems={options.responseVariableMapping}
onItemsChange={handleResponseMappingChange}
Item={ResponseMappingInputs}
addLabel="Add an entry"
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
)}
</Stack>
<Accordion allowToggle allowMultiple>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Query params
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<KeyValue>
initialItems={localWebhook.queryParams}
onItemsChange={handleQueryParamsChange}
Item={QueryParamsInputs}
addLabel="Add a param"
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Headers
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<KeyValue>
initialItems={localWebhook.headers}
onItemsChange={handleHeadersChange}
Item={HeadersInputs}
addLabel="Add a value"
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Body
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<CodeEditor
value={localWebhook.body ?? ''}
lang="json"
onChange={handleBodyChange}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Variable values for test
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<VariableForTest>
initialItems={
options?.variablesForTest ?? { byId: {}, allIds: [] }
}
onItemsChange={handleVariablesChange}
Item={VariableForTestInputs}
addLabel="Add an entry"
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
<Button
onClick={handleTestRequestClick}
colorScheme="blue"
isLoading={isTestResponseLoading}
>
Test the request
</Button>
{testResponse && (
<CodeEditor isReadOnly lang="json" value={testResponse} />
)}
{(testResponse || options?.responseVariableMapping) && (
<Accordion allowToggle allowMultiple>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Save in variables
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} as={Stack} spacing="6">
<TableList<ResponseVariableMapping>
initialItems={options.responseVariableMapping}
onItemsChange={handleResponseMappingChange}
Item={ResponseMappingInputs}
addLabel="Add an entry"
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
)}
</Stack>
)
}

View File

@ -0,0 +1,13 @@
import { withSentry } from '@sentry/nextjs'
import { NextApiRequest, NextApiResponse } from 'next'
import { methodNotAllowed } from 'utils'
const handler = (req: NextApiRequest, res: NextApiResponse) => {
console.log(req.method)
if (req.method === 'POST') {
return res.status(200).send(req.body)
}
return methodNotAllowed(res)
}
export default withSentry(handler)

View File

@ -62,7 +62,12 @@
"id": "soSmiE7zyb3WF77GxFxAjYX",
"blockId": "8XnDM1QsqPms4LQHh8q3Jo",
"type": "Webhook",
"options": { "responseVariableMapping": [], "variablesForTest": [] },
"options": {
"responseVariableMapping": [],
"variablesForTest": [],
"isAdvancedConfig": false,
"isCustomBody": false
},
"webhookId": "webhook1"
}
]

View File

@ -5,6 +5,7 @@ import {
PublicTypebot,
Step,
Typebot,
Webhook,
} from 'models'
import { CollaborationType, DashboardFolder, PrismaClient, User } from 'db'
import { readFileSync } from 'fs'
@ -19,6 +20,7 @@ export const teardownDatabase = async () => {
await prisma.user.deleteMany({
where: { id: { in: ['freeUser', 'proUser'] } },
})
await prisma.webhook.deleteMany()
await prisma.credentials.deleteMany(ownerFilter)
await prisma.dashboardFolder.deleteMany(ownerFilter)
return prisma.typebot.deleteMany(ownerFilter)
@ -37,8 +39,17 @@ export const createUsers = () =>
],
})
export const createWebhook = (typebotId: string) =>
prisma.webhook.create({ data: { method: 'GET', typebotId, id: 'webhook1' } })
export const createWebhook = async (
typebotId: string,
webhookProps?: Partial<Webhook>
) => {
try {
await prisma.webhook.delete({ where: { id: 'webhook1' } })
} catch {}
return prisma.webhook.create({
data: { method: 'GET', typebotId, id: 'webhook1', ...webhookProps },
})
}
export const createCollaboration = (
userId: string,

View File

@ -2,11 +2,31 @@ import test, { expect, Page } from '@playwright/test'
import { createWebhook, importTypebotInDatabase } from '../../services/database'
import path from 'path'
import { generate } from 'short-uuid'
const typebotId = generate()
import { HttpMethod } from 'models'
test.describe('Webhook step', () => {
test('its configuration should work', async ({ page }) => {
test('easy configuration should work', async ({ page }) => {
const typebotId = generate()
await importTypebotInDatabase(
path.join(__dirname, '../../fixtures/typebots/integrations/webhook.json'),
{
id: typebotId,
}
)
await createWebhook(typebotId, { method: HttpMethod.POST })
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.fill(
'input[placeholder="Your Webhook URL..."]',
`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/api/mock/webhook-easy-config`
)
await page.click('text=Test the request')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
'"statusCode": 200'
)
})
test('Generated body should work', async ({ page }) => {
const typebotId = generate()
await importTypebotInDatabase(
path.join(__dirname, '../../fixtures/typebots/integrations/webhook.json'),
{
@ -17,12 +37,38 @@ test.describe('Webhook step', () => {
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.fill(
'input[placeholder="Your Webhook URL..."]',
`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/api/mock/webhook-easy-config`
)
await page.click('text=Advanced configuration')
await page.click('text=GET')
await page.click('text=POST')
await page.click('text=Test the request')
await expect(page.locator('div[role="textbox"] >> nth=-1')).toContainText(
'"message": "This is a sample result, it has been generated ⬇️"'
)
})
test('its configuration should work', async ({ page }) => {
const typebotId = generate()
await importTypebotInDatabase(
path.join(__dirname, '../../fixtures/typebots/integrations/webhook.json'),
{
id: typebotId,
}
)
await createWebhook(typebotId)
await page.goto(`/typebots/${typebotId}/edit`)
await page.click('text=Configure...')
await page.fill(
'input[placeholder="Your Webhook URL..."]',
`${process.env.PLAYWRIGHT_BUILDER_TEST_BASE_URL}/api/mock/webhook`
)
await page.click('text=Advanced configuration')
await page.click('text=GET')
await page.click('text=POST')
await page.click('text=Query params')
await page.click('text=Add a param')
@ -45,6 +91,7 @@ test.describe('Webhook step', () => {
)
await page.click('text=Body')
await page.click('text=Custom body')
await page.fill('div[role="textbox"]', '{ "customField": "{{secret 4}}" }')
await page.click('text=Variable values for test')

View File

@ -1,11 +1,13 @@
import prisma from 'libs/prisma'
import {
defaultWebhookAttributes,
KeyValue,
PublicTypebot,
ResultValues,
Typebot,
Variable,
Webhook,
WebhookOptions,
WebhookResponse,
WebhookStep,
} from 'models'
@ -35,14 +37,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
where: { id: typebotId },
include: { webhooks: true },
})) as unknown as Typebot & { webhooks: Webhook[] }
const step = typebot.blocks.find(byId(blockId))?.steps.find(byId(stepId))
const webhook = typebot.webhooks.find(byId((step as WebhookStep).webhookId))
const step = typebot.blocks
.find(byId(blockId))
?.steps.find(byId(stepId)) as WebhookStep
const webhook = typebot.webhooks.find(byId(step.webhookId))
if (!webhook)
return res
.status(404)
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
const preparedWebhook = prepareWebhookAttributes(webhook, step.options)
const result = await executeWebhook(typebot)(
webhook,
preparedWebhook,
variables,
blockId,
resultValues
@ -52,6 +57,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return methodNotAllowed(res)
}
const prepareWebhookAttributes = (
webhook: Webhook,
options: WebhookOptions
): Webhook => {
if (options.isAdvancedConfig === false) {
return { ...webhook, body: '{{state}}', ...defaultWebhookAttributes }
} else if (options.isCustomBody === false) {
return { ...webhook, body: '{{state}}' }
}
return webhook
}
const executeWebhook =
(typebot: Typebot) =>
async (