Merge pull request #202 from anik-ghosh-au7/feat/webhooks
Feat/webhooks
This commit is contained in:
commit
ecab47b2ea
12
dashboard/package-lock.json
generated
12
dashboard/package-lock.json
generated
|
@ -2529,8 +2529,7 @@
|
||||||
"@chakra-ui/css-reset": {
|
"@chakra-ui/css-reset": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz",
|
||||||
"integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==",
|
"integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg=="
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"@chakra-ui/descendant": {
|
"@chakra-ui/descendant": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
|
@ -3134,8 +3133,7 @@
|
||||||
"@graphql-typed-document-node/core": {
|
"@graphql-typed-document-node/core": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz",
|
||||||
"integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==",
|
"integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg=="
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"@popperjs/core": {
|
"@popperjs/core": {
|
||||||
"version": "2.11.0",
|
"version": "2.11.0",
|
||||||
|
@ -3845,8 +3843,7 @@
|
||||||
"react-icons": {
|
"react-icons": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz",
|
||||||
"integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==",
|
"integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ=="
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
|
@ -4032,8 +4029,7 @@
|
||||||
"use-callback-ref": {
|
"use-callback-ref": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
|
||||||
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==",
|
"integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"use-sidecar": {
|
"use-sidecar": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
|
|
106
dashboard/src/components/DeleteWebhookModal.tsx
Normal file
106
dashboard/src/components/DeleteWebhookModal.tsx
Normal file
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={onOpen}>Delete</MenuItem>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Delete Webhook</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Text fontSize="md">Are you sure?</Text>
|
||||||
|
<Flex
|
||||||
|
padding="5%"
|
||||||
|
marginTop="5%"
|
||||||
|
marginBottom="2%"
|
||||||
|
border="1px solid #ff7875"
|
||||||
|
borderRadius="5px"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">
|
||||||
|
Webhook for event <b>{eventName}</b> will be deleted
|
||||||
|
permanently!
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaRegTrashAlt />}
|
||||||
|
colorScheme="red"
|
||||||
|
variant="solid"
|
||||||
|
onClick={deleteHandler}
|
||||||
|
isDisabled={false}
|
||||||
|
>
|
||||||
|
<Center h="100%" pt="5%">
|
||||||
|
Delete
|
||||||
|
</Center>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteWebhookModal;
|
|
@ -30,6 +30,7 @@ import {
|
||||||
FiMenu,
|
FiMenu,
|
||||||
FiUsers,
|
FiUsers,
|
||||||
FiChevronDown,
|
FiChevronDown,
|
||||||
|
FiAnchor,
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import { BiCustomize } from 'react-icons/bi';
|
import { BiCustomize } from 'react-icons/bi';
|
||||||
import { AiOutlineKey } from 'react-icons/ai';
|
import { AiOutlineKey } from 'react-icons/ai';
|
||||||
|
@ -111,6 +112,7 @@ const LinkItems: Array<LinkItemProps> = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ name: 'Users', icon: FiUsers, route: '/users' },
|
{ name: 'Users', icon: FiUsers, route: '/users' },
|
||||||
|
{ name: 'Webhooks', icon: FiAnchor, route: '/webhooks' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface SidebarProps extends BoxProps {
|
interface SidebarProps extends BoxProps {
|
||||||
|
|
596
dashboard/src/components/UpdateWebhookModal.tsx
Normal file
596
dashboard/src/components/UpdateWebhookModal.tsx
Normal file
|
@ -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<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<boolean>(false);
|
||||||
|
const [verifyingEndpoint, setVerifyingEndpoint] = useState<boolean>(false);
|
||||||
|
const [webhook, setWebhook] = useState<webhookDataType>({
|
||||||
|
...initWebhookData,
|
||||||
|
});
|
||||||
|
const [validator, setValidator] = useState<validatorDataType>({
|
||||||
|
...initWebhookValidatorData,
|
||||||
|
});
|
||||||
|
const [verifiedStatus, setVerifiedStatus] = useState<webhookVerifiedStatus>(
|
||||||
|
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 ? (
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaPlus />}
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="solid"
|
||||||
|
onClick={onOpen}
|
||||||
|
isDisabled={false}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<Center h="100%">Add Webhook</Center>{' '}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<MenuItem onClick={onOpen}>Edit</MenuItem>
|
||||||
|
)}
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>
|
||||||
|
{view === UpdateWebhookModalViews.ADD
|
||||||
|
? 'Add New Webhook'
|
||||||
|
: 'Edit Webhook'}
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
border="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
borderColor="gray.200"
|
||||||
|
p="5"
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
width="100%"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
marginBottom="2%"
|
||||||
|
>
|
||||||
|
<Flex flex="1">Event Name</Flex>
|
||||||
|
<Flex flex="3">
|
||||||
|
<Select
|
||||||
|
size="md"
|
||||||
|
value={webhook[WebhookInputDataFields.EVENT_NAME]}
|
||||||
|
onChange={(e) =>
|
||||||
|
inputChangehandler(
|
||||||
|
WebhookInputDataFields.EVENT_NAME,
|
||||||
|
e.currentTarget.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.entries(webhookEventNames).map(
|
||||||
|
([key, value]: any) => (
|
||||||
|
<option value={value} key={key}>
|
||||||
|
{key}
|
||||||
|
</option>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
width="100%"
|
||||||
|
justifyContent="start"
|
||||||
|
alignItems="center"
|
||||||
|
marginBottom="5%"
|
||||||
|
>
|
||||||
|
<Flex flex="1">Endpoint</Flex>
|
||||||
|
<Flex flex="3">
|
||||||
|
<InputGroup size="md">
|
||||||
|
<Input
|
||||||
|
pr="4.5rem"
|
||||||
|
type="text"
|
||||||
|
placeholder="https://domain.com/webhook"
|
||||||
|
value={webhook[WebhookInputDataFields.ENDPOINT]}
|
||||||
|
isInvalid={!validator[WebhookInputDataFields.ENDPOINT]}
|
||||||
|
onChange={(e) =>
|
||||||
|
inputChangehandler(
|
||||||
|
WebhookInputDataFields.ENDPOINT,
|
||||||
|
e.currentTarget.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
width="100%"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
marginBottom="5%"
|
||||||
|
>
|
||||||
|
<Flex flex="1">Enabled</Flex>
|
||||||
|
<Flex w="25%" justifyContent="space-between">
|
||||||
|
<Text h="75%" fontWeight="bold" marginRight="2">
|
||||||
|
Off
|
||||||
|
</Text>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
isChecked={webhook[WebhookInputDataFields.ENABLED]}
|
||||||
|
onChange={() =>
|
||||||
|
inputChangehandler(
|
||||||
|
WebhookInputDataFields.ENABLED,
|
||||||
|
!webhook[WebhookInputDataFields.ENABLED]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Text h="75%" fontWeight="bold" marginLeft="2">
|
||||||
|
On
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
width="100%"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
marginBottom="2%"
|
||||||
|
>
|
||||||
|
<Flex>Headers</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaPlus />}
|
||||||
|
colorScheme="blue"
|
||||||
|
h="1.75rem"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
paddingRight="0"
|
||||||
|
onClick={() => updateHeaders(ArrayInputOperations.APPEND)}
|
||||||
|
>
|
||||||
|
Add more Headers
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex flexDirection="column" maxH={220} overflowY="scroll">
|
||||||
|
{webhook[WebhookInputDataFields.HEADERS]?.map(
|
||||||
|
(headerData, index) => (
|
||||||
|
<Flex
|
||||||
|
key={`header-data-${index}`}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<InputGroup size="md" marginBottom="2.5%">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="key"
|
||||||
|
value={headerData[WebhookInputHeaderFields.KEY]}
|
||||||
|
isInvalid={
|
||||||
|
!validator[WebhookInputDataFields.HEADERS][index]?.[
|
||||||
|
WebhookInputHeaderFields.KEY
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
inputChangehandler(
|
||||||
|
WebhookInputDataFields.HEADERS,
|
||||||
|
e.target.value,
|
||||||
|
WebhookInputHeaderFields.KEY,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
width="30%"
|
||||||
|
marginRight="2%"
|
||||||
|
/>
|
||||||
|
<Center marginRight="2%">
|
||||||
|
<Text fontWeight="bold">:</Text>
|
||||||
|
</Center>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="value"
|
||||||
|
value={headerData[WebhookInputHeaderFields.VALUE]}
|
||||||
|
isInvalid={
|
||||||
|
!validator[WebhookInputDataFields.HEADERS][index]?.[
|
||||||
|
WebhookInputHeaderFields.VALUE
|
||||||
|
]
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
inputChangehandler(
|
||||||
|
WebhookInputDataFields.HEADERS,
|
||||||
|
e.target.value,
|
||||||
|
WebhookInputHeaderFields.VALUE,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
width="65%"
|
||||||
|
/>
|
||||||
|
<InputRightElement width="3rem">
|
||||||
|
<Button
|
||||||
|
width="6rem"
|
||||||
|
colorScheme="blackAlpha"
|
||||||
|
variant="ghost"
|
||||||
|
padding="0"
|
||||||
|
onClick={() =>
|
||||||
|
updateHeaders(ArrayInputOperations.REMOVE, index)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaMinusCircle />
|
||||||
|
</Button>
|
||||||
|
</InputRightElement>
|
||||||
|
</InputGroup>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
colorScheme={
|
||||||
|
verifiedStatus === webhookVerifiedStatus.VERIFIED
|
||||||
|
? 'green'
|
||||||
|
: verifiedStatus === webhookVerifiedStatus.PENDING
|
||||||
|
? 'yellow'
|
||||||
|
: 'red'
|
||||||
|
}
|
||||||
|
variant="outline"
|
||||||
|
onClick={verifyEndpoint}
|
||||||
|
isLoading={verifyingEndpoint}
|
||||||
|
isDisabled={!validateData()}
|
||||||
|
marginRight="5"
|
||||||
|
leftIcon={
|
||||||
|
verifiedStatus === webhookVerifiedStatus.VERIFIED ? (
|
||||||
|
<BiCheckCircle />
|
||||||
|
) : verifiedStatus === webhookVerifiedStatus.PENDING ? (
|
||||||
|
<BiErrorCircle />
|
||||||
|
) : (
|
||||||
|
<BiError />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{verifiedStatus === webhookVerifiedStatus.VERIFIED
|
||||||
|
? 'Endpoint Verified'
|
||||||
|
: verifiedStatus === webhookVerifiedStatus.PENDING
|
||||||
|
? 'Test Endpoint'
|
||||||
|
: 'Endpoint Not Verified'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="solid"
|
||||||
|
onClick={saveData}
|
||||||
|
isDisabled={!validateData()}
|
||||||
|
>
|
||||||
|
<Center h="100%" pt="5%">
|
||||||
|
Save
|
||||||
|
</Center>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateWebhookModal;
|
426
dashboard/src/components/ViewWebhookLogsModal.tsx
Normal file
426
dashboard/src/components/ViewWebhookLogsModal.tsx
Normal file
|
@ -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<boolean>(false);
|
||||||
|
const [webhookLogs, setWebhookLogs] = useState<webhookLogsDataTypes[]>([]);
|
||||||
|
const [paginationProps, setPaginationProps] = useState<paginationPropTypes>({
|
||||||
|
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<string, number>) => {
|
||||||
|
setPaginationProps({ ...paginationProps, ...value });
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
isOpen && fetchWebhookLogsData();
|
||||||
|
}, [isOpen, paginationProps.page, paginationProps.limit]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={onOpen}>View Logs</MenuItem>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Webhook Logs - {eventName}</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
border="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
borderColor="gray.200"
|
||||||
|
p="5"
|
||||||
|
>
|
||||||
|
{!loading ? (
|
||||||
|
webhookLogs.length ? (
|
||||||
|
<Table variant="simple">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>ID</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th>Http Status</Th>
|
||||||
|
<Th>Request</Th>
|
||||||
|
<Th>Response</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{webhookLogs.map((logData: webhookLogsDataTypes) => (
|
||||||
|
<Tr key={logData.id} style={{ fontSize: 14 }}>
|
||||||
|
<Td>
|
||||||
|
<Text fontSize="sm">{`${logData.id.substring(
|
||||||
|
0,
|
||||||
|
5
|
||||||
|
)}***${logData.id.substring(
|
||||||
|
logData.id.length - 5,
|
||||||
|
logData.id.length
|
||||||
|
)}`}</Text>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{dayjs(logData.created_at * 1000).format(
|
||||||
|
'MMM DD, YYYY'
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={
|
||||||
|
logData.http_status >= 400 ? 'red' : 'green'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{logData.http_status}
|
||||||
|
</Tag>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Tooltip
|
||||||
|
bg="gray.300"
|
||||||
|
color="black"
|
||||||
|
label={logData.request || 'null'}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={
|
||||||
|
logData.request ? 'gray' : 'yellow'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{logData.request ? 'Payload' : 'No Data'}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
{logData.request && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline"
|
||||||
|
marginLeft="5px"
|
||||||
|
h="21px"
|
||||||
|
onClick={() =>
|
||||||
|
copyTextToClipboard(logData.request)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegClone color="#bfbfbf" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Tooltip
|
||||||
|
bg="gray.300"
|
||||||
|
color="black"
|
||||||
|
label={logData.response || 'null'}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={
|
||||||
|
logData.response ? 'gray' : 'yellow'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{logData.response ? 'Preview' : 'No Data'}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
{logData.response && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline"
|
||||||
|
marginLeft="5px"
|
||||||
|
h="21px"
|
||||||
|
onClick={() =>
|
||||||
|
copyTextToClipboard(logData.response)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegClone color="#bfbfbf" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
{(paginationProps.maxPages > 1 ||
|
||||||
|
paginationProps.total >= 5) && (
|
||||||
|
<TableCaption>
|
||||||
|
<Flex
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
m="2% 0"
|
||||||
|
>
|
||||||
|
<Flex flex="1">
|
||||||
|
<Tooltip label="First Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={paginationProps.page <= 1}
|
||||||
|
mr={4}
|
||||||
|
icon={<FaAngleDoubleLeft />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Previous Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.page - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={paginationProps.page <= 1}
|
||||||
|
icon={<FaAngleLeft />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
flex="8"
|
||||||
|
justifyContent="space-evenly"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Text mr={8}>
|
||||||
|
Page{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{paginationProps.page}
|
||||||
|
</Text>{' '}
|
||||||
|
of{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{paginationProps.maxPages}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Text flexShrink="0">Go to page:</Text>{' '}
|
||||||
|
<NumberInput
|
||||||
|
ml={2}
|
||||||
|
mr={8}
|
||||||
|
w={28}
|
||||||
|
min={1}
|
||||||
|
max={paginationProps.maxPages}
|
||||||
|
onChange={(value) =>
|
||||||
|
paginationHandler({
|
||||||
|
page: parseInt(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={paginationProps.page}
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
</Flex>
|
||||||
|
<Select
|
||||||
|
w={32}
|
||||||
|
value={paginationProps.limit}
|
||||||
|
onChange={(e) =>
|
||||||
|
paginationHandler({
|
||||||
|
page: 1,
|
||||||
|
limit: parseInt(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pageLimits.map((pageSize) => (
|
||||||
|
<option key={pageSize} value={pageSize}>
|
||||||
|
Show {pageSize}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex="1">
|
||||||
|
<Tooltip label="Next Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.page + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={
|
||||||
|
paginationProps.page >=
|
||||||
|
paginationProps.maxPages
|
||||||
|
}
|
||||||
|
icon={<FaAngleRight />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Last Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.maxPages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={
|
||||||
|
paginationProps.page >=
|
||||||
|
paginationProps.maxPages
|
||||||
|
}
|
||||||
|
ml={4}
|
||||||
|
icon={<FaAngleDoubleRight />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</TableCaption>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
minH="25vh"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Center w="50px" marginRight="1.5%">
|
||||||
|
<FaExclamationCircle
|
||||||
|
style={{ color: '#f0f0f0', fontSize: 70 }}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
paddingRight="1%"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="#d9d9d9"
|
||||||
|
>
|
||||||
|
No Data
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Center minH="25vh">
|
||||||
|
<Spinner />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="solid"
|
||||||
|
onClick={onClose}
|
||||||
|
isDisabled={false}
|
||||||
|
>
|
||||||
|
<Center h="100%" pt="5%">
|
||||||
|
Close
|
||||||
|
</Center>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewWebhookLogsModal;
|
|
@ -153,3 +153,38 @@ export const envSubViews = {
|
||||||
ADMIN_SECRET: 'admin-secret',
|
ADMIN_SECRET: 'admin-secret',
|
||||||
DB_CRED: 'db-cred',
|
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',
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
370
dashboard/src/pages/Webhooks.tsx
Normal file
370
dashboard/src/pages/Webhooks.tsx
Normal file
|
@ -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<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Webhooks = () => {
|
||||||
|
const client = useClient();
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [webhookData, setWebhookData] = useState<webhookDataTypes[]>([]);
|
||||||
|
const [paginationProps, setPaginationProps] = useState<paginationPropTypes>({
|
||||||
|
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<string, number>) => {
|
||||||
|
setPaginationProps({ ...paginationProps, ...value });
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
fetchWebookData();
|
||||||
|
}, [paginationProps.page, paginationProps.limit]);
|
||||||
|
return (
|
||||||
|
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
||||||
|
<Flex margin="2% 0" justifyContent="space-between" alignItems="center">
|
||||||
|
<Text fontSize="md" fontWeight="bold">
|
||||||
|
Webhooks
|
||||||
|
</Text>
|
||||||
|
<UpdateWebhookModal
|
||||||
|
view={UpdateWebhookModalViews.ADD}
|
||||||
|
fetchWebookData={fetchWebookData}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
{!loading ? (
|
||||||
|
webhookData.length ? (
|
||||||
|
<Table variant="simple">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Event Name</Th>
|
||||||
|
<Th>Endpoint</Th>
|
||||||
|
<Th>Enabled</Th>
|
||||||
|
<Th>Headers</Th>
|
||||||
|
<Th>Actions</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{webhookData.map((webhook: webhookDataTypes) => (
|
||||||
|
<Tr
|
||||||
|
key={webhook[WebhookInputDataFields.ID]}
|
||||||
|
style={{ fontSize: 14 }}
|
||||||
|
>
|
||||||
|
<Td maxW="300">
|
||||||
|
{webhook[WebhookInputDataFields.EVENT_NAME]}
|
||||||
|
</Td>
|
||||||
|
<Td>{webhook[WebhookInputDataFields.ENDPOINT]}</Td>
|
||||||
|
<Td>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={
|
||||||
|
webhook[WebhookInputDataFields.ENABLED]
|
||||||
|
? 'green'
|
||||||
|
: 'yellow'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{webhook[WebhookInputDataFields.ENABLED].toString()}
|
||||||
|
</Tag>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Tooltip
|
||||||
|
bg="gray.300"
|
||||||
|
color="black"
|
||||||
|
label={JSON.stringify(
|
||||||
|
webhook[WebhookInputDataFields.HEADERS],
|
||||||
|
null,
|
||||||
|
' '
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Tag size="sm" variant="outline" colorScheme="gray">
|
||||||
|
{Object.keys(
|
||||||
|
webhook[WebhookInputDataFields.HEADERS] || {}
|
||||||
|
)?.length.toString()}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} variant="unstyled" size="sm">
|
||||||
|
<Flex
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" fontWeight="light">
|
||||||
|
Menu
|
||||||
|
</Text>
|
||||||
|
<FaAngleDown style={{ marginLeft: 10 }} />
|
||||||
|
</Flex>
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
<UpdateWebhookModal
|
||||||
|
view={UpdateWebhookModalViews.Edit}
|
||||||
|
selectedWebhook={webhook}
|
||||||
|
fetchWebookData={fetchWebookData}
|
||||||
|
/>
|
||||||
|
<DeleteWebhookModal
|
||||||
|
webhookId={webhook[WebhookInputDataFields.ID]}
|
||||||
|
eventName={webhook[WebhookInputDataFields.EVENT_NAME]}
|
||||||
|
fetchWebookData={fetchWebookData}
|
||||||
|
/>
|
||||||
|
<ViewWebhookLogsModal
|
||||||
|
webhookId={webhook[WebhookInputDataFields.ID]}
|
||||||
|
eventName={webhook[WebhookInputDataFields.EVENT_NAME]}
|
||||||
|
/>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
{(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
|
||||||
|
<TableCaption>
|
||||||
|
<Flex
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
m="2% 0"
|
||||||
|
>
|
||||||
|
<Flex flex="1">
|
||||||
|
<Tooltip label="First Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={paginationProps.page <= 1}
|
||||||
|
mr={4}
|
||||||
|
icon={<FaAngleDoubleLeft />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Previous Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.page - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={paginationProps.page <= 1}
|
||||||
|
icon={<FaAngleLeft />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
flex="8"
|
||||||
|
justifyContent="space-evenly"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Text mr={8}>
|
||||||
|
Page{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{paginationProps.page}
|
||||||
|
</Text>{' '}
|
||||||
|
of{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{paginationProps.maxPages}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Text flexShrink="0">Go to page:</Text>{' '}
|
||||||
|
<NumberInput
|
||||||
|
ml={2}
|
||||||
|
mr={8}
|
||||||
|
w={28}
|
||||||
|
min={1}
|
||||||
|
max={paginationProps.maxPages}
|
||||||
|
onChange={(value) =>
|
||||||
|
paginationHandler({
|
||||||
|
page: parseInt(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={paginationProps.page}
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
</Flex>
|
||||||
|
<Select
|
||||||
|
w={32}
|
||||||
|
value={paginationProps.limit}
|
||||||
|
onChange={(e) =>
|
||||||
|
paginationHandler({
|
||||||
|
page: 1,
|
||||||
|
limit: parseInt(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pageLimits.map((pageSize) => (
|
||||||
|
<option key={pageSize} value={pageSize}>
|
||||||
|
Show {pageSize}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex="1">
|
||||||
|
<Tooltip label="Next Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.page + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={
|
||||||
|
paginationProps.page >= paginationProps.maxPages
|
||||||
|
}
|
||||||
|
icon={<FaAngleRight />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Last Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.maxPages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={
|
||||||
|
paginationProps.page >= paginationProps.maxPages
|
||||||
|
}
|
||||||
|
ml={4}
|
||||||
|
icon={<FaAngleDoubleRight />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</TableCaption>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
minH="25vh"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Center w="50px" marginRight="1.5%">
|
||||||
|
<FaExclamationCircle style={{ color: '#f0f0f0', fontSize: 70 }} />
|
||||||
|
</Center>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
paddingRight="1%"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="#d9d9d9"
|
||||||
|
>
|
||||||
|
No Data
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Center minH="25vh">
|
||||||
|
<Spinner />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Webhooks;
|
|
@ -8,32 +8,34 @@ const Auth = lazy(() => import('../pages/Auth'));
|
||||||
const Environment = lazy(() => import('../pages/Environment'));
|
const Environment = lazy(() => import('../pages/Environment'));
|
||||||
const Home = lazy(() => import('../pages/Home'));
|
const Home = lazy(() => import('../pages/Home'));
|
||||||
const Users = lazy(() => import('../pages/Users'));
|
const Users = lazy(() => import('../pages/Users'));
|
||||||
|
const Webhooks = lazy(() => import('../pages/Webhooks'));
|
||||||
|
|
||||||
export const AppRoutes = () => {
|
export const AppRoutes = () => {
|
||||||
const { isLoggedIn } = useAuthContext();
|
const { isLoggedIn } = useAuthContext();
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Suspense fallback={<></>}>
|
<Suspense fallback={<></>}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
element={
|
element={
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route path="/" element={<Outlet />}>
|
<Route path="/" element={<Outlet />}>
|
||||||
<Route index element={<Environment />} />
|
<Route index element={<Environment />} />
|
||||||
<Route path="/:sec" element={<Environment />} />
|
<Route path="/:sec" element={<Environment />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="users" element={<Users />} />
|
<Route path="users" element={<Users />} />
|
||||||
<Route path="*" element={<Home />} />
|
<Route path="webhooks" element={<Webhooks />} />
|
||||||
</Route>
|
<Route path="*" element={<Home />} />
|
||||||
</Routes>
|
</Route>
|
||||||
</Suspense>
|
</Routes>
|
||||||
</div>
|
</Suspense>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in New Issue
Block a user