From 7e5417a0ebc4490b6b62d77ddb7814500798ec1d Mon Sep 17 00:00:00 2001 From: = Date: Sun, 29 Dec 2024 17:35:02 +0530 Subject: [PATCH] feat: completed migration of app connection --- .../integrations/Amazon Web Services.png | Bin 15275 -> 48039 bytes .../components/navigation/RegionSelect.tsx | 6 +- .../components/v2/FormControl/FormControl.tsx | 2 +- frontend-v2/src/const/routes.ts | 8 +- .../src/context/OrgPermissionContext/types.ts | 6 +- frontend-v2/src/helpers/appConnections.ts | 29 ++ frontend-v2/src/helpers/platform.ts | 4 + .../src/hooks/api/appConnections/enums.ts | 4 + .../src/hooks/api/appConnections/index.ts | 3 + .../hooks/api/appConnections/mutations.tsx | 58 ++++ .../src/hooks/api/appConnections/queries.tsx | 135 ++++++++ .../api/appConnections/types/app-options.ts | 24 ++ .../appConnections/types/aws-connection.ts | 23 ++ .../appConnections/types/github-connection.ts | 23 ++ .../hooks/api/appConnections/types/index.ts | 36 ++ .../appConnections/types/root-connection.ts | 9 + .../src/hooks/api/subscriptions/types.ts | 1 + .../GithubOauthCallbackPage.tsx | 128 +++++++ .../GithubOauthCallbackPage/route.tsx | 18 + .../components/OrgRoleModifySection.utils.ts | 3 +- .../RolePermissionsSection.tsx | 3 +- .../SettingsPage/SettingsPage.tsx | 2 +- .../AppConnectionsTab/AppConnectionsTab.tsx | 97 ++++++ .../components/AddAppConnectionModal.tsx | 47 +++ .../AppConnectionForm/AppConnectionForm.tsx | 117 +++++++ .../AppConnectionForm/AwsConnectionForm.tsx | 187 ++++++++++ .../GenericAppConnectionFields.tsx | 42 +++ .../GitHubConnectionForm.tsx | 164 +++++++++ .../components/AppConnectionForm/index.ts | 2 + .../components/AppConnectionHeader.tsx | 57 ++++ .../components/AppConnectionList.tsx | 86 +++++ .../components/AppConnectionRow.tsx | 169 +++++++++ .../components/AppConnectionsTable.tsx | 321 ++++++++++++++++++ .../components/DeleteAppConnectionModal.tsx | 51 +++ .../EditAppConnectionCredentialsModal.tsx | 33 ++ .../EditAppConnectionDetailsModal.tsx | 109 ++++++ .../AppConnectionsTab/components/index.tsx | 2 + .../components/AppConnectionsTab/index.tsx | 1 + .../components/ImportTab/ImportTab.tsx | 80 +++-- .../components/OrgTabGroup/OrgTabGroup.tsx | 53 ++- .../pages/organization/SettingsPage/route.tsx | 7 +- .../components/QuickSearchModal.tsx | 2 +- frontend-v2/src/routeTree.gen.ts | 31 +- frontend-v2/src/routes.ts | 6 +- frontend-v2/src/types/index.ts | 1 + 45 files changed, 2105 insertions(+), 85 deletions(-) create mode 100644 frontend-v2/src/helpers/appConnections.ts create mode 100644 frontend-v2/src/helpers/platform.ts create mode 100644 frontend-v2/src/hooks/api/appConnections/enums.ts create mode 100644 frontend-v2/src/hooks/api/appConnections/index.ts create mode 100644 frontend-v2/src/hooks/api/appConnections/mutations.tsx create mode 100644 frontend-v2/src/hooks/api/appConnections/queries.tsx create mode 100644 frontend-v2/src/hooks/api/appConnections/types/app-options.ts create mode 100644 frontend-v2/src/hooks/api/appConnections/types/aws-connection.ts create mode 100644 frontend-v2/src/hooks/api/appConnections/types/github-connection.ts create mode 100644 frontend-v2/src/hooks/api/appConnections/types/index.ts create mode 100644 frontend-v2/src/hooks/api/appConnections/types/root-connection.ts create mode 100644 frontend-v2/src/pages/organization/AppConnections/GithubOauthCallbackPage/GithubOauthCallbackPage.tsx create mode 100644 frontend-v2/src/pages/organization/AppConnections/GithubOauthCallbackPage/route.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/AppConnectionsTab.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AddAppConnectionModal.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AwsConnectionForm.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GenericAppConnectionFields.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GitHubConnectionForm.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/index.ts create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionHeader.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionList.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionRow.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionsTable.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/DeleteAppConnectionModal.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/EditAppConnectionCredentialsModal.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/EditAppConnectionDetailsModal.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/index.tsx create mode 100644 frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/index.tsx create mode 100644 frontend-v2/src/types/index.ts diff --git a/frontend-v2/public/images/integrations/Amazon Web Services.png b/frontend-v2/public/images/integrations/Amazon Web Services.png index 65b4a6ee8491bde66c4508a584a3d64ac7473057..d4025224e85920269af295dab1d00aeb6745c134 100644 GIT binary patch literal 48039 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

o+Uf9!a=SuYmdNCo!wh6ygLS2`KF z2|9i*2+xlW+TI{Vm7Yb#Pqy~{!X{yziSEFuhdni^xbmVupw54 z29t2V117U^`z1TxbM|}T_`mP}THt>z@c+jG({6B33Qf$cq9z9M%>V!a!6PUoDx~5Dd7cTct#UX&I0YI( z`A!oS5LN;yBqn6PDaTsF%-kk5Zqdj-wpbul*C3VFU_E2iFkxyLPgPTDE<5vke5aEf z1{)jK4gf%hHqL&{JltquBBdEevNPlS+`M!~GwpEPJj`<3aGaF+1{Xm3{};Finh~fl zxPRCrZBO(Ch9>Xh90v2uXOl#j%-xh6DE8&|Iz(=!N};Vlq5~Hta7p0eP-6s%JCkxh zD-i$kuwi|-?13^ZhukjvCf6+OmUu&RXdg24Yi!d}3XL!fF3A#V+J7FaH^0r~Nm&F@ zx+$$fvBV@L?WyY{1C18$D|Ch55G`?VL3k=$iasv6MipeIgu)DT8E%Xnu}?vGLIYES zN-W{EPlfF^chdFnZrs>z!M^MHy$>|55cHUPk1H%)8FdWJ7(I|CIIS^@8$V&*4*+w` zJ-W*o^Bvl~={8qOX_}W@5|f^M75yFvx)?6r zS}DY($TpgiF#WH%aPxvQJIY~lJ!vPhn7C@VYkd^F4>z20qWa~|Si|(4kw6$P z@lU9Bxg@hd!T40CD%Cr68%FB zA~cr0hP$_HCb)QIY^;26adDBJn|nooifWLWmX?;4m$%*1&dzQzCx=w;qfJXp^>23D zh-gG!3O0+on@4jtTl$15B_$;`000Q4me1g9Fd0h<;PZUQ;B&juH@CK~R8mzfY^s?S zaP$mZ6qN`=no+|X@Ni}p1+5UzeMFJcYjyCgmX{>VudjE6h)IS=MBKF9&B%Dh;Ip0N z!BJJ1O_g-#~VA;kdV0v~pRyJY|9nsDw8UG%SuL;q$mN7V4>)NarwP z&}qFJ?(}|@9vd5zdvjG7>sz_i4!jRt>95>-`3sqefL_rEC}cH~IxYA|Y*^@Gu8&xn z1}Ga_YjJ#)&Ej>J{QP*|^}IWpj`7_L28@?e<{8tWfznP+ZgyK*xPY2G>eagMxABxR zD2ID{AV{@+lNK${3|BOLEY)i2O5Efvs4)EoBAQBn4Kl}$%3W29-S$X^*5UUEx~j1D zy?B|y{-FWNS6JvEO<*y~YZg6@#>Ym7=d5~#M$JPXs=)Zj=xBi3={2YkI-3YEUryf? zkU$YI&z|#+*k4wODP;Nn{yvec=i|O4$FZK6n3BSoMPDk}n8Q2b2XdRw`cA`{0lxXP zuJ?0W%dYo_)!8$4VU-o7X1iz#I^sN4!2o57fPFV%_dgU9L{@Y)N}PhaY`!0Fk~n3l zUto~%4t93~-Bh%+fGCjuZ?(0xh$%yzFkN|POZj2A`*4mxlM zYRrOs-u1(Y)fA^Ft?i<<{4xN5=rdid=Tuu+os zzhz~oeU2xycLrnHK}i%uD8WIjEUAUcq97q5m()SC z+S*G2tk2KS&L)u#bM@x4Z*{e`4}{!Ka8()k0XLw4dvAj2YoP}jCk#=L73`1=r+IKw zGzMLNnZYmN;>C1bly1|SzTRVp0Z~zDLm~c0l9j+N235EBAWS*+!L38spa ztSl?NZo8Y0w)WaLR8&+EDjyft5}onk;iroQE!%gy*N5}wpPVOe1Gl8>kUUBhRPgE~ z3+l1^W^kB5W`(j#mE5jIO%p*)y()__dJI(=sZ(PA^U+Z$_DE+qa*aw@>O0vPd0h$p z1J1vjTDBd|3MwjCE8oUk9f)I{Jf5z$|9D=s9Nyb@KEpsdO7u6_jg!%U>b*)kGoxSP zRTx=E0a<|+#Fp=P71F}(Ad_YInb9C=Dd>v%FqtK8CN4DNMGlsRw>ncX*W-`|%sVjAqSf_QvAbkz6UuN1Ty=jzXY6>|u@w=XO9bR%>toxGCelg` z3=AA=HQ59=>>!y)wT5kL2T+DG5Z4sk--YJ%6jqSdw5N$dm+zBurb|cX9&Cq_T-@Ky zDhLer+%sWV^{%e+*o~(!_eDfRG!_*W_V78eBdX#uX}Iu7o~9NtHj-XY9^*uz^;mp` zYmFwWY^do_?^+2Z_Mq9h9wPZ{sDNIHiHT9@UaB#|L_|PnEG#bO$^U(Pv;&YS=P*~p z{Z4(ATAer_5Yw5=l_(Nlc068u1_uWZrHNJ& zA~-Z?)W1~fb@_aO-6cxB`4@N81|`&-M=xMhSThTABnFy9mG>yXC+*ZvcE-lX*X*!b zuGJHhkdQeMCP2r-!;83^0=%u+pBYq9iTCUlzKj1 zbUYi{dmYadhK{i5h!ABIH#d_5pf0w*Iw*>QCILl15zLQ~8uP}}IjT>;pz_)YU=ToXsaAP%+i`;5h+BjZe0Qi%)*!$P=c?wTDA7L@^3gmfck2x-y=$%%@K zFI4Gvyfi5)DvC%lA%&t4NoTWLuh4<#!dYL{z9hSIlVqI*?}Krf9VKOnV!LrVj7rP3 z^>rc_GLG~KX(=fwe)f6&YiX?(hy5x_nWPLLgSbouI|m+V^sBoS87?|KdPN8lIlbcC z0%Owycy%Ri)$a+AcrpZD3w=@4($Z@AJ)I}8={Uuw6^itA$Mf~PX-FZ1(_Yri&CScN z%a_92kh;F-H8y`X0uP_=0j`#^EKN7`?*7t|47^ z1*1l;Jdl~0*|f3BXNXQYP?CvoD28OS4jfofgj$3{3o2M`4#Y%%ZJ==D*9VPwTradg zr+hyg?{@|w6>9Ca39TYI3Wn-uMn*7Y#Xu+iKjkP^OTBlaI4~|dUoVE13S(Jea)W95 zAzx=xQ`;6hKVDAte4n;Mp9}tOa^hzs*df;qhokqD!7@|t=`Qb~gailATWz#I)9R|? z%LeFwDJ-NcUZ~!@ItM3HZ>r+rFu>f#=Br9FYew;6(H$Uu7#R}9YT&tPw3e8T{Q&l& zWPFJ}ovyBq*X#Xhvfcf>dNDZn+Z|40vvjbuupn$GOhsYYag?$gICBcqH_w@5jJASa z$NP9$(BcRR4jqamU4o2g^7i%)R8mqBSIFjHw*?Pw8|TBp@P}?QjS=-V#a<9yD7gmy zyW}aGv5cV7_Wn&b4=%7dHFzu?x(8DKZz`@W%y{Y#>Ns^ySKHOb$`o57gN{?{6!fhr zN@@{k86KE0on_4DFT~BE+2CZZ^tS8eq};OA`Sc)cg60kdRb(W* z79a{bJ6*Kt1ZV88o^EbWaisOk!yr>d$LcrH;O4WnU<6&B(b$U*{GzuN4ONKVV=g~C=s}agC|SSx`i=VUUv9SPfzLXNCKpE zI$imzscfyH;uZ3puH4Nqb?)(K;*x@!6^y$kVt#tP&Cum2a01=)9jTl1qO~ z?t0sw%XHmniR!ZTJYbppbR#|*w>S!#nf#`~$zvebLoPf;m+>CeCe{dZTcVf`A}gDy z{A~rui|_P?_3n6+&}?Fkx{=>g7Bc}P_1Kdq+y_tw$uQIHZ66EqiHID{q$(d3omLg_ zqi-MbGG4sC(b@iU-K-{aMLnPE{;}?H%jWbdw^;u>z;-1~K8Ej|MT_J9(0g%<^ko+@ zMtQA!36Y=J@P4ye{a?18Z)llieY<$r#+;PVQA@FpT$HO+c~xodrLU&MCGZ9Je5pab zp=9leA5G((bBoJzZWrSEE|-ThhHd0w>%yl{~C-cE2R z2L4u%Q0+Hh=3p7=24~`w4*`t3)+D6x0a$Loaao;f;@mnsEt zxLWv`6MJ#UL1n4Ii}QBEj1zmmcl^tjzQXwN?BMip=7$2WQ_mwjht0y87VY7`Y3_2A zDIV_|PR*wmL}X&$w92tVJ@?2Y;Japrtx=851)d*5BgCtO#PrUiZsA5p??~#%*w`wc z(cm9578aJX)G0SB%9zhI z@8II`aK?32*|gD$L1}2rlY)80{SxnQZ<365j3D_Z@Rwie&hUYWbeGTjTdT`CzR)?G zXlZ+1?W9iNzCS1GS_d)NI&HkskFZ}tq@%xhZ*Fdm^(WDtAoU@yNIMt^nn1?-rSv|B zsyC12Q=^h!nG=OYT9xeWr8)$!O4|(_@e8A6xi4Q=T}eab_YReJUu@q5cn+EN4>I>S zXP()7sN0;;bW!OLtLcVv2LBN1x{~~8Yqn+378R)4$U%rfAr5`yWojfDIyK~az^Opk z{N5Sq%+!;NH+vMYZsyWwyw8Q)6VUnJ3k z6FSrBue$H8cRRFmjNG*h3v*ciW_7puju!~3i+X^<92?hYh=SIc`_N4e&#vW1JEfR& zhcG%CR#ee4h1m;MWszh;f-H|0W|8LwFJ5wxJxd^Wf|fQmEP{8mZ8i=&L7%gfMAKba zvznBv+bWnmL|40ofrQfy5{(8^3N1e~&ghbGj0zB1BZ8jyF?>Y$h0FvbdLTCbvKjkC zixW4|;S}cdn7IQLqhyoKdY-PwaiL5K<8!a+=>V3RnlCYzaragm-2e!*7HHkDh0y4{ zibToHjenuvh8wfV{K@^&rMHpW%6EuaKKKolM7YjOPSuEun-~ZcMo&iBoyMFWl2EOX z+zP=4eQHyRDyo8D7PVET%tjt(rK{X3A<5v%t!yV`TLDtU!m`DFMcHa$clzPj6;7tm=jZG!edMU$8lKyk0QYtQ}a|RFSLpc^letJ zVSNCX#*+YaQAtS(N*#y|aB(|(5>+-|9JttOJSf^2aH71fFXnVe%DXnv`Z&0trul-x z?N}59%`z}F6w`K6R<3Wd%uRB|c}8l@>1}7)6QDr`ir%MayDiiF;_`kE7H-NK3A+~; zwd5fG`G<8o7*2B!sbsL&%LbmgG&^}dk`x$OiLt@#)VQxcw0;^H=tH;Vv`d?v5X@^e zB+e~5{;<#KGH(La*UL^QEgGpdqZ(S3j`R_%pKNly7Q9AXpOmC0RVAowMH}84CuuEH!8Y4qCa`L%q26M1vR$R*u@t7 z33aD=RkLLVstH!RNW>&EPg7;Hn9|fmzu3C?E>jm36@?gr<8CiRqOuYb zD?BkJ;&+gZLNx%OCv0qP#sqx%0xm3V=BUY$s#4*}|0(^L+niFL2>M)%|EyIt9pOCm zdmp4fW({8#JH=D7(d|sL$T^fJ^ZDCdLk(w44!bddW8i;ZpRP_|0087CO-0bJHgc5A zhAia}4i$$lsTgO&rTO-0dw6f>!IUC{hNB6`3PVYJvjkk#@%bE}CybL>%q4k)Lqh5; z;(C|ik&w<>z-3TD6w)&PR9s+fBvQTR(VtCH6_Qd!Yf7~zhj9Y~0|dQXC~!qb%ob*l zdxD9NF*7!`*~O#jb+vc&6A_UXMy5+s{`1+PCGBhU3SYDK0-e?HQ-~zUEw$G(WClX_ zm&!Oj&*K%ZGIdOD4F7B1h3irepV6TWTO{GvTW(Nm#8jd~#fWmlA5t1VkDwA?5qWie zzw9(?l(?}e0kP0%LR%V*H{C_sHF2AZ`%9uKIyJg<&86;3oHds()^bxn0rf(E5PzKk z9ZU!nULV@BkH%~}l`X#}Bw(1Y?A5TcgG1&i*LmlpjC6s+e5PQ;D_1AR(1oD;tyh|? z3UA*SE#q8EBx|0=A{}4S@#oK2cW?^7UJX`g`-6glw&UXBuB~*Og-m$ALN}|Ab?M1X zbv|(u@nsc9K=%(k@@68DGA)Njm*unR;D+?D7|0X`?aDydSMdFUJ?bCvQcZ;DHyLaE z!|jU3T!VfFb5g?sr+yixTmLEOqBCygDNcp_)k|^xbg#}NrG=-QVJrIZY_=E|8ULTx zA@R47&i50?yZ#@(#s->&5e;ps^kg>IKFqO9trq9vn$NqMjl6Hi@y7{A59!q=$v+|C^ z9hYA7?rc9gB6c1ydY)am#1iW+^j%vq-C0;X_YZsj7@CNHdg_{iZyY zq_Fagw(_C~qu)L+7pS>F7bqrXMq}ygjll}-wE95g1(?oikDxEzD+p?R1T11Oq!KFx z2nudhEc;%?u24zf60G4Ca)v2qq9&5PKL=AEGgk9X>?|mhf2}C9s13nLD&invT!S6D zIw80;J-u7ycAdw*zP?9J<8q{8*QlX>A{K_}O$7qa2*So%`&w%Zu!E}wAr%DP@361! z?RVSUJYN(1c{IA;-Cu$iRhlMO?nPdNe*bnw5igzr_4i<5;pIeN2EkNmvbX&K{Xsb( zRMV+VI*2idS4d}-Z_szbMF5Nwa1=XwnOD4g_RbC@1u44TcXX@%=_=`r`a?YQax}|$ zlI3W2pgV7JdpldjN5EdFNPhxb39OLXl~9&JfZzI<$l{7BlEstZVR`(gP|g9OY={

7t*rR<=-x28 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 TP5cD!t%Z;hlNYTL)(`j}ObumG diff --git a/frontend-v2/src/components/navigation/RegionSelect.tsx b/frontend-v2/src/components/navigation/RegionSelect.tsx index 090d5fcfc..bd916fe73 100644 --- a/frontend-v2/src/components/navigation/RegionSelect.tsx +++ b/frontend-v2/src/components/navigation/RegionSelect.tsx @@ -3,6 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2"; +import { isInfisicalCloud } from "@app/helpers/platform"; enum Region { US = "us", @@ -80,10 +81,7 @@ export const RegionSelect = () => { }; const shouldDisplay = - window.location.origin.includes("https://app.infisical.com") || - window.location.origin.includes("https://us.infisical.com") || - window.location.origin.includes("https://eu.infisical.com") || - window.location.origin.includes("http://localhost:8080"); + isInfisicalCloud() || window.location.origin.includes("http://localhost:8080"); // only display region select for cloud if (!shouldDisplay) return null; diff --git a/frontend-v2/src/components/v2/FormControl/FormControl.tsx b/frontend-v2/src/components/v2/FormControl/FormControl.tsx index 8651422a1..4d5446519 100644 --- a/frontend-v2/src/components/v2/FormControl/FormControl.tsx +++ b/frontend-v2/src/components/v2/FormControl/FormControl.tsx @@ -44,7 +44,7 @@ export const FormLabel = ({ )} {tooltipText && ( - + )} diff --git a/frontend-v2/src/const/routes.ts b/frontend-v2/src/const/routes.ts index 5eeb5071e..35fa7e444 100644 --- a/frontend-v2/src/const/routes.ts +++ b/frontend-v2/src/const/routes.ts @@ -43,7 +43,13 @@ export const ROUTE_PATHS = Object.freeze({ RoleByIDPage: setRoute( "/organization/roles/$roleId", "/_authenticate/_inject-org-details/organization/_layout/roles/$roleId" - ) + ), + AppConnections: { + GithubOauthCallbackPage: setRoute( + "/organization/app-connections/github/oauth/callback", + "/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback" + ) + } }, SecretManager: { ApprovalPage: setRoute( diff --git a/frontend-v2/src/context/OrgPermissionContext/types.ts b/frontend-v2/src/context/OrgPermissionContext/types.ts index 41a2e7e3c..4480bbad8 100644 --- a/frontend-v2/src/context/OrgPermissionContext/types.ts +++ b/frontend-v2/src/context/OrgPermissionContext/types.ts @@ -23,7 +23,8 @@ export enum OrgPermissionSubjects { Kms = "kms", AdminConsole = "organization-admin-console", AuditLogs = "audit-logs", - ProjectTemplates = "project-templates" + ProjectTemplates = "project-templates", + AppConnections = "app-connections" } export enum OrgPermissionAdminConsoleAction { @@ -47,6 +48,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] - | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]; + | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] + | [OrgPermissionActions, OrgPermissionSubjects.AppConnections]; export type TOrgPermission = MongoAbility; diff --git a/frontend-v2/src/helpers/appConnections.ts b/frontend-v2/src/helpers/appConnections.ts new file mode 100644 index 000000000..9d52fb14e --- /dev/null +++ b/frontend-v2/src/helpers/appConnections.ts @@ -0,0 +1,29 @@ +import { faGithub } from "@fortawesome/free-brands-svg-icons"; +import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons"; + +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + AwsConnectionMethod, + GitHubConnectionMethod, + TAppConnection +} from "@app/hooks/api/appConnections/types"; + +export const APP_CONNECTION_MAP: Record = { + [AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" }, + [AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" } +}; + +export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { + switch (method) { + case GitHubConnectionMethod.App: + return { name: "GitHub App", icon: faGithub }; + case GitHubConnectionMethod.OAuth: + return { name: "OAuth", icon: faPassport }; + case AwsConnectionMethod.AccessKey: + return { name: "Access Key", icon: faKey }; + case AwsConnectionMethod.AssumeRole: + return { name: "Assume Role", icon: faUser }; + default: + throw new Error(`Unhandled App Connection Method: ${method}`); + } +}; diff --git a/frontend-v2/src/helpers/platform.ts b/frontend-v2/src/helpers/platform.ts new file mode 100644 index 000000000..821febcb8 --- /dev/null +++ b/frontend-v2/src/helpers/platform.ts @@ -0,0 +1,4 @@ +export const isInfisicalCloud = () => + window.location.origin.includes("https://app.infisical.com") || + window.location.origin.includes("https://us.infisical.com") || + window.location.origin.includes("https://eu.infisical.com"); diff --git a/frontend-v2/src/hooks/api/appConnections/enums.ts b/frontend-v2/src/hooks/api/appConnections/enums.ts new file mode 100644 index 000000000..3c1a409a4 --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/enums.ts @@ -0,0 +1,4 @@ +export enum AppConnection { + AWS = "aws", + GitHub = "github" +} diff --git a/frontend-v2/src/hooks/api/appConnections/index.ts b/frontend-v2/src/hooks/api/appConnections/index.ts new file mode 100644 index 000000000..177955438 --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/index.ts @@ -0,0 +1,3 @@ +export * from "./mutations"; +export * from "./queries"; +export * from "./types"; diff --git a/frontend-v2/src/hooks/api/appConnections/mutations.tsx b/frontend-v2/src/hooks/api/appConnections/mutations.tsx new file mode 100644 index 000000000..bb8831342 --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/mutations.tsx @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections/queries"; +import { + TAppConnectionResponse, + TCreateAppConnectionDTO, + TDeleteAppConnectionDTO, + TUpdateAppConnectionDTO +} from "@app/hooks/api/appConnections/types"; + +export const useCreateAppConnection = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ app, ...params }: TCreateAppConnectionDTO) => { + const { data } = await apiRequest.post( + `/api/v1/app-connections/${app}`, + params + ); + + return data.appConnection; + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() }) + }); +}; + +export const useUpdateAppConnection = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ connectionId, app, ...params }: TUpdateAppConnectionDTO) => { + const { data } = await apiRequest.patch( + `/api/v1/app-connections/${app}/${connectionId}`, + params + ); + + return data.appConnection; + }, + onSuccess: (_, { connectionId, app }) => { + queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() }); + queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) }); + } + }); +}; + +export const useDeleteAppConnection = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ connectionId, app }: TDeleteAppConnectionDTO) => { + const { data } = await apiRequest.delete(`/api/v1/app-connections/${app}/${connectionId}`); + + return data; + }, + onSuccess: (_, { connectionId, app }) => { + queryClient.invalidateQueries({ queryKey: appConnectionKeys.list() }); + queryClient.invalidateQueries({ queryKey: appConnectionKeys.byId(app, connectionId) }); + } + }); +}; diff --git a/frontend-v2/src/hooks/api/appConnections/queries.tsx b/frontend-v2/src/hooks/api/appConnections/queries.tsx new file mode 100644 index 000000000..624a91f0c --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/queries.tsx @@ -0,0 +1,135 @@ +import { useMemo } from "react"; +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + TAppConnection, + TAppConnectionMap, + TAppConnectionOptions, + TGetAppConnection, + TListAppConnections +} from "@app/hooks/api/appConnections/types"; +import { + TAppConnectionOption, + TAppConnectionOptionMap +} from "@app/hooks/api/appConnections/types/app-options"; + +export const appConnectionKeys = { + all: ["app-connection"] as const, + options: () => [...appConnectionKeys.all, "options"] as const, + list: () => [...appConnectionKeys.all, "list"] as const, + listByApp: (app: AppConnection) => [...appConnectionKeys.list(), app], + byId: (app: AppConnection, templateId: string) => + [...appConnectionKeys.all, app, "by-id", templateId] as const +}; + +export const useAppConnectionOptions = ( + options?: Omit< + UseQueryOptions< + TAppConnectionOption[], + unknown, + TAppConnectionOption[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: appConnectionKeys.options(), + queryFn: async () => { + const { data } = await apiRequest.get( + "/api/v1/app-connections/options" + ); + + return data.appConnectionOptions; + }, + ...options + }); +}; + +export const useGetAppConnectionOption = (app: T) => { + const { data: options = [], isLoading } = useAppConnectionOptions(); + + return useMemo( + () => ({ + option: (options.find((opt) => opt.app === app) as TAppConnectionOptionMap[T]) ?? {}, + isLoading + }), + [options, app] + ); +}; + +export const useListAppConnections = ( + options?: Omit< + UseQueryOptions< + TAppConnection[], + unknown, + TAppConnection[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: appConnectionKeys.list(), + queryFn: async () => { + const { data } = + await apiRequest.get>("/api/v1/app-connections"); + + return data.appConnections; + }, + ...options + }); +}; + +export const useListAppConnectionsByApp = ( + app: T, + options?: Omit< + UseQueryOptions< + TAppConnectionMap[T][], + unknown, + TAppConnectionMap[T][], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: appConnectionKeys.listByApp(app), + queryFn: async () => { + const { data } = await apiRequest.get>( + `/api/v1/app-connections/${app}` + ); + + return data.appConnections; + }, + ...options + }); +}; + +export const useGetAppConnectionById = ( + app: T, + connectionId: string, + options?: Omit< + UseQueryOptions< + TAppConnectionMap[T], + unknown, + TAppConnectionMap[T], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: appConnectionKeys.byId(app, connectionId), + queryFn: async () => { + const { data } = await apiRequest.get>( + `/api/v1/app-connections/${app}/${connectionId}` + ); + + return data.appConnection; + }, + ...options + }); +}; diff --git a/frontend-v2/src/hooks/api/appConnections/types/app-options.ts b/frontend-v2/src/hooks/api/appConnections/types/app-options.ts new file mode 100644 index 000000000..bfc9e5903 --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/types/app-options.ts @@ -0,0 +1,24 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +export type TAppConnectionOptionBase = { + name: string; + methods: string[]; +}; + +export type TAwsConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.AWS; + accessKeyId?: string; +}; + +export type TGitHubConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.GitHub; + oauthClientId?: string; + appClientSlug?: string; +}; + +export type TAppConnectionOption = TAwsConnectionOption | TGitHubConnectionOption; + +export type TAppConnectionOptionMap = { + [AppConnection.AWS]: TAwsConnectionOption; + [AppConnection.GitHub]: TGitHubConnectionOption; +}; diff --git a/frontend-v2/src/hooks/api/appConnections/types/aws-connection.ts b/frontend-v2/src/hooks/api/appConnections/types/aws-connection.ts new file mode 100644 index 000000000..86074ca35 --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/types/aws-connection.ts @@ -0,0 +1,23 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum AwsConnectionMethod { + AssumeRole = "assume-role", + AccessKey = "access-key" +} + +export type TAwsConnection = TRootAppConnection & { app: AppConnection.AWS } & ( + | { + method: AwsConnectionMethod.AccessKey; + credentials: { + accessKeyId: string; + secretAccessKey: string; + }; + } + | { + method: AwsConnectionMethod.AssumeRole; + credentials: { + roleArn: string; + }; + } + ); diff --git a/frontend-v2/src/hooks/api/appConnections/types/github-connection.ts b/frontend-v2/src/hooks/api/appConnections/types/github-connection.ts new file mode 100644 index 000000000..d00936cda --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/types/github-connection.ts @@ -0,0 +1,23 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum GitHubConnectionMethod { + App = "github-app", + OAuth = "oauth" +} + +export type TGitHubConnection = TRootAppConnection & { app: AppConnection.GitHub } & ( + | { + method: GitHubConnectionMethod.OAuth; + credentials: { + code: string; + }; + } + | { + method: GitHubConnectionMethod.App; + credentials: { + code: string; + installationId: string; + }; + } + ); diff --git a/frontend-v2/src/hooks/api/appConnections/types/index.ts b/frontend-v2/src/hooks/api/appConnections/types/index.ts new file mode 100644 index 000000000..fcec4a1df --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/types/index.ts @@ -0,0 +1,36 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TAppConnectionOption } from "@app/hooks/api/appConnections/types/app-options"; +import { TAwsConnection } from "@app/hooks/api/appConnections/types/aws-connection"; +import { TGitHubConnection } from "@app/hooks/api/appConnections/types/github-connection"; + +export * from "./aws-connection"; +export * from "./github-connection"; + +export type TAppConnection = TAwsConnection | TGitHubConnection; + +export type TListAppConnections = { appConnections: T[] }; +export type TGetAppConnection = { appConnection: T }; +export type TAppConnectionOptions = { appConnectionOptions: TAppConnectionOption[] }; +export type TAppConnectionResponse = { appConnection: TAppConnection }; + +export type TCreateAppConnectionDTO = Pick< + TAppConnection, + "name" | "credentials" | "method" | "app" | "description" +>; + +export type TUpdateAppConnectionDTO = Partial< + Pick +> & { + connectionId: string; + app: AppConnection; +}; + +export type TDeleteAppConnectionDTO = { + app: AppConnection; + connectionId: string; +}; + +export type TAppConnectionMap = { + [AppConnection.AWS]: TAwsConnection; + [AppConnection.GitHub]: TGitHubConnection; +}; diff --git a/frontend-v2/src/hooks/api/appConnections/types/root-connection.ts b/frontend-v2/src/hooks/api/appConnections/types/root-connection.ts new file mode 100644 index 000000000..0dc4a616f --- /dev/null +++ b/frontend-v2/src/hooks/api/appConnections/types/root-connection.ts @@ -0,0 +1,9 @@ +export type TRootAppConnection = { + id: string; + name: string; + description?: string | null; + version: number; + orgId: string; + createdAt: string; + updatedAt: string; +}; diff --git a/frontend-v2/src/hooks/api/subscriptions/types.ts b/frontend-v2/src/hooks/api/subscriptions/types.ts index b1c4e224d..b57a2d26f 100644 --- a/frontend-v2/src/hooks/api/subscriptions/types.ts +++ b/frontend-v2/src/hooks/api/subscriptions/types.ts @@ -45,4 +45,5 @@ export type SubscriptionPlan = { pkiEst: boolean; enforceMfa: boolean; projectTemplates: boolean; + appConnections: boolean; }; diff --git a/frontend-v2/src/pages/organization/AppConnections/GithubOauthCallbackPage/GithubOauthCallbackPage.tsx b/frontend-v2/src/pages/organization/AppConnections/GithubOauthCallbackPage/GithubOauthCallbackPage.tsx new file mode 100644 index 000000000..2370daa95 --- /dev/null +++ b/frontend-v2/src/pages/organization/AppConnections/GithubOauthCallbackPage/GithubOauthCallbackPage.tsx @@ -0,0 +1,128 @@ +import { useEffect } from "react"; +import { useNavigate, useSearch } from "@tanstack/react-router"; + +import { createNotification } from "@app/components/notifications"; +import { ContentLoader } from "@app/components/v2"; +import { ROUTE_PATHS } from "@app/const/routes"; +import { + GitHubConnectionMethod, + TGitHubConnection, + useCreateAppConnection, + useUpdateAppConnection +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +type FormData = Pick & { + returnUrl?: string; + connectionId?: string; +}; + +export const GitHubOAuthCallbackPage = () => { + const navigate = useNavigate(); + const search = useSearch({ + from: ROUTE_PATHS.Organization.AppConnections.GithubOauthCallbackPage.id + }); + const updateAppConnection = useUpdateAppConnection(); + const createAppConnection = useCreateAppConnection(); + + const { code, state, installation_id: installationId } = search; + + useEffect(() => { + (async () => { + let formData: FormData; + + try { + formData = JSON.parse(localStorage.getItem("githubConnectionFormData") ?? "{}") as FormData; + } catch { + createNotification({ + type: "error", + text: "Invalid form state, redirecting..." + }); + navigate({ to: "/" }); + return; + } + + // validate state + if (state !== localStorage.getItem("latestCSRFToken")) { + createNotification({ + type: "error", + text: "Invalid state, redirecting..." + }); + navigate({ to: "/" }); + return; + } + + localStorage.removeItem("githubConnectionFormData"); + localStorage.removeItem("latestCSRFToken"); + + const { connectionId, name, description, returnUrl } = formData; + + try { + if (connectionId) { + await updateAppConnection.mutateAsync({ + app: AppConnection.GitHub, + ...(installationId + ? { + connectionId, + credentials: { + code: code as string, + installationId: installationId as string + } + } + : { + connectionId, + credentials: { + code: code as string + } + }) + }); + } else { + await createAppConnection.mutateAsync({ + app: AppConnection.GitHub, + name, + description, + ...(installationId + ? { + method: GitHubConnectionMethod.App, + credentials: { + code: code as string, + installationId: installationId as string + } + } + : { + method: GitHubConnectionMethod.OAuth, + credentials: { + code: code as string + } + }) + }); + } + } catch (e: any) { + createNotification({ + title: `Failed to ${connectionId ? "update" : "add"} GitHub Connection`, + text: e.message, + type: "error" + }); + navigate({ + to: returnUrl ?? "/organization/settings?selectedTab=app-connections" + }); + return; + } + + createNotification({ + text: `Successfully ${connectionId ? "updated" : "added"} GitHub Connection`, + type: "success" + }); + + navigate({ + to: returnUrl ?? "/organization/settings?selectedTab=app-connections" + }); + })(); + }, []); + + return ( +

+ +
+ ); +}; diff --git a/frontend-v2/src/pages/organization/AppConnections/GithubOauthCallbackPage/route.tsx b/frontend-v2/src/pages/organization/AppConnections/GithubOauthCallbackPage/route.tsx new file mode 100644 index 000000000..c7bd3e40f --- /dev/null +++ b/frontend-v2/src/pages/organization/AppConnections/GithubOauthCallbackPage/route.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { GitHubOAuthCallbackPage } from "./GithubOauthCallbackPage"; + +const GitHubOAuthCallbackPageQueryParamsSchema = z.object({ + code: z.string(), + state: z.string(), + installation_id: z.string() +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/organization/_layout/app-connections/github/oauth/callback" +)({ + component: GitHubOAuthCallbackPage, + validateSearch: zodValidator(GitHubOAuthCallbackPageQueryParamsSchema) +}); diff --git a/frontend-v2/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend-v2/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index aa8c4d7ec..56f922b52 100644 --- a/frontend-v2/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend-v2/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -49,7 +49,8 @@ export const formSchema = z.object({ identity: generalPermissionSchema, "organization-admin-console": adminConsolePermissionSchmea, [OrgPermissionSubjects.Kms]: generalPermissionSchema, - [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema + [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema, + [OrgPermissionSubjects.AppConnections]: generalPermissionSchema }) .optional() }); diff --git a/frontend-v2/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend-v2/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index 38bf47b2b..5d359423a 100644 --- a/frontend-v2/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend-v2/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -69,7 +69,8 @@ const SIMPLE_PERMISSION_OPTIONS = [ title: "External KMS", formName: OrgPermissionSubjects.Kms }, - { title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates } + { title: "Project Templates", formName: OrgPermissionSubjects.ProjectTemplates }, + { title: "App Connections", formName: OrgPermissionSubjects.AppConnections } ] as const; type Props = { diff --git a/frontend-v2/src/pages/organization/SettingsPage/SettingsPage.tsx b/frontend-v2/src/pages/organization/SettingsPage/SettingsPage.tsx index bdd2fee3d..32ee0ae1c 100644 --- a/frontend-v2/src/pages/organization/SettingsPage/SettingsPage.tsx +++ b/frontend-v2/src/pages/organization/SettingsPage/SettingsPage.tsx @@ -12,7 +12,7 @@ export const SettingsPage = () => { {t("common.head-title", { title: t("settings.org.title") })}
-
+

{t("settings.org.title")}

diff --git a/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/AppConnectionsTab.tsx b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/AppConnectionsTab.tsx new file mode 100644 index 000000000..6f775109c --- /dev/null +++ b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/AppConnectionsTab.tsx @@ -0,0 +1,97 @@ +import { + faArrowUpRightFromSquare, + faBookOpen, + faPlus, + faWrench +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { OrgPermissionCan } from "@app/components/permissions"; +import { Button } from "@app/components/v2"; +import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; +import { withPermission } from "@app/hoc"; +import { usePopUp } from "@app/hooks"; + +import { AddAppConnectionModal, AppConnectionsTable } from "./components"; + +export const AppConnectionsTab = withPermission( + () => { + const { subscription } = useSubscription(); + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const); + + // TODO: remove once live + if (!subscription?.appConnections) + return ( +
+ +
+
+ App Connections are currently unavailable. +
+ Check back soon. +
+
+ ); + + return ( +
+
+
+
+
+

App Connections

+ +
+ + Docs + +
+
+
+

+ Create and configure connections with third-party apps for re-use across Infisical + projects +

+
+ + {(isAllowed) => ( + + )} + +
+ + handlePopUpToggle("addConnection", isOpen)} + /> +
+
+ ); + }, + { + action: OrgPermissionActions.Read, + subject: OrgPermissionSubjects.AppConnections + } +); diff --git a/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AddAppConnectionModal.tsx b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AddAppConnectionModal.tsx new file mode 100644 index 000000000..c74985a3f --- /dev/null +++ b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AddAppConnectionModal.tsx @@ -0,0 +1,47 @@ +import { useState } from "react"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { TAppConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { AppConnectionForm } from "./AppConnectionForm"; +import { AppConnectionsSelect } from "./AppConnectionList"; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; +}; + +type ContentProps = { + onComplete: (appConnection: TAppConnection) => void; +}; + +const Content = ({ onComplete }: ContentProps) => { + const [selectedApp, setSelectedApp] = useState(null); + + if (selectedApp) { + return ( + setSelectedApp(null)} + app={selectedApp} + /> + ); + } + + return ; +}; + +export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => { + return ( + + + onOpenChange(false)} /> + + + ); +}; diff --git a/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx new file mode 100644 index 000000000..1032f9958 --- /dev/null +++ b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AppConnectionForm.tsx @@ -0,0 +1,117 @@ +import { createNotification } from "@app/components/notifications"; +import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; +import { + TAppConnection, + useCreateAppConnection, + useUpdateAppConnection +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { DiscriminativePick } from "@app/types"; + +import { AppConnectionHeader } from "../AppConnectionHeader"; +import { AwsConnectionForm } from "./AwsConnectionForm"; +import { GitHubConnectionForm } from "./GitHubConnectionForm"; + +type FormProps = { + onComplete: (appConnection: TAppConnection) => void; +} & ({ appConnection: TAppConnection } | { app: AppConnection }); + +type CreateFormProps = FormProps & { app: AppConnection }; +type UpdateFormProps = FormProps & { + appConnection: TAppConnection; +}; + +const CreateForm = ({ app, onComplete }: CreateFormProps) => { + const createAppConnection = useCreateAppConnection(); + const { name: appName } = APP_CONNECTION_MAP[app]; + + const onSubmit = async ( + formData: DiscriminativePick + ) => { + try { + const connection = await createAppConnection.mutateAsync(formData); + createNotification({ + text: `Successfully added ${appName} Connection`, + type: "success" + }); + onComplete(connection); + } catch (err: any) { + console.error(err); + createNotification({ + title: `Failed to add ${appName} Connection`, + text: err.message, + type: "error" + }); + } + }; + + switch (app) { + case AppConnection.AWS: + return ; + case AppConnection.GitHub: + return ; + default: + throw new Error(`Unhandled App ${app}`); + } +}; + +const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { + const updateAppConnection = useUpdateAppConnection(); + const { name: appName } = APP_CONNECTION_MAP[appConnection.app]; + + const onSubmit = async ( + formData: DiscriminativePick + ) => { + try { + const connection = await updateAppConnection.mutateAsync({ + connectionId: appConnection.id, + ...formData + }); + createNotification({ + text: `Successfully updated ${appName} Connection`, + type: "success" + }); + onComplete(connection); + } catch (err: any) { + console.error(err); + createNotification({ + title: `Failed to update ${appName} Connection`, + text: err.message, + type: "error" + }); + } + }; + + switch (appConnection.app) { + case AppConnection.AWS: + return ; + case AppConnection.GitHub: + return ; + default: + throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); + } +}; + +type Props = { onBack?: () => void } & Pick & + ( + | { app: AppConnection; appConnection?: undefined } + | { app?: undefined; appConnection: TAppConnection } + ); +export const AppConnectionForm = ({ onBack, ...props }: Props) => { + const { app, appConnection } = props; + + return ( +
+ + {appConnection ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AwsConnectionForm.tsx b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AwsConnectionForm.tsx new file mode 100644 index 000000000..d1137a887 --- /dev/null +++ b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/AwsConnectionForm.tsx @@ -0,0 +1,187 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { AwsConnectionMethod, TAwsConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TAwsConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.AWS) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(AwsConnectionMethod.AssumeRole), + credentials: z.object({ + roleArn: z.string().trim().min(1, "Role ARN required") + }) + }), + rootSchema.extend({ + method: z.literal(AwsConnectionMethod.AccessKey), + credentials: z.object({ + accessKeyId: z.string().trim().min(1, "Access Key ID required"), + secretAccessKey: z.string().trim().min(1, "Secret Access Key required") + }) + }) +]); + +type FormData = z.infer; + +export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.AWS, + method: AwsConnectionMethod.AssumeRole + } + }); + + const { + handleSubmit, + control, + watch, + formState: { isSubmitting, isDirty } + } = form; + + const selectedMethod = watch("method"); + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + {selectedMethod === AwsConnectionMethod.AssumeRole ? ( + ( + + onChange(e.target.value)} + /> + + )} + /> + ) : ( + <> + ( + + onChange(e.target.value)} + /> + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> + + )} +
+ + + + +
+ +
+ ); +}; diff --git a/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GenericAppConnectionFields.tsx b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GenericAppConnectionFields.tsx new file mode 100644 index 000000000..d9e25a0a7 --- /dev/null +++ b/frontend-v2/src/pages/organization/SettingsPage/components/AppConnectionsTab/components/AppConnectionForm/GenericAppConnectionFields.tsx @@ -0,0 +1,42 @@ +import { useFormContext } from "react-hook-form"; +import { z } from "zod"; + +import { FormControl, Input, TextArea } from "@app/components/v2"; +import { slugSchema } from "@app/lib/schemas"; + +export const genericAppConnectionFieldsSchema = z.object({ + name: slugSchema({ min: 1, max: 32, field: "Name" }), + description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish() +}); + +export const GenericAppConnectionsFields = () => { + const { + register, + formState: { errors } + } = useFormContext<{ name: string; description?: string | null }>(); + + return ( + <> + + + + +