1- import { useState } from 'react' ;
1+ import { Fragment , useMemo , useState } from 'react' ;
22import Head from 'next/head' ;
33import { HTTPError } from 'ky' ;
44import { Button as BSButton , Modal as BSModal , Spinner } from 'react-bootstrap' ;
55import { CopyToClipboard } from 'react-copy-to-clipboard' ;
6+ import { SubmitHandler , useForm } from 'react-hook-form' ;
67import {
78 Badge ,
89 Box ,
@@ -18,6 +19,7 @@ import {
1819 Text ,
1920 TextInput ,
2021} from '@mantine/core' ;
22+ import { useDisclosure } from '@mantine/hooks' ;
2123import { notifications } from '@mantine/notifications' ;
2224
2325import { ConnectionForm } from '@/components/ConnectionForm' ;
@@ -28,6 +30,7 @@ import api from './api';
2830import { useConnections } from './connection' ;
2931import { withAppNav } from './layout' ;
3032import { useSources } from './source' ;
33+ import { useConfirm } from './useConfirm' ;
3134
3235import styles from '../styles/TeamPage.module.scss' ;
3336
@@ -643,6 +646,219 @@ function TeamMembersSection() {
643646 ) ;
644647}
645648
649+ type WebhookForm = {
650+ name : string ;
651+ url : string ;
652+ description ?: string ;
653+ } ;
654+
655+ function CreateWebhookForm ( {
656+ service,
657+ onClose,
658+ onSuccess,
659+ } : {
660+ service : 'slack' | 'generic' ;
661+ onClose : VoidFunction ;
662+ onSuccess : VoidFunction ;
663+ } ) {
664+ const saveWebhook = api . useSaveWebhook ( ) ;
665+
666+ const form = useForm < WebhookForm > ( {
667+ defaultValues : { } ,
668+ } ) ;
669+
670+ const onSubmit : SubmitHandler < WebhookForm > = async values => {
671+ try {
672+ await saveWebhook . mutateAsync ( {
673+ service,
674+ name : values . name ,
675+ url : values . url ,
676+ description : values . description || '' ,
677+ } ) ;
678+ notifications . show ( {
679+ color : 'green' ,
680+ message : `Webhook created successfully` ,
681+ } ) ;
682+ onSuccess ( ) ;
683+ onClose ( ) ;
684+ } catch ( e ) {
685+ console . error ( e ) ;
686+ const message =
687+ ( e instanceof HTTPError ? ( await e . response . json ( ) ) ?. message : null ) ||
688+ 'Something went wrong. Please contact HyperDX team.' ;
689+ notifications . show ( {
690+ message,
691+ color : 'red' ,
692+ autoClose : 5000 ,
693+ } ) ;
694+ }
695+ } ;
696+
697+ return (
698+ < form onSubmit = { form . handleSubmit ( onSubmit ) } >
699+ < Stack mt = "sm" >
700+ < Text > Create Webhook</ Text >
701+ < TextInput
702+ label = "Webhook Name"
703+ placeholder = "Post to #dev-alerts"
704+ required
705+ error = { form . formState . errors . name ?. message }
706+ { ...form . register ( 'name' , { required : true } ) }
707+ />
708+ < TextInput
709+ label = "Webhook URL"
710+ placeholder = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
711+ type = "url"
712+ required
713+ error = { form . formState . errors . url ?. message }
714+ { ...form . register ( 'url' , { required : true } ) }
715+ />
716+ < TextInput
717+ label = "Webhook Description (optional)"
718+ placeholder = "To be used for dev alerts"
719+ error = { form . formState . errors . description ?. message }
720+ { ...form . register ( 'description' ) }
721+ />
722+ < Group justify = "space-between" >
723+ < Button
724+ variant = "outline"
725+ type = "submit"
726+ loading = { saveWebhook . isPending }
727+ >
728+ Add Webhook
729+ </ Button >
730+ < Button variant = "outline" color = "gray" onClick = { onClose } type = "reset" >
731+ Cancel
732+ </ Button >
733+ </ Group >
734+ </ Stack >
735+ </ form >
736+ ) ;
737+ }
738+
739+ function DeleteWebhookButton ( {
740+ webhookId,
741+ webhookName,
742+ onSuccess,
743+ } : {
744+ webhookId : string ;
745+ webhookName : string ;
746+ onSuccess : VoidFunction ;
747+ } ) {
748+ const confirm = useConfirm ( ) ;
749+ const deleteWebhook = api . useDeleteWebhook ( ) ;
750+
751+ const handleDelete = async ( ) => {
752+ if (
753+ await confirm (
754+ `Are you sure you want to delete ${ webhookName } webhook?` ,
755+ 'Delete' ,
756+ )
757+ ) {
758+ try {
759+ await deleteWebhook . mutateAsync ( { id : webhookId } ) ;
760+ notifications . show ( {
761+ color : 'green' ,
762+ message : 'Webhook deleted successfully' ,
763+ } ) ;
764+ onSuccess ( ) ;
765+ } catch ( e ) {
766+ console . error ( e ) ;
767+ const message =
768+ ( e instanceof HTTPError
769+ ? ( await e . response . json ( ) ) ?. message
770+ : null ) || 'Something went wrong. Please contact HyperDX team.' ;
771+ notifications . show ( {
772+ message,
773+ color : 'red' ,
774+ autoClose : 5000 ,
775+ } ) ;
776+ }
777+ }
778+ } ;
779+
780+ return (
781+ < Button
782+ color = "red"
783+ size = "compact-xs"
784+ variant = "outline"
785+ onClick = { handleDelete }
786+ loading = { deleteWebhook . isPending }
787+ >
788+ Delete
789+ </ Button >
790+ ) ;
791+ }
792+
793+ function IntegrationsSection ( ) {
794+ const { data : slackWebhooksData , refetch : refetchSlackWebhooks } =
795+ api . useWebhooks ( [ 'slack' ] ) ;
796+
797+ const slackWebhooks = useMemo ( ( ) => {
798+ return Array . isArray ( slackWebhooksData ?. data )
799+ ? slackWebhooksData ?. data
800+ : [ ] ;
801+ } , [ slackWebhooksData ] ) ;
802+
803+ const [
804+ isAddSlackModalOpen ,
805+ { open : openSlackModal , close : closeSlackModal } ,
806+ ] = useDisclosure ( ) ;
807+
808+ return (
809+ < Box >
810+ < Text size = "md" c = "gray.4" >
811+ Integrations
812+ </ Text >
813+ < Divider my = "md" />
814+ < Card >
815+ < Text mb = "xs" > Slack Webhooks</ Text >
816+
817+ < Stack >
818+ { slackWebhooks . map ( ( webhook : any ) => (
819+ < Fragment key = { webhook . _id } >
820+ < Group justify = "space-between" >
821+ < Stack gap = { 0 } >
822+ < Text size = "sm" > { webhook . name } </ Text >
823+ < Text size = "xs" opacity = { 0.7 } >
824+ { webhook . url }
825+ </ Text >
826+ { webhook . description && (
827+ < Text size = "xxs" opacity = { 0.7 } >
828+ { webhook . description }
829+ </ Text >
830+ ) }
831+ </ Stack >
832+ < DeleteWebhookButton
833+ webhookId = { webhook . _id }
834+ webhookName = { webhook . name }
835+ onSuccess = { refetchSlackWebhooks }
836+ />
837+ </ Group >
838+ < Divider />
839+ </ Fragment >
840+ ) ) }
841+ </ Stack >
842+
843+ { ! isAddSlackModalOpen ? (
844+ < Button variant = "outline" color = "gray.4" onClick = { openSlackModal } >
845+ Add Slack Webhook
846+ </ Button >
847+ ) : (
848+ < CreateWebhookForm
849+ service = "slack"
850+ onClose = { closeSlackModal }
851+ onSuccess = { ( ) => {
852+ refetchSlackWebhooks ( ) ;
853+ closeSlackModal ( ) ;
854+ } }
855+ />
856+ ) }
857+ </ Card >
858+ </ Box >
859+ ) ;
860+ }
861+
646862export default function TeamPage ( ) {
647863 const { data : team , isLoading } = api . useTeam ( ) ;
648864 const hasAllowedAuthMethods =
@@ -667,6 +883,7 @@ export default function TeamPage() {
667883 < Stack my = { 20 } gap = "xl" >
668884 < SourcesSection />
669885 < ConnectionsSection />
886+ < IntegrationsSection />
670887
671888 { hasAllowedAuthMethods && (
672889 < >
0 commit comments