forked from github/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWebhook.tsx
More file actions
260 lines (237 loc) · 9.52 KB
/
Webhook.tsx
File metadata and controls
260 lines (237 loc) · 9.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import { ActionList, ActionMenu, Flash } from '@primer/react'
import { useState, KeyboardEvent, useEffect } from 'react'
import useSWR from 'swr'
import { useRouter } from 'next/router'
import { slug } from 'github-slugger'
import cx from 'classnames'
import { useMainContext } from 'components/context/MainContext'
import { useVersion } from 'components/hooks/useVersion'
import { LinkIconHeading } from 'components/article/LinkIconHeading'
import { useTranslation } from 'components/hooks/useTranslation'
import type { WebhookAction, WebhookData } from './types'
import { ParameterTable } from 'components/parameter-table/ParameterTable'
import styles from './WebhookPayloadExample.module.scss'
type Props = {
webhook: WebhookAction
}
// fetcher passed to useSWR() to get webhook data using the given URL
async function webhookFetcher(url: string) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`${response.status} on ${url}`)
}
return response.json()
}
// We manually created decorated webhooks files for GHES versions older than
// 3.7, returns whether the given version is one of these versions of GHES.
//
// TODO: once 3.7 is the oldest supported version of GHES, we won't need this
// anymore.
function isScrapedGhesVersion(version: ReturnType<typeof useVersion>) {
const scrapedVersions = ['3.6', '3.5', '3.4', '3.3', '3.2']
if (!version.isEnterprise) return false
// getting the number part e.g. '3.6' from a version string like
// 'enterprise-server@3.6'
const versionNumber = version.currentVersion.split('@')[1]
return scrapedVersions.includes(versionNumber)
}
export function Webhook({ webhook }: Props) {
// Get version for requests to switch webhook action type
const version = useVersion()
const { t } = useTranslation('products')
const router = useRouter()
const context = useMainContext()
// Get more user friendly language for the different availability options in
// the webhook schema (we can't change it directly in the schema). Note that
// we specifically don't want to translate these strings with useTranslation()
// like we usually do with strings from data/ui.yml.
const rephraseAvailability = context.data.ui.products.webhooks.rephrase_availability
// The param that was clicked so we can expand its property <details> element
const [clickedBodyParameterName, setClickedBodyParameterName] = useState<undefined | string>('')
// The selected webhook action type the user selects via a dropdown
const [selectedWebhookActionType, setSelectedWebhookActionType] = useState(
webhook.actionTypes.length > 1 ? webhook.actionTypes[0] : ''
)
// The index of the selected action type so we can highlight which one is selected
// in the action type dropdown
const [selectedActionTypeIndex, setSelectedActionTypeIndex] = useState(0)
const webhookSlug = slug(webhook.data.category)
const webhookFetchUrl = `/api/webhooks/v1?${new URLSearchParams({
category: webhook.data.category,
version: version.currentVersion,
})}`
// When you load the page we want to support linking to a specific webhook type
// so this effect sets the webhook type if it's provided in the URL e.g.:
//
// webhook-events-and-payloads?actionType=published#package
//
// where the webhook is set in the hash (which is equal to webhookSlug) and
// the webhook action type is passed in the actionType parameter.
useEffect(() => {
const url = new URL(location.href)
const actionType = url.searchParams.get('actionType')
const hash = url.hash?.slice(1)
if (actionType && hash && webhook.actionTypes.includes(actionType) && hash === webhookSlug) {
setSelectedWebhookActionType(actionType)
setSelectedActionTypeIndex(webhook.actionTypes.indexOf(actionType))
}
}, [])
// callback for the action type dropdown -- sets the action type to the given
// type, index is the index of the selected type so we can highlight it as
// selected.
//
// Besides setting the action type state, we also want to:
//
// * clear the clicked body param so that no properties are expanded when we
// re-render the webhook
// * update the URL so people can link to a specific webhook action type
function handleActionTypeChange(type: string, index: number) {
setClickedBodyParameterName('')
setSelectedWebhookActionType(type)
setSelectedActionTypeIndex(index)
const { asPath, locale } = router
let [pathRoot, pathQuery = ''] = asPath.split('?')
const params = new URLSearchParams(pathQuery)
if (pathRoot.includes('#')) {
pathRoot = pathRoot.split('#')[0]
}
params.set('actionType', type)
router.push(
{ pathname: `/${locale}${pathRoot}`, query: params.toString(), hash: webhookSlug },
undefined,
{
shallow: true,
}
)
}
// callback to trigger useSWR() hook after a nested property is clicked
function handleBodyParamExpansion(event: KeyboardEvent<HTMLElement>) {
// need to cast it because 'closest' isn't necessarily available on
// event.target
const target = event.target as HTMLElement
setClickedBodyParameterName(target.closest('details')?.dataset.nestedParamId)
}
// fires when the webhook action type changes or someone clicks on a nested
// body param for the first time. In either case, we now have all the data
// for a webhook (i.e. all the data for each action type and all of their
// nested parameters)
const { data, error } = useSWR<WebhookData, Error>(
clickedBodyParameterName || selectedWebhookActionType ? webhookFetchUrl : null,
webhookFetcher,
{
revalidateOnFocus: false,
}
)
const currentWebhookActionType = selectedWebhookActionType || webhook.data.action
const currentWebhookAction = (data && data[currentWebhookActionType]) || webhook.data
return (
<div>
<h2 id={webhookSlug}>
<LinkIconHeading slug={webhookSlug} />
<code>{currentWebhookAction.category}</code>
</h2>
<div>
<div dangerouslySetInnerHTML={{ __html: currentWebhookAction.summaryHtml }}></div>
<h3
dangerouslySetInnerHTML={{
__html: t('webhooks.availability').replace(
'{{ WebhookName }}',
currentWebhookAction.category
),
}}
/>
<ul>
{currentWebhookAction.availability.map((availability) => {
// TODO: once 3.7 is the oldest supported version of GHES, we won't need this anymore.
if (isScrapedGhesVersion(version)) {
return (
<li
dangerouslySetInnerHTML={{ __html: availability }}
key={`availability-${availability}`}
></li>
)
} else {
return (
<li key={`availability-${availability}`}>
{rephraseAvailability[availability] ?? availability}
</li>
)
}
})}
</ul>
<h3
dangerouslySetInnerHTML={{
__html: t('webhooks.webhook_payload_object').replace(
'{{ WebhookName }}',
currentWebhookAction.category
),
}}
/>
{error && (
<Flash className="mb-5" variant="danger">
<p>{t('webhooks.action_type_switch_error')}</p>
<p>
<code className="f6" style={{ background: 'none' }}>
{error.toString()}
</code>
</p>
</Flash>
)}
{webhook.actionTypes.length > 1 && (
<div className="mb-4">
<div className="mb-3">
<ActionMenu>
<ActionMenu.Button
aria-label="Select a webhook action type"
className="text-normal"
>
{t('webhooks.action_type')}:{' '}
<span className="text-bold">{currentWebhookActionType}</span>
</ActionMenu.Button>
<ActionMenu.Overlay>
<ActionList selectionVariant="single">
{webhook.actionTypes.map((type, index) => {
return (
<ActionList.Item
selected={index === selectedActionTypeIndex}
key={`${webhook.name}-${type}`}
onSelect={() => handleActionTypeChange(type, index)}
>
{type}
</ActionList.Item>
)
})}
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</div>
</div>
)}
<div
className="mb-4 f5 color-fg-muted"
dangerouslySetInnerHTML={{ __html: currentWebhookAction.descriptionHtml }}
></div>
<div>
<ParameterTable
slug={slug(`${currentWebhookAction.category}-${selectedWebhookActionType}`)}
bodyParameters={currentWebhookAction.bodyParameters || []}
bodyParamExpandCallback={handleBodyParamExpansion}
clickedBodyParameterName={clickedBodyParameterName}
/>
</div>
</div>
{webhook.data.payloadExample && (
<>
<h3>{t('webhooks.webhook_payload_example')}</h3>
<div
className={cx(styles.payloadExample, 'border-top rounded-1 my-0')}
style={{ maxHeight: '32rem' }}
data-highlight={'json'}
>
<code>{JSON.stringify(webhook.data.payloadExample, null, 2)}</code>
</div>
</>
)}
</div>
)
}