|
| 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 | + 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 | +}; |
0 commit comments