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/AddWebhookModal.tsx b/dashboard/src/components/AddWebhookModal.tsx new file mode 100644 index 0000000..4ad99c6 --- /dev/null +++ b/dashboard/src/components/AddWebhookModal.tsx @@ -0,0 +1,401 @@ +import React, { useState } from 'react'; +import { + Button, + Center, + Flex, + Input, + InputGroup, + InputRightElement, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Switch, + Text, + useDisclosure, + useToast, +} from '@chakra-ui/react'; +import { FaMinusCircle, FaPlus } from 'react-icons/fa'; +import { useClient } from 'urql'; +import { ArrayInputOperations } from '../constants'; +import { validateEventName, validateURI } from '../utils'; + +enum INPUT_FIELDS { + EVENT_NAME = 'event_name', + ENDPOINT = 'endpoint', + ENABLED = 'enabled', + HEADERS = 'headers', +} + +enum HEADER_FIELDS { + KEY = 'key', + VALUE = 'value', +} + +interface headersDataType { + [HEADER_FIELDS.KEY]: string; + [HEADER_FIELDS.VALUE]: string; +} + +interface headersValidatorDataType { + [HEADER_FIELDS.KEY]: boolean; + [HEADER_FIELDS.VALUE]: boolean; +} + +const initHeadersData: headersDataType = { + [HEADER_FIELDS.KEY]: '', + [HEADER_FIELDS.VALUE]: '', +}; + +const initHeadersValidatorData: headersValidatorDataType = { + [HEADER_FIELDS.KEY]: true, + [HEADER_FIELDS.VALUE]: true, +}; + +interface webhookDataType { + [INPUT_FIELDS.EVENT_NAME]: string; + [INPUT_FIELDS.ENDPOINT]: string; + [INPUT_FIELDS.ENABLED]: boolean; + [INPUT_FIELDS.HEADERS]: headersDataType[]; +} + +interface validatorDataType { + [INPUT_FIELDS.EVENT_NAME]: boolean; + [INPUT_FIELDS.ENDPOINT]: boolean; + [INPUT_FIELDS.HEADERS]: headersValidatorDataType[]; +} + +const initWebhookData: webhookDataType = { + [INPUT_FIELDS.EVENT_NAME]: '', + [INPUT_FIELDS.ENDPOINT]: '', + [INPUT_FIELDS.ENABLED]: false, + [INPUT_FIELDS.HEADERS]: [{ ...initHeadersData }], +}; + +const initWebhookValidatorData: validatorDataType = { + [INPUT_FIELDS.EVENT_NAME]: true, + [INPUT_FIELDS.ENDPOINT]: true, + [INPUT_FIELDS.HEADERS]: [{ ...initHeadersValidatorData }], +}; + +const AddWebhookModal = () => { + const client = useClient(); + const toast = useToast(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [loading, setLoading] = useState(false); + const [webhook, setWebhook] = useState({ + ...initWebhookData, + }); + const [validator, setValidator] = useState({ + ...initWebhookValidatorData, + }); + const inputChangehandler = ( + inputType: string, + value: any, + headerInputType: string = HEADER_FIELDS.KEY, + headerIndex: number = 0 + ) => { + switch (inputType) { + case INPUT_FIELDS.EVENT_NAME: + setWebhook({ ...webhook, [inputType]: value }); + setValidator({ + ...validator, + [INPUT_FIELDS.EVENT_NAME]: validateEventName(value), + }); + break; + case INPUT_FIELDS.ENDPOINT: + setWebhook({ ...webhook, [inputType]: value }); + setValidator({ + ...validator, + [INPUT_FIELDS.ENDPOINT]: validateURI(value), + }); + break; + case INPUT_FIELDS.ENABLED: + setWebhook({ ...webhook, [inputType]: value }); + break; + case INPUT_FIELDS.HEADERS: + const updatedHeaders: any = [...webhook[INPUT_FIELDS.HEADERS]]; + const updatedHeadersValidatorData: any = [ + ...validator[INPUT_FIELDS.HEADERS], + ]; + const otherHeaderInputType = + headerInputType === HEADER_FIELDS.KEY + ? HEADER_FIELDS.VALUE + : HEADER_FIELDS.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 updateHeadersHandler = (operation: string, index: number = 0) => { + switch (operation) { + case ArrayInputOperations.APPEND: + setWebhook({ + ...webhook, + [INPUT_FIELDS.HEADERS]: [ + ...(webhook?.[INPUT_FIELDS.HEADERS] || []), + { ...initHeadersData }, + ], + }); + setValidator({ + ...validator, + [INPUT_FIELDS.HEADERS]: [ + ...(validator?.[INPUT_FIELDS.HEADERS] || []), + { ...initHeadersValidatorData }, + ], + }); + break; + case ArrayInputOperations.REMOVE: + if (webhook?.[INPUT_FIELDS.HEADERS]?.length) { + const updatedHeaders = [...webhook[INPUT_FIELDS.HEADERS]]; + updatedHeaders.splice(index, 1); + setWebhook({ + ...webhook, + [INPUT_FIELDS.HEADERS]: updatedHeaders, + }); + } + if (validator?.[INPUT_FIELDS.HEADERS]?.length) { + const updatedHeadersData = [...validator[INPUT_FIELDS.HEADERS]]; + updatedHeadersData.splice(index, 1); + setValidator({ + ...validator, + [INPUT_FIELDS.HEADERS]: updatedHeadersData, + }); + } + break; + default: + break; + } + }; + return ( + <> + + + + + Add New Webhook + + + + + Event Name + + + + inputChangehandler( + INPUT_FIELDS.EVENT_NAME, + e.currentTarget.value + ) + } + /> + + + + + Endpoint + + + + inputChangehandler( + INPUT_FIELDS.ENDPOINT, + e.currentTarget.value + ) + } + /> + + + + + Enabled + + + Off + + + inputChangehandler( + INPUT_FIELDS.ENABLED, + !webhook[INPUT_FIELDS.ENABLED] + ) + } + /> + + On + + + + + Headers + + + + + + {webhook[INPUT_FIELDS.HEADERS]?.map((headerData, index) => ( + + + + inputChangehandler( + INPUT_FIELDS.HEADERS, + e.target.value, + HEADER_FIELDS.KEY, + index + ) + } + width="30%" + marginRight="2%" + /> +
+ : +
+ + inputChangehandler( + INPUT_FIELDS.HEADERS, + e.target.value, + HEADER_FIELDS.VALUE, + index + ) + } + width="65%" + /> + + + +
+
+ ))} +
+
+
+ + + +
+
+ + ); +}; + +export default AddWebhookModal; 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/graphql/mutation/index.ts b/dashboard/src/graphql/mutation/index.ts index 46c5fcb..36cea16 100644 --- a/dashboard/src/graphql/mutation/index.ts +++ b/dashboard/src/graphql/mutation/index.ts @@ -79,3 +79,11 @@ export const GenerateKeys = ` } } `; + +export const AddWebhook = ` + mutation addWebhook($params: AddWebhookRequest!) { + _add_webhook(params: $params) { + message + } + } +`; diff --git a/dashboard/src/pages/Webhooks.tsx b/dashboard/src/pages/Webhooks.tsx new file mode 100644 index 0000000..c0717ac --- /dev/null +++ b/dashboard/src/pages/Webhooks.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Box, Flex, Text } from '@chakra-ui/react'; +import AddWebhookModal from '../components/AddWebhookModal'; + +const Webhooks = () => { + return ( + + + + Webhooks + + + + + ); +}; + +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 ( diff --git a/dashboard/src/utils/index.ts b/dashboard/src/utils/index.ts index eccc7a8..99e134a 100644 --- a/dashboard/src/utils/index.ts +++ b/dashboard/src/utils/index.ts @@ -86,3 +86,8 @@ export const validateURI = (uri: string) => { ? true : false; }; + +export const validateEventName = (name: string) => { + if (!name || name === '') return true; + return name.toLowerCase().match(/^.{4,}[.].{5,}$/) ? true : false; +};