1
- import { useState } from 'react' ;
1
+ import { Fragment , useMemo , useState } from 'react' ;
2
2
import Head from 'next/head' ;
3
3
import { HTTPError } from 'ky' ;
4
4
import { Button as BSButton , Modal as BSModal , Spinner } from 'react-bootstrap' ;
5
5
import { CopyToClipboard } from 'react-copy-to-clipboard' ;
6
+ import { SubmitHandler , useForm } from 'react-hook-form' ;
6
7
import {
7
8
Badge ,
8
9
Box ,
@@ -18,6 +19,7 @@ import {
18
19
Text ,
19
20
TextInput ,
20
21
} from '@mantine/core' ;
22
+ import { useDisclosure } from '@mantine/hooks' ;
21
23
import { notifications } from '@mantine/notifications' ;
22
24
23
25
import { ConnectionForm } from '@/components/ConnectionForm' ;
@@ -28,6 +30,7 @@ import api from './api';
28
30
import { useConnections } from './connection' ;
29
31
import { withAppNav } from './layout' ;
30
32
import { useSources } from './source' ;
33
+ import { useConfirm } from './useConfirm' ;
31
34
32
35
import styles from '../styles/TeamPage.module.scss' ;
33
36
@@ -643,6 +646,219 @@ function TeamMembersSection() {
643
646
) ;
644
647
}
645
648
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
+
646
862
export default function TeamPage ( ) {
647
863
const { data : team , isLoading } = api . useTeam ( ) ;
648
864
const hasAllowedAuthMethods =
@@ -667,6 +883,7 @@ export default function TeamPage() {
667
883
< Stack my = { 20 } gap = "xl" >
668
884
< SourcesSection />
669
885
< ConnectionsSection />
886
+ < IntegrationsSection />
670
887
671
888
{ hasAllowedAuthMethods && (
672
889
< >
0 commit comments