From 0daaffd13dd9dd0765866e74fb78cd5e36425beb Mon Sep 17 00:00:00 2001 From: James Renaud Date: Tue, 29 Jun 2021 09:25:10 -0400 Subject: [PATCH 01/42] chore: update doc with screenshots Signed-off-by: James Renaud --- plugins/pagerduty/README.md | 18 ++++++++++++++---- plugins/pagerduty/doc/pd1.png | Bin 0 -> 25299 bytes plugins/pagerduty/doc/pd2.png | Bin 0 -> 22614 bytes plugins/pagerduty/doc/pd3.png | Bin 0 -> 49915 bytes plugins/pagerduty/doc/pd4.png | Bin 0 -> 25070 bytes 5 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 plugins/pagerduty/doc/pd1.png create mode 100644 plugins/pagerduty/doc/pd2.png create mode 100644 plugins/pagerduty/doc/pd3.png create mode 100644 plugins/pagerduty/doc/pd4.png diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index e1ec7f89c7..12329288b7 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -8,11 +8,21 @@ There is also an easy way to trigger an alarm directly to the person who is curr This plugin requires that entities are annotated with an [integration key](https://support.pagerduty.com/docs/services-and-integrations#add-integrations-to-an-existing-service). See more further down in this document. -This plugin provides: +## Features -- A list of incidents -- A way to trigger an alarm to the person on-call -- Information details about the person on-call +### View any open incidents + +![View Open Incidents](doc/pd1.png "PagerDuty plugin showing no incidents and the on-call rotation") + +### Email link, and view contact information for staff on call + +![On-Call Contact Information](doc/pd2.png "PagerDuty plugin showing on-call rotation contact information") + +### Trigger an incident for a service + +![Trigger an Incident](doc/pd3.png "PagerDuty plugin popup modal for creating an incident") + +![Active Incidents](doc/pd4.png "PagerDuty plugin showing an active incident") ## Setup instructions diff --git a/plugins/pagerduty/doc/pd1.png b/plugins/pagerduty/doc/pd1.png new file mode 100644 index 0000000000000000000000000000000000000000..8242792b5a2bc72ebbf241afcf56c4da9525a63c GIT binary patch literal 25299 zcmcF~1ys~uwvl-CZ-ZbPgdP-8t02&@CY$0s_({A>Aq6AT8YtAq~=9Z}9(p z-~GSu-Fx3%Z>{HAEYFV=|1cWETs88WN zMLfhz!SKX$WIQT&!clrmL;0$ATH+Ep>WaD@w>7Nrpp#OfVo!!4? zJ2^|a!gu)xzW*s;Cv^`8bN099P7oKUskxM^xxF*>KQ}Wo{nuIt7pUzYO3h5!&27!? z;M1Joh;jaFD+eoxGsMXX@?X&Lui<}D;~*dbH8*yKK-D1-+kXm5<)2U}xVYH3DHt@t z_GS<_C&ovN{;|MZ(%9KtnCek#tQ>r-T>R=BKmlGp0S*8w2af;;$6r%Ha6K?Hb~gS$ zC-Vz%@(FMO|F_8yGq8on|70o%Bp_$+pefff}hizkBY*~RKNlPwKIn63E0lq(wyDF-ja&q zKd_O2*g~Knh#8#c+<)(vlaNq>LM*_x@Byc{GU60+QWAWe{CsecuyH=BJsb@7s{eOl{%7j`U&-O$4&eWb9R71+*dGn+ zUmC{#@5=XQ>~BUGF33M!aLfMK{Fh6C|M-_vGPj5G2?}>Pv=|gm5D;`5fz`T(`kaOSYS%u{}|qNC|&LbHyS~ZBE}WPisz1Z*~**x|>&eIWXI3goq@5fc=y) z2a+N3a4m9p&uZ)$cn2~brvaI=501UTS~mhGx!5yM`Nra%p#kj?Ah_ykT4 zHa%5}S6xUnOnA|1;WMX?!I9xLhBR z2}KM(xc>Mfp9JaO+Hrvi2=K0LqD#9LlPQ?jNrd5`L7wE(=)@_ft?xyUhWoEA^=a;V z$>!t+4`m@tJF^=q+?|&_X%0Ouns>tUHJ}&q>}mDYQ@q9tR#+c##jwrFK0=GP63Cmb z?&`wTpy4+O__yarYAEDf9tJf_VZD(PEFLaB#TpUsV7_-RxDsU`Emj?j(@3?=ck>q@ z{*CY9%)B>ywS?Him5O*Fp%U0gv!-#K4o`Q9puhMtT14;mUz7Vi%=yVqrUER712uhp zACeG5zMOA~uAXm-pw_9MMiW39DMa&rozugbZgK~dfSbl%vWRxqcmNExK63t}FYE;N zL7Ss4h9taKpHX1$n+Lt*t`C|T-NI<75^SwE-E~XeyPfP7K4*j921MO2BMtz#B_{np zjNMvq5chD4E{`t5vLEV<)}qD_TAidR!u5TxKh%w+pRDINqO{y1vW{D8K~I0&+524S zlGMPtZx*G2DH}qYTsSZG3~X)vzBgvIBbof(>F0BTfe)s+x8E*G2*~_~s53gQFAkb7 zZ~a7Jo9_|vZx2KPi+5dF=DaGuz1-3T*DJj|qeS`YuR}U}LCHnfQG)05c`$vi?X#1V zW(m)Y?FKOx-0@eN4k%@DfKwWX!`mIt@ab!_h8 zXfIICwl#Qn6B3rLu=%t^oqF<4@N-_B^^81<5$m0jw88JW*|Ia%{mI7f4yQ=~ivzc@ z?VHVKiPZ|FzRM}f27b#^MV`2o1)ij~R}-cW0EZn`n9t$V&3ML)Xoh>$$57R)pRr}+ zw^8(#O}B#p!&?Ar`Rch>3pJRZEnHFHY{LWQ<1WcqPzsh6N=;GNX^-a3v5g6eK@&X=$ROeT5yd7eB0!=#7w!FRGcxIV&}f24N2pxK=F--Yxg3F zuV7h0<*FPbq2G)ZHBpLZpV(%GlQbG7qbVl&?OlWnsk4J)lz1oT7|Ip;wzVKw5m^5I zckDzN>}$FcdQGhf^?-bPH#wP4mkhFk_jVFvv=L7SBioqQk2Y5mp88}FY zW}T{}$8^2A*bbu@SxhqM{DruyRI!n1&|MUEwuNesjXH%3!QaD*w=_6NV&_)tJktPt z43U*@mIolcs{=Keh4T*(I|oPQx57&guBHsqG*Ox~%u`)k1MfOFB5L%MDH(ZLUutk& z-0ae08RCLV%SsE5USW58a=4%8O$XaRzgwycBm<2#Vt+ zW(EN862k3`l9_N8>~Fq=C<-+BEh%6v+_mk<&eivl`jgYZ%?DWtiCQG`?yiK&jY1e1 znp7C={ozs`#P`8ZG^npnCkT5aj2^vtJ1oVX)g^?$_jI!IFfqN7`P+zGFM8|xEJ_Bk zn*5Zn=SY>|BCqLo!Lzp%(peuzp$MS{ipSljmR?=b6KokSovg|^r0yGF6bqaZ5!%3} zyRNnDtFw&OYxnoq!(X>i3;Sa=U_y60^mQ3-JQ+Nzr-3(ySEmx+LehMXDX%WYs89p; zo*+GaQlx*{zb3tbA#y9=F)+!i&?;Q)hfi6WLhiZou@z%qbmf4vLEw5Oj=n;&VY#7E zIh7}8I;dz{z-7F|Pc-;R*>^6shIJ2}U)LAVw?Dc^ZkJ!ytV~;moG&-CMfp&5d}mBx zEeRK3AM)N@!6i2yyIFLfKg&khSU5ko)vCWNn!fI+?Oscm+pIpC*VGn|Y`8an@n|%c ztdggG{%m^>jfshwi-2~te{GlT^L0xhaF%+fxa!i#!}=CxWOfr51@rTX(BW~}3A?&J zZudZ(Xh2NolW7XN1W z>kRKsvr}PkKVx&HaO35#p%Iwp;jTuhpB~g$E;lHH7Opuh-~Kp}*%qk7dqucA$q9e+ zH{TK$_#b}ePx~wM5ch9g;}P0p#e7T_x7#1T)-qZz_kN;49V1l$~^YuTuA7( zJ6~saDW(;oTDM}D{#fyl09Ee*_oW9lTlR;-?1Paw-8C{Cqq?f9{^v5k0#Z*Q zOP6lO2GTTzZAAW?2JSbzYV=WeTSfdCKCNE|mYys82vm6bu(gW<^HzQ3<*MP{b2+Gt zHAHl(Iw?b(J$}9N^Bdf{H72Ej$T?WUjT67n80u7t^3J08#l$xGX5>{f6u%}O#z=S6=Z3Puw9ENs9Zo!wJ6D`5`C5RQAgfZTNq{c6J-EghGqshy1O!kH!3DjkRbkIN1D z85vvNNG!(?XABPvsA#19wB>bOS}-q~(zxUC0>AT2IRC0Q0rUGEb%tUlM}KkHMy}=5 z*^~<-nsCGH>rf_oWa;?xssP{1$y80)-SAp8Fuo>`m|1wza2orU$M3lp^jKT^GjRu- z$sQmqHcZ9eE38LO8TK(YW@|;AmD1)$U*O7FDL89GpJgzUBT^xx{)!uOz(s$ecXP#2 zg&eNS>X?tQ6gwjdHNwAZw%I{I<4Q0w%{CTZLp8Zy>fKnQs5)cE^6H@S(#_%B>?@ZV z{}_XM|4DPKrps->$rR_d2d(Ymz=J-}TPq4n>}IWy#WA z)NMam#_$H}A?2iH?)i)6;85Xavqs?p=JEg-#<;>s^v`}*p@3a>m$rqg% zqjHux%rflk7J^|IoY-o12w}kzs%5%Wi1mH*@o;}<*`~%CH1Pv^B0}PY0q5GZX>x8B z18F$a`klz(o@IgX#|c?sp_G&E!3=_FE1_K~vvxogfVp5Z66Cjk#V#j5#(px;a&JD$ z4r6z&@7pvSdd|np-gF2){>*9m_Ke&D!1pfB@3=LiNFy9oP$ce+i}|wutI>gA8o*RP zZ&z|N_# zI4YE94KJ4%qnXC$i^a|ec_^W@aR1@o&DLSvV&>?aYx6#%r7Y?lN_Jnmt^S>12|vFA zi!46c-dWcG*6`4-#AC-b)u-0`#$`{pUHLve| zpEMi_KT#1k2>0i+rPxP4S>zc2o)f{Z4AY|(EhL(6-wCuj=wE(Pi`vhKO-;+dw=agx zU^6o?vat0N|7^|i0~^cvD#=U7B&I@VF57R;MvibZt8-)wT>FrHU)*_5UA&1|DT?RPj+e&tP~7Q@EJC@s~+0>Zt0T3bR1x3$pJ7Luk{FX0Y# zc73_zE8Nt{CR7TqONvM5er;M8$Zp{$e5XTapl1Hf!U-NLSu${7lu%;VZZdJCAmT`I zi?7H|EOFtYbxN~7(-QcCtqSrY$Riox0NK!cYV2rGSFZmqG@fL%@b>&}F_~XEK2tj* z%}wm0P*aTjDsTMJjic4hCY+oQWQg3tGB(QQ+Sbq6rlv%%hi~sf&fGVaG+Xs_IQ>?3 zTC%T02yWeb2Vk$8B!Lk7hMVyy5uf!m_?oatQtc+A4ZyU}c{@8Z^%~|Lagom9;V#Vg z=0spJVV*w0>$3FW#Uh>3jHaVqOJiG>LND0qscq{@x@W7;s%a$I|riPZgP9kjqFVFk*wA< zrgWq<;GN|oQ#GmecFR^$&r>i~y3TdGF8POK#NEOG8FGOCV$=0JhS}J1u}oRZefmS( zf!|?%Wa?3mjz^VkL#T+4!AZCRr8?CL-BrQ*iY`{a5M9b5;dtTB`VdYwT|Qxu7xe39 zNOwEkKr!Sq0LdlLR4chASBc#wy@E=8{3~taRjeAJa*&?hY7Ybvt}F!QpmkKXgb{X+ z#TZ%{^Nz+vD*A?|+}pX`4EuL)@5fS*vZQ^*+Wo1o&0gfVnhM+0nUV_SXyP}Ybwj-t z0@`qJ(Th@H%rVY=R8U#GSlPYxhns2xkUz3yQtP&2O4Ftu`_!g}CZ&Q8w8b0PCoO3b# zY(UFXA@UhdSGrrSJQFSAke0DG3E3 zvB!{>-(PSUFLyhnXY3b@LrA%bzXxviBLqpBnW{wPI9=1k0e-aDKCC2-kvi=_Zfojw zR=iz%stnDh0llRL*bI9stj^rVfp2W9vsA~!Xp1(=$>iuy&uvkYSZOOCGYFsAOj6{= z8gP>ujcR^__}v{ivl%0Bgn4HdCrISvj|MWa9j|<{8qBumcwhOa`zVB*4yx}mx9X?} z+FmZ-qN6$u2KRjab#_CelFZj?3yz54@_jJD8YNCPVJx_};;;%^q9Gta4^mW;KMEt| zpWaAp4==V#F6{OM78%r3E~I7>lWQ{ijOE;muu<%ykI){OsWw->qg|`gFCcl|eab(i z^M>B9&-GiyyHDk0;dGd0-}yDTlX+v-+r?1f#@m$@C`AnS^JCxo+i#%oj?ZjQ{}HOf zfBY2#1blrQ_(!|HUEw|a_&8%UmF^Z65(WEDKYcV-uQ<>VODkBy0if~n_Ha1BfIX^p zVEl@ds&k_wgE@S;89_ic7QCNAAMJmVg!#7|&EjVetVwtw!?~Z8onP5zqxJ0^_hw(l z&T}xyfkp1Zv2$!@>2b5UU@|5%tY$PZ6|9HfuGY}TWXV> z?7X99gT*l{ub*AuN0faud9OXfsCW8xy)?G5Y>^g?x)@yd~+!n4nJ)fmZ zCl>fxd1&kSfl+1P5Zv? z$-hp-Xb&zzgRJ~}f*l4+FJupAbw}h@>%U~RW8Gb#SoY46i}F`>lC3PqRBypoY`1hzZR|m5(&}MPE#9jWr9dt!*SN z&hb?%JCW$`L`(A4MjojxPVdzl(NZX%7GQI{GZ_Cc)qQ4ry3{}alkCbHH3||J>-#$L z29_jqncK6NE0BfQ!P^Ac#k0<$i;KQ&ha%fg*Za5aH+eWwBqT5ix(-9DGpU9nh z-Ehfm3hy8tSa*b)3Gc$uSV0SIgWQeG_(ykp(wUk z8GoQ&-g*H@qpcjwv^1>YTxB0*{nPwgCf$?ua`Zf-S|h#lh`CiVZuCGaF~R%0ZB|tLa7GrC+M$haFKG{nxK?DMQj6 zJ7X{Op0s|7Y~7}vDZZc1qs6MfIME1Oe>mSTZ}AVbZ`%Ket9hJ|Qrr(Sc(&feS2S8F zE|cJ%c)D^SdfaAsofGq`T@+yNXp)ct3_6tQ=-8Z2Lq;Lx1o!XP8EEd%m5ShP2fMUl zoJN1b>0R);iAchZCiSK)ZnUE5OLus}GU&0cGqZl^RNCM%w_Ptsg* zd7Y@|8h`m=RIlHA))7J-)J^WQ+oH0@)0bNO>$w2dqU(i=C$Rw6UUFPgm@hG6`*W%zV@lW5ig(g zL4AEQBC|DBBN`uS*56D%UdQ)@tz`H6ID7L^#%6~Y7H}O?>#7eiuk&~!9MZ6Ji^q-h zmd8wYdFj2pSnz(`7Jg>f1 z(X#5|jn>d#6E{o#cK0zd$QiB#ivn6#TP1+o2HW-Q%}8o(>i7@a4K|TK=+akI@^s@2 zHwnyJPIhxyoq)|d@?O^OLSXQzxuOU zObz1CyBT%VAf0NSk#_b|+f;6+oN4I#;wZ4IBP`B24LXHQFzyv&tDO)r_=B9AS6-7Z z^>go;$hnCYQ{$#iG>wnc5WZkF0i{&$gg+(o2JGSIivi}2FnW)XFTUM)Bkw=-9FQX# z>pg6cG}MAu5F$yS!l`euOIJ7nq7s^X!>^;2hrLb}w3?vaWfS*!MONIIw-;ish3Qm5 z>FSE|kqsS;=NYjmF`u5>yln99m?5KIJVf_Zjb>B9{E&y5{`kG$zI0&g20*Uoc+?Uf zI)xWjW-w8V%d$%zM%w*qN=g8jtiZgvlrTv7)p%cjceQ-Y&h(<^kvVt5JF|rEPxX4n z5Bx-&HryV3&LRj5zn21^8;mrbetB`TeqGDfT5mv&s3dxkzv^b-&1T)e)2<(bQFD?7 zE413tIq9cOA@r;~m=QQP zf3T|9@Z^X_F5FN1Wrf>Ay{(+AOa1uRSj1PhGw=1PG7F!FGv=~~i%CDjlmouxml|q1 zOGOq*pYEcDRa}Bmm(WEiUCNpdHz=ot2)@MCm5H2I#`eNa`;~RmC_n^1$koxK!Z%B+ zy&g1&bj;8h_oSK>)#+6mZ+mKr25iEnK?9cBPf=<@H^h?Arg{}f{Gj~44(!;!ms>AY@Zuct%<}S2h>Q(i~ z_-sk8AI|5CWI*+Yg(rDJw@V{I37tYWBMR(qr&|TrCzyY?u8)cNT*J<;FM>cAd`Lve zfl5;3`I->BgSSoJ2$?pW+ek@~YK1<6hTl6w`xAA$KdwV6zX463E z$|xN#I5Ok<>?6odY0uv#_ngd!k>%E5qV;rf+m zod&AgD2ZSar~8O=vadLvWD7CF`auO9b(Ncx3{0zys8!3>%Y~79{H(qC@ zk4SSE>*n$i_8h>;zK70fxGDu4kFjogzW$vvaNv18vqG{w%7LzBr2P&X$vucStu*N}Wzm!NBMST?Axrtv9Dk9c*%m=hcO$eeZ`cZxsizU)@& zGf2^blOItOf zG06M%`=_KsZ*G5~B1f*;erB++M3jp$GQE|igkNvLqFr)dWc09-bNO536hA2yD?XI# zgkXF{!$Vw26QaTGWYE0ys8@~df_N9W`u-@gSGx+asPDG<~_SKsHo zr;&rx2*3U{Ryw{d!EMG>u9>^;>1)nqe!3L*hLF6MJBT|BhmVJJ=~79CUl*3~i>44q z&<(+R4O)Mw8}b$U*#}2|I(szVDaA}CfG|(Q<+fU;t|Y1Jm`g}G(QrLCqCs!4Q6{vH zi&JA@fpeFJ?nQM4rk-y4=8Eb|sZWqc><(6cJ#{|xz2tj7?qx4Xd`Xw|>P?Dr2;X1~ zv1vRb=U(5^0$wwJaLpQyUgKJ-K*IFC>)KaD*}A8#k+Pa=r?A_CLWgNk*=$^koQZoR z$4D?!%cn{DDsQZ%ebf32s>v6VM!3v-4t-($a;Xg zPU-ucZ&y{wW$=55WU$ZD?^e0f2n+4A;A>5{bM2O=vs@UJY6td$1So&Wd&F;FYijol zE#R|zoluZS49&jO5ylZ$>JJX~Af}ekNpqS@XGHo7 z$KO*dBJB-`xu&=XktusSa9E~ug2vAyy`O|%2gebfOYNjwvwE8*fpX6^m4}mBo_2Fl z#SecTR=#oVX?1K|1UmCPswF-!CEj|j@}<Y5Ij8~lov>o~i7nf`cq<{pe&2u@uDTcQbnPafuuWV;lkQrP0awtBsslukCf zS%j^VY%MS8(s418aqLo`xl6(xYhY$=9KF{}W~Mt*M8YR?%Uf@xD~jJ2WNZ!bcP9{V zM-ZNYHYNFt2o68?u0_+xVIwWkIr{P;ZHpLPBaG=g5Y3)1w2%NH1ZNl*ek=XTZGcY{ z9FDL=F*U)kYdY8w$g%)|Eg{x99q9CgPMZ4BD3~sz2!h$yFR;||8^a(%EEm$>kJq-Q zS~8D@)FB2F6Q*g&85@$^Ue{Yrk7T*R)0*EG?JXznx?Vf~hz~w5adX+4_3v zbmd*kb6tVU4H}3+*Tkz+jDW|SmUH9J6mxwxO`(2Ax^Iy(LW}(ZxoPan##eP6aR+sq zDU8ncPY)w<+mhB~!v8$uF0$FrAPeGzYOw_)a<*04y3Q?fu(lXq)^)}mNENDSCkw=H zr4%eR;R)4m5kHn3-JqRI@``tj`9)0H#Ph2w_OIm7sa5?@0_X9z;4EQ6JnMvEw?ipz zP4oo#>Ts+c4FnQ-=XA;rcP2)795zGdd$+V*c0#sqe8eB?v!}vtHd<>Ea0*x&=V2_8Pe3bS_(NU_~=4d%imTCMUJ z-=Kb;%R8(-YH{()DTzK0L)%NYt#d7^jWnV|l`K;|^wI$)$brB%Q}UVDmyxI1ON!lSh3{?)YR+QQT91+2UlBA)A;EBXM0lI^fw7RL;%!DT zb@E)ipR6p(uIsLxd#^DArx4+&jR6q4c$SGRmr&@#7Q)=$2$hqyBL2ZG@_oStuFy2y zg`&mPHPWtGxk**QBDq$hYWQ8I%W8`UyWW+>cP8F;fa(Cc*0uI>O8j~C;!i)kgC@C8 zV7LiHHKDl0d^|?m0?Ftl8HvvNP{trSm)vBlG-QcqyXrghRhY#Iwy?oa_2$=^Espe; z$g3$;KF@lcjUPb08b}qGlA@Q--oPWFF%4cwxXTyZ_7_uqX5EszW>Axo%!TQuTKR@zbdpBl62VBzz-;-)qczJ~5jiJusFyIt93E>8 ziVN!%Nc}#mLNQf8NnoZ_Gn07=X@mt?uq z^IK6}S|}ea@mU*0@qc6n7{!u>H%}pT4QNS)*!kWwt+Gm^+%C<#-N{EDEYkYTMXTa% zlrW)I-K_d}feXv%`4GwkAWn{j`~u?#o$c$*YqY3Svk;IJE&Qwx@7W>EPd$b+wxYSC zF8Zoptax@7Y*vMSH$inuvVR?y?#3v*xT>ufmZm;eG1|9~klUmo@AM-q@W1SNXoR3% z(wV%4k%;+>m-JRS+F~TIy+muL-oW{T=ZlF?I4N2zbOT&MF1|T#s}yW1@`0Hf4yC)X zUawBl`6`By^?il3mdx@Ed#qs})vsy~BVKb4o!m|UC?)2pdz774rBvK4`HA7-@DGCk zHSPe5RnrgrxsJy^f)`#eqr#+AXFK^3+{_4%LwUN4;ZAOgYT zk2iyJ7x#2aZP&W$Ti}$Od3RBa*ZI27a3~OGKxO|vMZylYm7}{U@vy=dY#f@!fcn&T z{87w5$(P|?xAX?TT_7Si*`!wh@fi$RI9Geh<33A5SbK2+;J90jgY{$H_E~y^9$Vdf zgdw*X;Gwb$Zp6u#E!~A6c5d4A5w9Mi)o<0yy>k-1Lj|d=@~_i>rf(?5uc7uhMsa&1 zBCJp&0cOG<6!m%79ur#Ib8}e9afJT)$(k;%q z)Ho2g(;!<4@((FD9vL>7jU|8Hppoc}3v78*rph6P-nApi?9uNot8TnKyHoB0@y2V4 zpnVo#*ZM?{aCsT`rf%MJ$FM3c>Dt7lE%T8@n-agX9^a7!Fft70H*VociSGP|3EV%A zuDq3YlBv`AYAp3wrJsgX%DsF;rGgryVzxOqRU5CJLP5vbG2>aL5(H$iBN zd<@PhNF!Wo$)2!qE`&{24fkIPGYp%iFp0FF&vzEh?=bIt1 ziKj#s*=bwus);{teH*qTLe^3(rx|`jXaChs<2sTaSB7ji)M(Ykvxy1TkYc9msF7FY zQrb18?Hva4NaX&*b4zADdV?^{g5i?0uoCO}j~uBxK-{p;K^rxE9aiHQc)aY4F8jn1ubcTV3nhE= zJpzvAChUJs;&653xIA*yyxi**<+&GEarikYf ze|V9GxBIz7r$+!c8fbb$FD*M?Hr2S#3pP!2B_9Pm3SMBv(zlW9x+=oR%l^Q#@s39q zkem5<(FoWx!bq8!c?AHiIwRj0VKkDqfBmap{j8xze~y&eGNT5w9~n*s>bo-3fLlz= zXHoF%09*n8^>X<4_CH<`|K9cY_MZg9*61Ap1qCZc6SImMCM$`bG9J?rOnE*#{RzH;y8!8z z@q_6#Sxj`(W$wpUfHxwF?st10txV^9@=p83ro7~gvah*jkSj`M)m5#jr}tEKX)H(!i!=#`oS3+ z0z*bz&;{p_iI*KkDb^{{p9DB$P~+q56y=YBtQKh5bmloKne}=t6Tf69=gfe>8n`k(##K2!ruQ@oMhM@Z)QEm#phCOJ{iwlZm*V65L^?;| z(B9|vRM9_D`JIq4h}8A;ZbQCJmg*kfDsT_CoPQJ=J8gY!`0Z6Bzm>YN#D0$hP^G0H z*J=w`xsjmpuE&@f#Ubd6&&77I*3_s>JNF=9L73~Dv9ZpV05R@OcN!}nPE zn^QSJ_65v)y|Kz>Y+@SX`#bLXz`k`q!dy7!^y8cJd~q(*`#HDPTi$}b^%uW0qOB_i zs4ffz-ZlSPEi&WY_A~T$yBJqKlcx%{w?e)Y=Vj#|3|{6@*T$#y&n`#T>WQ>L%~W3Q zV#QNhc>hdw`Pp@XrB)9`ZmtcKMc(IC#Pg*up9T}{6E9uhineXNZoBZ^@$UL8tYwr3 zqbVjf7ID%|O$|?dC(?ytA({q!b-dokw;x~Y>LF!q96f(P8;s9)N*Rdto4fQh_J-|# zxvESWRqsB>&)Y%n{HsLSbVemhXIw0 zmNd$`iXCGmNt{Mnv@_A`8t0oog&p5W@^jh&*!{p3O4k=@6sU;E=s$*sbI~d)8s!Pg zF`Hh7Nm9@diFms=K5KEB_8~sOYT8D}n%K|WIre~gPv4YD@#2(l2rk-k=!=MQVn8w$ zs-1z_!?ce0YZY1|jCH5q!@T3mVAbG+xapGm%?iW%_7JQ=R%30d(6{nB!M~I?w1$5w z&N)lZkD|^5TZ##h_)O#5Y{M&g5e@h-Oh#vVjGm98$4c`D@zwbQ_btRDv;B-TZyVs{ zFBoC{{hHha$3mN~;Y!?*c<2*TyjVNarYad(nDOH;LkIP%lh5C9v~J3Go7MQ01&n2- zE@r3B&CMBcY#rS$4U`pHN0AA)0m>10oxxzRlM}Zs@)1U+hao--4-e0K_qo$+VCq%W z^G$t^#U}@ibf89v?8Bf;{IIT-#dgz-RttZ4)ON14A|?K(M7l5}V%f4M58wIs59)-S zPGo-lASP##ExDX-EPF0DdqrCyH49cDqHlOR#>MKE+newd*PlUZ+yyb`X%7Mf^{|Z- z9VL?Us})F1Bh=E$90a~fvod1H;2|s_-Pc}&E^O~r(+M69&ST& zYPCZ?at$e=sPVpCTxK0QSS;&JC|in2n#v@pASUxXnH40X?gAyg<5lthpnxS{wSDmr zy>)QLrm19VYMO-*mX@_K^9+AA0$ff4L$LPHjyLXgjf)lC$jM6w>8h|Q0#Dh4Xt7#& zOdUZI2Q3LbPn+ib+GzM9aw;IgM<$dC!;Kr=`GT!H+}mJgQ*)NxnXo_)*F&|D&Ead0 z?!)JJ$oONyf_-UY*_&-Fi!YxgWP&;QbHTMxR1l%jJ{f7eDJ$M5aqeB7S0dDbOepGI zZ^b&E<@VWOvuV?9^;d@u?2afZ$8oqJh<Tlip8DsVAV&`WY#r$DY(W5pVcbh+q# zf|!tgK3x&Z-sGm=y@AP;*OX4+qjUs?(;<#wvqJA5gg+Rz}N`Vz0sKba&e)hqm3b$S`7f zbSAAV6}jX0oj+p@#apBiyK5PIT?WA)VVsqZE60C(Z4ISM&2q!)cbD=k2ibJ*4ndYT zKItbAcWW094J8YClmSlXwYl&}(UYK~@?r)e^`NY_KLv;Iy#gMSjC zi?tfq__^b9E{nTh_oS);i^f&%-N>CByTlT^OxB*RZ&%wenR@qK0X#gw0i`MpRaKlW z0Q`BO3-JK6rUd}(+@G0^+DSURhpucC(eS$yI) z(H+=5a)E~qjd-B(FuBHOl}=+>oGy1gL)somDiVy&zZ4d?4uwCImlRu-JM6BjwWeyg z&d2#7!n(}hrM+W){c=i5OXbFaYB+%q_;+jv3meZ@FpK?nh$fz1_x1H@*V?}B63$`} zsN1$(Yk?Q&I(xE%RttmVTOtrrEvt2d!K6vSxh%IwHAXrLm3Jm{DBs);IjXPT1~Q{% z5DXjzP#ixU7*k5a&zKR&x;RLzj_M_Ae_vAt+$g!hIR+R zKRR$#O*D<}DHK?=x)W2h1^`AMK4h8r=1#1{a{@+$ulG>k&*Qmi9yA2;APukGwG0dI zTQv!NHJw7g3kG^_ZoHgT=fUrKrMI`XXO6vy9ToCPvliI%4upn*s+*&<(HJb{#eI(>6&mAR!gStCJjeB3%}aD}Gs zIf6A0HAZc@OZIjHGW+cFahBBgEX)mZUsV!*;esua^~ow?zr8j|Y>;2$2eS2!6U)C& zK9UY!r&SFSw+(zfoIiP9Q1^*YJ{HfGwo>Te$bErrPfHv-=cN8RYt0EdX2r)=*R4En z1mv@pExX0c!&-PKb2_*h@j6K;j_B`S*Hqkwe}dGz4SP2U5Oto1y?~AhuB*G5X^pH8 zB$VmY^n>M?85q90l+N-EzfYiwmetUBRUwl?#%Y2?XtkS#F7e_+N@qnPvP+9aWi8QO zyqqqjE5kVM@y}?0cAldGE^XaB_2H-7La5?sfva==()O-%n^ky)ExT5ioMxcOwhEsR zRz;Y-Rc@*7>A7=R8&K2-&QwP0Xi zfIuLlv^Zu>@yP~_3zRU}S3I;2^78U?XAz{RTb%s3Xdy1$)jZ*ms7&G|!#^33tR}=4 z5uOAIe~chCoT+%BzC|*E6FaVPW3Ti^)y3o|<-}h3kH|Qy{j&Mr2Xa`-S_Z3Lcrd@UMTeITXJ6OiL%!FxrK$jldIH~i8*Oy zS1#hq-12gVAD=1bnb5MH>W2_YaJn#js0t)h2^Nk;_+l&uQre^AYdq-zn`R#cip>}A z;aE}3IdsU)6CHLf&8F`M|F)`g9la+E4CYk|B#npzuI}~xY=pSaBxLWKIh!fp3~ra6 zGawy)#6(%mH>X(2Gzh>B0jI6E{dVO_-4Hy!A-_J~PWqV%bq}_DIpD|;IR>w2X4)J< zTp38@G{tAtQP(UljmT9IG^}#LQP!qdqzKO>wu}$J0hDd*=U?Va zJiz#}Vfa+2Ml1rz{=)qT^r39P7ojESDwGuxbim4A|S}i$#JGIgL*W@$UtAP4BeaT|9o$nn(ozDFkoL^Io_!yxYCC7VMZcr z)*pLcv|_J?B>6(xl)y{V>$8<$_%O~>a2iH6ZQpDqE`!DytONmw9Bn+|h-@!-+;+=*HEhPm}{16#Z8bTwY zCyEGgN%epJMoPs91#*MJiA1d$zTOn`gP&BqBn+&m&>5HZej(<~Ph2U^;mT$W1%6iT zfmTZmUHn;lh4}SfY=Zmt8wWQ6&$yna)UW_kszbvoXvOKptQPZW+anz#2Y|HW2;s}k zf^csjD9CI@MV3N3gJY8XC1*d!YaTJf$;%oD_XjN+)*w|j32>~uk5fE`l|aw zIC6%*S}N{NWIM-@Ipe~=6R6Wra&kK9k7Fh#BBCfPr+2Yf73)dnwShvR*QeWSu8nnu zp$qDqcdg@^_W5Z2jCr?rm%d7dWU z#kn^$Juj$~Wdd_!VX|2Rv~m=rwJz?}|EsUF4vI2-_dcCVD-D7)EX~s0wKPabNjCy6 z0@96yf`qViccX;VB8W&X-3^PhfJ(lPzcc5|yfbIcf6vVB&OXn*cU;%!`@OD(eR!cO z+fhb7K{AAUQ0TE0eX z{0-@DfB9>%m4{X_VdUslr)7~k(WXy3^R^E@={{-N*rDT{Y&bdtVYav-^F%e1W_QW< zhi&_mrLQk401Ply`vFsui-)K9vYkmBaV;g>93I59%zPyxE}O5LvD5Cmm&_<534y$_ zP~l1tx`^|&3i_)d$1J8x^Y=Wb{)Pl*`Ff-Fu=l-~%+>dO=*j(7>Dp@W<(ZgQk9YTc zy{?rG4E8;y3AM$(Hq|2%d87}|)Lv5rQxe5Q5CeK%D%|8{nzTMB2NBS}SoD1~ z-~4F4*3C%5yFa_^AZ9GSt_74-+L#>Hy!a15JcX4oR905HOctrUTnlq#GSM@BtVnoD z1We{Wtp*~apG+K3WScRqx+bznFeu>1yS#KFEgpvl)ZB7Yeh_jrQ#M=1)4itXymO86 zuB*!BPjC0Ho}0!O0Sl3HtG@j`d7;;BJt}mrBE=BbMrv_Q?PS##lhmJelKXr_Xo(jd zqK|quG9J{zIB$t?BS)c^t(Q4&u5`57MPv&C+P6BuE&3I6I?YrW59Hk;=-^RF%U%0)>$sqTlc`UBKOrvyL4j5Io3SBF}!>SluzLjO=UQN zp9A&fvH{Cs<(^-pgZ~r`F+gCmXau8xn3%f-J+PyZ2R$k}%lh}-3EM9tiy)Ak z5!=Wf#+fbn(s7sj_p}f`piHD`OH)->+OhZV5WT;T%XWGCjI=I)p<1CC?|KpVaC*WzJ4l>TB%lXW$k?9W#g=E;5_3akhYSp3kR6GJVS%)I#=rysF=` zOUTYlXV>VGc_n0Myi_vq!H(nT;TSn5A3guxhcWOT;db(R_{jekJ=JgMXNfn{8@O-I zr=Xo`66(H~A$ut~t@~*2@x*JL-hF8cm zx`Pv)6XAn9;pVq#&L7g?84I1i!=~l4Okq~Ew-Bh(9P=$ksdpFC#Cq&2&zVA+NwkqZ z(I#@O_+fTLX_-@5IXfXninTS+S^&Hn61pqF@x;I=EBMYhh2l?R?lv2pTP&15_~z2d z%6I9lgVt;k<7zp}&YRzMH2SSI7tbxlmifpSUm>EhZ*x8pa#tVx`XvOrH~J{ggch^^ zhxpg-XmxEZH1V0e+1U8_6G^{{Uq;3=zy=V5*!b*&ceI*v37qHtC##*uMUfbf&7vRt zpu+NE+1w+7#*?NQ{*(4D?@L$9WL@=%2P8m*FNDBpb^&oq6Jo%!jo1EiQ7A!Z5_~%~ zn&FV=J8Ia&`}}S?mkJ)ZJRfU)i-(cfG|q^!gfG4KVb8liL9(aF+>|K6Y18LxeG9$a zm)AT)jy6VcFS!;!rcJ++p$6TfG-un_gkKWvI{`O7lEDjvtzmIa06#3$`-U1>=*NYD zT7DMz7k04yv3o*{K@CM3L52JHQ#mJML}JRQ<$FC;5HQkS#c=Sq=wS$LI zYnKHyr=(eB9UZh8%mv|pa428rrHFOEudChJ(4*(*eUe#y6!;G?gOCCmj^+kUC$koL zH?F%8eVCeg0e|f>Z127hU?oFF<=#_a%S~*YB@eCsJ*?%=teF?E{ZU}W@b(=n-G`~{ zrDuNP5TAyIMrmoOY(5G7R+K`xJlX}zNYltpf|}FOF7dzPRtmq?5jw&~P-lhhOI34y zw)eHttn##2ik+wIE9?EmL!=L$zEPmk2;vi5Din21SDuU2{r>SxlcbfZ6Avzyp_v(~ zO9P5bNJ#ikGIvk&Nz2odug1E=&&AHJ7+H+jm!0;a0jl*g+tw63BnWf-c=u#$8)Cua z-9f-d|1Eh}+*gN(=1aYB2+jUyh(!p-m3j7|lSF0{58pwV%AzSO!cIn%%uSABmU>T*TKc)BVr|Z#}4KTWn9i)bFT8E!` zUu?|RSZ9Eatd~ED$k8S%etl-Rc6*fawn|LQr;|rRl7$5myt(`qQ$c&JJGgz;uVbK0 zIAsX4kc4w6KB&v;qz#7&ZDF8j!&JnP%;X(wjH7_C4v8G1zq+IDpe>A=0c>j;q1=pK zFowHL9?Xk*v{S)|GhNip4H_aDP61JWis4t z`_k3B?aq@o=f6-Ki9fBg*SN~B%Y|pdun8&M7h8sP09Wb2MgU{gPtm2gqWR_JF4r0E ziu&To{oZiQpsT|L9eO9)LU~_b!$|N5IEp^?ElwQhv-~?{#zVWRsyf#U8O;dv_wT+2 zN{7_XInbJmP#PN1iW)4O0v#vJ-sRUmg+|Ipmj+BG@+!`@D6s{t>!`-7C~Y)=zLP0` zn%jFB+3QENsevf+Pba7SnpHq=2yxLy7~<>WpMw$Y>mET^NIhGB?+>*iND=2Bb`v

6L(?2o*jltkguwFzb!F&IBx5glJ4wfkb{cBR{-f`~ zx@?I~1HZ7SCT2qtCQ9Pk=8>qlc(SYUd#%SvXt}d zx6Nupg-IS!8xlw>lB+3gNo``z;1`M<^lXz1YhRJu(8%7{G$l{89>-6REqU<`c7YQVo6SC z$SiFmuCdOW2PweVQ_f_a8g|?FcFs`GsRBvjUvHGmjf${KNwMqYNRkiqt_(G#5FYU!0GL4z${ zfYwl@8kh}S?)~wna-7Lx0l9?sMwhz3ax(-G=~Q)47}t5!62Zo7=ND`1k) znhAh8@L;uGd)47VHiUnA3f(5Xl}$wOZ+BBGxImq@NzD(vI4#3tB~p=dASGj?*JGz$ zGO=`8&6{v6k+*H#3ifuBro(ymLwWX(h%wC_j_5QM0ve*L4q%)Ei@wKMuQx} z+{v4tFH&8} z>YD{b`FEa`@GoTxxb_GN_&5cC%jqJ?{)x(f1q2x5e{|yizy4n=@aKCAZfa=7zm7@M z8sW@dr;&h@eu3vh@y|d*Lqh>-zd=)g83Ra=i;JtXx@=AaLYq{PO)N-H;_Y`Xn!IzU zKe35u^7y@Nt{UXKD+|FV+`*G3CQJfEQ@V7_CbOQ9U?49CQ@T~=9-ZAvB>%d(1M;Zo z=x7G8W|D~UbX0<7!a#2IJcO;Iy{xIOTOR^e@NQpW%pBsy$H?vPrSU{E1~of~Oz?(h zq6Vh?4s|5CNV=qw#vWH$fPP-GaopTt#LfawyTI@r9}5Hcu2W8pRL+EWK_a7h(u>|| z2n3=OOC0^|p`qnfPnYj@3r+s`-=dQ-B0*Y0v1iXRo*B>a;IOp6kB;S>D}sGv=J+)td)*}XJ9qEo5_^PS@&?ZKUUt0a!u{dR`C{nt>-$T763N}o(3RI! zR2clm__&xv(4;2K=bmO6>CRvR4wh3t96hO+Mkh|D8et{$T%ScD+sA4gIlJyZNh3|O zi3j6Vp1!Xr^5#p~zxwHy=lajNpgzaVLHTwxM{HaGlrlt-A8l@I%;CH)E=fk4`zpq^ z%gW2p5E+3@c(oS#AZI{qbQr?(be!7)vO7;<()FtslLcq@S@6}h`&A4AS=YInpSk93 z=e!%N8!Di2eKjfca-K{>YwQauIY?LQ%l`S&c=xSIPlwL}yMs}Bi}9VC#F%Ribpd!o~V!qdpSxy1BSiRDLMD<*BLkh5ED4RUbFL1b)T*EvM)GO~dXy z=(KCyC*B2xg0I_A&rYF~r>>-n!Tdhbxrpqtx;{ySfD3q_DW(q*wBfNM{zXz+2@APy zFTqR+Dh;diUGVA z$?qh0|D7YGocOQ_u(G!~~jT_&i%Tjw1Z<(ET(CJaYP|C_ga~ z6!*`T{#@d{dP!v^{;ZefIt|HM_wJLu?mNpFkMWgUD|qlKDm3RFNrI~@TP`BZR_=T& zsI9w6sVnR1L(Yu2l6zV|7fuD0dhvC*yyqUe8*Dg4LO*eF?#6%D#s|Ae8@CKpC*d4D z(i$D**D(MqoKLcg64kuOx@wse(Km0nab>zd-{Qe*JKMWxdNdV`FU}HeG%9*Gc%++19Jl} z8UeNhcqhVx4hOkl$IIzT-RwkU0G~!=%$Zbqxc8m3c zk?f+^v9R4Nk>D^#{t5h=+6<6rn~L0NqAm9D&gE<6f9riEb&J1C*SK|#vHJ}mcUir< z(JD;HP8*ZFUi~>{+Tnx17L&ajrTav5XgK}eCt3XG)g4ae{hb#D=7zJ0xeqJ1+6zrz z0Uw~UuKV|oS9hAf{F@wC-^W)xR&haAqq|nt)I_bb%gH^dIyJ5~fryH-Uk{3w;!D9s zXZAb}RSr}}{J<~Tt{%KzQg8;37X5V_2%x#Oo6MSa5ot-Q6mwR*QL6*Ll!Rm+x;-$zG4AAy+?h?zg&+Sr8b$h znW|i|Bvl@A-z+uI>1|%bn&T`Y>9yK*e(vMMig8M0bnU_Iwx>E-H`GoO`|ir_QP~XS zZ0~usJ8)87;W^JKILyM zu7QBVT4$)KH7z5IekSK*>WZ)&ZJNiLDFZ(N2E%JS?0@7k}S;0W{j zks8>nNHC3+W=H@*icYy6;2}`84wYA!$*5J|CGg8i>liyw(?gyJ!JR^)# zgN$1-tKXDP#tJ{hN}{kmQVVl$V9ppAcE7PCB6lW8rka#E-2;?>_FUw|Q5pi|@!$38 z+q?KSUn#UcCQx%QZr=AuVDO+I#O{ZVGMiG17Em9wDFnNa=qVXiv~#&TcUIc+%E$x* zL0g8*F57@uBo5Je;FmM>S$cV4(x1JGW@a?tpB=|O?FNR1OSLx;s37<)TaE8;P6n?* zM%}clWh8f=$$?I6A`9m0_|h$RA25QZjhEi--};dHAPI6G$6_m==8#Z!^g{+U2vPl(sf!yznwIb%!y0Nw5)t{Xdm2H3K` zAyik+zb)zZob4n!)gR6MBM3;(fctdR8lyuND1leC~O?vO9*oN z$b{8DVd3iDDnD~_`C4AGs^(i$5%Py{cIMy&GZq{#hA+a``8eBFp|S|=362+8M=3gv z{-ZxVob5KH;Gw6J6^>aE`&WU%eDS!vs18-IQ=9?i7)RB&>^^mjqmrk7CRk~OgV7AP zMc*o`id?g6)n7ObP1@jfRaU~1+iV5k@3J#8vJaI|07ypllAOitzO&8pptUTJ)I1B4 zVPK>uuNs5w%#bo%^6-Xb15o0z@nyJPiSGED{Grw~4wQY(QJhaNo7}^#FvDk1NQc)J zjs|T_mY;y5ioUYG^q=*}rR|2+=UkSRUR8vuMIA+MO~eps{__k9xZci$cONmcwg53y zqk)Asb0`N6-`-p>6{k&B7Qy?#WF*8(89^z|&_INx7!^H^t~sL}xrLRvUdEs`pw~+j zGnOT}pS))Y=IC7&qW&f#;hns_i)DiQ6@_D$V%*!5;pnO#=ug5y%9*!oqLhU|y3lrc zGt!SQt|DF5ofht1Q!-FK(kv*}r=x{VIMR5TtJ><+4HWtT!6Vf|AFE5bhsSzvxK;+3 z`6{@;B#1$4%*IXULfCJA0v4dd zm^e=Hk<~g!##l9CN%$aa^-#)Jd{L(?D}&g{l|&F4$vV;^Pw&mNh=!-z%N-LON)n}a zbtiZ6VV|LlvWp~6yAYht?ETSseQ;w`24HrbhwDsyGC^1NW-x%ZuRqQlX?;Z{IepS- z3c`2`sAg?a`|=U+f(}u$euV+$*x5(SF!I9VawVx|y&Pm4uO)}yH)vc1bqB;lsM%{b zszCJvYmC!IisPCdBA!0V3PbrrB7u+4Au&Tnp&7b}r_Z24aW0P@6N@@|aMfb2EG3!p z@D5C(s92Cv=`mhmRcWNkdYm*a9ZkzCV%Yl8Jxz=_MUPocuz$)5vxAelFovC=Uq2Rl z1WWQHQTkUjKz=eBOE!4^{P6e9v%2RC^YiM5bOKvvL|A&KBK-USdk=|M+G^ROd-0Kp zH{aqEFHt6dBTFqzGrQU8Q+Pw)R$=iJwcdlTG?6ukLAqZf)j6eq_+=_#33egrfP~*5 zT{v+VT45BIOKO=LZ1BlZDmtCG%#BD#p<1*D+wfOkco|m_x53T?4HavQiwnJ7rId|+ zD##M|Eb9VC|Fy3k2u|mmEP9Dwl-4&$C)r3rsV(&>3C7*}qen?i<{en|lQ82cVJy&f z^GEWw-j{yE8u@O}qGH77g&~KU48cp&Jt!;4 zD@g$e25$JI@?^iGSMvL!A~9odNP8QrzUHh<_-Ds&^Ipe;lU&WqJj~?MEzLwQ|^a$lQ z@{95W67z#&19sZ_5Pek@VM`Y$PID_43u{hq zCs(BOqer3=-md1B4%QGF3u{|DXEC~iwoW=4J1a3dJw8=#RaY5nklkxvcWW(QHEl~@ z2TLIhG=GUe9K`6P{xnFVuc|>K zlqY>BYn8;%>_Y5E2slqXP)!KuU0ceVifY-W<+g`hRJV zwFX8R_hG@XPA%!#^r79$_3!${?ny@39738 z_o7Zt|CR(n13S za`MnH>)AP5xp;wD{#f)+1#4Mzh_x8qpIPJJ=H~zkX>$t+a|48dd>q`o!ra_{i>f01 zz{(tA{y!6Tv9h!A`JafYstPMQgCXY5mez{0VsuEOIPL7Lgn6umY=A;m)*OPoyZ{b< z8(v-xAuC>M4ohnwHy^jPIS-$O&|h{T>&v=Wdi)u_KlT51TPqh!q>g`&kT5_1D2S}U z&%tjlWW`|v4ncDsOAaeuJ|1&xZXSMsjTIe@m8Gzai@TFK(ogK1%x$f?T%B#{ zX#Te~GA@oT?y4?U$T8>nj*10$!-=b!bVW$pIQm7^WaUrr}%Zu!U8#po>m zOtLkyoxh9i{*%%BpV|3$eJ_wTQt5v)`G1RnU2Gs;=I+)~w#bqGe|0Xd|H^o?7pJBBGnLFEBBY~5P?oVg9{y@y%lfw1?oUgy0{RcSumpBsR{#^bWT#+~b zhG%PMq-u90_!?_HZF%%alUGqzO51yGul=d3$yC5W+w`xSTX@iHt(8F|0LVz(_vPJI zrRoCeD~?ZJ7zCWg?Px9<)9jweU@`cpjbC5~xb5SCi?Q&ZOCP@;Piq9DZ7swdzb+!% z#Bs8tI8@o=?qez9OSWFMG^n*6tQ~!5pD}W{o9+x4jrWhK2ucn#ov($Aiu)aa83N8P z{AXP5h{fDidPDJEKEV!=&P&c_29hIxwPkQ~(aX6{$weXO1;S3Z|y}hlb_E~K$}hlQOlTO{IIPhe48#dyC8i8HuZ}T3pAAEdq3~Hzjf} z6h@S}+E1sAT#}St`SU>uBYLMRja)kK&ZA!na8kiA%I$f=lI*}^fsL`~m} zGTf^6UuRR!g5#$T?viq(meb>3fdAMQe!Xie5ykaY?(C;!EXQ62nk0*vu;-9opmoK5 zg(WcN()NMx{IT@<@}GGmi-u-T&x^~j_Ty#+_hM@(JzMgaIjlR2CKH}{)OTUnNBb|S zL8XUsEpDbL;c$G(GRe}&jmD|huUWm;lUH=w9NTRFcmPyh-gH=XKG>wmyX7&n^%R&x zUT<$Zs7+ZyBQC2KyD%&K!$9QjW#FaI?RWj%ie*IPocMEWK6U8*s>J`Y1pvMByly^c{|iO+4*34zit|M;J0-1f%-xi_%~R~*_5{!5aT%EyKmX(04UwRWwxzoT@R-hSoXqV-;K2nxg`fsgM?7*Z z*xLc-3Q3t3x2MVlk5eZEaUD|0QZFDumZjVle%&%*jaLDU4btvuAUYc}YC} zwX5(69xm*6aM)97#Ku#BW0~7=t~*W6lvS@I9+v0Ww{M#ljT8T@PWw_Ga$J@!uZ#V~ zhjW()&$R&pGu~oa8q#KdLBRpQ-PptJSm*st=O_zqjw=*31P|uYex{piE*GiEv%@{# zm3MVNKQ|{>f!E76`ul9gbg2cr(GhTS`J(R(&RAb+c8bIG<&hqZnoqNIDpQm=a!gIA zppS3e5bn2k%a{_&O|J3}D; zS590zlyUuzDP>m0_NOHeAMQCyFb@>b6m;1`tyVn2XuzN0;pMgDe=<`2hP-j*T zfcrn#j%NOTy&k&)LMZV1K>CCZxlH8sW;|R!$#ezm{seRZ6u`E&OhLQJ97dNdBgX3Q z|KOu?JX~3oCw6y4?M%Cp<_~THeHlfbu111&4jj>oZt~@3{}>ShecSp6c}w~T&KcWF zh-(B`utN;DzEVAqOwpZRDj>1Yq*-@bPW6dd|z`U~$%LZs^S#rUDk z`dLqZr=E+AUECG+*#FUQ82YWnIOe;#;Eou22Q~UH&CgQ4g_mNmADJBC-n%;>d-A0oX5j;S2Yt86K5S8HJCpQS-VN$yw_%(T3A?^ zo}OlDNNAw|+#^&A9FA3u2bOw#=(J8sHvCY6G7v+q(I3W%KCkU2FRhc#kUhxkrQCw= z!&n-ze-M$J0uaX{q*^m5C;Rc=o7E{2ax$B3!o8r`Keq^RFEoY>-t;OhNX%q8PNoGi zG{eq`_Y`~-xrLFd7nt_@xB2LLl=HC9;{5#l%*@Q>WPR-=h1|M3V9d0_ze9gbB9h_< zYA&7V$p=}wwAYWNI}Wg{*shfTi~#n}thpm=Gb?OI9~3Z>uq+?QuF*zav{BD83j1(6{?{a7XSc#cK0$a&%yq|p7 zm$3Xj9JMx{clwZV&N0oKw=4`SrJ3?2{_ee{{? zJkC|eLm^>WdxjDP_NqO&-3*q5bs?)t7!v(rEMlb@DwrzRPr2rb+%vfi{q^-W?dqP;sKne;5jczj96|T1d6?J3DXGvvJ!qUHLH0Rj8FK0lHl(UAH+~bns zRfL(yE8N%7 z0>4NpDYJSgJ-xh4y%s%&BH${xPYH!i8OcnBv#4K=QUv*dHh-4JgA;waK{-6aEcZnP z@oG4Q2@xL;;g8{EKx$7ViYF&QweHl*Y$L5-xZa^?M$we-yvDgFSWh|E(n=e&U4~ z+S_*P;aa%J9{Zx-K(rB_yL3G#A>IM|;L*sSMb}`qZsED&h*-FSPd* z7%V{%Am@Cce4Hz3&Ww=_!7BRg!u0t$y^E^7#m|klstSJPuaUc|7|GM3up|=^MZX;`LT+{G0OIb&@@lN&PPxD8@&P#)w8|LX4(YKcrj6| z3s)vD$+|aJWn_xw$5avDX|Y5l2+)CaMBNb0=bj8}ae0x9G-Z!)%jxinENq1mp2%ms zKKza%6K^6LKPc>f$;OaX&qeRN2a_R#xRQ*pjF6c;(-qF)N*a=7q|9>govB)d?r-ga zoRpATV{jElR9<;QT=(gFpm5?Kg}A@>+ou3-Zf*gYaDaR)t5yX?&2@s`hD%_s5#Tx? z_Tb?fv+c}ej=t^b6WW%PZnk!tw{Jn}FD z3!z%_qdCXFA@H-Qm1Lk>h7BzjU=+2aalVLLXU-3-ZK<@>NwGSea~|A$L>8UC${n|k zQz$1I3}LO|K6}9F2i=R@&N)<@D7{b+TgjY5ZXGC|uZ_eh?0lj72BHM1JiPL;KT$X* zS9ijvjSu*m?uK%)1KZqO9@H{8t{Q1KQH|&VUNb)gW{HEH%V>^4T(7TzA2i@KsJzx{ z`4y-ekG|oRmW9SxsZwg&g(tMW*1eGOeG@t-a!=zrmT)VsZPHUgn&u(7@Fw}wW3?2y zT>|-Gb%qQ>5xr`I`H40vq{-Ar)D6mM+hUj{M|ZLov=ygc0j+tTVCCfwi>)4%l$5t8 z5fKqu#cEk%ex4mfu{ZoKfpDwdB&hh4kJ0PT@c`FeOKu69`p@WMNQX-0MqT?5V~Zz2 zi>)W2Yy$O#gj8%1-`j>ui9Q@;`rATw{i_C2Q%*mi3pAOJGz=Z+fNy9_s4qebKj~ zvD6Z8H>jLjw~7pgr9VNdp5$5@gf(_G)W-M*oc3YMt+`zZh%ZhD?1%+j`yKd=MZXlz zK%*;%g@y6-U#{CbzMluhKBn-`glCg?erUBZxT%=)mwFS%B~-b)MIUP;e~ zPC9tCj=QfbPX3x>2Pi7%aV03dELqzs6GIg`KFhri(;9Kg$}$l{B9d?G45YECARf77 zC+QgBRm6-*Z)xO_uV3>rGBO^*o58`s>0Xq=kZ`|kU(Z^*iK6nV)-rgazx|0#qTHsG z=;tX+OF#(KwL={Il$PeYX=mnYLr$m1*u&Mx2}!NTzE4kURr}Kb{|!8kb^_B> z)L0CQQ{)bNd#kUz!D*g9T(GPUWdvX{rlwgsS!L2jE;bmsf}M9sL)-GH;zQ8vYq!^Q zS@)YkH~R{n?K>u7BZ{#f3vVZ{>TDI%vdBb+2q@uJaVnVkD<62spR>vg%jy#VdO$C9 zwKiAsm~epHi*u3+F|nbFAE};uYUCrxlr|^_$s&6vPTsDG62BHvvS_3Y5~P))o0^>K zrfqL6FLuzebx_vQeC|m9HMza9Gw4F3cyx3nm|_Am@Dj8rrGN$tncu%WT?;XmVd??2 zAKW~^_M0t#Dd3a5hp(=#Vm_s+0I#Oaem5{Zh#flLtJ{ntu`D#2*+$+O9#mC2;CRIy!@u+D@nr>6_86|E-lj$5O6yQP^y z9}il!wp3OIOqGr5>@CO@C)QFtBWT>60@zQEX%|OHc{z=$;FRbV(+^5nR@;D z(Si&%-jJv3H`~oH*&fM;o(GhVa*&wK{g&pw{#j$G5KCoZChzj=%hh+b>Mxl=%X#Q( zP24N!?1(z8S_cOq2Hry}DGJEYvs6x_pVDfM8-6#Q$+_ERm=VAad>@||%ZzxPiK|U5 zm4f4bV*w^iPuE<_RtVjK<6;ro7awF>OAA{HD;pXbS`>Mo9X*4yg~Kx&i4te4#qVyL zvIFcwqa}Q1_HsHeWGJ0R5N|xF3iBdku53v7E%eZvylo($MLALHhup!&MYAhSq3CjR=$x~qPxcP$M; z>6;~ps%0uzpymG7m-Ut7V?tkR!1bt6Q;^Ta;>(+93g4YeG{26!s33_qHM0!|bC+w` zevMA;E3$~;9>1KcBMBO&8X`E=P@pMEWDtM z^^E*c6&`;9TUiz#q&8G>>-FY~J`m@AOp5|SEiLH&l!q(eY^zg@OXm0abkW$-&HTl$ zO6Q3L+ghX*HBTl6<2 z?aK?)C_92wyVv1+Q%`p)G}hzO0xBq|K1xZaj(!!0WmwUZ= zROCw-9r(N|Aq2|5QgsE+dEQcYX_7tcelSc4IkUkT1mSOviP7O-v>o`VV-&^1amtdq zHX|?J$46eg`!v}cqP^%HkNJ6&5rY&~LaX)f}4n_j;dXLbWvUf&BdAS^Igy&ktw$jX%`es1K2lO2>?9U`SX%K3$zI3p>tsjYzp%-HF$yh zHmx=Nyx>CY3xXFH7kV@9DmQx+WMmW+nX|fM7Wm3{B(}t>qwJtsw&G|*}`6O5X)TwG9(m~#@Mb6s zUYFVf41&I81nb*mK_I9BlGn%hMm1sAUoLfMrDYtfXHSy|@T2f} z2CmHYr1E_+tSZ&`mREixq@wXI+SoXc#@}}n#;GSiX2E%R3-C=F#$e((*KS3mJLp!w z6^japiH>f+LS#(1b;f5ax#Lq0hj^krD)M`)J-bL=Na*%?Q!((IY8$mR-!pXM!(LO#p(j2~IaT~! zR@{DC#5zLGf>8sWB{!L1JZbzis~n-?!duT2+ip`vRBoHXrw}^Y)VScjboKZpX)u4q zj_@U{{j77T{ZqD5muX7hi<7>Y3(vdVmKW1n5yL+-AjEjv#-}qK-u%-UpGx_1WXG_? zoTf7HJAw)dkH znlQvAY}hEA9N~WS_14wgs@1nkQ3|#D;KyEW_v;>|(D!hA7S4H@-n1W}B^FYmO3$(I zw^<8XIG?n@wLhe%o{=Z}9gQ%?w}k(^z$%P{*7w#V9B7p-!En}AfZ$bC*7N&zPB<tzk}HO}u}@FKd&BWu^0}jKzGZlE^7^>>gUauAczOh}&d7ZiqQz+63A^sD+Zy!tJul{-li`M}OQ-$ZJ=%@Edg4a|t%!NErF z&t4PqhUct0B8fxR7{I+lS$dFC1zS@vP({+#SI)5cC4wr_c3bByXn8m9t`r>-_b%#P zGkwBU;?{{3Rqu*hV&dnW{*R$>?W%97Dn%LT<68_2VFR1%G{$TWZ|k*ZGGBDg6k6nI z46k3YB!B-Qr*=sD@f*oI?w2pQi*>(Z3G1)lI2E<^_^=)xmJq#sXUogqQNdj4>o1)V zxK8~_LX2;v2o>K(<<*;SM&oRn2KC0Q3tA|(xV7{)BCB%Ckch1h3~mT+LtncLotrr& zwA1Jek2;Ke;jK4{O8J~9_9L8EGiWnU#UxUd?!>IU)^QSEACkyrM@&r3F-kBhy3tgs zF}Sx?q)*dMUXf4D#vBkg<1SPfSNd<0h=-7rFaeo3P)f;|n(a8v8vohP->HeF+Qc z@Xt_1ba8Rsk-bj@VHoA2_M&Z8EZwN|C9O z8&iIHGh#32`i;PZ9Z@@rqeU^VU)wj2(Xvs62Vbh8ti}GYs_=c4c`9x?oQB1vWBJ4+>YUq>?qr!AHD_0F? zX~)=KDtq{=a=_L<)5^BV@{o_?>x&FQ==nTeK8s^xbdYD0Dc6+0S4%$rvQ~MMx7E&># z6@~KtWpcN3E*VEKCaBRIv)Rsul!F?}p|qEj(k?eN-ZFMd>0=k@Y4W%61P22G{TCG9 zq<)hZ=-Qiq{K;n^spg9MCTwugqfS%IAB%*QP>Bd{`t4I};1Eb>yL%UiDj7s3k0T$& zhT$DpXA#UYPLCLQZ(hrutnFus9>dUKx3UxNds|Jypv5w}3E{DYEYs(wByMvZrpj=r z2=!Y4eZ{%OZqk&)oFJv8Zx`C_w8Z-1*OOmf2T z%vDV=8)TwwIEWf>#LzLtLCU!bC56 zCg+SrpC*1bS^K#L99)ZIs^=Mgf*dO`b)Cs>6kJBbFORnkatFg$?C##s za8hFf&fO=>-Ev&UwX2kNSgyX=vNH26Yk?g)UuLi-1)!=9A%jaUjRN#H2Db^?8 zLs#@DW_zlW) z+MV!SW4TMBj9|49bDePQJ`kOH_+fe66HUzsS=KIE8QdbS3{qc3?(~88UyyO`Su%U) zoSU1Qmn?M|q40J04nYrdd!W&BID&uT=@**Zjw;W(Zs1gKeOiWeX_c*+uh4#Y3pf`~ z$D<`|G?{N#C)cTL$jX|6hi|-B(6SD_;S7l|5Q2t6eIVj9a zX-Sn#8h-OVJ!7-C0MC~*n!NwxoCu%#1ayNS49>vcqYzsvJZ)OJ07|y0-#f`nOK#Lk z9L;k4A*BMUcEGewkajyW>OEb@*zB|AWlkA#Qx_8zHCCU1BYBJY`Js039S;^40@hqVP0LV{y18z2OE0h%aL28xoWOh9GwrT|}|&75Q2>L>UnA|_gHd+)Tz3a6tt!dW_D zNBoLE0%TfLRlOIb2D03VjO6-+O=X<2GImZT{bNoUWYwef=LSDbQ7St=#h1^Fv_a@E z>2EMgvu#{@7g*pOGxImihidiiN`Zf6#j9IRY{=EhbQNkb@P<;Qyy>l}Cw1cGABSiaJ5J>wwEb*#<+jzyxvpTScFSt%A9Y-2P3>KD#3A+ zJM3nS=JgXQ_PM7&#w|`)OoC+qM54G@g{?KQJZ=V-bG2N*aAO4ngcQFJm_%2dyJ=>i zafihN*o89Y>ms6arnW~EX+wjpM2UZwxg##^TJWb^-g5WHx9Jrr$@jmFEc0Vk@16C) z5H&|J7La9{pdMBfWi_?m(0uRsZDI9zNFWZLL|8YWUBZS~@ZrXmqh#kcdn?v?SZk)2 zXOp-4Qful(gSs34P=jT^Q?%Po8YUd(#e~@80V{b`}u7^hf*dPuK*Z3d2aBp z)C>EqfAM%oqVS*Oi~l1V<4@f`q&zYm<8L1Cze?r!J8|RxsEcGI|5P<;U2ClwHn@M8 za`}fBERpL2emH9|Z=purarE8fGi3(l3PO4y2+08HOtAPrl9MOi&J6O+ktuO-{leroLdehDVrAjy6^<6h zD2V(raM_Jm%kDE@9!3UQU<&o2M90QUF3O9p3CUzbc_UpJ!<7h2tjj2g>@~jovRn3`i+*Wv`_^{p*6cv#qN6j# z8}(~}b~AUN&E@IVgyrf+U16MegcUIBHN!_EUE(yQli?!@^a$BUCFj=1D7i^C{-FmV zHE9h|;DKhbZ9j{gL53x-_XkGr$%BkEN?!DCHvRH-@H}rR%KO#ovGWCB=ziS-yIA5L zgZ{dR7V)!t@18?};DW|x3kM#ri4>1PO_@J^p~+I=dzHwdsEh$pKNO-R$a?k=oHXr* ziOyJi|Dgt(x9vmZxK-5g)1rcs5j*aqpY=ogv1vC98*8FmF8 zqC&=*CxN!76!H{cD&I+)`o?j>e%X0@g`B&4IA&wN08y;?EXct`|K&jH0tVe!+^()c5sD-Elc?+3-v$$>LZ80v_gd6bCG&G^(u z2!G0$4xL5+QTFWVKJiC{UZ}fbnQ?L)Vzj-qu+nm>jL)Q8&&%@(SJt81;j<9KQRt`@ z5SfjVJtKpQi#zQ5#LY$?hxlQ;oUr;uy$&GUw^^G1r&G%-H;%?%8RFQvnFl8hneYIq za`h*+r!0?)J@xW0VsI8IwkwfzYV!2l$cDqu|DuUKake$HvQ z+@jA;9NDKSWHq?~b+&UjFm7nIb-Ex|+d5B%(ET=_3zp{4lRsH{n0f2Br&%VrO%OIU znk8bZ7w6?BFY!#dfbXq`cI68G)vM)~o-d-Lw6!fTX;rE2zRxVp?=+z=Oh;C%P7bbi zZPpByY8p%`3>gX@bw7r4g9c0*nD81oZL6$!FF_(aZ%8)Ygy)AuAUIp~S&0h-3+VB; z{sffU%ggOpJ_bS;8LYyR%nn zqDAKQ`K%vF8|)>9UFm)pfA;Qq7?_;U4W`CRt7#r3}}g=k`88BsR8RZ2Ii7Y4>oE{|3{ z_J0>;IH&iq_s&6egF_3pS^6mxzgP6^{*IA&xX2T{eOh4_V z=vi3%c{pe(!xXB9pOe-c#}*3JkQfC8+nHMb&}gqP=1n>qDeF~Zf)A_s*Fbf<8*F&P zAX38)YY)fn&X#eG&dx>+MYbl?wh%8bJq2aq?%0q{9;DY>ozA;-it+Ml7k{84B`xup z$&(3dF0Qg=%HVfu-7YIfyo0xvpgA+t;Mpgwr|m_kX0E=XelZv^J4hLOx0~_c_Y?~q z2KzdBc{nIFx~XM5nw7))QAL;Ck9Fr=D+mzyWKkFri(iI%U0W3ZOchb9p(&4G#H2eYH~3ZYZq*8beexmpwJ+gD^#>ItH_!(*WfV2PG%y* zlVa6eI1$=1k+QC;HPd}?*JYTh`Bf=o5U$l9ljCixP1nla=7IhrS?0r>fUE9@_bQ%U zCzH3trZuVQEvq*5&q$q&xLKG?*k6Q15zDvIjKAPQl7v;;hCNFYr&T6(t*}=~PoBX` z$m;Lz+LnYoJc*yKS&1I@C~T~L`YX7<-Q{lvAr7`kB5j{jZL<+k)Ybo8WKw0jOo95o{x@tzM-HrQ^hRfq;c<3Rk7*3K0R6TJuf8Vzh5(H z3cCOG^eC#+2brSS`hvWS=f%8hC)9EA3bN?HXhroP`I)|l(qd~C?$9dnrh~c!b3-OcHAW^|3 zXjRb0iEp=lEA&@q(8J`UcGkXLRFrAi=x@ZFXxwhDod@>KkZd9-D>i$PW%PG`zH@Kba1bfxgx8q)y z{TSgSMV9*)ftM3Gl!CX&bx8vYc^i1OE>Eoq2n)%vy2GE_Y@ByCn(ibRr&CxkCThvn z9mwKZA5HO@tTz4>rzlA;tAXS{o_USr+!PFC^&K2rCHvX#=avf?y++1rC7L%xZWChz zm-X1VqoHp#IJ}TKc_}Echzgq<Im5AV<9elJR6JNWorxhtw5rk>D%jC1b&C18#t} zkgNu;Qth(p>S`G5!)gQKF~tNC5mA~-Q8r6dilHCL4pbdURBq$5>*~VNlBvlMV6O>mW##JXYGWie zFU0XgeR6{>Nf4c^fMMm)h$Jm69**aLXjxtm z|3i9EOyKP$qFp1IkeqG}BrnmNPDVHw^Sw-{!_bH|6fJ}fbB?{ynWkiJrUcV~YnlRuSH;69J1xtVGHLN6LH<6O3huM~s`LddTNBGM1stA=>F@)m{?mR+g?dYsco|7cA;^1z>Gk^MbQQ zJyH+S#2KHHoEWN%!Gc64DtQJnb_!ghRq+);pJDb%I!87sr zaD4i`d7m#*M~(`rgTc3-)cr;KdWG%=g|3K1=LVl5a5-wpx&DmOb|ulY)%M;uQ8#mZ z7@9rvx5@nv{@krKR1c%l41Lmh3}krx^5FjT55n7 zFZ30V{n zpH#<J4+^$WH z3JXv`{kF2xSe$)RMJ5FOh8HdN9KYePS*Ro8ppRr_STz4FaL)@5c9z6pQRQb-^ZA!C zBkM5(c=5yx>zAx%#St?nZ;iPDGBMXACwVu$uC}w?-o=#pdCzBbjx?6~=4B{jBwSX3 z8vDm1*Q9xQl{VvkNcu-vKM?g-eCvF?eEOaud*O_W2Udf3m6i&J6-SIf*&oTD`b=8T z-!gAGN_+7wZ_Fx(#|Wp62-AJ8MQ~Yun=2DF#Q%)yTZ&P?5Wmr)z7%$b$qqXU8NvS| z3~dZ~zw#<3b5{;XUgiJ!3v84AmEOvk*wZXtY%;Nz$TZlVu%}}?w2vP@zP`RD_YL37 z$mCk9oi&bW+tegYk~2|w{=NYN8+iHT;Ai(2EWu4_J~POguo$XcqfG&(%dyUCK;Ndu z-J7gDRq5r^e4c}|V1xMYB%fM%x1u&Y$)`r=E6g{|4Bg;G@b7j}S%OeTdk)zbHq4SZ zP0H4KOg3S7g|Up2-};NvV^ht-(`{WyM z4w`7FJS~h%6|dp8+O#f9f8eXT{;&|#rfc(uId7>|J?4)PcUvY7-9E~6ZEd-qzMvsJ z9lZT_d9r@!3K{^Qhq54z9AGx2roXI8=o`pl=hvfyVO`FhV7Vg3&$2~dGyrpGB$YiD zOjx?elp0<1S--U|tqu6Ccp1(SWB@N{$}EqH`_$jKtix4!Xr<5zSsEJ$u{vt9&BU$`e67$J}7~>W@0HrxE6mCCF z;Z|91#88PdMsZPz?Y;L1W<#N45SE{HHXFZn?^q;~K4+U-J*dqK+uLeh4OK(Q<{>dQ zbn);E%!Kp5&Fz&Pk8f`!lC`;l& z+Ek(bHI(eAg(rUGlLu&di$IkJnf}+_5Ub0yaS6Ixi^yv3*)82_{-CK<2_kc1y4P=` z$Us}u_Mi8pX{5;9z8wrB6m`n4ImZuyX1{##S4#Q6L+z|}8T|Xt&b$gW^Jr4M@r3wG zr0VhB>(tbph19=?dnVKyF;&jm30V_5QnC@1;iUhj>H!Hg7C)vbJM_b z7Y$`O@E4oaY|VLo%jB{BaprwwBWFJz5&DHS?n8ylSiv-Ues4CV+s}&VqDqsfnWfpZ z1$>(<8};n@+ zx=A&)PTID`Qan5bn9xSIk9-Gebv9v>_?u<(oI)lk7OB>aDqq3ZA5w{_-J*sT(%u1l z6O|-q-c_8eVMQHOX6NYSSA`w<9PC>^LW!1^=kn`orEg0fMU751n5pSF@AY)esN#YQ zqxMU#o)&4~$r@N07R=o{KbK#qkc&3K*xrQ=la}vK96&BItK2|NPuc=@?46`mQxdZe;nOc#=b)pKx6PTB>God@%FU-ws&)p7F&^gQ$?n>j_k~lOx<{8 z!Byr;VksJ+F5GEKdUfA$_0Qp$_S>ytx_c&QJ$fOTZLU72F(<`-X@eae`IO&+@$+HK z>?x6b(r?Q&)-sU{^pa#mch@b7+D*2{6vNB{w@WM;`m_mz&sbnKeZg zMKxH4d>*I7e{rhh-H!#n6znU<^RH_mIEh-b#vNT*yv2F-r4%7pRSXXs?#RCwRZ1q4 zl-MHYS}DAZ;NxUCLqd0y8HD*k_Du;IDXVCG&XWY`m3oF^7ESVy)(X;*_LF#e1)3kJ zS(nxK$b(VM#o0HBH&BOI4DSc=OJ3$7Vw;;eI zB_$OHaIAD5Cz^cc*|bqz-54L<8S(^Z3K7i1f@X{L{iY^Bg3srEp`{LHX*&j0SFu69 z*1rz#AM78@TnS1Hg(r-&rky`Gu)_mRDCzA%ACqI^Lv`J1ZwcSyztN+MuKxyZ!F}KX zgm3wvblMbXs;X`k5#Q7HsRIZupV&R$2AZF~bol}h78VW@+1=mo*l*q8^pa0eDx4@* zw^fT~NN~z>XcZPQ%FvQpCLm767K+*Au~kry%_b8P2n=M!gsNR8z=3bL#mU)`G1X^A z`WY%5rFh96Sd`|(@UO8i6*#>ZsA*N|RdVP_2iVbI)^sC};H3~PEwC~e%=Qqcrp(4c z8{G+Jo+PwA%|!Ek%WcxP38((mv^v4H-S#)pux^W)vv3>VKR_AO@sMpPa>*q9SHPLS zZMJCw#Mq}PQS%zuZQn3UZ$NMmIq~x z^#?V`2zKw!fwXundU|@xRU(qw?+Sh$r*MiN3ldCLbbHFOI~I|4+^Qy$GItd45XhXR zk?@W$K@ABq#Q|%CjDxcNwyTpkgb^D)!olIhQCZ2sQ8}V&7f7p!JTO=CYHU*oDu@s` zEitRV9#3k;Ar-6hl<+#ee`zy^)%j@m_B1LGI9unrc+E!{yyU*#AaXy!(PZqU`Y<nX6$|2BwJa*dikU#TG}mj7Kcedy!`QG|B7$YwHsm1po4f^q+>uu}mU96-j`{$qwt z3z;+&;H#Zxu^v8^vLF(4g0>h5Kk)N^n34Edc`!~j8<6Wc{lZAt>kg4~w`Td0DJ<;X z!`%uLSvlvuSA9FqXEa^utO$2!5fBpR2l@(I2JkYy`Yow;N%-UaFcp+IQrZp=bc4yr zP-aZVNUhXgVV`b2MKNrS&Nb3>M1iRq9h>|lB~=%Sp#E-d;E&BBEI~^k2bOMXYHILu zRTCIeAxFY9hxH!-_;);gqYD{N-^itL2^BTmcma^#k+i(#8bmJ9+8J+q@RNvLB6prK zoN1Tq$%7?7>sHY>xJJI$O>f(+(JtV2A;uq0@eg{tV%0+B3Cxq+5Lb9`anQhX0R!h?3s7llLS`YDy zCNBq{+d`DfA3^-W%pgdY#;(Ihp2X$(|9Fstx#i)4%E@6%f4(VD$7^XWdgg7DW&in3 zljF9t(^0&}C z(lp%lH0m0+s3@;w_`~+#x^}a>h|?QyX8@YX+4i#uO8O+Q>~UOn&g-Iy5sn49CND#I zCZcgoY8l%DD0#Dta`y~NmRf9y<4ScY?}9dX^WS1>lzP5u8> z%9%$)`Tl*JgzQ_^k}Sz?7{)SWAK8f^WR0V$80%!D zLiW&NU-C@f-yhF&&hz~9obSK)IoExzbDwjab6@ZG>-Cw$Dc*Vg3hyqaYmYnA`z4O@ z;z0Rio!QRV%pU319JsjtR z`ivdIED(b?9z%)jDr&LubePV*ExMSg8SdD3i~S}RR~ZVS0L2t>G2G>bYC$Cdo7lJ~}vb^s{EavU_s0G7w_w^!@1TVaUn$ z`mvj++0b$s8=qhAP)go8F~J1RCjK2QPAz2O>f$U>y~tM2^ejGlrpu_-(Z*ODlaL9G zL7~E>Zr9nL6-UFq4(!i66wU@%ng)Ki&74WBfqdagf44kYpF%SPCnqxr;W^X(3aRcJ z|31lg!e17ys?12@;!U^spuLy1eKy`U{RiKzTO{4h=Gl6#<3Ay9mrxZ*mz?{KNJXR2XpK2rU; zM!Y0O_~7<=NZ(8T1+0A(Wqfp$N>yTJ&-INWrrLL8wfwi4k%y+48O!(cMl)SEz_e?I zu{CT(6?y;u(btYx#BCj*tJd1uap+WAw1DU@@3`>WZnYVvaE<6O8!zDO8?$3{%4SK? z73vLf&A#-N#gn6#o5vK<=rszlfp9<7*h{5gz}meL5IXz*nZ6p2bPm|Z*T-mr$;QC( zIoa&oD??w`uk2Qu&r%~=B+5!9y;(Prg&6-OMMG|LOO*lVvH-_P%*29wV z(!~{@OeK7(C^%wkd4v(RwMfdP#mxN!+wu9BH>GL<7$ChS>K`@JYT?)ZH7#_)(^?X=@th)a{}I3K&A~K<@pV)O)NEIE(u{27!jjq4T>@+ zTnqT4fwfMYTDiauE^<7D*ZL|gUF#8jupgp(ILdzR;jhYRy@#9e+Iy~$W7^fYh$fRI z#=+AOY5UotZH?qbA%(2>VKvv~k}8YSyt!>6)V=ln3GqkS>+`R|e23u&6oWJz9}i4| zmr!UGZkB*OH6&JfE}eI}+o($BVt%CY)@h;g+?R%HY43nRT!oIO@tbcEhgp$dT5Cc` zxP}zH>ZR%SyczZ+Ed`C-WWqu<&DW0fnt+)OYL8@nH#p zu=?q)bq{-!i;D}bKB;;2fWg=x&ywF5oA=J$*d*e7A_NjvsyqrE=uZX~E%D1SlM>2s zu^*l|*f3-%rhj$p*D-VPh6&|tJ8f~EiE#-K`B2w9E_CTj+@~02i!xu*h04sjQ^KU~ zf6)*8I!4$Iu=Mk4J12HH3x^39hBCHyXYh+11v%?oK3p=01~Qi^vum2Z{m2+T87sz= z$||V1r`&r6dxO*NX!cw(+iA3BKgJ-S!B;^dr6)o563=?$6vGBI`x$F-vUJ8?Yo@?e`v7w<(>PdgcY}b4Jd0Iq$O!797(N3 zk|7N<5!2=ej9$nX=R0qoHC0s6jpkpy9^Enc19yDQbV_FYl8Q)|+wY|-vSEHdTXe7s zpRwTORgyqD?JtLGW1sWn8(wU0g4I$IFX$kTrj75o=A(w02=@3Kt#R^gwsqLjNw?J# z4SK``^%SX1i5m+W*0wDjK#gqsdkMMGF9%xAXM45Yy@mM|UGPvCHT%rLsL%s-6UBBm z`y=6GLQhdh8Lt!s89vzY9a`27+Pqfg;Njt6YRcRjMB>mS(E4Xptfj8KK$i3R%nk4C z8lh*3d5ZY?X@4Cs+sEc=*pT(y3FROr_vZjgP|@^nUY}K&JmfXo)70MH-kf=p8nV>a zM`K?G>zirP#Dv3}-NDtkmxy3P<%sW5xZ1!~ubS_Vp!`~)|`7Ue5p|Fa+Kl%0(QT@CV4Q$;e7 z$o4BaL6y#}$ZdrkEf8pDF*n4z_m~MOSLR#r{;Il^YRH~nr@xhe%Ra_B{v!n z*;H=fTj$oB+e{IMIW{F(nU1$*OmUj+q%;0;u54e(MFg{iwl>xlH8!!9O7b+6)j5&; z2{x1)jOR;k$qWb5Q#(}QpD!#6=H^`D)B*bOs%Tf`m@r6^Q3yBYhr~!PYnHvWH;r?n zst+&M77Obdg^W6@mBL3ne(#`YIe^>;ll|~bkcnWBTE%B-ugZCOqZDAAHem=r9qj|S z7>?wBAh8#EPZ@S!ik$?c@0{YEPe)dHYAc&yer_TcaV^;ju@A`Ba85t0aiBe>F_!VV zJpO_#n4?6wq;hQx#~CO-pJr-%7+EO`&4%&&fCSm@A#NHgOG-w{E_5(5Efo(}@q_Q| z#V9{$8JUdCOSE~g@?w9@$|cks2t>Nuz|4Ti|F{egj!2QSqm&k=`I>*+d z^(UbN{+b-#wVa;BB~qV1n?FH-i_WKf>g%fRlL)djVDgEh4kHAJn0*r>?~R7tuAT!> zziCxFH67jNK3xt!b4>&x9Qc#cp96(MC`mYwau>Z zptQcTY&B%HiNbGAN~%L8ijc$Hn~=fZ+IYmffK`Q{bv1#AfjBq^9>?RMaR1GRqz^6I zCI+H32yDoR^4uMZkq91~$Yw)36fWG_-6xDJ zD>Uy|#wUC}u{Y0OK>jgMAncr*%p!b0I3>1OW;Eq0=&%?ac(OVL%Aa`s>v=vu{*OUFE%Tnm6Dd8gXuXhOaR_L1=ApkYPg0L;@i*Ko57u%~;RMzS8TknR>wG3VN zo$Q!{PQ9TQdv5V1Nircz7#(g7(8kz8L>_M1^(_mJ+YX!@sJA4>C5^=00O>s^Evexr zl)(-XLXA0HhSLGMYz*c4gLeuNQ3&>1<>+ox4}LS5LSxWAkagDds!AxcZC5-q}KeFMucFKLx>@7jhEVu8yyr%JU3& z2jq0p+b(zXQ#tXvweLZL4Yt2mf_V(TRr=03$c(X0Mn%-f^3erpn=b0MC0P+u3H6qz zIpq>clXk{076yIBaS#(z`BiTvp00c12>YTVJnw8a5tXa|N%*~Fxs-7$HletTGNO_Z z%B%u+acE8WHKJOW#gfJqqIgE#-JQcN{W__6o~oEeQZRvhPk&m(1H#0KCO&dqc_IYr z(xC;o?tA2$0z9S)sY-%zshMb{gaa^B$aCGfshBInK%m5+ww?4^!nAV{{zfE791-8A zv&x&=l+3EV5Q(9egAPOmJBv%)&~dpr&TlIJ9%kCXoi#PPDxh3)}A5%{%#p%!~w~; z?hF6+s$VqskLC9+-du=kf7Mg zBkgj*zGo(;iusbP1no24HtzNgrGW$ysGRAgKY#ul416bWfxEK3GVPmfx#e&3u}GCa zD9D#`#!u9hfqb53pNpV#n&WFr955!C7!HPWN6MjE8XI857QOi84dQM4Ex(`q59_rT z?l7_It`QPDZu&6fW~bJ7qtt~tp#63}tcwXsKj2nG`<=O_-*Z;#k=@}IyaoC`kTdEz zR%INBPEkMilB&zI7jd+sGxdML_cJv3{#3y2;^gkf?D%AHz6sA5Irkkk5qY@l0F%{u z0r|LEW#vR*PS@z%$LMeG{UhJkfvDU#iTqrlo- z!hVF7AnN1!l%)7uPSM|RNQbiInjA)^JmR2#s7xjB+DjQ&~eCkRV z&znICRRTVFi(2&54y3!v^XPAv^Aq6ar4~w({6zX00*|4S9^A-mh#Q7_`F7-`VD zqO6IXvCRvesnOBVk&zf>=6{}9Sz3~5L9UIpwR_aU^R~@-`Ny5PP$KPc|{R_W-{J0~Xv9hs2_+%`|%9yee`8jIoQxp8CO(yAw uI_({$CYYtDPx4hRb%C8Ij{N-m6FmhL&CECFk8=M`{xvhPHm)@EeEeVNhiYvA literal 0 HcmV?d00001 diff --git a/plugins/pagerduty/doc/pd3.png b/plugins/pagerduty/doc/pd3.png new file mode 100644 index 0000000000000000000000000000000000000000..89d7f1820d2d8895ffa0aad48caf0af71d6ad637 GIT binary patch literal 49915 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 P&qq>JPNZ7MAn<}F#RAB}*( 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 literal 0 HcmV?d00001 From e15c4156a9262649d8b210ec3cf5cf0daed3b89c Mon Sep 17 00:00:00 2001 From: James Renaud Date: Wed, 30 Jun 2021 10:08:07 -0400 Subject: [PATCH 02/42] fix: remove image tooltips Signed-off-by: James Renaud --- plugins/pagerduty/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index 12329288b7..bdafeb984a 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -12,17 +12,17 @@ This plugin requires that entities are annotated with an [integration key](https ### View any open incidents -![View Open Incidents](doc/pd1.png "PagerDuty plugin showing no incidents and the on-call rotation") +![PagerDuty plugin showing no incidents and the on-call rotation](doc/pd1.png) ### Email link, and view contact information for staff on call -![On-Call Contact Information](doc/pd2.png "PagerDuty plugin showing on-call rotation contact information") +![PagerDuty plugin showing on-call rotation contact information](doc/pd2.png) ### Trigger an incident for a service -![Trigger an Incident](doc/pd3.png "PagerDuty plugin popup modal for creating an incident") +![PagerDuty plugin popup modal for creating an incident](doc/pd3.png) -![Active Incidents](doc/pd4.png "PagerDuty plugin showing an active incident") +![PagerDuty plugin showing an active incident](doc/pd4.png) ## Setup instructions From b809edd82a40572d708381ccfe2eb0b127bcb3e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jul 2021 04:34:49 +0000 Subject: [PATCH 03/42] chore(deps): bump @date-io/luxon from 1.3.13 to 2.10.11 Bumps [@date-io/luxon](https://github.com/dmtrKovalenko/date-io) from 1.3.13 to 2.10.11. - [Release notes](https://github.com/dmtrKovalenko/date-io/releases) - [Commits](https://github.com/dmtrKovalenko/date-io/compare/v1.3.13...v2.10.11) --- updated-dependencies: - dependency-name: "@date-io/luxon" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- plugins/ilert/package.json | 2 +- yarn.lock | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 479fd2223d..3b8dfd4f46 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -26,7 +26,7 @@ "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.2.4", "@backstage/theme": "^0.2.8", - "@date-io/luxon": "1.x", + "@date-io/luxon": "2.x", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", diff --git a/yarn.lock b/yarn.lock index dceed09a91..708f46bfe6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1689,6 +1689,11 @@ resolved "https://registry.npmjs.org/@date-io/core/-/core-2.10.7.tgz#0fe1fa0ef02c827919e23c2802a4b25589ac522d" integrity sha512-EG/1qDiQvd12RoNJ6H+sZcHVswC/3uMx/ySvfaJ24vB30rLjkgHggEXbgMbfgki7wMuiQ/zXI8QlmF1k3kWRGQ== +"@date-io/core@^2.10.11": + version "2.10.11" + resolved "https://registry.npmjs.org/@date-io/core/-/core-2.10.11.tgz#b1a3d57730f3eaaab54d5658be4a71727297e357" + integrity sha512-keXQnwH0LM8wyvu+j5Z2KGK56D+eItjy7DnwuWl/oV+DM2UEYl0z5WhdPMpfswSyt/kjuPOzcVF/7u/skMLaoA== + "@date-io/date-fns@^1.1.0": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz#7798844041640ab393f7e21a7769a65d672f4735" @@ -1696,12 +1701,12 @@ dependencies: "@date-io/core" "^1.3.13" -"@date-io/luxon@1.x": - version "1.3.13" - resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" - integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== +"@date-io/luxon@2.x": + version "2.10.11" + resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.10.11.tgz#d0981b9fdf5e5f17f8ce59265a3ac6c335565fac" + integrity sha512-SS6SIkp0Y9GFwpQycCTUAyW3OZTW05CWI1DJu10hUzcg8SmjJfhjs7hQY3TOeW+JT6VtXGTVGwbWPUBJsNkhZg== dependencies: - "@date-io/core" "^1.3.13" + "@date-io/core" "^2.10.11" "@emotion/cache@^10.0.27": version "10.0.29" From b7cd1f31516662f28ae7f642985d8a0ebff4085a Mon Sep 17 00:00:00 2001 From: "roylisto.pradana" Date: Wed, 30 Jun 2021 13:55:53 +0700 Subject: [PATCH 04/42] Add edit button to Group Profile card Signed-off-by: roylisto.pradana --- .../src/layout/InfoCard/InfoCard.tsx | 4 +++- .../Cards/Group/GroupProfile/GroupProfileCard.tsx | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index 763038388b..cc15bbd9f4 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -37,7 +37,6 @@ const useStyles = makeStyles(theme => ({ }, }, header: { - display: 'inline-block', padding: theme.spacing(2, 2, 2, 2.5), }, headerTitle: { @@ -121,6 +120,7 @@ type Props = { children?: ReactNode; headerStyle?: object; headerProps?: CardHeaderProps; + action?: ReactNode; actionsClassName?: string; actions?: ReactNode; cardClassName?: string; @@ -141,6 +141,7 @@ export const InfoCard = ({ children, headerStyle, headerProps, + action, actionsClassName, actions, cardClassName, @@ -190,6 +191,7 @@ export const InfoCard = ({ }} title={title} subheader={subheader} + action={action} style={{ ...headerStyle }} titleTypographyProps={titleTypographyProps} {...headerProps} diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 853dbc53e8..a96c53da6e 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -33,10 +33,12 @@ import { ListItemIcon, ListItemText, Tooltip, + IconButton, } from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; +import EditIcon from '@material-ui/icons/Edit'; import Alert from '@material-ui/lab/Alert'; import React from 'react'; import { Avatar, InfoCard, InfoCardVariants } from '@backstage/core-components'; @@ -80,6 +82,18 @@ export const GroupProfileCard = ({ title={} subheader={description} variant={variant} + action={ + { + // window.open(entityMetadataEditUrl ?? '#', '_blank'); + }} + > + + + } > From 5a80de8ff602b185575bdcbcb9daa891aaeb659d Mon Sep 17 00:00:00 2001 From: "roylisto.pradana" Date: Wed, 30 Jun 2021 14:14:48 +0700 Subject: [PATCH 05/42] get metadata edit url Signed-off-by: roylisto.pradana --- .../Cards/Group/GroupProfile/GroupProfileCard.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index a96c53da6e..8d44e9157a 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -23,6 +23,7 @@ import { EntityRefLinks, getEntityRelations, useEntity, + getEntityMetadataEditUrl, } from '@backstage/plugin-catalog-react'; import { Box, @@ -74,6 +75,8 @@ export const GroupProfileCard = ({ kind: 'group', }); + const entityMetadataEditUrl = getEntityMetadataEditUrl(group); + const displayName = profile?.displayName ?? name; const emailHref = profile?.email ? `mailto:${profile.email}` : undefined; @@ -85,10 +88,10 @@ export const GroupProfileCard = ({ action={ { - // window.open(entityMetadataEditUrl ?? '#', '_blank'); + window.open(entityMetadataEditUrl ?? '#', '_blank'); }} > From 34352a79c4f2c397e7d21f2aab916ae99da698c9 Mon Sep 17 00:00:00 2001 From: "roylisto.pradana" Date: Wed, 30 Jun 2021 14:33:00 +0700 Subject: [PATCH 06/42] add changeset Signed-off-by: roylisto.pradana --- .changeset/six-birds-approve.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/six-birds-approve.md diff --git a/.changeset/six-birds-approve.md b/.changeset/six-birds-approve.md new file mode 100644 index 0000000000..ce666dd09c --- /dev/null +++ b/.changeset/six-birds-approve.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-org': patch +--- + +Add edit button to Group Profile Card From 4d2528ad4aced5c8163880ce7aa94e3e89d96238 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 1 Jul 2021 22:07:15 +0200 Subject: [PATCH 07/42] docs: Document URL Reader Concept Interface Using inside a plugin Writing a new one Signed-off-by: Himanshu Mishra --- docs/integrations/url-reader.md | 219 ++++++++++++++++++++++++++++++++ microsite/sidebars.json | 3 +- mkdocs.yml | 1 + 3 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 docs/integrations/url-reader.md diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md new file mode 100644 index 0000000000..e9d6caf17c --- /dev/null +++ b/docs/integrations/url-reader.md @@ -0,0 +1,219 @@ +--- +id: url-reader +title: URL Reader +sidebar_label: URL Reader +# prettier-ignore +description: URL Reader is a backend core API responsible for reading files from external locations. +--- + +## Concept + +Some of the core plugins of Backstage have to read files from an external +location. [Software Catalog](../features/software-catalog/index.md) has to read +the [`catalog-info.yaml`](../features/software-catalog/descriptor-format.md) +entity descriptor files to register and track an entity. +[Software Templates](../features/software-templates/index.md) have to download +the template skeleton files before creating a new component. +[TechDocs](../features/techdocs/README.md) has to download the markdown source +files before generating a documentation site. + +Since, the requirement for reading files is so essential for Backstage plugins, +the +[`@backstage/backend-common`](https://github.com/backstage/backstage/tree/master/packages/backend-common) +package provides a dedicated API for reading from such URL based remote +locations like GitHub, GitLab, Bitbucket, Google Cloud Storage, etc. This is +commonly referred to as "URL Reader". It takes care of making authenticated +requests to the remote host so that private files can be read securely. If users +have [GitHub App based authentication](../plugins/github-apps.md) set up, URL +Reader even refreshes the token, to avoid reaching the GitHub API rate limit. + +As a result, plugin authors do not have to worry about any of these problems +when trying to read files. + +## Interface + +When the Backstage backend starts, a new instance of URL Reader is created. You +can see this in the index file of your Backstage backend +i.e.`packages/backend/src/index.ts`. +[Example](https://github.com/backstage/backstage/blob/ebbe91dbe79038a61d35cf6ed2d96e0e0d5a15f3/packages/backend/src/index.ts#L57) + +```ts +// File: packages/backend/src/index.ts + +import { URLReaders } from '@backstage/backend-common'; + +function makeCreateEnv(config: Config) { + // .... + const reader = UrlReaders.default({ logger, config }); + // +} +``` + +This instance contains all +[the default URL Reader providers](https://github.com/backstage/backstage/blob/master/packages/backend-common/src/reading/UrlReaders.ts) +in the backend-common package including GitHub, Gitlab, Bitbucket, Azure, Google +GCS. As the need arises, more URL Readers are being written to support different +providers. + +The generic interface of a URL Reader instance looks like this. + +```ts +export type UrlReader = { + /* Used to read a single file and return its content. */ + read(url: string): Promise; + /* Used to read a file tree and download as a directory. */ + readTree(url: string, options?: ReadTreeOptions): Promise; + /* Used to search a file in a tree. */ + search(url: string, options?: SearchOptions): Promise; +}; +``` + +## Using inside a plugin + +The `reader` instance is available in the backend plugin environment and passed +on to all the backend plugins. You can see an +[example](https://github.com/backstage/backstage/blob/b0be185369ebaad22255b7cdf18535d1d4ffd0e7/packages/backend/src/plugins/techdocs.ts#L31). +When any of the methods on this instance is called with a URL, URL Reader +extracts the host for that URL (e.g. `github.com`, `ghe.mycompany.com`, etc.). +Using the +[`@backstage/integration`](https://github.com/backstage/backstage/tree/master/packages/integration) +package, it looks inside the +[`integrations:`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/app-config.yaml#L134-L158) +config of the `app-config.yaml` to find out how to work with the host based on +the configs provided like authentication token, API base URL, etc. + +Make sure your plugin-specific backend file at +`packages/backend/src/plugins/.ts` is forwarding the `reader` instance +passed on as the `PluginEnvironment` to the actual plugin's `createRouter` +function. See how this is done in +[Catalog](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend/src/plugins/catalog.ts#L25-L27) +and +[TechDocs](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend/src/plugins/techdocs.ts#L31-L36) +backend plugins. + +Once the reader instance is available inside the plugin, one of its methods can +directly be used with a URL. Some example usages - + +- [`read`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/plugins/catalog-backend/src/ingestion/processors/codeowners/read.ts#L24-L33) - + Catalog using the `read` method to read the CODEOWNERS file in a repository. +- [`readTree`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/techdocs-common/src/helpers.ts#L198-L220) - + TechDocs using the `readTree` method to download markdown files in order to + generate the documentation site. +- [`readTree`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/techdocs-common/src/stages/prepare/url.ts#L33-L54) - + TechDocs using `NotModifiedError` to maintain cache and speed up and limit the + number of requests. +- [`search`](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts#L88-L108) - + Catalog using the `search` method to find files for a location URL containing + a glob pattern. + +## Writing a new URL Reader + +If the available URL Readers are not sufficient for your use case and you want +to add a new URL Reader for any other provider, you are most welcome to +contribute one! + +Feel free to use the +[GitHub URL Reader](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/GithubUrlReader.ts) +as a source of inspiration. + +### Add an integration + +The provider for your new URL Reader can also be called an "integration" in +Backstage. The `integrations:` section of your Backstage `app-config.yaml` +config file is supposed to be the place where a Backstage integrator defines the +host URL for the integration, authentication details and other integration +related configurations. + +The `@backstage/integration` package is where most of the integration specific +code lives, so that it is shareable across Backstage. Functions like "read the +integrations config and process it", "construct headers for authenticated +requests to the host" or "convert a plain file URL into its API URL for +downloading the file" would live in this package. + +### Create the URL Reader + +Create a new class which implements the +[`UrlReader` type](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/types.ts#L21-L28) +inside `@backstage/backend-common`. Create and export a static `factory` method +which reads the integration config and returns a map of host URLs the new reader +should be used for. See the +[GitHub URL Reader](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/GithubUrlReader.ts#L50-L63) +for example. + +### Implement the methods + +We want to make sure all URL Readers behave in the same way. Hence if possible, +all the methods of the `UrlReader` interface should be implemented. However it +is okay to start by implementing just one of them and create issues for the +remaining. + +#### read + +`read` method expects a user-friendly URL, something which can be copied from +the browser naturally when a person is browsing the provider in their browser. + +- ✅ Valid URL : + `https://github.com/backstage/backstage/blob/master/ADOPTERS.md` +- ❌ Not a valid URL : + `https://raw.githubusercontent.com/backstage/backstage/master/ADOPTERS.md` +- ❌ Not a valid URL : `https://github.com/backstage/backstage/ADOPTERS.md` + +Upon receiving the URL, `read` converts the user-friendly URL into an API URL +which can be used to request the provider's API. + +`read` then makes an authenticated request to the provider API and returns the +file's content. + +#### readTree + +`readTree` method also expects user-friendly URLs similar to `read` but the URL +should point to a tree (could be the root of a repository or even a +sub-directory). + +- ✅ Valid URL : `https://github.com/backstage/backstage` +- ✅ Valid URL : `https://github.com/backstage/backstage/blob/master` +- ✅ Valid URL : `https://github.com/backstage/backstage/blob/master/docs` + +Using the provider's API documentation, find out an API endpoint which can be +used to download either a zip or a tarball. You can download the entire tree +(e.g. a repository) and filter out in case the user is expecting only a +sub-tree. But some APIs are smart enough to accept a path and return only a +sub-tree in the downloaded archive. + +#### search + +`search` method expects a glob pattern of a URL and returns a list of files +matching the query. + +- ✅ Valid URL : + `https://github.com/backstage/backstage/blob/master/**/catalog-info.yaml` +- ✅ Valid URL : `https://github.com/backstage/backstage/blob/master/**/*.md` +- ✅ Valid URL : + `https://github.com/backstage/backstage/blob/master/*/package.json` +- ✅ Valid URL : `https://github.com/backstage/backstage/blob/master/READM` + +The core logic of `readTree` can be used here to extract all the files inside +the tree and return the files matching the pattern in the `url`. + +### Caching + +All of the methods above support an ETag based caching (except `read` which is +work in progress!). If the method is called without an `etag`, the response +contains an ETag of the resource (should ideally forward the ETag returned by +the provider). If the method is called with an `etag`, it first compares the +ETag and returns a `NotModifiedError` in case the resource has not been +modified. This approach is very similar to the actual +[ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and +[If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) +HTTP headers. + +### Debugging + +When debugging one of the URL Readers, you can straightforward use the +[`reader` instance created](https://github.com/backstage/backstage/blob/ebbe91dbe79038a61d35cf6ed2d96e0e0d5a15f3/packages/backend/src/index.ts#L57) +when the backend starts and call one of the methods with your debugging URL. +This will be run every time you restart the backend. Note that after any change +in the URL Reader code, you need to kill the backend and restart, since the +`reader` instance is memoized and does not update on hot module reloading. Also, +there are a lot of unit tests written for the URL Readers, which you can make +use of. diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 0231d8f692..5530fc6f61 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -149,7 +149,8 @@ "type": "subcategory", "label": "LDAP", "ids": ["integrations/ldap/org"] - } + }, + "integrations/url-reader" ], "Plugins": [ "plugins/index", diff --git a/mkdocs.yml b/mkdocs.yml index c3d77e179f..47b7084146 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -98,6 +98,7 @@ nav: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: - Org Data: 'integrations/ldap/org.md' + - URL Reader: 'integrations/url-reader.md' - Plugins: - Intro to plugins: 'plugins/index.md' - Existing plugins: 'plugins/existing-plugins.md' From 31621bee6943d38daad97112781d81933a0ccf83 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 1 Jul 2021 22:07:43 +0200 Subject: [PATCH 08/42] backend-common: Update doc strings of URL Reader interface methods Signed-off-by: Himanshu Mishra --- packages/backend-common/src/reading/types.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index ecaa582612..8efc833ead 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -22,6 +22,7 @@ import { Config } from '@backstage/config'; * A generic interface for fetching plain data from URLs. */ export type UrlReader = { + /* Used to read a single file and return its content. */ read(url: string): Promise; /** @@ -32,8 +33,9 @@ export type UrlReader = { */ readUrl?(url: string, options?: ReadUrlOptions): Promise; + /* Used to read a file tree and download as a directory. */ readTree(url: string, options?: ReadTreeOptions): Promise; - + /* Used to search a file in a tree using a glob pattern. */ search(url: string, options?: SearchOptions): Promise; }; From ce338869136eb554c2560edba9fd584d21c17bcb Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 1 Jul 2021 22:12:45 +0200 Subject: [PATCH 09/42] docs: fix vale spelling errors Signed-off-by: Himanshu Mishra --- .github/styles/vocab.txt | 1 + docs/integrations/url-reader.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 846e050c49..736b654486 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -137,6 +137,7 @@ maintainership makefile md memcache +memoized microservice microservices microsite diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md index e9d6caf17c..23500a3dcb 100644 --- a/docs/integrations/url-reader.md +++ b/docs/integrations/url-reader.md @@ -51,7 +51,7 @@ function makeCreateEnv(config: Config) { This instance contains all [the default URL Reader providers](https://github.com/backstage/backstage/blob/master/packages/backend-common/src/reading/UrlReaders.ts) -in the backend-common package including GitHub, Gitlab, Bitbucket, Azure, Google +in the backend-common package including GitHub, GitLab, Bitbucket, Azure, Google GCS. As the need arises, more URL Readers are being written to support different providers. From 7edef795a391e546e6ad2ccfb0ce5c18321c65cc Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 2 Jul 2021 16:00:56 +0200 Subject: [PATCH 10/42] docs: Add a note about readUrl with read Signed-off-by: Himanshu Mishra --- docs/integrations/url-reader.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md index 23500a3dcb..83f6a743d9 100644 --- a/docs/integrations/url-reader.md +++ b/docs/integrations/url-reader.md @@ -149,6 +149,8 @@ remaining. #### read +NOTE: Use `readUrl` instead of `read`. + `read` method expects a user-friendly URL, something which can be copied from the browser naturally when a person is browsing the provider in their browser. @@ -164,6 +166,13 @@ which can be used to request the provider's API. `read` then makes an authenticated request to the provider API and returns the file's content. +#### readUrl + +`readUrl` is a new interface that allows complex response objects and is +intended to replace the `read` method. This new method is currently optional to +implement which allows for a soft migration to `readUrl` instead of `read` in +the future. + #### readTree `readTree` method also expects user-friendly URLs similar to `read` but the URL @@ -197,12 +206,11 @@ the tree and return the files matching the pattern in the `url`. ### Caching -All of the methods above support an ETag based caching (except `read` which is -work in progress!). If the method is called without an `etag`, the response -contains an ETag of the resource (should ideally forward the ETag returned by -the provider). If the method is called with an `etag`, it first compares the -ETag and returns a `NotModifiedError` in case the resource has not been -modified. This approach is very similar to the actual +All of the methods above support an ETag based caching. If the method is called +without an `etag`, the response contains an ETag of the resource (should ideally +forward the ETag returned by the provider). If the method is called with an +`etag`, it first compares the ETag and returns a `NotModifiedError` in case the +resource has not been modified. This approach is very similar to the actual [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) HTTP headers. From 253136fba7739a94d3c476102f5e736f4cd1874f Mon Sep 17 00:00:00 2001 From: Eugene Sh Date: Fri, 2 Jul 2021 22:17:32 +0300 Subject: [PATCH 11/42] Make branch protection attempt less restrictive In case if something wrong happens along the way when trying to apply protection of a newly created branch - log error, instead of throwing an it, making scaffolder exit with non-zero exit code. This one addresses the issue #6330 Signed-off-by: Eugene S --- .changeset/wet-queens-deny.md | 5 +++++ .../src/scaffolder/actions/builtin/publish/github.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/wet-queens-deny.md diff --git a/.changeset/wet-queens-deny.md b/.changeset/wet-queens-deny.md new file mode 100644 index 0000000000..1e446c619c --- /dev/null +++ b/.changeset/wet-queens-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +removing mandatory of protection for the default branch, that could be handled by the GitHub automation in async manner, thus throwing floating errors diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 856197a808..7dd790cb62 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -268,8 +268,8 @@ export function createPublishGithubAction(options: { defaultBranch, }); } catch (e) { - throw new Error( - `Failed to add branch protection to '${newRepo.name}', ${e}`, + ctx.logger.warn( + `Skipping: default branch protection on '${newRepo.name}', ${e.message}`, ); } From 76fc5e29c9677383958d73ca10c2ae26c853ecd5 Mon Sep 17 00:00:00 2001 From: shoukoo Date: Sun, 4 Jul 2021 08:36:16 +1000 Subject: [PATCH 12/42] feat:add default branch prop for publish:gitlab action Signed-off-by: shoukoo --- .../publish/{gitab.test.ts => gitlab.test.ts} | 24 +++++++++++++++++++ .../actions/builtin/publish/gitlab.ts | 13 +++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) rename plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/{gitab.test.ts => gitlab.test.ts} (88%) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts similarity index 88% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitab.test.ts rename to plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index 79de6fd669..67064efde7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -139,6 +139,30 @@ describe('publish:gitlab', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, + defaultBranch: 'master', + remoteUrl: 'http://mockurl.git', + auth: { username: 'oauth2', password: 'tokenlols' }, + logger: mockContext.logger, + }); + }); + + it('should call initRepoAndPush with the correct default branch', async () => { + mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Projects.create.mockResolvedValue({ + http_url_to_repo: 'http://mockurl.git', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + defaultBranch: 'main', remoteUrl: 'http://mockurl.git', auth: { username: 'oauth2', password: 'tokenlols' }, logger: mockContext.logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 3f24b98e6c..57c7fdf752 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -28,6 +28,7 @@ export function createPublishGitlabAction(options: { return createTemplateAction<{ repoUrl: string; + defaultBranch?: string; repoVisibility: 'private' | 'internal' | 'public'; sourcePath?: string; }>({ @@ -48,6 +49,11 @@ export function createPublishGitlabAction(options: { type: 'string', enum: ['private', 'public', 'internal'], }, + defaultBranch: { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, + }, sourcePath: { title: 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', @@ -70,7 +76,11 @@ export function createPublishGitlabAction(options: { }, }, async handler(ctx) { - const { repoUrl, repoVisibility = 'private' } = ctx.input; + const { + repoUrl, + repoVisibility = 'private', + defaultBranch = 'master', + } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl); @@ -114,6 +124,7 @@ export function createPublishGitlabAction(options: { await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl: http_url_to_repo as string, + defaultBranch, auth: { username: 'oauth2', password: integrationConfig.config.token, From 330f2ac32ded4b124afea3b7c0b56688b1f2a1b1 Mon Sep 17 00:00:00 2001 From: shoukoo Date: Sun, 4 Jul 2021 08:52:48 +1000 Subject: [PATCH 13/42] feat:add default branch prop for publish:bitbucket action Signed-off-by: shoukoo --- .../actions/builtin/publish/bitbucket.test.ts | 43 +++++++++++++++++++ .../actions/builtin/publish/bitbucket.ts | 8 ++++ 2 files changed, 51 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 00ada3a4bc..16b84737a7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -286,6 +286,49 @@ describe('publish:bitbucket', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, remoteUrl: 'https://bitbucket.org/owner/cloneurl', + defaultBranch: 'master', + auth: { username: 'x-token-auth', password: 'tokenlols' }, + logger: mockContext.logger, + }); + }); + + it('should call initAndPush with the correct default branch', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/owner/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/owner/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/owner/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/owner/cloneurl', + defaultBranch: 'main', auth: { username: 'x-token-auth', password: 'tokenlols' }, logger: mockContext.logger, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index ce937108c4..295f4d1574 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -190,6 +190,7 @@ export function createPublishBitbucketAction(options: { return createTemplateAction<{ repoUrl: string; description: string; + defaultBranch?: string; repoVisibility: 'private' | 'public'; sourcePath?: string; enableLFS: boolean; @@ -215,6 +216,11 @@ export function createPublishBitbucketAction(options: { type: 'string', enum: ['private', 'public'], }, + defaultBranch: { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, + }, sourcePath: { title: 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', @@ -245,6 +251,7 @@ export function createPublishBitbucketAction(options: { const { repoUrl, description, + defaultBranch = 'master', repoVisibility = 'private', enableLFS = false, } = ctx.input; @@ -288,6 +295,7 @@ export function createPublishBitbucketAction(options: { ? integrationConfig.config.appPassword : integrationConfig.config.token ?? '', }, + defaultBranch, logger: ctx.logger, }); From 73195b6e65a84e2b0e29fa460f28338dda84a98d Mon Sep 17 00:00:00 2001 From: shoukoo Date: Sun, 4 Jul 2021 08:59:14 +1000 Subject: [PATCH 14/42] feat:add default branch prop for publish:azure action Signed-off-by: shoukoo --- .../actions/builtin/publish/azure.test.ts | 23 +++++++++++++++++++ .../actions/builtin/publish/azure.ts | 13 ++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index 2f468cda25..d3cb4ccafc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -162,6 +162,29 @@ describe('publish:azure', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: mockContext.workspacePath, remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + }); + }); + + it('should call initRepoAndPush with the correct default branch', async () => { + mockGitClient.createRepository.mockImplementation(() => ({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + })); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + defaultBranch: 'main', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', auth: { username: 'notempty', password: 'tokenlols' }, logger: mockContext.logger, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 21b2537614..eb1b0522c6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -30,6 +30,7 @@ export function createPublishAzureAction(options: { return createTemplateAction<{ repoUrl: string; description?: string; + defaultBranch?: string; sourcePath?: string; }>({ id: 'publish:azure', @@ -48,6 +49,11 @@ export function createPublishAzureAction(options: { title: 'Repository Description', type: 'string', }, + defaultBranch: { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, + }, sourcePath: { title: 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', @@ -70,9 +76,9 @@ export function createPublishAzureAction(options: { }, }, async handler(ctx) { - const { owner, repo, host, organization } = parseRepoUrl( - ctx.input.repoUrl, - ); + const { repoUrl, defaultBranch = 'master' } = ctx.input; + + const { owner, repo, host, organization } = parseRepoUrl(repoUrl); if (!organization) { throw new InputError( @@ -120,6 +126,7 @@ export function createPublishAzureAction(options: { await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl, + defaultBranch, auth: { username: 'notempty', password: integrationConfig.config.token, From dad4817939094eaf64c2a24ba7be2ce8399c0ebd Mon Sep 17 00:00:00 2001 From: shoukoo Date: Sun, 4 Jul 2021 09:01:44 +1000 Subject: [PATCH 15/42] Add new changeset Signed-off-by: shoukoo --- .changeset/brown-olives-hang.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brown-olives-hang.md diff --git a/.changeset/brown-olives-hang.md b/.changeset/brown-olives-hang.md new file mode 100644 index 0000000000..64ce4e25d5 --- /dev/null +++ b/.changeset/brown-olives-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +add default branch property for publish GitLab, Bitbucket and Azure actions From 2ac448e380f6e389e3684664bdf16185acdfff42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 04:20:00 +0000 Subject: [PATCH 16/42] chore(deps): bump @types/yup from 0.29.11 to 0.29.12 Bumps [@types/yup](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/yup) from 0.29.11 to 0.29.12. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/yup) --- updated-dependencies: - dependency-name: "@types/yup" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b04756527c..d73d3fe955 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6866,9 +6866,9 @@ integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== "@types/yup@^0.29.8": - version "0.29.11" - resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" - integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== + version "0.29.12" + resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.12.tgz#59c9577bea11d2b3d78717ea7591caacad6dfa1b" + integrity sha512-fA7bXyBzWEAgOwX2SD/5/iaZY/4In0EvJEzFmBWzaGNF4vxr8d5iOFUMFBpL4cMEmlSx2wW9ginJNnoZjE/vOg== "@types/zen-observable@^0.8.0", "@types/zen-observable@^0.8.2": version "0.8.2" From 28f557a7dd4c5a97a6b8bc4e95187bda3bb89f52 Mon Sep 17 00:00:00 2001 From: Brett Wright Date: Mon, 5 Jul 2021 10:07:05 +0200 Subject: [PATCH 17/42] add harbor plugin Signed-off-by: Brett Wright --- microsite/data/plugins/harbor.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 microsite/data/plugins/harbor.yaml diff --git a/microsite/data/plugins/harbor.yaml b/microsite/data/plugins/harbor.yaml new file mode 100644 index 0000000000..a851f99545 --- /dev/null +++ b/microsite/data/plugins/harbor.yaml @@ -0,0 +1,13 @@ +--- +title: Harbor +author: BESTSELLER +authorUrl: bestsellerit.com +category: Discovery +description: This plugin will show you information about docker images within harbor. +documentation: https://github.com/BESTSELLER/backstage-plugin-harbor/blob/master/README.md +iconUrl: https://bestsellerit.com/img/terraform-harbor/goharbor.jpeg +npmPackageName: '@bestsellerit/backstage-plugin-harbor' +tags: + - goharbor + - harbor + - docker From 9813f6c2c72a7c61ec9173385ae2f0137ee61f6e Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 5 Jul 2021 10:21:31 +0200 Subject: [PATCH 18/42] document how to add a new url reader (oss vs private) and update section title Signed-off-by: Himanshu Mishra --- docs/integrations/url-reader.md | 34 +++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md index 83f6a743d9..c37549cf8b 100644 --- a/docs/integrations/url-reader.md +++ b/docs/integrations/url-reader.md @@ -68,7 +68,7 @@ export type UrlReader = { }; ``` -## Using inside a plugin +## Using a URL Reader inside a plugin The `reader` instance is available in the backend plugin environment and passed on to all the backend plugins. You can see an @@ -116,7 +116,7 @@ Feel free to use the [GitHub URL Reader](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/GithubUrlReader.ts) as a source of inspiration. -### Add an integration +### 1. Add an integration The provider for your new URL Reader can also be called an "integration" in Backstage. The `integrations:` section of your Backstage `app-config.yaml` @@ -130,7 +130,7 @@ integrations config and process it", "construct headers for authenticated requests to the host" or "convert a plain file URL into its API URL for downloading the file" would live in this package. -### Create the URL Reader +### 2. Create the URL Reader Create a new class which implements the [`UrlReader` type](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/types.ts#L21-L28) @@ -140,7 +140,7 @@ should be used for. See the [GitHub URL Reader](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/GithubUrlReader.ts#L50-L63) for example. -### Implement the methods +### 3. Implement the methods We want to make sure all URL Readers behave in the same way. Hence if possible, all the methods of the `UrlReader` interface should be implemented. However it @@ -204,7 +204,29 @@ matching the query. The core logic of `readTree` can be used here to extract all the files inside the tree and return the files matching the pattern in the `url`. -### Caching +### 4. Add to available URL Readers + +There are two ways to make your new URL Reader available for use. + +You can choose to make it open source, by updating the +[`default` factory](https://github.com/backstage/backstage/blob/d5c83bb889b8142e343ebc4e4c0b90a02d1c1a3d/packages/backend-common/src/reading/UrlReaders.ts#L62-L81) +method of URL Readers. + +But for something internal which you don't want to make open source, you can +update your `packages/backend/src/index.ts` file and update how the `reader` +instance is created. + +```ts +// File: packages/backend/src/index.ts +const reader = UrlReaders.default({ + logger: root, + config, + // This is where your internal URL Readers would go. + factories: [myCustomReader.factory], +}); +``` + +### 5. Caching All of the methods above support an ETag based caching. If the method is called without an `etag`, the response contains an ETag of the resource (should ideally @@ -215,7 +237,7 @@ resource has not been modified. This approach is very similar to the actual [If-None-Match](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) HTTP headers. -### Debugging +### 6. Debugging When debugging one of the URL Readers, you can straightforward use the [`reader` instance created](https://github.com/backstage/backstage/blob/ebbe91dbe79038a61d35cf6ed2d96e0e0d5a15f3/packages/backend/src/index.ts#L57) From d1bd7bb825423304a1108eebf7291a81af5dd965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 5 Jul 2021 10:23:39 +0200 Subject: [PATCH 19/42] Update README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/breezy-kings-applaud.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/breezy-kings-applaud.md diff --git a/.changeset/breezy-kings-applaud.md b/.changeset/breezy-kings-applaud.md new file mode 100644 index 0000000000..867d135739 --- /dev/null +++ b/.changeset/breezy-kings-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-pagerduty': patch +--- + +Update README From 918e2be318282d21fb8bea5bb9ed476e6ce0fa28 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 5 Jul 2021 10:42:56 +0200 Subject: [PATCH 20/42] docs: Add an example code block for debugging new url reader Signed-off-by: Himanshu Mishra --- docs/integrations/url-reader.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/integrations/url-reader.md b/docs/integrations/url-reader.md index c37549cf8b..a2108d5c71 100644 --- a/docs/integrations/url-reader.md +++ b/docs/integrations/url-reader.md @@ -242,6 +242,23 @@ HTTP headers. When debugging one of the URL Readers, you can straightforward use the [`reader` instance created](https://github.com/backstage/backstage/blob/ebbe91dbe79038a61d35cf6ed2d96e0e0d5a15f3/packages/backend/src/index.ts#L57) when the backend starts and call one of the methods with your debugging URL. + +```ts +// File: packages/backend/src/index.ts + +async function main() { + // ... + const createEnv = makeCreateEnv(config); + + const testReader = createEnv('test-url-reader').reader; + const response = await testReader.readUrl( + 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + ); + console.log((await response.buffer()).toString()); + // ... +} +``` + This will be run every time you restart the backend. Note that after any change in the URL Reader code, you need to kill the backend and restart, since the `reader` instance is memoized and does not update on hot module reloading. Also, From a6b4aebbd214a0409ae78f7fd9ff230f8a765bc0 Mon Sep 17 00:00:00 2001 From: roylisto Date: Mon, 5 Jul 2021 16:20:45 +0700 Subject: [PATCH 21/42] Use Link from @backstage/core-components Signed-off-by: roylisto --- .../Group/GroupProfile/GroupProfileCard.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 8d44e9157a..637a7279f6 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -28,7 +28,6 @@ import { import { Box, Grid, - Link, List, ListItem, ListItemIcon, @@ -42,7 +41,12 @@ import GroupIcon from '@material-ui/icons/Group'; import EditIcon from '@material-ui/icons/Edit'; import Alert from '@material-ui/lab/Alert'; import React from 'react'; -import { Avatar, InfoCard, InfoCardVariants } from '@backstage/core-components'; +import { + Avatar, + InfoCard, + InfoCardVariants, + Link, +} from '@backstage/core-components'; const CardTitle = ({ title }: { title: string }) => ( @@ -75,10 +79,10 @@ export const GroupProfileCard = ({ kind: 'group', }); - const entityMetadataEditUrl = getEntityMetadataEditUrl(group); + const entityMetadataEditUrl = getEntityMetadataEditUrl(group) ?? '#'; const displayName = profile?.displayName ?? name; - const emailHref = profile?.email ? `mailto:${profile.email}` : undefined; + const emailHref = profile?.email ? `mailto:${profile.email}` : '#'; return ( { - window.open(entityMetadataEditUrl ?? '#', '_blank'); - }} + component={Link} + target="_blank" + to={entityMetadataEditUrl} > @@ -112,7 +116,7 @@ export const GroupProfileCard = ({ - {profile.email} + {profile.email} )} From 62c2f10f7d3e4e54fc2007052a782ca087bf6a72 Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Fri, 2 Jul 2021 17:16:39 +0100 Subject: [PATCH 22/42] feat(scaffolder): add fs:delete custom step Signed-off-by: Andrea Falzetti --- .changeset/eight-pots-remember.md | 5 + .../actions/builtin/createBuiltinActions.ts | 6 + .../actions/builtin/filesystem/delete.test.ts | 120 ++++++++++ .../actions/builtin/filesystem/delete.ts | 58 +++++ .../actions/builtin/filesystem/index.ts | 18 ++ .../actions/builtin/filesystem/rename.test.ts | 206 ++++++++++++++++++ .../actions/builtin/filesystem/rename.ts | 93 ++++++++ .../src/scaffolder/actions/builtin/index.ts | 1 + 8 files changed, 507 insertions(+) create mode 100644 .changeset/eight-pots-remember.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/index.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts diff --git a/.changeset/eight-pots-remember.md b/.changeset/eight-pots-remember.md new file mode 100644 index 0000000000..34c6d018b5 --- /dev/null +++ b/.changeset/eight-pots-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Added filesystem remove/rename built-in actions diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 598d36938e..46b8db72b4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -24,6 +24,10 @@ import { } from './catalog'; import { createDebugLogAction } from './debug'; import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch'; +import { + createFilesystemDeleteAction, + createFilesystemRenameAction, +} from './filesystem'; import { createPublishAzureAction, createPublishBitbucketAction, @@ -68,5 +72,7 @@ export const createBuiltinActions = (options: { createDebugLogAction(), createCatalogRegisterAction({ catalogClient, integrations }), createCatalogWriteAction(), + createFilesystemDeleteAction(), + createFilesystemRenameAction(), ]; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts new file mode 100644 index 0000000000..f8fc1f1dc5 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as os from 'os'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { createFilesystemDeleteAction } from './delete'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import fs from 'fs-extra'; + +const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const workspacePath = resolvePath(root, 'my-workspace'); + +describe('fs:delete', () => { + const action = createFilesystemDeleteAction(); + + const mockContext = { + input: { + files: ['unit-test-a.js', 'unit-test-b.js'], + }, + workspacePath, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.restoreAllMocks(); + + mockFs({ + [workspacePath]: { + 'unit-test-a.js': 'hello', + 'unit-test-b.js': 'world', + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should throw an error when files is not an array', async () => { + await expect( + action.handler({ + ...mockContext, + input: { files: undefined }, + }), + ).rejects.toThrow(/files must be an Array/); + + await expect( + action.handler({ + ...mockContext, + input: { files: {} }, + }), + ).rejects.toThrow(/files must be an Array/); + + await expect( + action.handler({ + ...mockContext, + input: { files: '' }, + }), + ).rejects.toThrow(/files must be an Array/); + + await expect( + action.handler({ + ...mockContext, + input: { files: null }, + }), + ).rejects.toThrow(/files must be an Array/); + }); + + it('should throw when file name is not relative to the workspace', async () => { + await expect( + action.handler({ + ...mockContext, + input: { files: ['/foo/../../../index.js'] }, + }), + ).rejects.toThrow(/Relative path is not allowed to refer to a directory outside its parent/); + + await expect( + action.handler({ + ...mockContext, + input: { files: ['../../../index.js'] }, + }), + ).rejects.toThrow(/Relative path is not allowed to refer to a directory outside its parent/); + }); + + it('should call fs.rm with the correct values', async () => { + const files = ['unit-test-a.js', 'unit-test-b.js']; + + files.forEach(file => { + const filePath = resolvePath(workspacePath, file); + const fileExists = fs.existsSync(filePath); + expect(fileExists).toBe(true); + }); + + await action.handler(mockContext); + + files.forEach(file => { + const filePath = resolvePath(workspacePath, file); + const fileExists = fs.existsSync(filePath); + expect(fileExists).toBe(false); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts new file mode 100644 index 0000000000..ec8d60f955 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createTemplateAction } from '../../createTemplateAction'; +import { InputError } from '@backstage/errors'; +import { resolveSafeChildPath } from '@backstage/backend-common'; +import fs from 'fs-extra'; + +export const createFilesystemDeleteAction = () => { + return createTemplateAction<{ files: string[] }>({ + id: 'fs:delete', + schema: { + input: { + required: ['files'], + type: 'object', + properties: { + files: { + title: 'Files', + description: 'A list of files or directories that will be deleted', + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + async handler(ctx) { + if (!Array.isArray(ctx.input?.files)) { + throw new InputError('files must be an Array'); + } + + for (const file of ctx.input.files) { + const filepath = resolveSafeChildPath(ctx.workspacePath, file); + + try { + await fs.remove(filepath); + ctx.logger.info(`File ${filepath} deleted successfully`); + } catch (err) { + ctx.logger.error(`Failed to delete file ${filepath}:`, err); + throw err; + } + } + }, + }); +}; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/index.ts new file mode 100644 index 0000000000..84e477b16b --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createFilesystemDeleteAction } from './delete'; +export { createFilesystemRenameAction } from './rename'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts new file mode 100644 index 0000000000..58bb558ff5 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts @@ -0,0 +1,206 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as os from 'os'; +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { createFilesystemRenameAction } from './rename'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import fs from 'fs-extra'; + +const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; +const workspacePath = resolvePath(root, 'my-workspace'); + +describe('fs:rename', () => { + const action = createFilesystemRenameAction(); + + const mockInputFiles = [ + { + from: 'unit-test-a.js', + to: 'new-a.js', + }, + { + from: 'unit-test-b.js', + to: 'new-b.js', + }, + ]; + const mockContext = { + input: { + files: mockInputFiles, + }, + workspacePath, + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.restoreAllMocks(); + + mockFs({ + [workspacePath]: { + 'unit-test-a.js': 'hello', + 'unit-test-b.js': 'world', + 'unit-test-c.js': 'i will be overwritten :-(', + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should throw an error when files is not an array', async () => { + await expect( + action.handler({ + ...mockContext, + input: { files: undefined }, + }), + ).rejects.toThrow(/files must be an Array/); + + await expect( + action.handler({ + ...mockContext, + input: { files: {} }, + }), + ).rejects.toThrow(/files must be an Array/); + + await expect( + action.handler({ + ...mockContext, + input: { files: '' }, + }), + ).rejects.toThrow(/files must be an Array/); + + await expect( + action.handler({ + ...mockContext, + input: { files: null }, + }), + ).rejects.toThrow(/files must be an Array/); + }); + + it('should throw an error when files have missing from/to', async () => { + await expect( + action.handler({ + ...mockContext, + input: { files: ['old.md'] }, + }), + ).rejects.toThrow(/each file must have a from and to property/); + + await expect( + action.handler({ + ...mockContext, + input: { files: [{ from: 'old.md' }] }, + }), + ).rejects.toThrow(/each file must have a from and to property/); + + await expect( + action.handler({ + ...mockContext, + input: { files: [{ to: 'new.md' }] }, + }), + ).rejects.toThrow(/each file must have a from and to property/); + }); + + it('should throw when file name is not relative to the workspace', async () => { + await expect( + action.handler({ + ...mockContext, + input: { files: [{ from: 'index.js', to: '/core/../../../index.js' }] }, + }), + ).rejects.toThrow(/Relative path is not allowed to refer to a directory outside its parent/); + + await expect( + action.handler({ + ...mockContext, + input: { files: [{ from: '/core/../../../index.js', to: 'index.js' }] }, + }), + ).rejects.toThrow(/Relative path is not allowed to refer to a directory outside its parent/); + }); + + it('should throw is trying to override by mistake', async () => { + const destFile = 'unit-test-c.js'; + const filePath = resolvePath(workspacePath, destFile); + const beforeContent = fs.readFileSync(filePath, 'utf-8'); + + await expect( + action.handler({ + ...mockContext, + input: { + files: [ + { + from: 'unit-test-a.js', + to: 'unit-test-c.js', + }, + ], + }, + }), + ).rejects.toThrow(/dest already exists/); + + const afterContent = fs.readFileSync(filePath, 'utf-8'); + + expect(beforeContent).toEqual(afterContent); + }); + + it('should call fs.move with the correct values', async () => { + mockInputFiles.forEach(file => { + const filePath = resolvePath(workspacePath, file.from); + const fileExists = fs.existsSync(filePath); + expect(fileExists).toBe(true); + }); + + await action.handler(mockContext); + + mockInputFiles.forEach(file => { + const filePath = resolvePath(workspacePath, file.from); + const fileExists = fs.existsSync(filePath); + expect(fileExists).toBe(false); + }); + }); + + it('should override when requested', async () => { + const sourceFile = 'unit-test-a.js'; + const destFile = 'unit-test-c.js'; + const sourceFilePath = resolvePath(workspacePath, sourceFile); + const destFilePath = resolvePath(workspacePath, destFile); + + const sourceBeforeContent = fs.readFileSync(sourceFilePath, 'utf-8'); + const destBeforeContent = fs.readFileSync(destFilePath, 'utf-8'); + + expect(sourceBeforeContent).not.toEqual(destBeforeContent); + + + await action.handler({ + ...mockContext, + input: { + files: [ + { + from: sourceFile, + to: destFile, + overwrite: true, + }, + ], + }, + }); + + const destAfterContent = fs.readFileSync(destFilePath, 'utf-8'); + + expect(sourceBeforeContent).toEqual(destAfterContent); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts new file mode 100644 index 0000000000..bcfb35025c --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createTemplateAction } from '../../createTemplateAction'; +import { resolveSafeChildPath } from '@backstage/backend-common'; + +import { InputError } from '@backstage/errors'; +import { JsonObject } from '@backstage/config'; +import fs from 'fs-extra'; + +interface FilesToRename extends JsonObject { + from: string; + to: string; +} + +export const createFilesystemRenameAction = () => { + return createTemplateAction<{ files: FilesToRename }>({ + id: 'fs:rename', + schema: { + input: { + required: ['files'], + type: 'object', + properties: { + files: { + title: 'Files', + description: 'A list of file names that will be renamed', + type: 'array', + items: { + type: 'object', + required: ['from', 'to'], + properties: { + from: { + type: 'string', + title: 'The source location of the file to be renamed', + }, + to: { + type: 'string', + title: 'The destination of the new file', + }, + overwrite: { + type: 'boolean', + title: + 'Overwrite existing file or directory, default is false', + }, + }, + }, + }, + }, + }, + }, + async handler(ctx) { + if (!Array.isArray(ctx.input?.files)) { + throw new InputError('files must be an Array'); + } + + for (const file of ctx.input.files) { + if (!file.from || !file.to) { + throw new InputError('each file must have a from and to property'); + } + + const sourceFilepath = resolveSafeChildPath(ctx.workspacePath, file.from); + const destFilepath = resolveSafeChildPath(ctx.workspacePath, file.to); + + try { + await fs.move(sourceFilepath, destFilepath, { + overwrite: file.overwrite || false, + }); + ctx.logger.info( + `File ${sourceFilepath} renamed to ${destFilepath} successfully`, + ); + } catch (err) { + ctx.logger.error( + `Failed to rename file ${sourceFilepath} to ${destFilepath}:`, + err, + ); + throw err; + } + } + }, + }); +}; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index 047bcb421d..a5d5092327 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -18,4 +18,5 @@ export * from './catalog'; export { createBuiltinActions } from './createBuiltinActions'; export * from './debug'; export * from './fetch'; +export * from './filesystem'; export * from './publish'; From 19b459d2ba2b7499cd597f9e51cbc75d43ab325c Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 5 Jul 2021 14:46:04 +0200 Subject: [PATCH 23/42] move url reader docs to plugins -> backend apis Signed-off-by: Himanshu Mishra --- docs/{integrations => plugins}/url-reader.md | 12 ++++++++++-- microsite/sidebars.json | 6 +++--- mkdocs.yml | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) rename docs/{integrations => plugins}/url-reader.md (96%) diff --git a/docs/integrations/url-reader.md b/docs/plugins/url-reader.md similarity index 96% rename from docs/integrations/url-reader.md rename to docs/plugins/url-reader.md index a2108d5c71..41aba98a66 100644 --- a/docs/integrations/url-reader.md +++ b/docs/plugins/url-reader.md @@ -24,8 +24,8 @@ package provides a dedicated API for reading from such URL based remote locations like GitHub, GitLab, Bitbucket, Google Cloud Storage, etc. This is commonly referred to as "URL Reader". It takes care of making authenticated requests to the remote host so that private files can be read securely. If users -have [GitHub App based authentication](../plugins/github-apps.md) set up, URL -Reader even refreshes the token, to avoid reaching the GitHub API rate limit. +have [GitHub App based authentication](github-apps.md) set up, URL Reader even +refreshes the token, to avoid reaching the GitHub API rate limit. As a result, plugin authors do not have to worry about any of these problems when trying to read files. @@ -61,6 +61,14 @@ The generic interface of a URL Reader instance looks like this. export type UrlReader = { /* Used to read a single file and return its content. */ read(url: string): Promise; + /** + * A replacement for the read method that supports options and complex responses. + * + * Use this whenever it is available, as the read method will be deprecated and + * eventually removed in the future. + */ + readUrl?(url: string, options?: ReadUrlOptions): Promise; + /* Used to read a file tree and download as a directory. */ readTree(url: string, options?: ReadTreeOptions): Promise; /* Used to search a file in a tree. */ diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 5530fc6f61..5999524073 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -149,8 +149,7 @@ "type": "subcategory", "label": "LDAP", "ids": ["integrations/ldap/org"] - }, - "integrations/url-reader" + } ], "Plugins": [ "plugins/index", @@ -167,7 +166,8 @@ "plugins/proxying", "plugins/backend-plugin", "plugins/call-existing-api", - "plugins/github-apps" + "plugins/github-apps", + "plugins/url-reader" ] }, { diff --git a/mkdocs.yml b/mkdocs.yml index 47b7084146..4d984e63e3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -98,7 +98,6 @@ nav: - Locations: 'integrations/google-cloud-storage/locations.md' - LDAP: - Org Data: 'integrations/ldap/org.md' - - URL Reader: 'integrations/url-reader.md' - Plugins: - Intro to plugins: 'plugins/index.md' - Existing plugins: 'plugins/existing-plugins.md' @@ -113,6 +112,7 @@ nav: - Backend plugin: 'plugins/backend-plugin.md' - Call existing API: 'plugins/call-existing-api.md' - GitHub Apps for Backend Authentication: 'plugins/github-apps.md' + - URL Reader: 'plugins/url-reader.md' - Testing: - Testing with Jest: 'plugins/testing.md' - Publishing: From 00f590ad5341672b1fa18c64680fab1d5262d24b Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Mon, 5 Jul 2021 13:37:03 +0100 Subject: [PATCH 24/42] feat(scaffolder): add description and tests for directories Signed-off-by: Andrea Falzetti --- .../actions/builtin/filesystem/delete.test.ts | 11 +++++++++-- .../actions/builtin/filesystem/delete.ts | 3 ++- .../actions/builtin/filesystem/rename.test.ts | 16 +++++++++++++--- .../actions/builtin/filesystem/rename.ts | 11 ++++++++--- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts index f8fc1f1dc5..ba8a145721 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts @@ -46,6 +46,9 @@ describe('fs:delete', () => { [workspacePath]: { 'unit-test-a.js': 'hello', 'unit-test-b.js': 'world', + 'a-folder': { + 'unit-test-in-a-folder.js2': 'content', + }, }, }); }); @@ -90,14 +93,18 @@ describe('fs:delete', () => { ...mockContext, input: { files: ['/foo/../../../index.js'] }, }), - ).rejects.toThrow(/Relative path is not allowed to refer to a directory outside its parent/); + ).rejects.toThrow( + /Relative path is not allowed to refer to a directory outside its parent/, + ); await expect( action.handler({ ...mockContext, input: { files: ['../../../index.js'] }, }), - ).rejects.toThrow(/Relative path is not allowed to refer to a directory outside its parent/); + ).rejects.toThrow( + /Relative path is not allowed to refer to a directory outside its parent/, + ); }); it('should call fs.rm with the correct values', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts index ec8d60f955..068f777ec5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.ts @@ -21,6 +21,7 @@ import fs from 'fs-extra'; export const createFilesystemDeleteAction = () => { return createTemplateAction<{ files: string[] }>({ id: 'fs:delete', + description: 'Deletes files and directories from the workspace', schema: { input: { required: ['files'], @@ -28,7 +29,7 @@ export const createFilesystemDeleteAction = () => { properties: { files: { title: 'Files', - description: 'A list of files or directories that will be deleted', + description: 'A list of files and directories that will be deleted', type: 'array', items: { type: 'string', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts index 58bb558ff5..8452735083 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts @@ -37,6 +37,10 @@ describe('fs:rename', () => { from: 'unit-test-b.js', to: 'new-b.js', }, + { + from: 'a-folder', + to: 'brand-new-folder', + }, ]; const mockContext = { input: { @@ -57,6 +61,9 @@ describe('fs:rename', () => { 'unit-test-a.js': 'hello', 'unit-test-b.js': 'world', 'unit-test-c.js': 'i will be overwritten :-(', + 'a-folder': { + 'file.md': 'content', + }, }, }); }); @@ -124,14 +131,18 @@ describe('fs:rename', () => { ...mockContext, input: { files: [{ from: 'index.js', to: '/core/../../../index.js' }] }, }), - ).rejects.toThrow(/Relative path is not allowed to refer to a directory outside its parent/); + ).rejects.toThrow( + /Relative path is not allowed to refer to a directory outside its parent/, + ); await expect( action.handler({ ...mockContext, input: { files: [{ from: '/core/../../../index.js', to: 'index.js' }] }, }), - ).rejects.toThrow(/Relative path is not allowed to refer to a directory outside its parent/); + ).rejects.toThrow( + /Relative path is not allowed to refer to a directory outside its parent/, + ); }); it('should throw is trying to override by mistake', async () => { @@ -184,7 +195,6 @@ describe('fs:rename', () => { const destBeforeContent = fs.readFileSync(destFilePath, 'utf-8'); expect(sourceBeforeContent).not.toEqual(destBeforeContent); - await action.handler({ ...mockContext, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts index bcfb35025c..aaadd5df57 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.ts @@ -28,6 +28,7 @@ interface FilesToRename extends JsonObject { export const createFilesystemRenameAction = () => { return createTemplateAction<{ files: FilesToRename }>({ id: 'fs:rename', + description: 'Renames files and directories within the workspace', schema: { input: { required: ['files'], @@ -35,7 +36,8 @@ export const createFilesystemRenameAction = () => { properties: { files: { title: 'Files', - description: 'A list of file names that will be renamed', + description: + 'A list of file and directory names that will be renamed', type: 'array', items: { type: 'object', @@ -70,12 +72,15 @@ export const createFilesystemRenameAction = () => { throw new InputError('each file must have a from and to property'); } - const sourceFilepath = resolveSafeChildPath(ctx.workspacePath, file.from); + const sourceFilepath = resolveSafeChildPath( + ctx.workspacePath, + file.from, + ); const destFilepath = resolveSafeChildPath(ctx.workspacePath, file.to); try { await fs.move(sourceFilepath, destFilepath, { - overwrite: file.overwrite || false, + overwrite: file.overwrite ?? false, }); ctx.logger.info( `File ${sourceFilepath} renamed to ${destFilepath} successfully`, From c93fb2c93d67c23a13d85c75c4a3ed296503a4d6 Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Mon, 5 Jul 2021 16:03:45 +0100 Subject: [PATCH 25/42] docs(scaffolder): update docs Signed-off-by: Andrea Falzetti --- plugins/scaffolder-backend/api-report.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 792913749b..7528388a47 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -143,6 +143,12 @@ export function createFetchPlainAction(options: { integrations: ScmIntegrations; }): TemplateAction; +// @public (undocumented) +export const createFilesystemDeleteAction: () => TemplateAction; + +// @public (undocumented) +export const createFilesystemRenameAction: () => TemplateAction; + // @public (undocumented) export function createLegacyActions(options: Options): TemplateAction[]; From 24b4b01cd73a1f5338074dba76c242193974ebf5 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 5 Jul 2021 17:31:03 +0200 Subject: [PATCH 26/42] ACustomize scrollbar for sidebar items wrapper Signed-off-by: Philipp Hugenroth --- .../src/layout/Sidebar/Items.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index dbaa50b76e..e53cdfaa57 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -291,10 +291,21 @@ export const SidebarDivider = styled('hr')({ margin: '12px 0px', }); -export const SidebarScrollWrapper = styled('div')({ +export const SidebarScrollWrapper = styled('div')(({ theme }) => ({ flex: '0 1 auto', - overflowY: 'auto', + overflowX: 'hidden', + // 5px space to the right of the scrollbar + width: 'calc(100% - 5px)', // Display at least one item in the container // Question: Can this be a config/theme variable - if so, which? :/ minHeight: '48px', -}); + '&::-webkit-scrollbar': { + backgroundColor: theme.palette.background.default, + width: '5px', + borderRadius: '5px', + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: theme.palette.text.hint, + borderRadius: '5px', + }, +})); From 612e25fd7c5be2366c94f3e518837d6a571f85b3 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 5 Jul 2021 17:34:29 +0200 Subject: [PATCH 27/42] Add changeset Signed-off-by: Philipp Hugenroth --- .changeset/young-pigs-beg.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/young-pigs-beg.md diff --git a/.changeset/young-pigs-beg.md b/.changeset/young-pigs-beg.md new file mode 100644 index 0000000000..bbbc7b0f74 --- /dev/null +++ b/.changeset/young-pigs-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add custom styles for scrollbar of sidebar items wrapper to fix flaky behavior From c97e06e1c15c9ea42b76224a250714bd6afdd1ef Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 5 Jul 2021 18:01:09 +0200 Subject: [PATCH 28/42] Fix changeset spelling Signed-off-by: Philipp Hugenroth --- .changeset/young-pigs-beg.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/young-pigs-beg.md b/.changeset/young-pigs-beg.md index bbbc7b0f74..f40c02b872 100644 --- a/.changeset/young-pigs-beg.md +++ b/.changeset/young-pigs-beg.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Add custom styles for scrollbar of sidebar items wrapper to fix flaky behavior +Add custom styles to Scrollbar of the Sidebar Wrapper to fix flaky behaviour From 4ca8e569a5154207bd4adfe6187caf9145062434 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 5 Jul 2021 18:06:12 +0200 Subject: [PATCH 29/42] Add changeset Signed-off-by: Ben Lambert --- .changeset/rotten-cooks-try.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rotten-cooks-try.md diff --git a/.changeset/rotten-cooks-try.md b/.changeset/rotten-cooks-try.md new file mode 100644 index 0000000000..b5a09f399f --- /dev/null +++ b/.changeset/rotten-cooks-try.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-ilert': patch +--- + +chore: bump @date-io/luxon from 1.3.13 to 2.10.11 From 0b684849f37df9880621fd82e790f6acf29934c6 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 5 Jul 2021 18:06:57 +0200 Subject: [PATCH 30/42] Please - don't embarrass me - I'm not a native speaker, bot... Signed-off-by: Philipp Hugenroth --- .changeset/young-pigs-beg.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/young-pigs-beg.md b/.changeset/young-pigs-beg.md index f40c02b872..bfdf726911 100644 --- a/.changeset/young-pigs-beg.md +++ b/.changeset/young-pigs-beg.md @@ -2,4 +2,4 @@ '@backstage/core-components': patch --- -Add custom styles to Scrollbar of the Sidebar Wrapper to fix flaky behaviour +Add custom styles to scroll bar of the sidebar wrapper to fix flaky behaviour From 6cbeafcbb98a75551ab8f07f297fd91da9c23a4a Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Mon, 5 Jul 2021 18:14:25 +0200 Subject: [PATCH 31/42] Update spelling Signed-off-by: Ben Lambert --- .changeset/rotten-cooks-try.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rotten-cooks-try.md b/.changeset/rotten-cooks-try.md index b5a09f399f..ccbd315ebe 100644 --- a/.changeset/rotten-cooks-try.md +++ b/.changeset/rotten-cooks-try.md @@ -2,4 +2,4 @@ '@backstage/plugin-ilert': patch --- -chore: bump @date-io/luxon from 1.3.13 to 2.10.11 +chore: bump `@date-io/luxon` from 1.3.13 to 2.10.11 From 3a55daf7b6ec515e29984cad376edea1ff17ccfd Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Mon, 5 Jul 2021 23:03:36 +0200 Subject: [PATCH 32/42] Authorize scaffolder list actions request Signed-off-by: Erik Larsson --- plugins/scaffolder/src/api.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 85df77f2f5..37d78d0fda 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -245,7 +245,10 @@ export class ScaffolderClient implements ScaffolderApi { */ async listActions(): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); - const response = await fetch(`${baseUrl}/v2/actions`); + const token = await this.identityApi.getIdToken(); + const response = await fetch(`${baseUrl}/v2/actions`, { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); if (!response.ok) { throw ResponseError.fromResponse(response); From bd764f78accb90d76cd5c192bad496bb1ced9023 Mon Sep 17 00:00:00 2001 From: Erik Larsson Date: Mon, 5 Jul 2021 23:04:33 +0200 Subject: [PATCH 33/42] changeset Signed-off-by: Erik Larsson --- .changeset/tasty-chairs-flash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tasty-chairs-flash.md diff --git a/.changeset/tasty-chairs-flash.md b/.changeset/tasty-chairs-flash.md new file mode 100644 index 0000000000..186c10e98f --- /dev/null +++ b/.changeset/tasty-chairs-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Authorize scaffolder list actions request From 3e3227afc06aabb3c35e8f6cf4567edc59d287bc Mon Sep 17 00:00:00 2001 From: roylisto Date: Tue, 6 Jul 2021 10:27:56 +0700 Subject: [PATCH 34/42] Add infoCardAction Signed-off-by: roylisto --- .../Group/GroupProfile/GroupProfileCard.tsx | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 637a7279f6..b44fbe6c5c 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -79,28 +79,31 @@ export const GroupProfileCard = ({ kind: 'group', }); - const entityMetadataEditUrl = getEntityMetadataEditUrl(group) ?? '#'; + const entityMetadataEditUrl = getEntityMetadataEditUrl(group); const displayName = profile?.displayName ?? name; const emailHref = profile?.email ? `mailto:${profile.email}` : '#'; + const infoCardAction = entityMetadataEditUrl ? ( + + + + ) : ( + + + + ); return ( } subheader={description} variant={variant} - action={ - - - - } + action={infoCardAction} > From 30f28ef11ce18d94e2c846fbe5ce24e72e9cc83b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jul 2021 04:10:52 +0000 Subject: [PATCH 35/42] chore(deps-dev): bump @types/aws4 from 1.5.1 to 1.5.2 Bumps [@types/aws4](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/aws4) from 1.5.1 to 1.5.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/aws4) --- updated-dependencies: - dependency-name: "@types/aws4" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 272e7e229f..6cf36a7dfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5459,9 +5459,9 @@ integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== "@types/aws4@^1.5.1": - version "1.5.1" - resolved "https://registry.npmjs.org/@types/aws4/-/aws4-1.5.1.tgz#361fadab198a030ab398269183ae3fa86e958ed9" - integrity sha1-Nh+tqxmKAwqzmCaRg64/qG6Vjtk= + version "1.5.2" + resolved "https://registry.npmjs.org/@types/aws4/-/aws4-1.5.2.tgz#34e35b4405a619b9205be3e7678963bc7c8a47db" + integrity sha512-r8+XOv0BKw3Br0oU6w9cfu21PaQq/5ZeXvMOivNQYJLc9WcPCpEFDaQu72QNEQjYv5Otu48VhjjnbtwrSmk/rg== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.9" From c669940656302de5384fd6c6e1beb0a369b55b87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jul 2021 04:14:07 +0000 Subject: [PATCH 36/42] chore(deps-dev): bump @types/archiver from 5.1.0 to 5.3.0 Bumps [@types/archiver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/archiver) from 5.1.0 to 5.3.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/archiver) --- updated-dependencies: - dependency-name: "@types/archiver" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 272e7e229f..7e5e209489 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5442,9 +5442,9 @@ integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== "@types/archiver@^5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@types/archiver/-/archiver-5.1.0.tgz#869f4ce4028e49cf9a0243cf914415f4cc3d1f3d" - integrity sha512-baFOhanb/hxmcOd1Uey2TfFg43kTSmM6py1Eo7Rjbv/ivcl7PXLhY0QgXGf50Hx/eskGCFqPfhs/7IZLb15C5g== + version "5.3.0" + resolved "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.0.tgz#2b34ba56d4d7102d256b922c7e91e09eab79db6f" + integrity sha512-qJ79qsmq7O/k9FYwsF6O1xVA1PeLV+9Bh3TYkVCu3VzMR6vN9JQkgEOh/rrQ0R+F4Ta+R3thHGewxQtFglwVfg== dependencies: "@types/glob" "*" From 34143f5c9ab5897e59141b78d8bc80d72fb67214 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 6 Jul 2021 11:15:21 +0200 Subject: [PATCH 37/42] Update tasty-chairs-flash.md Signed-off-by: Ben Lambert --- .changeset/tasty-chairs-flash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tasty-chairs-flash.md b/.changeset/tasty-chairs-flash.md index 186c10e98f..8be48d92b3 100644 --- a/.changeset/tasty-chairs-flash.md +++ b/.changeset/tasty-chairs-flash.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': patch --- -Authorize scaffolder list actions request +Pass through the `idToken` in `Authorization` Header for `listActions` request From a632f1afd8d20f729b8c0149062eecc241e61f6a Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 6 Jul 2021 11:42:28 +0200 Subject: [PATCH 38/42] chore: fixing hopefully fixing the cypress tugboat tests Signed-off-by: blam --- cypress/src/integration/catalog.ts | 2 +- cypress/src/integration/integrations.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cypress/src/integration/catalog.ts b/cypress/src/integration/catalog.ts index 1db2eb8a13..20dcdd9b8a 100644 --- a/cypress/src/integration/catalog.ts +++ b/cypress/src/integration/catalog.ts @@ -23,7 +23,7 @@ describe('Catalog', () => { cy.visit('/catalog'); - cy.contains('Owned (8)').should('be.visible'); + cy.contains('Owned (10)').should('be.visible'); }); }); }); diff --git a/cypress/src/integration/integrations.ts b/cypress/src/integration/integrations.ts index 3e3a10aca2..d732e25edf 100644 --- a/cypress/src/integration/integrations.ts +++ b/cypress/src/integration/integrations.ts @@ -27,8 +27,10 @@ describe('Integrations', () => { type: 'url', }); + cy.wait(1000); + cy.visit('/catalog'); - cy.contains('All').click(); + cy.get('[data-testid="user-picker-all"]').click(); cy.get('table').should('contain', 'github-repo'); cy.get('table').should('contain', 'github-repo-nested'); }); @@ -52,8 +54,10 @@ describe('Integrations', () => { type: 'url', }); + cy.wait(1000); + cy.visit('/catalog'); - cy.contains('All').click(); + cy.get('[data-testid="user-picker-all"]').click(); cy.get('table').should('contain', 'gitlab-repo'); cy.get('table').should('contain', 'gitlab-repo-nested'); }); @@ -67,8 +71,10 @@ describe('Integrations', () => { type: 'url', }); + cy.wait(1000); + cy.visit('/catalog'); - cy.contains('All').click(); + cy.get('[data-testid="user-picker-all"]').click(); cy.get('table').should('contain', 'bitbucket-repo'); cy.get('table').should('contain', 'bitbucket-repo-nested'); }); From b055ef88aafc2ecb33d0baa694fe2ea615ecf2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20A=CC=8Ahsberg?= Date: Mon, 5 Jul 2021 15:09:31 +0000 Subject: [PATCH 39/42] Add extension points for the LDAP processor for modifying group and user entities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mathias Åhsberg --- .changeset/large-mice-sin.md | 6 + docs/integrations/ldap/org.md | 44 ++- .../catalog-backend-module-ldap/api-report.md | 27 +- .../src/ldap/index.ts | 8 +- .../src/ldap/read.ts | 269 ++++++++++-------- .../src/ldap/types.ts | 47 +++ .../src/ldap/util.ts | 25 +- .../src/processors/LdapOrgReaderProcessor.ts | 27 +- 8 files changed, 325 insertions(+), 128 deletions(-) create mode 100644 .changeset/large-mice-sin.md create mode 100644 plugins/catalog-backend-module-ldap/src/ldap/types.ts diff --git a/.changeset/large-mice-sin.md b/.changeset/large-mice-sin.md new file mode 100644 index 0000000000..77aba8d3c5 --- /dev/null +++ b/.changeset/large-mice-sin.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': minor +--- + +Add extension points to the `LdapOrgReaderProcessor` to make it possible to do more advanced modifications +of the ingested users and groups. diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 836601971c..d1438aba8b 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -122,8 +122,8 @@ The DN under which users are stored, e.g. #### users.options The search options to use when sending the query to the server, when reading all -users. All of the options are shown below, with their default values, but they -are all optional. +users. All the options are shown below, with their default values, but they are +all optional. ```yaml options: @@ -158,8 +158,8 @@ set: Mappings from well known entity fields, to LDAP attribute names. This is where you are able to define how to interpret the attributes of each LDAP result item, -and to move them into the corresponding entity fields. All of the options are -shown below, with their default values, but they are all optional. +and to move them into the corresponding entity fields. All the options are shown +below, with their default values, but they are all optional. If you leave out an optional mapping, it will still be copied using that default value. For example, even if you do not put in the field `displayName` in your @@ -204,8 +204,8 @@ The DN under which groups are stored, e.g. #### groups.options The search options to use when sending the query to the server, when reading all -groups. All of the options are shown below, with their default values, but they -are all optional. +groups. All the options are shown below, with their default values, but they are +all optional. ```yaml options: @@ -282,3 +282,35 @@ map: # the spec.children field of the entity. members: member ``` + +## Customize the Processor + +In case you want to customize the ingested entities, the +`LdapOrgReaderProcessor` allows to pass transformers for users and groups. + +1. Create a transformer: + +```ts +export async function myGroupTransformer( + vendor: LdapVendor, + config: GroupConfig, + group: SearchEntry, +): Promise { + // Transformations may change namespace, change entity naming pattern, fill + // profile with more or other details... + + // Create the group entity on your own, or wrap the default transformer + return await defaultGroupTransformer(vendor, config, group); +} +``` + +2. Configure the processor with the transformer: + +```ts +builder.addProcessor( + LdapOrgReaderProcessor.fromConfig(config, { + logger, + groupTransformer: myGroupTransformer, + }), +); +``` diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index d5e9d731ea..55405cab41 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -16,6 +16,15 @@ import { SearchEntry } from 'ldapjs'; import { SearchOptions } from 'ldapjs'; import { UserEntity } from '@backstage/catalog-model'; +// @public (undocumented) +export function defaultGroupTransformer(vendor: LdapVendor, config: GroupConfig, entry: SearchEntry): Promise; + +// @public (undocumented) +export function defaultUserTransformer(vendor: LdapVendor, config: UserConfig, entry: SearchEntry): Promise; + +// @public +export type GroupTransformer = (vendor: LdapVendor, config: GroupConfig, group: SearchEntry) => Promise; + // @public export const LDAP_DN_ANNOTATION = "backstage.io/ldap-dn"; @@ -40,14 +49,18 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: LdapProviderConfig[]; logger: Logger; + groupTransformer?: GroupTransformer; + userTransformer?: UserTransformer; }); // (undocumented) static fromConfig(config: Config, options: { logger: Logger; + groupTransformer?: GroupTransformer; + userTransformer?: UserTransformer; }): LdapOrgReaderProcessor; // (undocumented) readLocation(location: LocationSpec, _optional: boolean, emit: CatalogProcessorEmit): Promise; -} + } // @public export type LdapProviderConfig = { @@ -57,15 +70,25 @@ export type LdapProviderConfig = { groups: GroupConfig; }; +// @public +export function mapStringAttr(entry: SearchEntry, vendor: LdapVendor, attributeName: string | undefined, setter: (value: string) => void): void; + // @public export function readLdapConfig(config: Config): LdapProviderConfig[]; // @public -export function readLdapOrg(client: LdapClient, userConfig: UserConfig, groupConfig: GroupConfig): Promise<{ +export function readLdapOrg(client: LdapClient, userConfig: UserConfig, groupConfig: GroupConfig, options: { + groupTransformer?: GroupTransformer; + userTransformer?: UserTransformer; + logger: Logger; +}): Promise<{ users: UserEntity[]; groups: GroupEntity[]; }>; +// @public +export type UserTransformer = (vendor: LdapVendor, config: UserConfig, user: SearchEntry) => Promise; + // (No @packageDocumentation comment for this package) diff --git a/plugins/catalog-backend-module-ldap/src/ldap/index.ts b/plugins/catalog-backend-module-ldap/src/ldap/index.ts index 194a75ac60..a9f127a3a7 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/index.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/index.ts @@ -15,6 +15,7 @@ */ export { LdapClient } from './client'; +export { mapStringAttr } from './util'; export { readLdapConfig } from './config'; export type { LdapProviderConfig } from './config'; export { @@ -22,4 +23,9 @@ export { LDAP_RDN_ANNOTATION, LDAP_UUID_ANNOTATION, } from './constants'; -export { readLdapOrg } from './read'; +export { + defaultGroupTransformer, + defaultUserTransformer, + readLdapOrg, +} from './read'; +export type { GroupTransformer, UserTransformer } from './types'; diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index a662b36d87..f3ee09d32d 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -26,74 +26,97 @@ import { LDAP_UUID_ANNOTATION, } from './constants'; import { LdapVendor } from './vendors'; +import { Logger } from 'winston'; +import { GroupTransformer, UserTransformer } from './types'; +import { mapStringAttr } from './util'; + +export async function defaultUserTransformer( + vendor: LdapVendor, + config: UserConfig, + entry: SearchEntry, +): Promise { + const { set, map } = config; + + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: '', + annotations: {}, + }, + spec: { + profile: {}, + memberOf: [], + }, + }; + + if (set) { + for (const [path, value] of Object.entries(set)) { + lodashSet(entity, path, value); + } + } + + mapStringAttr(entry, vendor, map.name, v => { + entity.metadata.name = v; + }); + mapStringAttr(entry, vendor, map.description, v => { + entity.metadata.description = v; + }); + mapStringAttr(entry, vendor, map.rdn, v => { + entity.metadata.annotations![LDAP_RDN_ANNOTATION] = v; + }); + mapStringAttr(entry, vendor, vendor.uuidAttributeName, v => { + entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v; + }); + mapStringAttr(entry, vendor, vendor.dnAttributeName, v => { + entity.metadata.annotations![LDAP_DN_ANNOTATION] = v; + }); + mapStringAttr(entry, vendor, map.displayName, v => { + entity.spec.profile!.displayName = v; + }); + mapStringAttr(entry, vendor, map.email, v => { + entity.spec.profile!.email = v; + }); + mapStringAttr(entry, vendor, map.picture, v => { + entity.spec.profile!.picture = v; + }); + + return entity; +} /** * Reads users out of an LDAP provider. * * @param client The LDAP client * @param config The user data configuration + * @param opts */ export async function readLdapUsers( client: LdapClient, config: UserConfig, + opts?: { transformer?: UserTransformer }, ): Promise<{ users: UserEntity[]; // With all relations empty userMemberOf: Map>; // DN -> DN or UUID of groups }> { - const { dn, options, set, map } = config; + const { dn, options, map } = config; const vendor = await client.getVendor(); - const entries = await client.search(dn, options); - const entities: UserEntity[] = []; const userMemberOf: Map> = new Map(); - for (const entry of entries) { - const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: '', - annotations: {}, - }, - spec: { - profile: {}, - memberOf: [], - }, - }; + const transformer = opts?.transformer ?? defaultUserTransformer; - if (set) { - for (const [path, value] of Object.entries(set)) { - lodashSet(entity, path, value); - } + const entries = await client.search(dn, options); + + for (const user of entries) { + const entity = await transformer(vendor, config, user); + + if (!entity) { + continue; } - mapStringAttr(entry, vendor, map.name, v => { - entity.metadata.name = v; - }); - mapStringAttr(entry, vendor, map.description, v => { - entity.metadata.description = v; - }); - mapStringAttr(entry, vendor, map.rdn, v => { - entity.metadata.annotations![LDAP_RDN_ANNOTATION] = v; - }); - mapStringAttr(entry, vendor, vendor.uuidAttributeName, v => { - entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v; - }); - mapStringAttr(entry, vendor, vendor.dnAttributeName, v => { - entity.metadata.annotations![LDAP_DN_ANNOTATION] = v; - }); - mapStringAttr(entry, vendor, map.displayName, v => { - entity.spec.profile!.displayName = v; - }); - mapStringAttr(entry, vendor, map.email, v => { - entity.spec.profile!.email = v; - }); - mapStringAttr(entry, vendor, map.picture, v => { - entity.spec.profile!.picture = v; - }); - - mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => { + mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => { ensureItems(userMemberOf, myDn, vs); }); @@ -103,82 +126,103 @@ export async function readLdapUsers( return { users: entities, userMemberOf }; } +export async function defaultGroupTransformer( + vendor: LdapVendor, + config: GroupConfig, + entry: SearchEntry, +): Promise { + const { set, map } = config; + const entity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: '', + annotations: {}, + }, + spec: { + type: 'unknown', + profile: {}, + children: [], + }, + }; + + if (set) { + for (const [path, value] of Object.entries(set)) { + lodashSet(entity, path, value); + } + } + + mapStringAttr(entry, vendor, map.name, v => { + entity.metadata.name = v; + }); + mapStringAttr(entry, vendor, map.description, v => { + entity.metadata.description = v; + }); + mapStringAttr(entry, vendor, map.rdn, v => { + entity.metadata.annotations![LDAP_RDN_ANNOTATION] = v; + }); + mapStringAttr(entry, vendor, vendor.uuidAttributeName, v => { + entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v; + }); + mapStringAttr(entry, vendor, vendor.dnAttributeName, v => { + entity.metadata.annotations![LDAP_DN_ANNOTATION] = v; + }); + mapStringAttr(entry, vendor, map.type, v => { + entity.spec.type = v; + }); + mapStringAttr(entry, vendor, map.displayName, v => { + entity.spec.profile!.displayName = v; + }); + mapStringAttr(entry, vendor, map.email, v => { + entity.spec.profile!.email = v; + }); + mapStringAttr(entry, vendor, map.picture, v => { + entity.spec.profile!.picture = v; + }); + + return entity; +} + /** * Reads groups out of an LDAP provider. * * @param client The LDAP client * @param config The group data configuration + * @param opts */ export async function readLdapGroups( client: LdapClient, config: GroupConfig, + opts?: { + transformer?: GroupTransformer; + }, ): Promise<{ groups: GroupEntity[]; // With all relations empty groupMemberOf: Map>; // DN -> DN or UUID of groups groupMember: Map>; // DN -> DN or UUID of groups & users }> { - const { dn, options, set, map } = config; - const vendor = await client.getVendor(); - - const entries = await client.search(dn, options); - const groups: GroupEntity[] = []; const groupMemberOf: Map> = new Map(); const groupMember: Map> = new Map(); - for (const entry of entries) { - const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: '', - annotations: {}, - }, - spec: { - type: 'unknown', - profile: {}, - children: [], - }, - }; + const { dn, map, options } = config; + const vendor = await client.getVendor(); - if (set) { - for (const [path, value] of Object.entries(set)) { - lodashSet(entity, path, value); - } + const transformer = opts?.transformer ?? defaultGroupTransformer; + + const entries = await client.search(dn, options); + + for (const group of entries) { + const entity = await transformer(vendor, config, group); + + if (!entity) { + continue; } - mapStringAttr(entry, vendor, map.name, v => { - entity.metadata.name = v; - }); - mapStringAttr(entry, vendor, map.description, v => { - entity.metadata.description = v; - }); - mapStringAttr(entry, vendor, map.rdn, v => { - entity.metadata.annotations![LDAP_RDN_ANNOTATION] = v; - }); - mapStringAttr(entry, vendor, vendor.uuidAttributeName, v => { - entity.metadata.annotations![LDAP_UUID_ANNOTATION] = v; - }); - mapStringAttr(entry, vendor, vendor.dnAttributeName, v => { - entity.metadata.annotations![LDAP_DN_ANNOTATION] = v; - }); - mapStringAttr(entry, vendor, map.type, v => { - entity.spec.type = v; - }); - mapStringAttr(entry, vendor, map.displayName, v => { - entity.spec.profile!.displayName = v; - }); - mapStringAttr(entry, vendor, map.email, v => { - entity.spec.profile!.email = v; - }); - mapStringAttr(entry, vendor, map.picture, v => { - entity.spec.profile!.picture = v; - }); - - mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => { + mapReferencesAttr(group, vendor, map.memberOf, (myDn, vs) => { ensureItems(groupMemberOf, myDn, vs); }); - mapReferencesAttr(entry, vendor, map.members, (myDn, vs) => { + mapReferencesAttr(group, vendor, map.members, (myDn, vs) => { ensureItems(groupMember, myDn, vs); }); @@ -199,22 +243,30 @@ export async function readLdapGroups( * with all relations etc filled in. * * @param client The LDAP client - * @param logger A logger instance * @param userConfig The user data configuration * @param groupConfig The group data configuration + * @param options */ export async function readLdapOrg( client: LdapClient, userConfig: UserConfig, groupConfig: GroupConfig, + options: { + groupTransformer?: GroupTransformer; + userTransformer?: UserTransformer; + logger: Logger; + }, ): Promise<{ users: UserEntity[]; groups: GroupEntity[]; }> { - const { users, userMemberOf } = await readLdapUsers(client, userConfig); + const { users, userMemberOf } = await readLdapUsers(client, userConfig, { + transformer: options?.userTransformer, + }); const { groups, groupMemberOf, groupMember } = await readLdapGroups( client, groupConfig, + { transformer: options?.groupTransformer }, ); resolveRelations(groups, users, userMemberOf, groupMemberOf, groupMember); @@ -228,21 +280,6 @@ export async function readLdapOrg( // Helpers // -// Maps a single-valued attribute to a consumer -function mapStringAttr( - entry: SearchEntry, - vendor: LdapVendor, - attributeName: string | undefined, - setter: (value: string) => void, -) { - if (attributeName) { - const values = vendor.decodeStringAttribute(entry, attributeName); - if (values && values.length === 1) { - setter(values[0]); - } - } -} - // Maps a multi-valued attribute of references to other objects, to a consumer function mapReferencesAttr( entry: SearchEntry, diff --git a/plugins/catalog-backend-module-ldap/src/ldap/types.ts b/plugins/catalog-backend-module-ldap/src/ldap/types.ts new file mode 100644 index 0000000000..e32e4c5b42 --- /dev/null +++ b/plugins/catalog-backend-module-ldap/src/ldap/types.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { SearchEntry } from 'ldapjs'; +import { LdapVendor } from './vendors'; +import { GroupConfig, UserConfig } from './config'; + +/** + * Customize the ingested User entity + * + * @param vendor The LDAP vendor that can be used to find and decode vendor specific attributes + * @param config The User specific config used by the default transformer. + * @param user The found LDAP entry in its source format. This is the entry that you want to transform + * @return A `UserEntity` or `undefined` if you want to ignore the found user for being ingested by the catalog + */ +export type UserTransformer = ( + vendor: LdapVendor, + config: UserConfig, + user: SearchEntry, +) => Promise; + +/** + * Customize the ingested Group entity + * + * @param vendor The LDAP vendor that can be used to find and decode vendor specific attributes + * @param config The Group specific config used by the default transformer. + * @param group The found LDAP entry in its source format. This is the entry that you want to transform + * @return A `GroupEntity` or `undefined` if you want to ignore the found group for being ingested by the catalog + */ +export type GroupTransformer = ( + vendor: LdapVendor, + config: GroupConfig, + group: SearchEntry, +) => Promise; diff --git a/plugins/catalog-backend-module-ldap/src/ldap/util.ts b/plugins/catalog-backend-module-ldap/src/ldap/util.ts index 74dd84cdc9..21ee40f52c 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/util.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/util.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Error as LDAPError } from 'ldapjs'; +import { Error as LDAPError, SearchEntry } from 'ldapjs'; +import { LdapVendor } from './vendors'; /** * Builds a string form of an LDAP Error structure. @@ -24,3 +25,25 @@ import { Error as LDAPError } from 'ldapjs'; export function errorString(error: LDAPError) { return `${error.code} ${error.name}: ${error.message}`; } + +/** + * Maps a single-valued attribute to a consumer + * + * @param entry The LDAP source entry + * @param vendor The LDAP vendor + * @param attributeName The source attribute to map. If the attribute is undefined the mapping will be silently ignored. + * @param setter The function to be called with the decoded attribute from the source entry + */ +export function mapStringAttr( + entry: SearchEntry, + vendor: LdapVendor, + attributeName: string | undefined, + setter: (value: string) => void, +) { + if (attributeName) { + const values = vendor.decodeStringAttribute(entry, attributeName); + if (values && values.length === 1) { + setter(values[0]); + } + } +} diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts index 79c0ed6964..f87afc38b9 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts @@ -18,10 +18,12 @@ import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { Logger } from 'winston'; import { + GroupTransformer, LdapClient, LdapProviderConfig, readLdapConfig, readLdapOrg, + UserTransformer, } from '../ldap'; import { CatalogProcessor, @@ -35,8 +37,17 @@ import { export class LdapOrgReaderProcessor implements CatalogProcessor { private readonly providers: LdapProviderConfig[]; private readonly logger: Logger; + private readonly groupTransformer?: GroupTransformer; + private readonly userTransformer?: UserTransformer; - static fromConfig(config: Config, options: { logger: Logger }) { + static fromConfig( + config: Config, + options: { + logger: Logger; + groupTransformer?: GroupTransformer; + userTransformer?: UserTransformer; + }, + ) { const c = config.getOptionalConfig('catalog.processors.ldapOrg'); return new LdapOrgReaderProcessor({ ...options, @@ -44,9 +55,16 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { }); } - constructor(options: { providers: LdapProviderConfig[]; logger: Logger }) { + constructor(options: { + providers: LdapProviderConfig[]; + logger: Logger; + groupTransformer?: GroupTransformer; + userTransformer?: UserTransformer; + }) { this.providers = options.providers; this.logger = options.logger; + this.groupTransformer = options.groupTransformer; + this.userTransformer = options.userTransformer; } async readLocation( @@ -81,6 +99,11 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { client, provider.users, provider.groups, + { + groupTransformer: this.groupTransformer, + userTransformer: this.userTransformer, + logger: this.logger, + }, ); const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); From 98e5a4919379f9547bf508fae9c0c5ea0b08a2eb Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 6 Jul 2021 12:24:28 +0200 Subject: [PATCH 40/42] chore: change timeout Signed-off-by: Ben Lambert --- cypress/src/integration/integrations.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/src/integration/integrations.ts b/cypress/src/integration/integrations.ts index d732e25edf..4703c18881 100644 --- a/cypress/src/integration/integrations.ts +++ b/cypress/src/integration/integrations.ts @@ -27,7 +27,7 @@ describe('Integrations', () => { type: 'url', }); - cy.wait(1000); + cy.wait(5000); cy.visit('/catalog'); cy.get('[data-testid="user-picker-all"]').click(); @@ -54,7 +54,7 @@ describe('Integrations', () => { type: 'url', }); - cy.wait(1000); + cy.wait(5000); cy.visit('/catalog'); cy.get('[data-testid="user-picker-all"]').click(); @@ -71,7 +71,7 @@ describe('Integrations', () => { type: 'url', }); - cy.wait(1000); + cy.wait(5000); cy.visit('/catalog'); cy.get('[data-testid="user-picker-all"]').click(); From 76bb7aeda08ad5f9cda1f6d787bb31bc662b01fb Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 6 Jul 2021 14:01:23 +0200 Subject: [PATCH 41/42] Show scroll bar on hover only Signed-off-by: Philipp Hugenroth --- .changeset/late-pants-itch.md | 5 +++++ packages/core-components/src/layout/Sidebar/Items.tsx | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/late-pants-itch.md diff --git a/.changeset/late-pants-itch.md b/.changeset/late-pants-itch.md new file mode 100644 index 0000000000..7bd3087316 --- /dev/null +++ b/.changeset/late-pants-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Show scroll bar of the sidebar wrapper only on hover diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index e53cdfaa57..90dae01cd4 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -299,12 +299,17 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => ({ // Display at least one item in the container // Question: Can this be a config/theme variable - if so, which? :/ minHeight: '48px', - '&::-webkit-scrollbar': { + overflowY: 'hidden', + + '&:hover': { + overflowY: 'auto', + }, + '&:hover::-webkit-scrollbar': { backgroundColor: theme.palette.background.default, width: '5px', borderRadius: '5px', }, - '&::-webkit-scrollbar-thumb': { + '&:hover::-webkit-scrollbar-thumb': { backgroundColor: theme.palette.text.hint, borderRadius: '5px', }, From f1cffe88075c04357712066ef408a3e869bbbb5f Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 6 Jul 2021 14:41:08 +0200 Subject: [PATCH 42/42] Don't hide scrollbar on touch devices Signed-off-by: Philipp Hugenroth --- .../src/layout/Sidebar/Items.tsx | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 90dae01cd4..28abbce53c 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -21,8 +21,10 @@ import { makeStyles, styled, TextField, + Theme, Typography, } from '@material-ui/core'; +import { CreateCSSProperties } from '@material-ui/core/styles/withStyles'; import SearchIcon from '@material-ui/icons/Search'; import clsx from 'clsx'; import React, { @@ -291,26 +293,31 @@ export const SidebarDivider = styled('hr')({ margin: '12px 0px', }); -export const SidebarScrollWrapper = styled('div')(({ theme }) => ({ - flex: '0 1 auto', - overflowX: 'hidden', - // 5px space to the right of the scrollbar - width: 'calc(100% - 5px)', - // Display at least one item in the container - // Question: Can this be a config/theme variable - if so, which? :/ - minHeight: '48px', - overflowY: 'hidden', - - '&:hover': { - overflowY: 'auto', - }, - '&:hover::-webkit-scrollbar': { +const styledScrollbar = (theme: Theme): CreateCSSProperties => ({ + overflowY: 'auto', + '&::-webkit-scrollbar': { backgroundColor: theme.palette.background.default, width: '5px', borderRadius: '5px', }, - '&:hover::-webkit-scrollbar-thumb': { + '&::-webkit-scrollbar-thumb': { backgroundColor: theme.palette.text.hint, borderRadius: '5px', }, -})); +}); + +export const SidebarScrollWrapper = styled('div')(({ theme }) => { + const scrollbarStyles = styledScrollbar(theme); + return { + flex: '0 1 auto', + overflowX: 'hidden', + // 5px space to the right of the scrollbar + width: 'calc(100% - 5px)', + // Display at least one item in the container + // Question: Can this be a config/theme variable - if so, which? :/ + minHeight: '48px', + overflowY: 'hidden', + '@media (hover: none)': scrollbarStyles, + '&:hover': scrollbarStyles, + }; +});