Skip to content

Commit 3bb8513

Browse files
authored
Added webhook api key and documentation (#665)
* Added webhook api key and documentation * Add missing payload fields and code refactor
1 parent 7b3e6aa commit 3bb8513

File tree

12 files changed

+723
-7
lines changed

12 files changed

+723
-7
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as yup from 'yup';
2+
3+
import { Endpoint, nullSchema } from '@/api-helpers/global';
4+
import { Integration } from '@/constants/integrations';
5+
import { dec } from '@/utils/auth-supplementary';
6+
import { db } from '@/utils/db';
7+
8+
const pathnameSchema = yup.object().shape({
9+
org_id: yup.string().uuid().required()
10+
});
11+
12+
const endpoint = new Endpoint(pathnameSchema);
13+
14+
endpoint.handle.GET(nullSchema, async (req, res) => {
15+
const response = await db('Integration')
16+
.select('*')
17+
.where('org_id', req.payload.org_id)
18+
.where('name', Integration.WEBHOOK)
19+
.first();
20+
21+
if (!response) return res.send({ webhook_api_key: '' });
22+
const apiKey = dec(response.access_token_enc_chunks);
23+
24+
res.send({ webhook_api_key: apiKey });
25+
});
26+
27+
export default endpoint.serve();

web-server/pages/integrations.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ROUTES } from '@/constants/routes';
1313
import { FetchState } from '@/constants/ui-states';
1414
import { GithubIntegrationCard } from '@/content/Dashboards/GithubIntegrationCard';
1515
import { GitlabIntegrationCard } from '@/content/Dashboards/GitlabIntegrationCard';
16+
import { WebhookIntegrationCard } from '@/content/Dashboards/WebhookIntegrationCard';
1617
import { PageWrapper } from '@/content/PullRequests/PageWrapper';
1718
import { useAuth } from '@/hooks/useAuth';
1819
import { useBoolState, useEasyState } from '@/hooks/useEasyState';
@@ -163,6 +164,7 @@ const Content = () => {
163164
<FlexBox gap={2}>
164165
<GithubIntegrationCard />
165166
<GitlabIntegrationCard />
167+
<WebhookIntegrationCard />
166168
</FlexBox>
167169
{showCreationCTA && (
168170
<FlexBox mt={'56px'} col fit alignStart>

web-server/src/constants/integrations.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export enum Integration {
99
PAGERDUTY = 'pagerduty',
1010
OPSGENIE = 'opsgenie',
1111
MICROSOFT = 'azure-ad',
12-
CIRCLECI = 'circle_ci'
12+
CIRCLECI = 'circle_ci',
13+
WEBHOOK = 'webhook'
1314
}
1415

1516
export enum CIProvider {
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import { LoadingButton } from '@mui/lab';
2+
import { Box, Divider, Typography, Paper, Tab, Tabs } from '@mui/material';
3+
import { useSnackbar } from 'notistack';
4+
import { memo, useCallback } from 'react';
5+
6+
import { FlexBox } from '@/components/FlexBox';
7+
import { Line } from '@/components/Text';
8+
import { Integration } from '@/constants/integrations';
9+
import { useModal } from '@/contexts/ModalContext';
10+
import { useAuth } from '@/hooks/useAuth';
11+
import { useBoolState, useEasyState } from '@/hooks/useEasyState';
12+
import { getWebhookAPIKey } from '@/slices/org';
13+
import { useDispatch, useSelector } from '@/store';
14+
import {
15+
linkProvider,
16+
unlinkProvider,
17+
generateRandomToken
18+
} from '@/utils/auth';
19+
import { depFn } from '@/utils/fn';
20+
21+
import {
22+
CopyTextComponent,
23+
WebhookDocumentation
24+
} from './WebhookDocumentation';
25+
26+
export const ConfigureWebhookModalBody = () => {
27+
const dispatch = useDispatch();
28+
const { addModal } = useModal();
29+
const { enqueueSnackbar } = useSnackbar();
30+
const { orgId } = useAuth();
31+
const isLoading = useBoolState(false);
32+
const webhookAPIKey = useSelector((state) => state.org.webhookAPIKey);
33+
34+
const handleCreateKey = useCallback(async () => {
35+
depFn(isLoading.true);
36+
const randomApiKey = generateRandomToken();
37+
38+
linkProvider(randomApiKey, orgId, Integration.WEBHOOK)
39+
.then(async () => {
40+
await dispatch(getWebhookAPIKey({ orgId }));
41+
enqueueSnackbar('API Key created successfully', {
42+
variant: 'success',
43+
autoHideDuration: 2000
44+
});
45+
})
46+
.catch(() => {
47+
enqueueSnackbar('Failed to create API key', {
48+
variant: 'error',
49+
autoHideDuration: 2000
50+
});
51+
})
52+
.finally(isLoading.false);
53+
}, [dispatch, enqueueSnackbar, isLoading.false, isLoading.true, orgId]);
54+
55+
const handleDeleteKey = useCallback(async () => {
56+
depFn(isLoading.true);
57+
58+
unlinkProvider(orgId, Integration.WEBHOOK)
59+
.then(async () => {
60+
await dispatch(getWebhookAPIKey({ orgId }));
61+
enqueueSnackbar('API Key deleted successfully', {
62+
variant: 'success',
63+
autoHideDuration: 2000
64+
});
65+
})
66+
.catch(() => {
67+
enqueueSnackbar('Failed to delete API key', {
68+
variant: 'error',
69+
autoHideDuration: 2000
70+
});
71+
})
72+
.finally(isLoading.false);
73+
}, [dispatch, enqueueSnackbar, isLoading.false, isLoading.true, orgId]);
74+
75+
const openConfirmDeleteAPIKeyModal = useCallback(async () => {
76+
addModal({
77+
title: `Confirm Delete API Key`,
78+
body: (
79+
<FlexBox
80+
gap2
81+
flexDirection="column"
82+
maxWidth={500}
83+
sx={{ color: 'text.secondary' }}
84+
>
85+
<Line>
86+
Deleting this API key is permanent. Any services using it to send
87+
webhook data will no longer be processed by our system. You can
88+
generate a new API key anytime if needed.
89+
</Line>
90+
<Line sx={{ fontWeight: 500 }}>
91+
Are you sure you want to proceed?
92+
</Line>
93+
</FlexBox>
94+
),
95+
actions: {
96+
confirm: handleDeleteKey,
97+
confirmType: 'error'
98+
},
99+
showCloseIcon: true
100+
});
101+
}, [addModal, handleDeleteKey]);
102+
103+
return (
104+
<FlexBox gap2 maxWidth="1400px">
105+
<FlexBox gap2 minWidth={'400px'} col>
106+
<FlexBox>Webhook API key for payload validation</FlexBox>
107+
{webhookAPIKey ? (
108+
<CopyTextComponent text={webhookAPIKey} />
109+
) : (
110+
<FlexBox
111+
p={2}
112+
mt={2 / 3}
113+
sx={{
114+
backgroundColor: 'rgba(255, 255, 255, 0.04)',
115+
borderRadius: 1
116+
}}
117+
>
118+
Your API key will appear here once generated.
119+
</FlexBox>
120+
)}
121+
122+
<FlexBox justifyEnd>
123+
{webhookAPIKey ? (
124+
<LoadingButton
125+
loading={isLoading.value}
126+
variant="outlined"
127+
color="error"
128+
onClick={openConfirmDeleteAPIKeyModal}
129+
>
130+
Delete Key
131+
</LoadingButton>
132+
) : (
133+
<LoadingButton
134+
loading={isLoading.value}
135+
variant="outlined"
136+
onClick={handleCreateKey}
137+
>
138+
Generate New Key
139+
</LoadingButton>
140+
)}
141+
</FlexBox>
142+
</FlexBox>
143+
<Divider orientation="vertical" flexItem />
144+
<DocumentContainer />
145+
</FlexBox>
146+
);
147+
};
148+
149+
const DocumentContainer = memo(() => {
150+
const selectedTab = useEasyState(0);
151+
return (
152+
<FlexBox col gap1 maxWidth={'100%'} overflow={'auto'}>
153+
<div
154+
style={{
155+
overflow: 'hidden',
156+
borderRadius: '12px',
157+
height: 'calc(100vh - 300px)',
158+
overflowY: 'auto',
159+
transition: 'all 0.8s ease',
160+
position: 'relative',
161+
maxWidth: '100%'
162+
}}
163+
>
164+
<Paper sx={{ p: 1 }}>
165+
<Tabs
166+
value={selectedTab.value}
167+
onChange={(_, newValue) => selectedTab.set(newValue)}
168+
sx={{
169+
borderBottom: 1,
170+
borderColor: 'divider',
171+
'& .MuiTabs-indicator': {
172+
backgroundColor: 'primary.main'
173+
}
174+
}}
175+
>
176+
<Tab label="Workflow" />
177+
<Tab label="Incident" />
178+
</Tabs>
179+
180+
{selectedTab.value === 0 ? (
181+
<DeploymentsWebhookDocs />
182+
) : (
183+
<IncidentsWebhookDocs />
184+
)}
185+
</Paper>
186+
</div>
187+
</FlexBox>
188+
);
189+
});
190+
191+
const DeploymentsWebhookDocs = () => {
192+
const requestBody = {
193+
workflow_runs: [
194+
{
195+
workflow_name: 'string',
196+
repo_urls: ['string'],
197+
event_actor: 'string',
198+
head_branch: 'string',
199+
workflow_run_unique_id: 'string',
200+
status: 'string',
201+
duration: 'string',
202+
workflow_run_conducted_at: 'string (ISO 8601 format)',
203+
html_url: 'string'
204+
}
205+
]
206+
};
207+
208+
const fields = [
209+
{
210+
primaryText: 'workflow_name',
211+
secondaryText: 'Name of the workflow'
212+
},
213+
{
214+
primaryText: 'repo_urls',
215+
secondaryText:
216+
'Array of repository urls ( Ex: https://github.com/middlewarehq/middleware ) associated with the workflow. Ensure they are valid and linked with Middleware.'
217+
},
218+
{
219+
primaryText: 'event_actor',
220+
secondaryText: 'Username of the person who triggered the workflow'
221+
},
222+
{
223+
primaryText: 'head_branch',
224+
secondaryText: 'Branch on which the workflow was run'
225+
},
226+
{
227+
primaryText: 'workflow_run_unique_id',
228+
secondaryText: 'Unique identifier for the workflow run'
229+
},
230+
{
231+
primaryText: 'status',
232+
secondaryText:
233+
'Status of the workflow run ( PENDING, SUCCESS, FAILURE, CANCELLED )'
234+
},
235+
{
236+
primaryText: 'workflow_run_conducted_at',
237+
secondaryText:
238+
'Timestamp of when the workflow was conducted ( ISO 8601 format )'
239+
},
240+
{
241+
primaryText: 'duration ( Optional )',
242+
secondaryText: (
243+
<Line>
244+
Duration the workflow ran for. It can be automatically inferred by
245+
calling this webhook at both the start and end of the workflow with
246+
the correct value of{' '}
247+
<Line color="lightgreen">workflow_run_conducted_at</Line>. Example:
248+
Call the webhook with status <b>"PENDING"</b> at the start, and later
249+
with <b>"SUCCESS"</b>, <b>"FAILURE"</b>, or <b>"CANCELLED"</b> to mark
250+
the end. <Line color="lightgreen">workflow_run_conducted_at</Line> can
251+
be kept as the current time for both the invocations.
252+
</Line>
253+
)
254+
},
255+
{
256+
primaryText: 'html_url ( Optional )',
257+
secondaryText: 'Link to your workflow run'
258+
}
259+
];
260+
261+
return (
262+
<Box p={4}>
263+
<WebhookDocumentation
264+
title="Deployments Webhook Documentation"
265+
description="Webhooks in our system allow external services to send real-time deployment data to the system via HTTP POST request."
266+
endpoint="<your-backend-url>/public/webhook/workflow"
267+
requestBody={requestBody}
268+
requestBodyFields={fields}
269+
/>
270+
271+
<Divider sx={{ my: 2 }} />
272+
273+
<Typography variant="h4" gutterBottom mb={2}>
274+
After you have uploaded your data
275+
</Typography>
276+
277+
<FlexBox gap2 flexDirection={'column'} fontSize="medium">
278+
<Line>
279+
Once you have sent data to our system through the webhook, we will
280+
process that data to be analyzed by our system. You will be able to
281+
see the data shortly after you have called our webhook.
282+
</Line>
283+
<Line>
284+
Next you can go over to <b>Manage Teams {'->'} Edit a Team.</b>
285+
&nbsp;From the repos you provided in{' '}
286+
<Line color="lightgreen">repo_urls</Line>, Select <b>WORKFLOW</b> as
287+
Source of deployment and you will see a list of Workflow names.
288+
</Line>
289+
<Line>
290+
Go over to the DORA Metrics page and click on the Deployment Frequency
291+
box. Select your repo name and you will find the list of the workflow
292+
runs you uploaded.
293+
</Line>
294+
</FlexBox>
295+
</Box>
296+
);
297+
};
298+
299+
const IncidentsWebhookDocs = () => {
300+
return (
301+
<Box p={4} width="1200px">
302+
<Line fontSize={24} mb={1}>
303+
Incidents Webhook Documentation
304+
</Line>
305+
</Box>
306+
);
307+
};

web-server/src/content/Dashboards/GithubIntegrationCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { FlexBox } from '@/components/FlexBox';
1212
import { Line } from '@/components/Text';
1313
import { track } from '@/constants/events';
1414
import { FetchState } from '@/constants/ui-states';
15-
import { githubIntegrationsDisplay } from '@/content/Dashboards/githubIntegration';
15+
import { githubIntegrationsDisplay } from '@/content/Dashboards/integrationDisplayConfigs';
1616
import { useIntegrationHandlers } from '@/content/Dashboards/useIntegrationHandlers';
1717
import { useAuth } from '@/hooks/useAuth';
1818
import { useBoolState } from '@/hooks/useEasyState';

web-server/src/content/Dashboards/GitlabIntegrationCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { FlexBox } from '@/components/FlexBox';
1212
import { Line } from '@/components/Text';
1313
import { track } from '@/constants/events';
1414
import { FetchState } from '@/constants/ui-states';
15-
import { gitLabIntegrationDisplay } from '@/content/Dashboards/githubIntegration';
15+
import { gitLabIntegrationDisplay } from '@/content/Dashboards/integrationDisplayConfigs';
1616
import { useIntegrationHandlers } from '@/content/Dashboards/useIntegrationHandlers';
1717
import { useAuth } from '@/hooks/useAuth';
1818
import { useBoolState } from '@/hooks/useEasyState';

0 commit comments

Comments
 (0)