diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index c04cac0..41d31f9 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -2529,8 +2529,7 @@ "@chakra-ui/css-reset": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz", - "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==", - "requires": {} + "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==" }, "@chakra-ui/descendant": { "version": "2.1.1", @@ -3134,8 +3133,7 @@ "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", - "requires": {} + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" }, "@popperjs/core": { "version": "2.11.0", @@ -3845,8 +3843,7 @@ "react-icons": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz", - "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==", - "requires": {} + "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==" }, "react-is": { "version": "16.13.1", @@ -4032,8 +4029,7 @@ "use-callback-ref": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz", - "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==", - "requires": {} + "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==" }, "use-sidecar": { "version": "1.0.5", diff --git a/dashboard/src/components/DeleteWebhookModal.tsx b/dashboard/src/components/DeleteWebhookModal.tsx new file mode 100644 index 0000000..49e1443 --- /dev/null +++ b/dashboard/src/components/DeleteWebhookModal.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { + Button, + Center, + Flex, + MenuItem, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, + Text, + useToast, +} from '@chakra-ui/react'; +import { useClient } from 'urql'; +import { FaRegTrashAlt } from 'react-icons/fa'; +import { DeleteWebhook } from '../graphql/mutation'; +import { capitalizeFirstLetter } from '../utils'; + +interface deleteWebhookModalInputPropTypes { + webhookId: string; + eventName: string; + fetchWebookData: Function; +} + +const DeleteWebhookModal = ({ + webhookId, + eventName, + fetchWebookData, +}: deleteWebhookModalInputPropTypes) => { + const client = useClient(); + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + const deleteHandler = async () => { + const res = await client + .mutation(DeleteWebhook, { params: { id: webhookId } }) + .toPromise(); + if (res.error) { + toast({ + title: capitalizeFirstLetter(res.error.message), + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + + return; + } else if (res.data?._delete_webhook) { + toast({ + title: capitalizeFirstLetter(res.data?._delete_webhook.message), + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + } + onClose(); + fetchWebookData(); + }; + return ( + <> + Delete + + + + Delete Webhook + + + Are you sure? + + + Webhook for event {eventName} will be deleted + permanently! + + + + + + + + + + + ); +}; + +export default DeleteWebhookModal; diff --git a/dashboard/src/components/Menu.tsx b/dashboard/src/components/Menu.tsx index 8593bb0..460e6c3 100644 --- a/dashboard/src/components/Menu.tsx +++ b/dashboard/src/components/Menu.tsx @@ -30,6 +30,7 @@ import { FiMenu, FiUsers, FiChevronDown, + FiAnchor, } from 'react-icons/fi'; import { BiCustomize } from 'react-icons/bi'; import { AiOutlineKey } from 'react-icons/ai'; @@ -111,6 +112,7 @@ const LinkItems: Array = [ ], }, { name: 'Users', icon: FiUsers, route: '/users' }, + { name: 'Webhooks', icon: FiAnchor, route: '/webhooks' }, ]; interface SidebarProps extends BoxProps { diff --git a/dashboard/src/components/UpdateWebhookModal.tsx b/dashboard/src/components/UpdateWebhookModal.tsx new file mode 100644 index 0000000..532a2e3 --- /dev/null +++ b/dashboard/src/components/UpdateWebhookModal.tsx @@ -0,0 +1,596 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Center, + Flex, + Input, + InputGroup, + InputRightElement, + MenuItem, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Select, + Switch, + Text, + useDisclosure, + useToast, +} from '@chakra-ui/react'; +import { FaMinusCircle, FaPlus } from 'react-icons/fa'; +import { useClient } from 'urql'; +import { + webhookEventNames, + ArrayInputOperations, + WebhookInputDataFields, + WebhookInputHeaderFields, + UpdateWebhookModalViews, + webhookVerifiedStatus, +} from '../constants'; +import { capitalizeFirstLetter, validateURI } from '../utils'; +import { AddWebhook, EditWebhook, TestEndpoint } from '../graphql/mutation'; +import { rest } from 'lodash'; +import { BiCheckCircle, BiError, BiErrorCircle } from 'react-icons/bi'; + +interface headersDataType { + [WebhookInputHeaderFields.KEY]: string; + [WebhookInputHeaderFields.VALUE]: string; +} + +interface headersValidatorDataType { + [WebhookInputHeaderFields.KEY]: boolean; + [WebhookInputHeaderFields.VALUE]: boolean; +} + +interface selecetdWebhookDataTypes { + [WebhookInputDataFields.ID]: string; + [WebhookInputDataFields.EVENT_NAME]: string; + [WebhookInputDataFields.ENDPOINT]: string; + [WebhookInputDataFields.ENABLED]: boolean; + [WebhookInputDataFields.HEADERS]?: Record; +} + +interface UpdateWebhookModalInputPropTypes { + view: UpdateWebhookModalViews; + selectedWebhook?: selecetdWebhookDataTypes; + fetchWebookData: Function; +} + +const initHeadersData: headersDataType = { + [WebhookInputHeaderFields.KEY]: '', + [WebhookInputHeaderFields.VALUE]: '', +}; + +const initHeadersValidatorData: headersValidatorDataType = { + [WebhookInputHeaderFields.KEY]: true, + [WebhookInputHeaderFields.VALUE]: true, +}; + +interface webhookDataType { + [WebhookInputDataFields.EVENT_NAME]: string; + [WebhookInputDataFields.ENDPOINT]: string; + [WebhookInputDataFields.ENABLED]: boolean; + [WebhookInputDataFields.HEADERS]: headersDataType[]; +} + +interface validatorDataType { + [WebhookInputDataFields.ENDPOINT]: boolean; + [WebhookInputDataFields.HEADERS]: headersValidatorDataType[]; +} + +const initWebhookData: webhookDataType = { + [WebhookInputDataFields.EVENT_NAME]: webhookEventNames.USER_LOGIN, + [WebhookInputDataFields.ENDPOINT]: '', + [WebhookInputDataFields.ENABLED]: false, + [WebhookInputDataFields.HEADERS]: [{ ...initHeadersData }], +}; + +const initWebhookValidatorData: validatorDataType = { + [WebhookInputDataFields.ENDPOINT]: true, + [WebhookInputDataFields.HEADERS]: [{ ...initHeadersValidatorData }], +}; + +const UpdateWebhookModal = ({ + view, + selectedWebhook, + fetchWebookData, +}: UpdateWebhookModalInputPropTypes) => { + const client = useClient(); + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [loading, setLoading] = useState(false); + const [verifyingEndpoint, setVerifyingEndpoint] = useState(false); + const [webhook, setWebhook] = useState({ + ...initWebhookData, + }); + const [validator, setValidator] = useState({ + ...initWebhookValidatorData, + }); + const [verifiedStatus, setVerifiedStatus] = useState( + webhookVerifiedStatus.PENDING + ); + const inputChangehandler = ( + inputType: string, + value: any, + headerInputType: string = WebhookInputHeaderFields.KEY, + headerIndex: number = 0 + ) => { + if ( + verifiedStatus !== webhookVerifiedStatus.PENDING && + inputType !== WebhookInputDataFields.ENABLED + ) { + setVerifiedStatus(webhookVerifiedStatus.PENDING); + } + switch (inputType) { + case WebhookInputDataFields.EVENT_NAME: + setWebhook({ ...webhook, [inputType]: value }); + break; + case WebhookInputDataFields.ENDPOINT: + setWebhook({ ...webhook, [inputType]: value }); + setValidator({ + ...validator, + [WebhookInputDataFields.ENDPOINT]: validateURI(value), + }); + break; + case WebhookInputDataFields.ENABLED: + setWebhook({ ...webhook, [inputType]: value }); + break; + case WebhookInputDataFields.HEADERS: + const updatedHeaders: any = [ + ...webhook[WebhookInputDataFields.HEADERS], + ]; + const updatedHeadersValidatorData: any = [ + ...validator[WebhookInputDataFields.HEADERS], + ]; + const otherHeaderInputType = + headerInputType === WebhookInputHeaderFields.KEY + ? WebhookInputHeaderFields.VALUE + : WebhookInputHeaderFields.KEY; + updatedHeaders[headerIndex][headerInputType] = value; + updatedHeadersValidatorData[headerIndex][headerInputType] = + value.length > 0 + ? updatedHeaders[headerIndex][otherHeaderInputType].length > 0 + : updatedHeaders[headerIndex][otherHeaderInputType].length === 0; + updatedHeadersValidatorData[headerIndex][otherHeaderInputType] = + value.length > 0 + ? updatedHeaders[headerIndex][otherHeaderInputType].length > 0 + : updatedHeaders[headerIndex][otherHeaderInputType].length === 0; + setWebhook({ ...webhook, [inputType]: updatedHeaders }); + setValidator({ + ...validator, + [inputType]: updatedHeadersValidatorData, + }); + break; + default: + break; + } + }; + const updateHeaders = (operation: string, index: number = 0) => { + if (verifiedStatus !== webhookVerifiedStatus.PENDING) { + setVerifiedStatus(webhookVerifiedStatus.PENDING); + } + switch (operation) { + case ArrayInputOperations.APPEND: + setWebhook({ + ...webhook, + [WebhookInputDataFields.HEADERS]: [ + ...(webhook?.[WebhookInputDataFields.HEADERS] || []), + { ...initHeadersData }, + ], + }); + setValidator({ + ...validator, + [WebhookInputDataFields.HEADERS]: [ + ...(validator?.[WebhookInputDataFields.HEADERS] || []), + { ...initHeadersValidatorData }, + ], + }); + break; + case ArrayInputOperations.REMOVE: + if (webhook?.[WebhookInputDataFields.HEADERS]?.length) { + const updatedHeaders = [...webhook[WebhookInputDataFields.HEADERS]]; + updatedHeaders.splice(index, 1); + setWebhook({ + ...webhook, + [WebhookInputDataFields.HEADERS]: updatedHeaders, + }); + } + if (validator?.[WebhookInputDataFields.HEADERS]?.length) { + const updatedHeadersData = [ + ...validator[WebhookInputDataFields.HEADERS], + ]; + updatedHeadersData.splice(index, 1); + setValidator({ + ...validator, + [WebhookInputDataFields.HEADERS]: updatedHeadersData, + }); + } + break; + default: + break; + } + }; + const validateData = () => { + return ( + !loading && + !verifyingEndpoint && + webhook[WebhookInputDataFields.EVENT_NAME].length > 0 && + webhook[WebhookInputDataFields.ENDPOINT].length > 0 && + validator[WebhookInputDataFields.ENDPOINT] && + !validator[WebhookInputDataFields.HEADERS].some( + (headerData: headersValidatorDataType) => + !headerData.key || !headerData.value + ) + ); + }; + const getParams = () => { + let params: any = { + [WebhookInputDataFields.EVENT_NAME]: + webhook[WebhookInputDataFields.EVENT_NAME], + [WebhookInputDataFields.ENDPOINT]: + webhook[WebhookInputDataFields.ENDPOINT], + [WebhookInputDataFields.ENABLED]: webhook[WebhookInputDataFields.ENABLED], + [WebhookInputDataFields.HEADERS]: {}, + }; + if (webhook[WebhookInputDataFields.HEADERS].length) { + const headers = webhook[WebhookInputDataFields.HEADERS].reduce( + (acc, data) => { + return data.key ? { ...acc, [data.key]: data.value } : acc; + }, + {} + ); + if (Object.keys(headers).length) { + params[WebhookInputDataFields.HEADERS] = headers; + } + } + return params; + }; + const saveData = async () => { + if (!validateData()) return; + setLoading(true); + const params = getParams(); + let res: any = {}; + if ( + view === UpdateWebhookModalViews.Edit && + selectedWebhook?.[WebhookInputDataFields.ID] + ) { + res = await client + .mutation(EditWebhook, { + params: { + ...params, + id: selectedWebhook[WebhookInputDataFields.ID], + }, + }) + .toPromise(); + } else { + res = await client.mutation(AddWebhook, { params }).toPromise(); + } + setLoading(false); + if (res.error) { + toast({ + title: capitalizeFirstLetter(res.error.message), + isClosable: true, + status: 'error', + position: 'bottom-right', + }); + } else if (res.data?._add_webhook || res.data?._update_webhook) { + toast({ + title: capitalizeFirstLetter( + res.data?._add_webhook?.message || res.data?._update_webhook?.message + ), + isClosable: true, + status: 'success', + position: 'bottom-right', + }); + setWebhook({ + ...initWebhookData, + [WebhookInputDataFields.HEADERS]: [{ ...initHeadersData }], + }); + setValidator({ ...initWebhookValidatorData }); + fetchWebookData(); + } + view === UpdateWebhookModalViews.ADD && onClose(); + }; + useEffect(() => { + if ( + isOpen && + view === UpdateWebhookModalViews.Edit && + selectedWebhook && + Object.keys(selectedWebhook || {}).length + ) { + const { headers, ...rest } = selectedWebhook; + const headerItems = Object.entries(headers || {}); + if (headerItems.length) { + let formattedHeadersData = headerItems.map((headerData) => { + return { + [WebhookInputHeaderFields.KEY]: headerData[0], + [WebhookInputHeaderFields.VALUE]: headerData[1], + }; + }); + setWebhook({ + ...rest, + [WebhookInputDataFields.HEADERS]: formattedHeadersData, + }); + setValidator({ + ...validator, + [WebhookInputDataFields.HEADERS]: new Array( + formattedHeadersData.length + ) + .fill({}) + .map(() => ({ ...initHeadersValidatorData })), + }); + } else { + setWebhook({ + ...rest, + [WebhookInputDataFields.HEADERS]: [{ ...initHeadersData }], + }); + } + } + }, [isOpen]); + const verifyEndpoint = async () => { + if (!validateData()) return; + setVerifyingEndpoint(true); + const { [WebhookInputDataFields.ENABLED]: _, ...params } = getParams(); + const res = await client.mutation(TestEndpoint, { params }).toPromise(); + if (res.data?._test_endpoint?.response?.success) { + setVerifiedStatus(webhookVerifiedStatus.VERIFIED); + } else { + setVerifiedStatus(webhookVerifiedStatus.NOT_VERIFIED); + } + setVerifyingEndpoint(false); + }; + return ( + <> + {view === UpdateWebhookModalViews.ADD ? ( + + ) : ( + Edit + )} + + + + + {view === UpdateWebhookModalViews.ADD + ? 'Add New Webhook' + : 'Edit Webhook'} + + + + + + Event Name + + + + + + Endpoint + + + + inputChangehandler( + WebhookInputDataFields.ENDPOINT, + e.currentTarget.value + ) + } + /> + + + + + Enabled + + + Off + + + inputChangehandler( + WebhookInputDataFields.ENABLED, + !webhook[WebhookInputDataFields.ENABLED] + ) + } + /> + + On + + + + + Headers + + + + + + {webhook[WebhookInputDataFields.HEADERS]?.map( + (headerData, index) => ( + + + + inputChangehandler( + WebhookInputDataFields.HEADERS, + e.target.value, + WebhookInputHeaderFields.KEY, + index + ) + } + width="30%" + marginRight="2%" + /> +
+ : +
+ + inputChangehandler( + WebhookInputDataFields.HEADERS, + e.target.value, + WebhookInputHeaderFields.VALUE, + index + ) + } + width="65%" + /> + + + +
+
+ ) + )} +
+
+
+ + + + +
+
+ + ); +}; + +export default UpdateWebhookModal; diff --git a/dashboard/src/components/ViewWebhookLogsModal.tsx b/dashboard/src/components/ViewWebhookLogsModal.tsx new file mode 100644 index 0000000..e26089f --- /dev/null +++ b/dashboard/src/components/ViewWebhookLogsModal.tsx @@ -0,0 +1,426 @@ +import React, { useEffect, useState } from 'react'; +import dayjs from 'dayjs'; +import { + Button, + Center, + Flex, + MenuItem, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, + Text, + Spinner, + Table, + Th, + Thead, + Tr, + Tbody, + IconButton, + NumberDecrementStepper, + NumberIncrementStepper, + NumberInput, + NumberInputField, + NumberInputStepper, + Select, + TableCaption, + Tooltip, + Td, + Tag, +} from '@chakra-ui/react'; +import { useClient } from 'urql'; +import { + FaAngleDoubleLeft, + FaAngleDoubleRight, + FaAngleLeft, + FaAngleRight, + FaExclamationCircle, + FaRegClone, +} from 'react-icons/fa'; +import { copyTextToClipboard } from '../utils'; +import { WebhookLogsQuery } from '../graphql/queries'; +import { pageLimits } from '../constants'; + +interface paginationPropTypes { + limit: number; + page: number; + offset: number; + total: number; + maxPages: number; +} + +interface deleteWebhookModalInputPropTypes { + webhookId: string; + eventName: string; +} + +interface webhookLogsDataTypes { + id: string; + http_status: number; + request: string; + response: string; + created_at: number; +} + +const ViewWebhookLogsModal = ({ + webhookId, + eventName, +}: deleteWebhookModalInputPropTypes) => { + const client = useClient(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [loading, setLoading] = useState(false); + const [webhookLogs, setWebhookLogs] = useState([]); + const [paginationProps, setPaginationProps] = useState({ + limit: 5, + page: 1, + offset: 0, + total: 0, + maxPages: 1, + }); + const getMaxPages = (pagination: paginationPropTypes) => { + const { limit, total } = pagination; + if (total > 1) { + return total % limit === 0 + ? total / limit + : parseInt(`${total / limit}`) + 1; + } else return 1; + }; + const fetchWebhookLogsData = async () => { + setLoading(true); + const res = await client + .query(WebhookLogsQuery, { + params: { + webhook_id: webhookId, + pagination: { + limit: paginationProps.limit, + page: paginationProps.page, + }, + }, + }) + .toPromise(); + if (res.data?._webhook_logs) { + const { pagination, webhook_logs } = res.data?._webhook_logs; + const maxPages = getMaxPages(pagination); + if (webhook_logs?.length) { + setWebhookLogs(webhook_logs); + setPaginationProps({ ...paginationProps, ...pagination, maxPages }); + } else { + if (paginationProps.page !== 1) { + setPaginationProps({ + ...paginationProps, + ...pagination, + maxPages, + page: 1, + }); + } + } + } + setLoading(false); + }; + const paginationHandler = (value: Record) => { + setPaginationProps({ ...paginationProps, ...value }); + }; + useEffect(() => { + isOpen && fetchWebhookLogsData(); + }, [isOpen, paginationProps.page, paginationProps.limit]); + return ( + <> + View Logs + + + + Webhook Logs - {eventName} + + + + {!loading ? ( + webhookLogs.length ? ( + + + + + + + + + + + + {webhookLogs.map((logData: webhookLogsDataTypes) => ( + + + + + + + + ))} + + {(paginationProps.maxPages > 1 || + paginationProps.total >= 5) && ( + + + + + + paginationHandler({ + page: 1, + }) + } + isDisabled={paginationProps.page <= 1} + mr={4} + icon={} + /> + + + + paginationHandler({ + page: paginationProps.page - 1, + }) + } + isDisabled={paginationProps.page <= 1} + icon={} + /> + + + + + Page{' '} + + {paginationProps.page} + {' '} + of{' '} + + {paginationProps.maxPages} + + + + Go to page:{' '} + + paginationHandler({ + page: parseInt(value), + }) + } + value={paginationProps.page} + > + + + + + + + + + + + + + paginationHandler({ + page: paginationProps.page + 1, + }) + } + isDisabled={ + paginationProps.page >= + paginationProps.maxPages + } + icon={} + /> + + + + paginationHandler({ + page: paginationProps.maxPages, + }) + } + isDisabled={ + paginationProps.page >= + paginationProps.maxPages + } + ml={4} + icon={} + /> + + + + + )} +
IDCreated AtHttp StatusRequestResponse
+ {`${logData.id.substring( + 0, + 5 + )}***${logData.id.substring( + logData.id.length - 5, + logData.id.length + )}`} + + {dayjs(logData.created_at * 1000).format( + 'MMM DD, YYYY' + )} + + = 400 ? 'red' : 'green' + } + > + {logData.http_status} + + + + + + {logData.request ? 'Payload' : 'No Data'} + + + {logData.request && ( + + )} + + + + + + {logData.response ? 'Preview' : 'No Data'} + + + {logData.response && ( + + )} + +
+ ) : ( + +
+ +
+ + No Data + +
+ ) + ) : ( +
+ +
+ )} +
+
+ + + +
+
+ + ); +}; + +export default ViewWebhookLogsModal; diff --git a/dashboard/src/constants.ts b/dashboard/src/constants.ts index f74f1fc..2f55b6f 100644 --- a/dashboard/src/constants.ts +++ b/dashboard/src/constants.ts @@ -153,3 +153,38 @@ export const envSubViews = { ADMIN_SECRET: 'admin-secret', DB_CRED: 'db-cred', }; + +export enum WebhookInputDataFields { + ID = 'id', + EVENT_NAME = 'event_name', + ENDPOINT = 'endpoint', + ENABLED = 'enabled', + HEADERS = 'headers', +} + +export enum WebhookInputHeaderFields { + KEY = 'key', + VALUE = 'value', +} + +export enum UpdateWebhookModalViews { + ADD = 'add', + Edit = 'edit', +} + +export const pageLimits: number[] = [5, 10, 15]; + +export const webhookEventNames = { + USER_SIGNUP: 'user.signup', + USER_CREATED: 'user.created', + USER_LOGIN: 'user.login', + USER_DELETED: 'user.deleted', + USER_ACCESS_ENABLED: 'user.access_enabled', + USER_ACCESS_REVOKED: 'user.access_revoked', +}; + +export enum webhookVerifiedStatus { + VERIFIED = 'verified', + NOT_VERIFIED = 'not_verified', + PENDING = 'verification_pending', +} diff --git a/dashboard/src/graphql/mutation/index.ts b/dashboard/src/graphql/mutation/index.ts index 46c5fcb..83c857e 100644 --- a/dashboard/src/graphql/mutation/index.ts +++ b/dashboard/src/graphql/mutation/index.ts @@ -79,3 +79,36 @@ export const GenerateKeys = ` } } `; + +export const AddWebhook = ` + mutation addWebhook($params: AddWebhookRequest!) { + _add_webhook(params: $params) { + message + } + } +`; + +export const EditWebhook = ` + mutation editWebhook($params: UpdateWebhookRequest!) { + _update_webhook(params: $params) { + message + } + } +`; + +export const DeleteWebhook = ` + mutation deleteWebhook($params: WebhookRequest!) { + _delete_webhook(params: $params) { + message + } + } +`; + +export const TestEndpoint = ` + mutation testEndpoint($params: TestEndpointRequest!) { + _test_endpoint(params: $params) { + http_status + response + } + } +`; diff --git a/dashboard/src/graphql/queries/index.ts b/dashboard/src/graphql/queries/index.ts index 3f646a0..ac9ba15 100644 --- a/dashboard/src/graphql/queries/index.ts +++ b/dashboard/src/graphql/queries/index.ts @@ -101,3 +101,43 @@ export const EmailVerificationQuery = ` } } `; + +export const WebhooksDataQuery = ` + query getWebhooksData($params: PaginatedInput!) { + _webhooks(params: $params){ + webhooks{ + id + event_name + endpoint + enabled + headers + } + pagination{ + limit + page + offset + total + } + } + } +`; + +export const WebhookLogsQuery = ` + query getWebhookLogs($params: ListWebhookLogRequest!) { + _webhook_logs(params: $params) { + webhook_logs { + id + http_status + request + response + created_at + } + pagination { + limit + page + offset + total + } + } + } +`; diff --git a/dashboard/src/pages/Webhooks.tsx b/dashboard/src/pages/Webhooks.tsx new file mode 100644 index 0000000..78d863e --- /dev/null +++ b/dashboard/src/pages/Webhooks.tsx @@ -0,0 +1,370 @@ +import React, { useEffect, useState } from 'react'; +import { useClient } from 'urql'; +import { + Box, + Button, + Center, + Flex, + IconButton, + Menu, + MenuButton, + MenuItem, + MenuList, + NumberDecrementStepper, + NumberIncrementStepper, + NumberInput, + NumberInputField, + NumberInputStepper, + Select, + Spinner, + Table, + TableCaption, + Tag, + Tbody, + Td, + Text, + Th, + Thead, + Tooltip, + Tr, +} from '@chakra-ui/react'; +import { + FaAngleDoubleLeft, + FaAngleDoubleRight, + FaAngleDown, + FaAngleLeft, + FaAngleRight, + FaExclamationCircle, +} from 'react-icons/fa'; +import UpdateWebhookModal from '../components/UpdateWebhookModal'; +import { + pageLimits, + WebhookInputDataFields, + UpdateWebhookModalViews, +} from '../constants'; +import { WebhooksDataQuery } from '../graphql/queries'; +import DeleteWebhookModal from '../components/DeleteWebhookModal'; +import ViewWebhookLogsModal from '../components/ViewWebhookLogsModal'; + +interface paginationPropTypes { + limit: number; + page: number; + offset: number; + total: number; + maxPages: number; +} + +interface webhookDataTypes { + [WebhookInputDataFields.ID]: string; + [WebhookInputDataFields.EVENT_NAME]: string; + [WebhookInputDataFields.ENDPOINT]: string; + [WebhookInputDataFields.ENABLED]: boolean; + [WebhookInputDataFields.HEADERS]?: Record; +} + +const Webhooks = () => { + const client = useClient(); + const [loading, setLoading] = useState(false); + const [webhookData, setWebhookData] = useState([]); + const [paginationProps, setPaginationProps] = useState({ + limit: 5, + page: 1, + offset: 0, + total: 0, + maxPages: 1, + }); + const getMaxPages = (pagination: paginationPropTypes) => { + const { limit, total } = pagination; + if (total > 1) { + return total % limit === 0 + ? total / limit + : parseInt(`${total / limit}`) + 1; + } else return 1; + }; + const fetchWebookData = async () => { + setLoading(true); + const res = await client + .query(WebhooksDataQuery, { + params: { + pagination: { + limit: paginationProps.limit, + page: paginationProps.page, + }, + }, + }) + .toPromise(); + if (res.data?._webhooks) { + const { pagination, webhooks } = res.data?._webhooks; + const maxPages = getMaxPages(pagination); + if (webhooks?.length) { + setWebhookData(webhooks); + setPaginationProps({ ...paginationProps, ...pagination, maxPages }); + } else { + if (paginationProps.page !== 1) { + setPaginationProps({ + ...paginationProps, + ...pagination, + maxPages, + page: 1, + }); + } + } + } + setLoading(false); + }; + const paginationHandler = (value: Record) => { + setPaginationProps({ ...paginationProps, ...value }); + }; + useEffect(() => { + fetchWebookData(); + }, [paginationProps.page, paginationProps.limit]); + return ( + + + + Webhooks + + + + {!loading ? ( + webhookData.length ? ( + + + + + + + + + + + + {webhookData.map((webhook: webhookDataTypes) => ( + + + + + + + + ))} + + {(paginationProps.maxPages > 1 || paginationProps.total >= 5) && ( + + + + + + paginationHandler({ + page: 1, + }) + } + isDisabled={paginationProps.page <= 1} + mr={4} + icon={} + /> + + + + paginationHandler({ + page: paginationProps.page - 1, + }) + } + isDisabled={paginationProps.page <= 1} + icon={} + /> + + + + + Page{' '} + + {paginationProps.page} + {' '} + of{' '} + + {paginationProps.maxPages} + + + + Go to page:{' '} + + paginationHandler({ + page: parseInt(value), + }) + } + value={paginationProps.page} + > + + + + + + + + + + + + + paginationHandler({ + page: paginationProps.page + 1, + }) + } + isDisabled={ + paginationProps.page >= paginationProps.maxPages + } + icon={} + /> + + + + paginationHandler({ + page: paginationProps.maxPages, + }) + } + isDisabled={ + paginationProps.page >= paginationProps.maxPages + } + ml={4} + icon={} + /> + + + + + )} +
Event NameEndpointEnabledHeadersActions
+ {webhook[WebhookInputDataFields.EVENT_NAME]} + {webhook[WebhookInputDataFields.ENDPOINT]} + + {webhook[WebhookInputDataFields.ENABLED].toString()} + + + + + {Object.keys( + webhook[WebhookInputDataFields.HEADERS] || {} + )?.length.toString()} + + + + + + + + Menu + + + + + + + + + + +
+ ) : ( + +
+ +
+ + No Data + +
+ ) + ) : ( +
+ +
+ )} +
+ ); +}; + +export default Webhooks; diff --git a/dashboard/src/routes/index.tsx b/dashboard/src/routes/index.tsx index f611e92..6ae73df 100644 --- a/dashboard/src/routes/index.tsx +++ b/dashboard/src/routes/index.tsx @@ -8,32 +8,34 @@ const Auth = lazy(() => import('../pages/Auth')); const Environment = lazy(() => import('../pages/Environment')); const Home = lazy(() => import('../pages/Home')); const Users = lazy(() => import('../pages/Users')); +const Webhooks = lazy(() => import('../pages/Webhooks')); export const AppRoutes = () => { const { isLoggedIn } = useAuthContext(); if (isLoggedIn) { return ( -
- }> - - - - - } - > - }> - } /> - } /> - - } /> - } /> - - - -
+
+ }> + + + + + } + > + }> + } /> + } /> + + } /> + } /> + } /> + + + +
); } return (