From ec023e8286d7b5f99d0890a2a35d5df1db339914 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 25 Mar 2021 16:01:10 +0000 Subject: [PATCH 01/19] github-deployments plugin Signed-off-by: Andrew Johnson --- packages/app/package.json | 1 + packages/app/src/plugins.ts | 1 + plugins/github-deployments/.eslintrc.js | 3 + plugins/github-deployments/README.md | 75 +++++++++++++++ plugins/github-deployments/dev/index.tsx | 19 ++++ .../docs/github-deployments-card.png | Bin 0 -> 106131 bytes plugins/github-deployments/package.json | 51 ++++++++++ plugins/github-deployments/src/api/index.ts | 89 +++++++++++++++++ .../components/GithubDeploymentsCard.test.tsx | 85 +++++++++++++++++ .../src/components/GithubDeploymentsCard.tsx | 75 +++++++++++++++ .../GithubDeploymentsTable.tsx | 90 ++++++++++++++++++ plugins/github-deployments/src/index.ts | 19 ++++ plugins/github-deployments/src/mocks/mocks.ts | 67 +++++++++++++ plugins/github-deployments/src/plugin.test.ts | 22 +++++ plugins/github-deployments/src/plugin.ts | 45 +++++++++ plugins/github-deployments/src/setupTests.ts | 17 ++++ yarn.lock | 29 ++++++ 17 files changed, 688 insertions(+) create mode 100644 plugins/github-deployments/.eslintrc.js create mode 100644 plugins/github-deployments/README.md create mode 100644 plugins/github-deployments/dev/index.tsx create mode 100644 plugins/github-deployments/docs/github-deployments-card.png create mode 100644 plugins/github-deployments/package.json create mode 100644 plugins/github-deployments/src/api/index.ts create mode 100644 plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx create mode 100644 plugins/github-deployments/src/components/GithubDeploymentsCard.tsx create mode 100644 plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx create mode 100644 plugins/github-deployments/src/index.ts create mode 100644 plugins/github-deployments/src/mocks/mocks.ts create mode 100644 plugins/github-deployments/src/plugin.test.ts create mode 100644 plugins/github-deployments/src/plugin.ts create mode 100644 plugins/github-deployments/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index 341953a204..e5dbbaa5ff 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -19,6 +19,7 @@ "@backstage/plugin-explore": "^0.3.2", "@backstage/plugin-gcp-projects": "^0.2.5", "@backstage/plugin-github-actions": "^0.4.2", + "@backstage/plugin-github-deployments": "^0.1.1", "@backstage/plugin-gitops-profiles": "^0.2.6", "@backstage/plugin-graphiql": "^0.2.9", "@backstage/plugin-jenkins": "^0.4.1", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 13b0daf7e3..d7ce9025ba 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -45,3 +45,4 @@ export { plugin as Org } from '@backstage/plugin-org'; export { plugin as Kafka } from '@backstage/plugin-kafka'; export { todoPlugin } from '@backstage/plugin-todo'; export { badgesPlugin } from '@backstage/plugin-badges'; +export { plugin as githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; diff --git a/plugins/github-deployments/.eslintrc.js b/plugins/github-deployments/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/github-deployments/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md new file mode 100644 index 0000000000..2c4a4cb49a --- /dev/null +++ b/plugins/github-deployments/README.md @@ -0,0 +1,75 @@ +# Github Deployments Plugin + +The Github Deployments Plugin displays recent deployments from Github. + +![github-deployments-card](./docs/github-deployments-card.png) + +## Getting Started + +1. Install the Github Deployments Plugin + +```bash +# packages/app + +yarn add @backstage/plugin-github-deployments +``` + +2. Add proxy and auth token for Github + +```yaml +# app-config.yaml + +proxy: + ... + '/github/api': + target: https://api.github.com + changeOrigin: true + secure: true + headers: + Authorization: + # Content: 'token OAUTH-TOKEN' + $env: GITHUB_OAUTH_TOKEN +``` + +3. Add the plugin to the app + +```typescript +// packages/app/src/plugins.ts + +export { plugin as GithubDeployments } from '@backstage/plugin-github-deployments'; +``` + +4. Add the ... to the EntityPage: + +```typescript +// packages/app/src/components/catalog/EntityPage.tsx + +import { EntityGithubDeploymentsCard } from '@backstage/plugin-github-deployments'; + +const OverviewContent = ({ entity }: { entity: Entity }) => ( + + // ... + + + + // ... + +); +``` + +5. Add the github.com/project-slug annotation to your catalog-info.yaml file: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage + description: | + Backstage is an open-source developer portal that puts the developer experience first. + annotations: + github.com/project-slug: YOUR_PROJECT_SLUG +spec: + type: library + owner: CNCF + lifecycle: experimental +``` diff --git a/plugins/github-deployments/dev/index.tsx b/plugins/github-deployments/dev/index.tsx new file mode 100644 index 0000000000..dbb737c3c8 --- /dev/null +++ b/plugins/github-deployments/dev/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { createDevApp } from '@backstage/dev-utils'; +import { githubDeploymentsPlugin } from '../src/plugin'; + +createDevApp().registerPlugin(githubDeploymentsPlugin).render(); diff --git a/plugins/github-deployments/docs/github-deployments-card.png b/plugins/github-deployments/docs/github-deployments-card.png new file mode 100644 index 0000000000000000000000000000000000000000..f56301c0fbbe7881d7ffef1070c72a198ce3ea38 GIT binary patch literal 106131 zcmeEuWmuG5*ES3x31g zK6xF+){BIMZEPVf4wDrZr-C`ynp#+yAR)=TkJh}UrP@K1q@yBh?hi|QGcB=(8hKYN zgo;WXFFF$Su3RYQ)7Egd+}UwSg0A3#;L6o?=nri?`}~mC-mL6IY8u0>>Q-4bn3#Ee z-bG- za%o7bq)+eB;2i|wWr_+`XPH-%vA-E59q*!=M&T4#;p(mNo$Di=TRAw8Dxxc-rk0i= z_$GF}c~ioDqZ15`ZWg|0l=^;&Q+EB*=(a79LWH_8SxfCe!YQhAKD7_?omcBPNOFYC z#wt9=S%IRBfk!9`K|*gXgS3KJslq=JwY6Iu@l}0fplGoxgO@yPts&T&OsA22`7Oq= z#LGjaiK92#k#D@}w%1vtL7DgWZ&cB#OZALIdvDJSqJvcjs52mb&*UFk?BQ*QS*0<^ zgb$4xmZ!h4xhx9P{hSs)L-C?qhU|X9T>CeNrDw9*p3gtLcw&&J%QDw$Y42S8gjPzG z(yGu;grJ_WC5(oV-Z@S>EslUUAp}NnC%fbR2A-^H+1ZPem#uTM<5k|@nk4dtl!yqW za*xXK;+f?>mO1X(x7yjiI^BEn-2O!{osytHvWW0Ad|Z9VJQA@45)rH^1|=iFi)wAv zdOfh7`@u6m+(jzfAiwV#T!Ih0%_X489dLq6oDUcbQp(;gN&Hxa|Sd0x#P>SMu4ld&4PlD4;?YVyv3v75uKl^&O561NvSSl%=}CV8NKQ zq4SqZ3VND)Gk1ubB}ED&=!O$M9uh zZ%;1g)H^BZ?K&gPw3}&SecHbphNCHhv1<*#<@$-V2;$N}YPyJaDT;@I^1)D028odh zMIq3csQHNH7FVNg5yqzgxgu!Zt@wt=MJSudGXVi`%zJ3`jeWC_O6&sv3A-z&05dpl z4LSqtEQCedy@%NAsN@fy6xcVA1;*vs7g=>PJ8dl11h8pjWow2;FmN`az1Wry<4QfLL6m$Pz=q3=C+a*5uGX0lQD{a$=P~*Z58F4;|%0VP$J}W+# z9h0?4(f#VAik^k8M*oU~C-951oDRAWzZRtxH-Y(u8V zrlh9WrdB)%gl-9iPpj3c)H2nId&C}k95J4UpX++pU3*=TT|GGGJ-oP0f}Vtqi7tuW zb^Dksfy?cc%p)i6P;LS;HqJ>Sx6WN^6ys6ON^E3fPGkB7?$n7O^Tn4fT%Ei*+~?C- z%~^e;{iE$qdfZ~{!}d6h3Gt2jAGvpnEz2!GU3Omf#FxhpfB)mbB21HdfgCz?t+HUF!lzX<@+Q44gBHy;?U>! zG^lgG4E3&;6c@cOel~7Zu~tM{4=%FwMe5+fp_gy zAr8T<@_l!md!9bov#)5>S*I*rD zvET@joJvP}!k+d|!!s8W+TOQq-8`%>_Z+6O7n*lsLZ4w-4Lu3HHWYA)}A5{OqShB4V`lMqm$T zZ{RcoHL^ioE8dRxC!@X8yfXfKe0=(01+Rp~&)mFg3XdMRYwu4ljxf$KGOOBT@@F!s zP7E9ld>dFFm`;;UE0on#BonZhPQxNyCjA(?te|45yTDaAs zGmmj4}HNi$$reERBm+G_yzdKL9Zn( zC6pZk97h&~d$4+S;zu6f5Nf{nq}A=9>j=4wyR<&Dh2Dj_5MvW%5O1^75StL+|3D^( zm9{mQpV9Yan$a;Tx=W14>Nqa+@5OSut2T>&HO z1^(gcVCTYNWgXvw&dgU6=k$r_z#Vh53 zq+|Y@>grk|PtD=fqt5dr@8=h6zLb03jsc#{$5-7~(YG3qm;7gZeq7I;H;x8azZ-o= z=Hql~e_+%$7c7-#?_z8aRVh1^@N?4r<>iO+7`QL`8qul8!%M~rZd%qgU&fUVF5KwAQ625T!)tA|8*PHN!-0OI?s=-mZx1^6( zZ})q@+s~8vIRh_WzR9_fpKcvDDhEq`U95=<{1L8!E{QdF?Uz<^YjTMTS`9X8l02@K z!}7>;;+x`?Z3(A6_AEOJQ7VY1sVB25bA2n;lUi%XeJd}G>pPX5H+@>JLU2D)PWmvo zoo;@Z|M_m4HexLzH%Ut*+jITs&|F!qZ)t|^X820s*a}{8=^8M@E8^;Ny+CQ?tGgNh zYvfAo`o(VF5_^ySw$IW*PQFO-&Gg3C5fodd=`XcY2jN%S-4}-^%IKy(RNh(FtNX=E zs+*Yt0g?d$kLnUcoCq2W#T^}yPaS!Y4zZC)V&~3`zAg9iJ`7q;;(ycN+w{eAF7gz) zA4%ZIK5+utfINDSSj=Yhm-Nvm4nGgi2=06ahuT_hU2eAp>YDrayeu%0veEn^_8S}h zSpxzn=k61JM-$z^?$J|am2l%d9TpF(&cpR*mvqFvVrgghgoiiW@*2Cr-^mUdtJ9~e z0a-_|iI%LXq9PI#c#naEicE%t2HqiqA7Nzjf4`SPWgvko%Ee~uV8+hL$H&Lc@reD=BUW$)tE0P(laU*% zjpKuV+~i;PkuY&IcCfH>vaq$GLfqHrg{`xb5Dg7tqJRJU$2v{iEdFOE8^?b>7I;8* z#5e4mY#i+WzBd>ui1?HrX5nUHr6plu4P*x95a#6Jc`W$HfWLh8KU4l=sJf$xgSf3V z80jSZKT`ka;Q#&Qp9lUi=kx!WllRI0HRb>M=AR=4*%6QazeMp5Isfq~5VSBg_#8WjlN0@>9X(Wbk z;el(<&KF06m2QVVuWip|AH9h76)rBbc*i8OZAa6B6X)8(Y&yj+hjK*>a6bt2cfWWb zs-&>2pY}1ozXVDEK_|GR`rRGCtwrULQJ}2vbw!cU2w=bYh0SnE`n&JIApaPM5Eett zez37X#V?otaG#8#1~pa1p(xi)ez+l03jMW>#BZ*x>-Wn$ zr|GvaOxf}itMGqgGqMp9POko&2O)qN%AoPz#*=vKGGfULhY=HG#S^=>N&o)d1SC5C z)eTRlYAQ!qQK0I`0Tvmb>AE?Zv5m$pGH=&B6R;(k`SqX1$324lIyl(zy}2+r1;5Gl zJ=W4U$t!YJ3U4zkzQ$CDcVGV8d^o6_>@7~u^!wdzI3;wO3nXqT=@X9MVt^5=WnnX@ zq2Ze*IuC!-+8Q5y0R|e$&uC!2<>9r-9`t&|`(HRM({6D@_0`Y)CaFCsyn+ z9JZaO~ChM0KxZu=edA&E9a@0wehvwVm?&%lorMl~JJVwTCKkuG8)UQ@3I@ ze|r!_{q5OyeQ!>DjlNi9FdjRo2@%KyNOV3xfi7Hze)s(S!#h!niu#K}jOj>VtB`*6 z->Y352CzMJP0x=>2!FecBrBT(G>PP8@1nRx&Pc<4mgMI%3$iV z-^vuu4xaeDXiz{>p zbK7maMSf#bIrnzA35&gS^p(%!iDd8Nu@WxZ`m5vJ`4D!~l`zwJ@Jg}##j;?NZo7J; zV4kFe>z4UE)%^vpgC1g{=Py4LI2nK-arc+1GSBtixC(nw0|e_$AhG_F@BLRNGn7eg z^Y1qW|2|mcxPu1b!h|FDl|hosH`@UU0rf7GGDvw@^J;3QRi$aOvxmnzv z)qyE|wHM)wT`}!pS-ICn-duY!EwFBlvw&Vcqe?$8niihq6>aS`Bq zwP8TH4EA}`=X#0V7qd_J!s>5V!JxN#gs(U5AmQh5@&w6qM<)RZKjqXA%V|(~LJYMX5dXT*gf1MvWu3_*pe4dy53g zU?eeIdC#sVh58nN+AO%k&HqG(qH9Pr&Q+hSO(1mXPaSw*w2v5qG% zY7Tm_GLwBgy0*w*=CZ$rfXKd#T_0wO1d8MEJw$|sJzR9N%;0Hv({TH#$zde$sHK8G z=MWrEQL!0nJieO6TbJ9dXJ?|fDXwasjgQo6gyzC3yiuUe{8N2gaE^iSVos9WIgy*Q zz8|0Q&4ZBxF`d5M+J)U_z@C+LjQ#UAk~n~%sxWD`8ApI6uegnjRhfq`w-6}JZlCWp zm*ClAludfJF_~%K2dCw^hqum<8fbyg_F3 z?Lfi(lZk}_H@qKi?%^<`u1T1DmGK^bGZ3uArRmnD9azaIrlaehR}zN=!54<_)}7R{ zZ+_pYE6e=yiz6+FT3tc?x%*Od2+>Rkd!5@5&D{0H4z9pXjpNd#jTs3nE0#RNuSr_u z{3lIZG{qh`Yxn=ir7~|P*Q;yl@ziWt+ge(}_FO!A({^~Q`oQE9r1uLJIR8EdG$cW# za}qTI8;vz{F?|9F;M9zMN8!P6&#nPkvhYog$-I*X%RS%AJ^wwDoKCP5N}KOn6kC63!g=aw zqhB$Y@lw>NgEkgBgtc5N&Me8j>7Y-9669jpyA}0U`K#VPx2{h-Yn4!-A#By=_MvIX z9(%3C%|a)Ws`q9-WjWn6+!BTzI%Pky;DtAwb9Hlwk9zLZHoqme(JHHYajsc^K4dp$ z5~GTFAD9ZGeoX+-AnX-9=q~y}6SAlq zY=Nz<6T6)Cs+HYq4hvy@J9`V{JtdZ&4QXK~gToslC;Ql=36uWKT4r)xvA1@rCK|^! zP}?>K!4!w6h2ZqWPM184I;*;~Wl2d-U0(0BubW_fII+48vi$BXj!fT3N@z4~z}GMA zHYaPG#*;j^Nl9#JG;#P>;dfY1C-|h8W-7)^)N$skk-$GL+N;WeNt}_ zn#%dYpF(lfM3j2_F5O09y^j~){2o{Vj^|d{NYI)({`BQC?>p9t$wztRt9kA~t{>!S7;E_`O&;(Wk|un@oWBW^ZlpPPcsKUPApK&xmeG zB~HyzzAfu(-zAfIOGu_yozGr~!5ebh8vCYW{a^~=KO11UXi3AH+YBtlEq5i;HM!bv zJdfH%u8Ym5u5L0)Z$j@Kt8FeqqfJ96L<23Wif}fT<;EW+%4st-d}l1J^xg5Cnb0az zH@O&87P+4lsc#akPPXw&C}sZyc$<8yM5WyTQKbS{qesgf=|xi>MS;uR<`B8>8Se{g z;df(*`YbG*Ljh*w`Q&sqAcUxO%|pEp$Y}=sIE9jR_u>vrYjcQ#HZ##hJ1b{#nX3vY z4k2rHZusZ5((`Gbi!GG4Ln-r$-Op5nB+2R3Vy!N0SEC>j(L()ldQSswy>lX68p*#_ zV7oF^Fn7(F9Re*ar=M*%rVs4YzShXm&d28#SdKIP7T@v_t|U-vT9nEAv>@&2V9rM0 zj?c;WubD)IOZUv!R#m4Tvrg^NYe}ZGqYD|0KaaH}Ku#&?<l`sY8++Iw-(WhT z=W4xC-OZOuOy1a`sqdb?7FTz<#7^vAo$>yD4P5%}NkJ(`5_IKjyeIm~hYk8hwUEE! zPJW2J=pU~uP(S`4O+lK4u~n|7KK-rMfmX#UA_pq893d%c6szZgnS8lO-|W!EO6*#k zFYRGeWV8LGT}RZv0m-7!G|7FrJe-wHLf*Ko*<4+gBG&e#m=YOnI==5~{MI5tzy84IVWxOz4g1IMb99(%~-0-29cv!1;{ajphj$Y(U_Kw8H!$=9kh^;8Pu;6{Ao3<>}`r~m+@(azQmw6)bX>G;{ ziAr6{zUPBdeG!#5g(y%|e;mHW$W98YJ?{+)dgUZtuVI<7mw$tE&{>K4mkL{k_%b%d7p3-jGMg!lyn7I*-q)3fdQ73&e4S^mKfHTZSr`Di)XQpeQ z&=0b*w_$MOu~cw9drJf(Gr>X5<$Ax1-5~VhL&jJq+vp3LMpTt*ju_DbH8g@Beah1( zHdy(MeZX;VuBT!$-1AsXktF-P`Bor7X4X$lWJ$~;CBkd*0m4iSgs2}G1~KpHCR@G%gKnAWnYpx|Qh9j+P*96oUU4E?SMLy|WDEYd#q>OP%oU>*V$AYTKPQAyy*qCuHqfn#4;6?cpLpFV!<4ka zg3<6(E?(n2R95B4wKaYQsA+V5RW1Ne6EnVj=4MHAoYx&9Y^$eo6d;cS8O#to6O3@Q@T;$u8s^bq_o{JvB=EiZ>jn$p`z3xrS7 z-KO`$g?O^RUw`6l5N-C#aJW#{FTeb9(niONF7E!kVDNDENA%wL*u(TY?V3DwG9D~T zq657GJNGJ++M6n|Lv=ndU-sBj3^ZvG9}1@2bN=n{AFBiIH-lNXW_ZDk>j{ zLOyw3DlS2Nlj)Pa9Bg6+|AHzSLjLf!?7nARH1uBEx>4&v1e~hXztmULkpuUA5l!SKh7?$F)`vgHMlc`xaGOB_qu8{VN2+VC|*Ird5}GkhL%y_W(sEp@(P#Z)WRuzI_+VQBqXj#!~z4ssc$2mWPm^mEhC z=`yO(o(2PDoYUfGj%_Zh7LB#+#Bb)cr44(({v}(%*D(}BC-|}C4U!`DB=-Toex~p0 zV{Qv!%m?u!`5^o|dCOc_>uHXZ+kty?-tK~P6{*R*vv^n_oB;wsn7+|XJ^V(JZ!2_HMV>R^s;9F@_k(hb2E~LLDQU5$%B;1Zg4FnuIUy|?@s4BlOAHq4do+-6i{2A$1xaW^a z3WG~>v6|#Uqhqk+#&RO-y;=!1t;$A&&eRF{5#qc9gR6Y%tjXzohLgvX4n4?PO>5#} zU0{Wa=2~j=i=3~fuGxgUDdnD*4QpGrIMJyk_H8z?(j9ZbVXg%vqOs7EQZEp$uxLkS z4!S9Qx!0YQR>)kct9 z0+~akubScoK$dP@I@5QEIs>1!sfU(waW6$q^0$wpqu7Y#~V*MUwgnt95DSX|2Sn1l<~U%n#}_@O_9`>*X=KNT zt{B8+tq$zQw_s4JAZ@@-1ewiBpcKjzH%pA!i8A91CDR~R9F_aN@VR8)^iKP zzsGv#LH&w2)*3sFr&_{*fT&uDeCtCkKQbS-(KJY{*J~gbO`a>M{t18=^?%RIL5%_} zCCDd-$LNk`CV9N-QkK-C)tc&5XC2>QI&Ay;4$|%la=~Bs<$k_O9=@phDBu1FxN!Be zOUHI`TK>i?-INz!L^6KH_@EPHZR%T>4SoCmL-ZFo@vg@DdgDo{RQ)zb6zX=jyvNy5>-e+>xjyA$&D9w>*$z&@-&o_twC=FfP9uX`{ zo_C;hp|Fl>N07DH(d1;xl^uPqr)T4(wg_-B+gw351)n@${W=ot$e&r`)|QNA)x zCk5vnP3I(;k&>TMoOdNy))*-Z2+_INj zbsKs+!SPmtDtn4(ZUw*42xLB}#uDYRnThyg9A_4A42usT?JxN?xt&`%C}2-sC*NFV zZtn&+hB_|1EgJY&DsD1DsVM4ypuCftQ*7|f_HQ6R{ng?1#Rzjtp zw)zyLSvY1Sx{S~3t~eZzr03>fF_c1MSgKdQK4{Afx)@XPbGf-X!6mn;G}299EB3li zrkvcflI&aOP~(3{C37axJQt=ZbgQ%-|lW5u_WU0U#WZXxA#SUhnA#n&`Ak&#Lx$Tv-C{!Da}RbEGb?Xs{sF(_DK1DVc{^;hi)r6OZ{9L9GMweEl~Wg;16c${ z=uN-P+&%jOhd`oFdQAQmRo^d~Y@7X=GMT=F@rz6oJq#okI7t_%RtmGyCXJ z$njas)!Ufh4L%=ef<*~V(rmgOL8*$X~4r^&Nz2FnzSJ*H@Rp6%X0A zk=IMlKn~<8$=fP@X)39GSn#crY0TPdRh$x<_Cr>F$goAN=p$~uP0^^6fai9V8P$n* z0f#x8g_x8M*n?5vgdl{c>Lx3Rt~e(%zP`qfQBZ6FKpZx5<<@jO2<^=AYuc)qp;&e9 zw@IOOQ>V2Z|CqexOa?QI+?$NUW3M}Xu`~*;x-unSLhf2{7}58%UOK&ntyDYd(A6qQ z9ubP`PoHdoZjrH~SQSa1?5=JCgFqM7O87941KsLkK`>MVuz8{pB}iyuhL{Ji+`*ov z}`~kG|3f$K?1y@>2z5>(vNF=e;qg?0$v-^?=kO z#Q;FM(qr(!TV;_er@_)3VqgPbs0L>{RXLnzJ59Hd+up+nUW6hK01Sn>Kjbw9-cpkr zT^e;RQ@~vq2{;{3p$us&iG8;gW?1Sk_gh8KhY+CPpyanc_R87U<4Hx# zp+FN_@s$NR`(Kshz^W8!T{8oJJw)KxOYlB?KHyf%)2DRL(tBcEZK;qni3T zmj*iFzQt@Bc;t~cWh43mo~ui`H)DbN@iKjJy4nv+N5ULA4!XH+C1fP~oZpFG5!83= z;qmuR?F!VrIULU-7j*mQ=4XKpVEaC5IZe@&>ngs%yLr{%Q|*F%(w) znzSwR)jw;$QB~-&b|_GNKVXv*y<*>fUvn(@^OnM*9|WaUAH;#rdtJ1@a%FL3XXOky zJCVUEH@|fS6UdF-m!lha@Y+wTpr0vGO~yjf*`~@RTxrk>5-JlJja@3gJqg z+KkO+pI;N?nv zuT3YI>9Fzc45?`xwawQ(nLxql8E4k3je*3X6o5Xfg?{j?Q_mq|a<`yOW-f)&-&bLm zD}+F8JqF{0z0MZp3YkMM8cuPm8~`mV5G2fAEiC&-KywPpJasLmoJVe#GO!af)tw2* z$m@xws9)byCRP{AyD6jhDaj+mMe$f!=tyn$U@V8e71({sUVuYe^`i{-Jf6sNAVDM4;(vKnDptsph)ytyL)art z;r`NumW{O|;>moF)fduX`gFYHF#S&&$Y=v&3Q2-|7aE{aW@}jUrj`9%J%gKb%{%6~ZkU1p9ffDyF$phvyHYndwWfB{#r6Xb$>SgG4K5(QDzuyVOJS1!_ljt8{SD*|!S?9_$0=MmhzhCX=`xG8`l} z1sr&S$Vn~(9sT&h-nN;g_-Td$0f6V|+um$eG6SMEsmu}if_~K?%NtOsB`0|h;AA%$ zp9HGXUwH|UOLeLJS7d3J!RZF z5CITtVn3ef?Hv^3u{Po-LJmX(YIA-lI9XGAOzIu(%B?*4+h(AgGrqLYZG!^MQwB%4 zdmB_XIcB^L0tq#=s1XGp`@u2kHuWbrmj@*W)t{fFL!fG%6;V6M6ukG0*}`zzrEM|0 zDFE_EV@+TKqDoy8GEOGWZ0?oG7P!`dF+ z8Np597X*8VAQS*OG6?Rw>woJthSu=Wn{nTpt7Z^v#7ybC)8w~XNKPwJ3lXkls;?cN zH@lpX)BgC#j0li^GC$yDS6Ue5owRE{{kB&8s%T?xcWl#D)Cd4W%x!xXkZkwjlW9+4 zU-H8@RiJVUsjYTcD)_$5w0z(W;+vIVNtxEN>+_9U=y8Qmc=VA zyKQ-W988W`-EnId@NjvHD5rk?nlC6N4lr6ofq^QkUx^eKgj<=iV`8`wHqhu)lflYF zB}9(4yil$!q>`=3eCc3$rm->9lYAFB5Fr2GW#-f?DAxqhAGMVc2Xe~GxJNUqBcr{> zb0*xYvQ8gdZ+pM`7~XK5VAq6+sJF|`r8}=;=Yh9=eCz*~&pFS<nIH_F-7l34c>J+jaF!3t*3wG4Z zZYWC4*RECrqdovNC6UkLQ>tJqyHjX|^A!dMHvAyQ(8#`|PsI71ZWs(j5v8w9`d5^& z3QFBFUNCEdICT=3Ps0l4^qu7sC z12~)m2h*Q=K(35w3H-t3$HQ2k({^cIrsz>+SDQz`xmO}AABE}y>NEvOT*_( z#u|FgL!wIS)nf7s6Snn2UK=_yugDIc@HUu;K%krCe675IoIx{$mi@@Vqz08>zV3rg z(k#;1_gpAS*drxm)++mgF6LFNq4-(_L#@%ZkF$W2wbN^R5v{2e3!7G)aJ_qm z+g^1t7h(f6*=-`85(JF5_=b~v_*cP7M%j`J|3GY=gJJ)bQqMbg|F0n*Xu0~|s+oyGTqUTS|v(f$}*3Ya)j%&X^s2ab=70(F;im>HA@)S4tFkM(}6 z*S~mxetL6qIN7ka0W{u-np7~HjBi~8jiE7JWSdRXA}4_X7-m=S;}*geQwo|bL2 zIZ?L`fRqvsK>)c#PXw9X-|TZNSW5L9M;ns$e2|A3rhCylh+0VIzTdiG5q_Pu!F%&Px(~0|89%^xn^SqLs28_J#Wk3 z&x9@sW`ZX$o~ZtQGdaQO<9K^oSN1o`@Xwh7KudrTtoPGJ_P@pTXVw0%+WdYq|Eo6t zRhvJZ7moiwtqptyIS^47goRK22j3v-{}>Iykmfl0rTQ*A<4evSdqHQZR>l`@ivi}n zR^~$PB=n?qUqt5|h~1+9KJ@s9?RP`Qq zFPr>7_|N|?<~Jh#?>7BTum0akX3*YMLY}ou&FzoY0YI_Y9gt@a1bkzf%cDZy+qfc^ zyOk4~u0YG_k{SSvMZOI9w}?tyAV3Ly=Q`+M^@S9_ooxFY!0aUg_8q>uNm z0gvY`U}w`^ZrS>7c*N^%1u_2tA%%J(Z<8X7&z{bk3-l8TpAbY}z#JeKQ=DAv-~a@& zg&-~x8O?{~^0ZxKdp#88I0e@`t9ZMj?kK;g<*q!pJ_4mM0_7!ErgSp=D0&2=x^nev z$VWzM1POT#5Lg^f2V(1fabMvlpGs5$zFK@nI(rtp9$;Mv_VZ9&_w^;{&A>(!^F{%_ zN;Y_gTmlegUj0JQ7%(->1E7Jb;e*t`{ThGaTtY`d~r@4A(*+qp|-M0ko%q0DpG86p(xfXLzz3I-&sygLk)f19X^s+8hU2AQ+E( z-gbaJge}M(3vkRTbG2q$e?aRi75u(;4s_MG*dPqEP{>F`>B-!i{l$!t+`Mmcd-rQB z=5yH*b*p({{LF*+wDpPxfYXyAij$S*$9wPq93}|e(q{w@yczUK48>h`DEmBO0X4y} z5d^90bWPb;TP?vGAomsuJIfu6nF)7Kg^UnhY8};K*f zZ9R2HUxcud#rIx?TXX}?1fW)k?0e@u6v6YLSBm9uScFC4)wu~gBISnn>-L_%=W zSf<#0p$x&~5md9vmHmhXfL`1r=;XPKPkZj@aeoZMvKP78kFB31*iEt5F5I?6AkL>h zq=oO!6r{F;;(A#Y{mXQha&bMP;h(FZ`KWkLTzVegbq>J>0Kym;dcJ}iCKsnwceQ29 zi$)OVbH0}O4x9&ewt>SRJ)34C7taSv6?vyh&2LM#V&})b-~+%n%0wX((8!A>K!KRx zj`udE%;r{Rzw{ad#jnZIy3m0*iRr>2da`9>@)GRZhY_t z`{C%+aQ+;N;|QO_)H}$5&cn|$*E~i`#n#YRiz8iFi@Cw4K6}V5G^(W9VR`;Clf^Tie-W z^LW1P`;D}SUv~g$zHc9uKzP?5ms_LfA8LsiM%slOSn89vO`$oh;iY(=_$1xIuih)- zebRk0-D3j?Z3dDD`;MipdzTLAC5N+p80c{^-;`{=Rd=C9 zkcH0yhh;{69#InY?_2`37smeTGF<@JMA0kblpa3!y*eV##e1JVkejm`@VYZy%Uo8% zgfCs!Vj8r|c@-j>$Lv&@iD}@a;fUtMu98;Ow}?4BSc~$^<{4R-h!}Pn57E zCLdS&PL%%!5V4Hm?{K1JYx90Z>=c9kyO*AF{lOyEOTW5KwxvapHNCpfc(qYhJeXme zQXx9q0SKZsd8dLd)9%*L@<=2uJZ*;eNO$#i7S`E zwf?}5BN`xy9ux)?fy%6B$nBB^x?uAR4U9eD`z~j5pUOAoRx0petIJ$58!31*u?NvE z_d^FaAW&3mheir*UIme>LwkJ53NeWGQWIbG<$hOJly`nZ14iV2NPR$Qzu2cfp4#Nf z@Spn*0kPVfexT&X@s(wXSyK7TP;I+YFtpUH2p<4^)yK; z81@>1dcRUBeWqlLm@MX#Ct*LAE5Jx8??f*9G`hOgDL?Bd0!|nhT%*5{x}6JG}onmX1QD>YW2GdF`f=SwD?`PY~2)GMDa4i2X+&WUgB5Aqp)iv1SZr5L4|3JGu+d3&L^v>VCXFpr0IO~a0 zZ#$Z-dj{g6=aVa+N4hSw3(SybRJD)1j6_zw9Y8130r!MB^%TzSJ6W;0uUh~hpZmIL+gK%(P4f0I(V+^Rt_;z2y=FsnA@qRsgO`3^NNs2o~v$s&6 zsmSpnqqR9?ksZD>X4FJ#J(ROgUWH0m)Ry)~&63ZYwgts#>PE_TH3vRoUCfa8tpS}H zTJQ9U)vg^;l)69{i`6?_=SwYSVr{P_nn4y|rc&bjwDd&%Pc9LiDwRfUSNT{BEhe22 zPLvAm8gSZUK%Z0B?;e3-*EGD!%^z0te=3PbnhnK3I=`jE?#pH!9~qMC;ya zCeny@L6wEkJ(4=#Se@Btn2oNTPF`qCjGYTwKjY&ls*CNmj0H>LZR_}yUJ2fHqC>OL z9uwbTA!B~_o&Zor{7tly+Ju(A0mnqRfD)Z~Pz^{&**FfjHM87)gwYetluY@F>DFCp z)x~loOR1_wYJq&(-v;&k4vgW1O{u=#krG~z%9O5j=E-pcTPhvBM?WI&BcE1}rLBjX zo%$Tm0>~6nufm0B0X@{yQV^*Pjh4qs8zZO1)07X6=h>Lvg--xaVX4g;^yKg2SjH6+ z$?5#o#YQgtB{b?olG$)at%1AF)Z~Z*av*|LworU~djZm|hlgnR+&Fu|;;jW4v=u29t<<*$)c%%P>M{dooyWC_{X!-EYmkh)c6sk&8Go?b>LhHFoFgN^8d<0wmYPyqKlnfF zy=7FDQM)xP9f~vv(gwZGW@eG6Ptq zgSz_CN8S87kGf*M)K-sAB<|1LNcEX^yNnsINNjw4$Z`A2QI*Nur8AWo+gw;zu3x?C^puc+Pk?hnqlHp+NtS6 z_W<54e%9nV&NdQ?$m zy`3hE)(sor=>fN}q1Yf|??m?iOB@$nUqij0au8IZ=6OB!0b!tEL*7S0?T2bCs)o4w zG{{>Wfz+pWiJ7Wowg|bhGR~o1xcG6B(qbGlsL6Jay+P$t$hIw90R_^V&pRdx@FT|A zSZBdfM2SWQjp_uV$(^Hi7v+RP?@1Vm6bku~k4%qNT+D=P!KU>(fhA^7{K4xUuS`*Q z%@WnJY1eX!<|h+EVKh?Zc59Ju3|X0e*QTEzfV#L_N<>{dwL-S znIq?m$AcS{6JusT5%hq!GCY)qZ)QOkgP_Rc z;64m{qg4Uu=x|9hy>ZI1dr_UOnc_lj_SRPii>=&C(}2A+>)^dpBthEqp)e&#Ca?y; zW;4=_^7!_4(2KQSk-oDAZ`E+5e1VOEi}j zbwq)yg7izQM+Kxq$Fsy3RGubdXlw`kD5gB@$v_ab1af@l*hd#k`!>U*cx{PYSx?c4 z*7T0l13w^dbZ=hia%_w~DgOGL#T94Mp&2M=3PoFIw-p{M+RZ8Kr6RQ1DjdxcGfVl+ zfA?C<0LM(lPg@lC49#Dd%i;2}Wc}n`cwQl@E*{7X`IUA4rO4_Y;Y!ULwxC9(GC6My zo@bJ5@GNvE?@O+k+8^M`<-5MDSy^t{q7|gZjDLRx!>m*jmZWJl8k}NXT}P;v(?K9| zY0A7+%7Od*#b}VNu*jq-7bt22U^MWEZgdbZCq1}k%=8-*BABL8oPJvS-%bWqlaVd9 z88mWo$v$yoE-Eajv41cM0vugqa=pMzQ>*YZd(n%ib7}I4 z<4n3_?bd!Dptt)d5gXPliPBW}!JvgXcHADpUzYJMejYtS>*l)TOSS&YmqG>XqLf)W z%@?q@@*oKDzcVE3(y7r>-Y142I3d+WV6pBc_OjCGH?R0#cMlPhYH^rWCr(guwdx?D zIk##^4B^J}raTuo-yz1oZ_PeVv05k3j$1{M!Y)QIrp$UWu}lJza6?ow2uCZHqf@lu zAD|{v^OCo%)}3Wq4(`kmuj`E7$$0b?@X;RHe78&m6AVt;7h-1M*E)#4G>!I3^7ntl zSPqcX0yF$Y{4&zriy65CA-9QKMV>L%fAm>Xuwrs6P2(V3JXE-&5$`pPxEXzZ=nk9guW?xNMF z^s}M}3%JvO9D(I?v2o7Kqm@*$@>D9v{lsI@2gi@F7bA=FU--EE5{kYje9oi*geKFu zTz2!ppIB~Rc`cmMF|A7NcAk2WTzB2p?oynAyr`{#1y}f&`175D1oFrvu}|MuTNDjV zOh3qwU`5%dk$6LXJs!>zX930QDXVoa{kg$4FwntOT)Harrr|RMQ&vo1N@{%r^A!1( za!VFrXKHBiT(L zOZs$znr;K_Nh~D;7`x}1D%H}ge)?C=uYNM-IPMgtyaSLQ5bNFlAvnMnfzDAz#h*ig zYesA-FgzrFH!xCJ|B)O`w8c0}CdrYt3<17`|EZewDR7(-Fl-H*cg9yzBz2_gyOfE%_eR4SoO2$_O<`fN=Hh{m$(do}b((cXG_#$SPp+JvYE6C+ zEv)=SpDgdgw!xqj%c*j%9{7rYX1irra1WKmR3GY3sPr6ow4juHC{-v*x;hokC)H)M zF2tD_xP+3GM}L*AbiG=(%Ll3z^7!AvgRQC$e)o9oJi>M&k3|*P8CEX)$a^n~gE0H# zTNu@=eXY)Jx4#7|L1LP2B;x1Q1?)HLbt+UY){h$BIkVeCFZ{lGd_G-HFj92^ShQFo ztBMt|;+}2~U;cYtwq6PsCH0*#@IhUbs?~88^7$B7uu4p5_N8y^rS5FnIFk<<5GU0y zb#N_-6bG#D$%)YxHuPX?Pie>JF|2wA>^(?nLaFLJpK}g=6jxW=!Eu76BRZp5u`3; z-tikwP1xTfZB8l9V4G)){rP&}SwtMG=%eSyr`&B`dEg$-$O&q)F z!Y5>=Y22nf>lF?}Emb+x-;1ptpeAr5#?#LHS2}EywWDU7%GUBC_jmG5bN$13XRYxd zlnb;Ry=I6wV(KOe+4()Og=%PpiKocV96Qj;GfNWfRU%vui35}%Pvzi_q~!ldP*Cd^2qX{A){gIwgoBO;SaDdV9X z_aVL+qO#=kS_<(wgarqGh;A{SSyhIsc#}H{Hlo<`94Ol#^P_qDIc8X&H$3_YxZ$Hi zmQf>kBAnx$mh`f4Ko4fE^Q6ybdQh>x9Yh)_LX;O0i(SX`HAS=s@`LjR_z`)^xNQ6?mpN4w)YL{oL921*jnV-7p`g!UyHf-QR82^j>yC$rwQu$|uU9%fi0Yr2+&|z$RwuN=b?>uf14{YTKR3K{=$D)*J26C zOv@oUVfdau+8+5FkH@_;!ERT504cB>+X7%M6`D7|MxB`4(5oGB7|)#bFZ|OvCRp12 zo8R~j^JV$mccZmd5ZKfVB%N65hEMb&a(|T5@y6t9wfVkhes8lTR{2^T2&}e@h!roX z-b>gGv}R{*igh;^?-LP`xZw*f5CC07< zNG2XR<=y`U&!SSQ{)LuJr2yxA?A5feM$rs-Qn9I1_rO3hzhkX^FU#tI;T*10R)s*b3W@U*WroH|Ee}7XAO=IGW3{rj91VdfL%7pzdSEmcw+hLOKMCJAmFojcNRz_y0+s$3dlvX z`;eQEc$-8|WZFs-UsgGagj??q>>;L9M(sI+^5f{oBHAV&u{GYp}K zfO!l=(Wrn)teoDC8w{WmX^;V1=C7Va+W;Nf?KsN_=2gu=3*m zI;Ixi{EnM=;BvR`imIZLSBgsH$O}U0ZzZQxItQ-|+j^A&GtbTXG)q4C*QD(jiqLPo zuJm`PTme+EWlO!s)iulDM*isK(;>UgyA&VCr$c%7{sQt8K-@3Ro1XPRD`yL6Yy;~= zP|-`V*DWl~> zJV`Dj#cX}zT$m7m%sv{XR>c3cQCTDKX%@1%Fn|KGoxi@fu70EzMj*<97h#ZcdM`3G zBh^4|a0asJlDT%@=_)iHsinB8KgpdHXbj@%`AGCHPCk`ZE%EB6*E>h-)m<+;c~Oz! zrD=Is(-u8%eP*i0KMefBkbb&;Pj-%+cgAM$-yRL7g;_-|tbO1-1+`&SU8~$s@`V2T z^UAv&J?Mzs8M_Y8r0Swhhbc0lE$p7vrKotf*i37k2l5Obi}Sdo)Fb`V-p;dx!}y)^ zx#zK2&NphRj{_Z}pLAQKxz`!Y#iJ8BpTin&PXz_REH1DfO8Lefdy1kH`f8Wk#4=WcMgVseefR$oN!+^&B%7329EC6uW(AoJgl> zc>{oBs|{pGJUs!*z33x8wm>c9 zf>vBcXP%X|@|&4bdjg0eA4;_xbIy*yy3x*O`;01IH-P%fs+7jm%Uk1DeniKsVj}ub zyd+~Si^5Q9MfkRF3rJc&jwxh^IX3~WxQQXhM>rRK<0}`JP26{2??%AeWuPTprq`L6 zD1BWW2Zq&02XR`K%VKdzn|yNnDo@`GsFg!evHK_~F|1)eTr)p(2a+I8-oAE48rinQ zVszs)XdMI1r;hN5tGXmjrbEh<=vV`OcOYC8=QymxTclZfEW$sl+J^e6?P|B%tl6OH z`dERrLd$?@!n{##v^4Zf{>`?D59%B+q!?V$ZR{cNt;C-0g8$9RW$DEW@k*XGRThT2 zCi@ykGcP`hbByMjwz^Uua*N3Nypt1U7aV~)&QuVsuA6-5XBWFi-VxqyWS?;YDh^G| z;@_{DmJQ$o$pF3+L}jp&`AD*yuuNDTWLt+6YA`Rh{3#=AI6%^y1R?lYSEnIWTb-xa zbKU)&%GK8BUep>n;qJ7jovFdIGXzgtV^a-R zycMfmZXA2rAk@uF>XYb>oOs~7bc9dc)iW7YksjGq4=k$1t?kYYFsPK^yFia z$`qh&JgHKxc68`TyJejz8+#tX8@MCh3#Xa^W?p(12pEE&f2~@$2UVo)g-;^-OOQK! zjjv|VCl;Nlo}tgC-Nl*ESbpf{7nPaz%p`KK2#0u$mzo&iQ<^7@5vz6Jm|Il$SS=?g z!n`DmBL%SW2gjOuDqO?&Gz6q%)S!rs>ikt))2W5*6}3NGVJNnJ5Hi%TV8VQ2YjJcZ zQ|+L6<_$o|@s;7|Y8*hR>AWtH*c$~hH?`$Y%`-6eS5QM+5RjCA}#^=)aqVv~fz7uTB%_hq35 zzczyjMWhrJ?8`b;pmD?(Aseuc8n-`2`^b3)=R9GPGJ+x#{j<`Fs{TS#8cma_uyXJZ zD5>ZbAS(xV@7tvZAc``4Yx???cFY)mANJ&REJ7!ia$>?0orAr?ZuT!UjwKHGEwp^9 z8{QI3L3ct-cD)o$!1O`2S_j3+o~D*;Q5%_P-FNygFX9B87bhVHkC#ihwIsw!^#l^Fa_B}5j3*?;f z5GtN)Lp?A-CKrAS11S|8US>2AoIs=Nr4&{Q&lEiZkh!bVu$eh$Sv|1*m;)BS0u(^p zDs0aJ(y`?|g!9$J@fVs~ys_ibap3uwBxjxWP+#I`z0}|W=^HMd?0bhI1LuAQnQpDh z$55b+U@3_a`8^ABV)Fb0U{=>H`akyi9kQg|vv~pbz{Uvqx;tQc!M=e~^>KHAWN;@F zXuNUynyy&wO|E|Tx0GUFx?5*1nB~rLHfY9e0C+4iRrR~AslzFr8k~iO(G%J#Y!kyi zwFZ@Vn2elu&9{rrPUIE-eYgWDQ_i{E(b`X=8i3uw;Pcj_0qf*>!xcE9x}<`(&0fD> zzwnMB#h?aWYLJegCjrFRBQp;JnOML&aWj4Z zNFAOyqb*A4q!4Pu1bX^T4SWlgu=t)TN;BNta|w*)0z(Vx*#lImdFHx1CPUu96KCd? zbr?`S`!xf~82LJ5*DWi5R36e47E=bErH-io>6G1*9S3T1Km3bu^HFdu8~lKL4lIAd z-mm5(>jCIRFZn7QTn1)YP`4gz>8;TzJq$svrd|=~gQWngpbhcELzl)`i)3Uw6TF=H92+2uM^%bzyyP z0C^!A>Eh!vU=D^Vv90m3z751XSuJFgO}v|*c&Cn{)W%+lCXfqldy^bv)oxkgq)a>M z${@S(Sy3pB)jT_YW{T*R_rPzn7cT|4DyDpT&q}E-b7czgOQM}ccy(hrx?Hadq1uMA z*vatbSoT)dacYzAk)fw_cAwtdgf*5UXkFr;&>EkO)du(=od=$_&>zbD3ef&jaPtJs zKqxfApXEmhBu}#4_cmWL%c$wJFAuq}6y(kW0N$ieV%cGU{A4vtaB>JMBn5T`x zZzm>&ogTw`{ef7Y+F$G_J@{0vNo#^l>WY3h+=>_axgG9f&<<)$&zL^b{%OQWI4&}{ zxRpYwU<|G|Zhqel=*SRI>Ds&!!OKBANS$B7vl^;Lnf2C&XKIZA@WL$C^Elr2>;%e2?JN z%sKK$@8$H>rw+E>8R0grDf+9lWgHh~y@iT#&o9*vB8ZW#1grCMWs2ml9haB_zQz`~ zI}_{dqeKg1YN;`g+=fu^0i7){TrGtz(d|i0VcF#hFq*9C_DdWwl(-UV&S#{ zXknZ6)8qdldHmO}m^??&vU|NwA^+w*z$^dfVg50k{~EM^{r7)H=3hhcpCa)8*vOm; z=Q!S7Zk@ZmUupz5ez4^J+kce20s)2ZnUcua3*o->;rWqTpcp=Aj}&_wSY|z@#_;TWqV~joN<=vHuV?`~YOUr@mX0^#5LB zk+TJoNN<&0^#2zD-TyqyzZv5HnWlfYl>cXB{x6Qqo!IYT6qShMoAr_XC8yJe*kQaf z_(bq(6-lLWtDrq7uH9TT`X*EH)_L@KE z=NyOJLJciCq`!{+M5*6+SS)*4ZniWFo!n$0BVmf)L&ggbPaqUmvvMv_!u;nq?xE7$ z`!8Qw`J>_yrL9;F5KAC_>0hr2rBgGo=+`zJDrK#(y5zU$f1MlC_2qwi0iNz5CJm%q zj=CQ(Ty0u?&Q-F<&fCsK()nz)y;J%BzVg>T!)TkIkL^rlX8S$A1|1!pnLX2HkHa^? z=r}cjC>Zm3GLb)lFHl`LA=CU?cckt2iCRllRu-1hYS9(!CA{ycJiP-01Ld|lTM(CL zE}KfWV|k%&d(%Ulwkm zl>bjS)MJ6suMUuFtGhCecFb8>>UXnRbZged73=3l%S$^Wl80*Y7>xBzHy^8n6yFND z4!!=|KGF1K;k?4nyv{BMqW_kmkj>5S-S z98o}s{|WeKp9cmM7C`p_+sDK1i22*&LqZKb+g|qqtyApQqxr$;c3@CP<;1}{9wUCP ztZ1V5-uzk7AqDH9{u^R*V*lguq{8VpZ-n-qRiuA5((=IYG#vi9neI^kf4_++*YZh%zjJDs8`QKzvg1!@X zNLvV7%xs(4s#nu6<;Oli)bgdu5vWztx(VTL8}@6Eae8m5Nehk}rm zH)IPjqE}Yepoq^sU5yw76GOt{#REG<_jxuqat@)6Qf>VsIgdiL(AU@g+MnTA96q!R zojoHuDmQDfcNQ}DMPnRnemPI&K;O6>r`bl1!1iuV@fE*c zsQN}4S1#_DD%LMS_Ug9>;>Sq6KhQPDoFifoTX_|@a`6kC?3poTI0m%>71G&U;dHei zMrI^xDB!@COdmZ!gngALST4G;qLheX|F)_HBFI8{H8R(Db}`yk)>4+UdGEAxdh1qn z42t954*V6pnObVlPY;0&+UFG@IP%(JWoPHZ!bNb1ZyT*AU~?f@gm%Dd0A}~MDFWrv zDX{uZ%{Yg{OZrAYr-30$a%tS02Z49D*Vj0X{EyoU&|PfBg{A<%_IgRTlj&)k3N z3IS$G-o&&gC)XSoy&5#Yu;D+<&g7dFYXV64l9 zklE+;x)1oT)(5MNZu-Rx>jMe%BMB-u)IT%(Mn{auE`%+$h$+T$*1;jSkf zE4`@Fe%MF*bVx|FmTu0z80xV!U(U-=l@it75wWTKT{yV~79sLy<2lhw)9JUZeenV& zLmEeuOzA(HXTA>zAwu~Qivl=R)Pvf>2K7cgOTIfWZ9F;eo0mJEQMSg!R(_Gt1SZ2x z`JTip!?(yE*6AVACcj7!J6s^95}9bXCBvk-w>j$r3Xv)fsgoG>jrO}+KojFls%2$n zu10zI{OjgWdWuUs;B1Vx0kz#V1P1n;#Vp@(0aMd@U}ZK7G&LL40X8nb5q^>GfY^6I z1`t%8m(`{+cJ-5a8ddpZ*!rwo5_v(=nC41s28~bm+Vs7EhsnobpyfEI0UXJx*!yJI z;5!)zI}w3%a}>lDbe1GuBlWgyQS|HC7YB~p~u)k=7h=I zPUp8I{P$KN#Ua5H(Kb#q9|_WoiFW4}7o{1so1OgRkH1~79H&%xi^;v4jXAQeLKt8$ z?(gF$3i>bMD(wxg2RV#FAYOYOJIdbq-%-sW@&?R)m#^R6lA-7#BKsFRO%6eJ!-{_k z4jFgXO8c&8eF=_4Zb>*dH3)kSEU=3!UR9gXL%IU05NAbBIMyTs53PS+j|IyU0<7%J z9JbKER`f)>e&d@R^5tTO1yxv+O7N(>K|3Ny zI=8vPbaga7(P`WnScESZ>g|=+dJsENd2d*x>*sS0%OX_!b)%-iF#DAr;gtGthhsj zc#>b@mhkjtp0haM2DTUic@FIGmilHDqwR^>jW7s2K}{3^EGz;d8y+meh~g`tQ?z({ zmB1?Up_`l@V%5BId*A^hkOfBc$(g`|Q!mEk?n>)GKJQ&uv&2?!@@gwY)%Pfa+F0O* zVCG|E3WG&#^;&p?<#&9NN#k7aU`pTM&G-fm>@ptXJ*PLRLKeoQ2d%MCd-vBYE#h>t z(ho+dpy^GF6RqdY$|C0_SvI|i|XZ+y<&)8qYmd&5aX)kRFRsJ}& z)fV14+&TLahzMkRTADvY0A7YGQrxbQwpYOiQ~0+h2L}dxtr^UBceANh;mv?v9K+zY=ZuI53-KJ}`!>1ER<|78aJyQSa8$ zuClB^*{DaafY)H4*zb4-MZ4cW=h{xoHg!0|4PbjSwUNG-e%HUx7>uVpxpUXf=h{xgvlH8^-n>LBRKjkCIf)Nw06H` z<+jq}cdt|F?^=atUHS6sE{N`8clV8z-A~OKG;g=4f%*TPUkZnz(nC4{-!&Mg_HBT* zaT<5;PSpd%nzqK|eIi6l(Zr>Py{lLS1XZ)(c(KJ5y~ugnI2tVT{voArz2ui4>2Cr6 z_J;H4r88)5y8!HnC{^gU%KSYdA|r2c+@MhCv{N)Ok^Af`3{Gt;e{)@FYO78psV4fQ zSL4CTPfA?hSGxBNaWU)x;&-id!w* ziZYcBL97#6q9V^aDQgFlwVR>|u`*t$#Z}=oj9xTZjahQku0=!*B`w^{4{>!RIX18G z+50np9tx+^u2{)?9vo$;_xHRX#tPxiY)-iYy3^gvVH^Qw^PRgNxNm=Tl+696=Cet} z<Gj9ZhYOes(l`{ILjYtXA&y{+NQeQ>s)jgSbw zL)J4NVANLSrxZ%}yFHuvjcAlBs`hN*x53^MB?zI=>6^i4Do6@~zXy_RxtBYiqZu$O zKDn6c^loJ_(;v~q&WkV6D$7Vy*Mv1xx=bkZM^~z}lZss~*PuNt!Z<=l#w$*`7ZfB_ zKtAn!vBl$O-A_iu#sS&BzYoO9M?=rPeWZc7{+t~EfOJkq&dahIXM1M#63> zHO_;-89uqH$=WiZz4=}me}!9Wlbv2Fj#mkc#$ciC>h3Klt=-@pfxbtSFRM%fSKvUZ zelmwO{~PQr&&UOj1 z!Aq}CG*0iL`IonEbq3f#M*U|$6lJK8h7N6UciET>2RnW!87*#KSCFY%4Hn8R0y_4n}}ObLA_+d6`|f~l20WX;8la- z>lvu2F%X;u5%Oo&W*K&Ae0MVt$xlp9zWRQj%L_#Bt?&)lcXw4m=EcV&U^iLryx8b; zH)u5Mo5X|-!T*M}k%*#2k_G;V?bD3pmm?G+4^PB7kBoZ0^|awjQeb9}`}K!QJP1=zmtGg<8z0 zJ|`^lbz4rC|2G_7>Dps+A197Mbo)HFq`W#PXjtllG_b5KV20`zM&|+b?(O%)iXx2n z2oLGz{-B&3jkn%U`z<4cHU3~CB42!h!119w6XS&_#(jhAVB9zEg+6%YS1G+cFafT;QNJ~LkCA>D&;M=&Hyu)L(mT2Tz%1qXxvi` z;qJ4u)Fac)$^${x-?(2i;Q!$T(AfV;(;_$|)K0t3G>a9*?fv@Ei~6DRV8qo#1+&lE zA5giy|3v$W&>pw9F}Z;I*Oia^(u^@kH`}z-TpuP8$DfEk?^q9#Y;oWBJwzAy1*N1v zm5m&x-D(Iy3P)y%BUn!GH&!Yz6ex6qIwio#>jX*ug+E{vb#9OB%*Wpa9UbDyG)qTC92{42gg^AkXwxc;T8;&H3m*7nli!RY1S z5hug*k2%FhuP0MaHWF0lfx7kQ`>w4~1wU)+mhpy8(i#PQ`TZn~$80aLB3BEr%uXY6 z@CrM85J4ABU&VonW=;;tlePZroQN@j`xAu9tBBop;M+boTwjbPLEFqR<70kW(u)dCYVjOqEG>t|e3vTfL%J;; zz5eYLL5%Cg@(EUQu{kft=;FI(OzgBOn*{2{aXF0FnL=E9%3B{j$gDn4p_0$ex|BIp zt&wOqoe&Rqtbo(;j}tuXpp5HD8y)m&`$2>E5Q@p8{C9)h)XO_E@l`6HM15Yhd?oB55AP^DE_oob!%pe#PV@ z_-00<_s~j7Sa*7VwPLZwmXO#wzF3b1Buv+D932MJ`Es}k+m-1No;{)Uw&ib*ryl1$ zs}`q|f#4MSas5SKBywybjRi5zIoeQF`x_VY3Mp2h!Aq^pgKQ8eifPg_XYH~awfXr{ zbV{?SrG~S0;-o_#lEw#tuUp#MN@`bDH(ANvwi2_a?UJ4qRc|F{S)HZx!IMfWPS0u$ z>Lrx7w7j$PLZD|tv*wl*8(;3QvOlY9-8DG~va5?1e$?2kA!1yy4CgdfIV@c!n7VC0 z)P-KiP7Vx&r7Orn?e8?(n=993eINeC0(kiX5pu<4i~{+$0-vCUd9)^e2jp=S!_9 zDh4&^#pS-4TA7i_voxY;5|+#wJ*=L6-u%!oHYnOH(GoJ#p8RtRa1RS?21tx!IWQo_ z%BOhZoQQh=tb9GF6NB2<^l{itKjMYM4(vL%~Kl>WOUI z!t8Qt(NJF-3nP%DkDmO<{xVU+SJWK6*kb$U^2Cow!?z_lT|rH4g8vr1bhCIspkK0y zeky7TMRhSKXY}J$-I0LaJMdm=)W}If2HQTg7aG#|mj1pmuOsU#bvo7Bn_qwL-Z8@P zLPOj7S+cl8K%L~!ZgqSyZo%1P=QgoaGxb&1_fQI zZVN~zbGC>t_vDEHo+;g%uM1f#miO>Q28(Ri5|L z2xcTdou{#FEq2R1w_j@h><|yrDI{Q3U`j6$NY(I2a;{jxNTu_T46gDlDX?>~%G}LW z2+goA(aoX5x2Ae3B(4L78elYNW{RY$%zDwuqrOm`oJA= zl5~OPBIon0ubPUFPf8m-=wK^hzc2jIk9nOv&HdVc6M!-zOR%rJCx)zpJ6PF~BbFZ{GCnpPg_brX)rgR&D zqM$Jrat-*_0~|v+d$Nb~oV;(R-AX0+{D`E{g2itE{QunO=xRzmgbpFY52F z8gp`}3x4Gj_3ZK$jEB0FmM|qfPe*&NG%Nc~lBVMyPzUX399wARM2mj*`HVE$6kJKN zAZ|Po^g$oZXelNd2PpW~;}YgPdH9Fdz|$z9y$OoKyA-!oyS8R}4Vu%z>7yol~P0Kc$?gWhy)M$~0)0+e3DU$YI zt@kHc^G=~DGJv^b7#@Rln2^~}HDIp^=ZY|_-R|N)e5cH3#QHvSJxvj}F3-HyFu{_E zWAa;Ri~V}KsBKDo17~$!xACNz!#l*uY^9G(8>tJIC#FcK(0*$eTkPYIG^1WZzw~E| z=L|!wN=;ZwV(b$8HMHMepb^UWk_>sb2=?|^jQO#^MfzG$Pz>1>?sl7jIn=y^6#A0n z-2hREb*(qc14_ZkdeE-M)j=z>jN&FOp92Q#6;wSdR=dj)8Dp zvwKi^TvQ__8;Ov;+|*-+y3^crk|80d`P!YMmTpRpLw}iB#CSuwc_cWEwTv^kb{EIp zaZUaMA*R7dsQHSttYurYg+*echN=wfrsYEuEO6WSs!yk;t!*(P_UB9N>y%GhpvGfA z`61X?zV#6i`==QLchGNS4s=&OArF$oLwVxZ=OqTfd~!|cT9z6tY}!ua!pJ&9oLJzQ zE@5l|d07jnV`1fLFX+(52X;8cpoU%XRV~s;;V1-nuB|+l!w=HOKc(?=Kp^4@d7(xp zNmw&a0#ladc!(tPta>wOjBBWX@~$eE7yP*XWG8k)=> zc^Vz>s+y--E6}YYAV+;IA{->C%~l=)7A#$0sj|P-lwZ58%-~it=JknzdG^o3?RZN5 z>Rp`oEj%vDVMW5*Up7l%@t3HfyGAW7@4ho{$?Vux_M2BS_H~kD2m4XC zOO`k|c}>3eJ-^s?NWRz|s$$-=t#R`xTf9_gukh@&R|)YH+IwE1bnf->Ixy)=innMf zg@4K6<6PMxmLG|OrRXj1W<}hc64+dV2CbXkRZ}5uk{wtOY~vV9J>HK+xfoPjrXhwt z6|9t7N-=N3iRYBg_nTF91adszNp2UC?W>YxOEE;cim`*N)yi+1His(L#<85B@IEOA zZ<2mQ^67`-Jd-D>-09G9p>&ySnZ*8FXPHYS$q;GsIe-CtT3PyKC(9gN1vkAx+*z**ea2??`cjnZ>&0fWgtjkO@VZ#ev?)~|ADV~lt1XdtKrU;gfF z-pIQpPeEBSyFyJ}`vsKnJmMzLQj0g5ZwjN4}+ zp*D)+Z#X_c%$}^6(f3RYEPf=+Nm-h?(sbxU555#b&5gC#!-lS?6q|!JZ!E0?*J_ve z^Qt^F>X-d3C5IL`-)a252;dI*7KwiUz%5C}w*#%j*~v*l;OkYWVz(S-9B6*d)Y6ybaPsNsa`XXYPX*R}`mS zeX%7?iG=}wj|A~|k|C*+FJ;0M^pf&rhcir0wnoks9E%+csU>LBvPd>xHoKN^66SJ_ z(@MR2O>7Fz0``B@ScEXQJ*3omt|-+{qiwvVE#~FSV^3*v+e9L^!_ySDb^_(}?v1T5 zM312a=jAQ?3wdZ+e=I9O3;i{kcgavM$8*)m_01NKsMFd0Q$(7^5cDs!qe)Oa|5IKk z&5Bw^x~ZpMRqu#&+QAg z+dR4uavsvRHvz21r*1Z+q=qoN=+Z#BMy!Pk4H*VA|3hzSwzGv+-KyKmgP@leei2M7 zwxHBkh@0|La58gZgiUW#p}Dz@pm4V}uB=_!n{yce6nGGdz*VJbd@Uid^po~BIozS- z&)Ipm#77)X5)b$4?H9a}#0w>f`&lFJVUV*zZKTcl{m3kPIk^j6f2>M z736N?$J4I%X-0_di#;v9oM~pca0Qmjri7$qyVhJ%0^SeaIEhiMUtngf_**0@bdBK5GgtiNMA`MLGdeNb85&vDE>JC*2Xr1 zcc5e%oXpoiahU6>`5d=Z7LCx{7y@BVFIgM`!v39F>^j5VxOF44G3N`9o$>BNn5|lJ zP?pfHsrvi25aj)$z46c6da|vQiQPF)+RcR=f>XC*~Zw%#LI?_(0>WAY0CqYC6 zKVl|Fc~K{y=>CbXX#V6B(ozzcJX`}1|Er#%v7bnkpS2|TgS4r;of|@k-zn8 zdkkK;anlh@82at(YNfXFJ)Zeu+t(D@$|}1U&tlB$kmqu@f~P&R1Eo_u3pl)1ijJZ* zzzy$Wn}MA0%;Yo4IbfQ5XxU%UE4ou!&RfUzkJ@znxfPd^-(#R;}x>3L1AxacGKHkQ1;T8>fz|Yi{-ca)OBm;2caJ zD~wf-ofD`g4&`uH60iropz#9qpP#^D;P+#Qpr4KBlbRw3U{rz90%(@0UanT zebv#sug{4j>-Pd00b(IBo}6dzkwjG7q4H4PRGGw|rvRyR0!%N8cB?-qd>&-No>i*+ zn(TZ?Ha7QrA_PPiXmwEb$S0mlFqAyNY53HYYJe;q zeXEOvKupQ-)%vLgIvFU6Pu1&ao!bTzd5qLgcdJX{7o78KV~WpQI!ajB3%uYou?s-V zv7LVox7Dzw7Fe?75%}r${a~r(+AhNDWYY^jm#gh5pQ^TYkaQ#&kHrP3#spx)Y?D@$ zdk3b3^~~HVXj$ohmhDmxIL5WB1j&m|SXTElrhBSOKqd0bGmrOspMl45N5;gCQP)x{ zrX-{PZwPXTu&lbnYXlgv9dHLb#{ZMsadhS4emNoyZ@)g>S??w?kKd@dHqf*@(^3Pi ztbtGM3}~r)4;hUS+?(#vLx?Wgxnb|Y+kg$=KLY)qUqUed(tqwyS&s6Z#i;!+UP;7g z7QdKs)HBcAD9|Ed38P z$#{BF>^0`-&Wb_%Kl+;z>sD_*u)%(9rU##6mL5{x^5>oa_kZ^>0st6*Xoc+#8oL*I z=zsC7K4gvi#3|wZ%lksMvjY^(?UtIGsH6pyKr|-m`7wfUG+Fvfr)5~5&q4Gs)Yip`0IWh8|grH7++Cho`W&#ki`;R~Qzu0@ru&TDUZCH?yg@mNi zDBY3*(j~p9MTgRjq|zZMsDM(^z36Tv1VNDQP6262KMj8`G3Q}tehp% z^=lk@Z{hv3k@@@ILc)#-LSGRR6K7webL)}*)sGA|=q;*PjP{Ra$OX(0U8=k7A05R{DPV?Y-mwN)|7eCLNHYwH4gF^`d<{0_!5Lc} z!9PBJ3)0WsW+z0}dj8LC@$c`LfCo0@&jVd&`afR-c2OYG&)wByK(xI=iUk$!Hq zVp-wO*MRj#2Zs@qt5OLGYWm}&S&1V3T)*Y_m_J{G^m8C1NjI^IzxxlbNHgG#^mD2G zA6fs|&%Fc7+A_fX@E=}LJnam0G_6Ea>9zma&)o&f8klbW>>pmy|9=eZ-{1Cs4D5gJ z(Ele4jEO72ETbpCTPU8spzuYzfk&lTKt$)^-#vhi->nN(2n2*-sqZNSxi6U?gL=I? zs266%ksnxeM$+DM0V9RI?seGo%xwKDVwpEW-JYtd07VW2_9qdqYWk=n7jY!j0|Ov1 zFV9bE_V=9=1zq6EDUTn}x?tZ>jf!$ogOBkrOCkuNVn_BiJm}h5a zk5$=fH$V2+F(}B*?G%pS4_V>_4boT+LmNtA_exli-e|q?UFi7ItG(ZaOC55we0|qa zdLLht0wG*^>f*~B58b!ughSmhO*__l2S?&<3>~d*2biYeqV#LWLbFLW)NqmUlPp8rs^P-oIy zzosAK>BCjq?h@k~>6MTFMH2yh z4IWb9WCE&G7W~wq6JV_E_Etr|%2y;#ESVNHOMrLbJz6l3+GlQDt$n_PPc+2);wJEx zCYQ9(xiP^Nuf9rjO45n^A+4)6JMSMk`hnd&-B{~hh`=98!X8F>tzzDfl~lr zw(Gkk__e*c;|}nk`T$;P^Uk=*yY4+1Ul$PTl88DG{p35ovGir!cenYvDf#8k_$vyx z;&SkHr}b(a>?1@Gxm8w!tzb@vtQWY$Wwn7H9m5Q$I{H>nB`~NYS*<3ygv`6rln=HSc`h&RCT)@=Q^t= zNpSC3#}s**&5{^STK^b@HC$}2vHb38Fg*$YQ^m&_EPZ{HPTJ}P92ABH*9=%#S(QbZ zk#)u|HSwU(T6OrDsx;Ni~J8dzP_m*6SHpHGmI-Szp!;)2y2jQf}xX zAYM^+w(b^~m}L>Vo$pq>F;oqktdAp)bM3eea@p!v*kIwK z%?fj)*t(`yRKI7joipRWPNLk09w$P&{wD!|&(Y`;qWz29ng{ z{sjf|lnD-0Waa>Xzpj+|GXHR!wYaub?8fNNt$F8ecAt;{_pt_6Z3_>C86lDTw7tMN zn31zfUFv)Kl^V%!8(PKpJIE2c{GJmt5BBOYXkd>z;s&;Y&L8z>FeqW^^S>Gbjf~Jq zcm(0?zF(`8dzqU>X6teHRvRi$PVCM?yk0y{{~A4=^y!!UH+OSRHVW4Sj|Rm^tMhi# zd(+r;k!&nwGC?ZOvp*nCm4aXuMhes_87)7QBaXU@EJZDgn!7HFt5(v8W3=l{8Q% zRF=>pZfQ^!N-zYIkI)6F0wpC%xtT%5C>%tMkABsmp=UR2gJJ6Mrd#=jMIi~@vFsJ- z*d(74*sp=RE>f$f)y6{?e7!8LE~U&jmLvoK6U_ie*-#oT>WPay85jKC`g=0Go8_-D z6{UW^-C;k!kOHCoVQd~ULT&=^z12Vs1 zwXram^n9QlVp&FrD(a$(o?0)8>B6J(ke9ibWix3QFqYCQefw`Ln0y3x9h$bd%Xmzn#z+F4jxlg+jWJ4p77i;~gF zY}c_VVsr^;QK^d47`N}57=Cs?x+(^>$4Mjv7IbHS#ScBT^D!cZ^4U#rO#^@3c&*rB z1X^D{K$dYX0)95Cct36(akP~|P*VR5%>Eu>Q2MlRGHQ^H8I0&`@2<5`L6=&um~b6d zz7#{Zw#JPKA3cb<@VpCZ;g3K<6mHBfA>o#)l_DvIT348#-vz{GU`XNjAt`_oK+c8R z0?exZDVl@rn)zx#DxD(XF!+=b^e_JHz#$sh$7KAz3?%C#rW8ydI%Pw>q{^Kc{E8=L zw>@2Rq8zz|Gau~?!ZX!G2k(`(xKo2f>%uGgDEVPW4Sw6gv{E1wGzHjSm}1w+u>$Wf z6Py~LJ4p0hh2NFSds8e4TKpfxph`E6mapf2@IH#3&a47fakbQidMVtP=l!KW`4$&% zBZW_>k;rdsTu6BTgtzCWMY@N(F@ zTjnSB>q^Dt5d!YkH%+IVv$RjIM33BfXosba1v<;c^Re__qPIg%IE%sE~W2>K(} z^ctzv8Y+VHIsJ-#epoaWmL)rNQTF?6fbG%QF-aXt7iTMR+d#YQAT_?)2aK?L7|Nlh!L{q?cdoxV-|Xk=hG;xB1kvEt!IP3@Kxl?K z0&~CxVEbED#hJWEGC8sz5$RCV?p&GXhNZy3=>AxCeTz%(+b*5)u7Jc5BJ;c3DxP)& zO?H=SDz?U8E-hZa$jbcdJeAR|kFe{SN!Z;XoD0D1U8;Ohh|yKAi$s`vsu?~MD(sBYI5-oT!FNIwf{Fi z`X>-@PF-j!lEmpg8@+EUcw$doXFQvc=_qP2=-@9VVoq5ZznA6LoN>+}8YR&iyW$;J zrNh-iptbXPG6u^=c>8Iy!}|l#MFjq1#|=4rw_MK6x(HTsSO58(B{_jHryYUinTr6I zE9QL>au&mTaBp40jgnH6HAf#FYd!;qll6H+lCNEdg~Z$Z&y^EZe>GB%oY1n96;eI) zvmH0iscdvRJ>@JeJGjBwq#5IO^gi!xoZoi2DEtu>=Sb(fC)EDGch1-yB+p|n!mpCO zdZLwVcvbPvApyPSpyWwQ7af$h(OaU61~yiMeT!s(*rZZx0L9_HbslL#pQ3X9=(y7DXx&01 zdjed|Iae_GaW6*5byv*v+il}=tyL}3VL@gWy?BzYL#TNK7$Z3VS54jF;m|~pZZHRN zbj>Un;CFAB#p@i5hl@APflmMy=CLl758`l7cdjc73YWll060sam~O6Z=oZ7QFP+*B z*urZOCH|~FB*+wv`Gw;BSBS63SpQy!CFptO3WE@;gN22O$yqZjr|C(Myx}O`yQb5l zl}x)JP<b$^?8H!lw1@2WlyXv`weEAO|CQ5;@d+x>l)xt2*iL@&X17bJ(~JR zZ=VEROj=ij3(0wAyHuVWWXEW-sQDMq!@yxVjR?=H?aD=25(c=q)*-pOq`aVCF-A5TV>9itY~)TO}% z#d#hwr9gwc6AYB(pngLi@{FX{B$895kbVH|Ed)X!w;UI>Rthi%pTXSEE#Td5NnHYl zho&A(m#D&`mmBLY;xV%Gcm>LF6cxE?T&`;ahjE2Y`QH+n8ZsI9w2d8Fvg$Eb<(&wE z?LA{QJZm(JGvQN1`Fxa08=F8=fRyCr@8(DV0|Vf01UG}hi10TFwN{1XbHHy=i*734 zQASRlL)b`BY75S}MNMmt5e2pa)q@?y%k^*icCG-R9~78S-;T z-9x6tX?8wYol#>_2?;`u*)?%(gw<}L zOI$DICdEN!QiZQF9x9IAR$V(DNbrFeLLZ;G5mTmkJbIOG{^5rdnJ}B%T3=V(Q|EVO zKCa28HkO`!_tjDxh1iO0%FMsMzc`Vf+7{*@Tx(1IvxX7IUa3M@^6>pvWaYEa7B$QS ze9Y^e&2!dMjd%!>wlNRZLexn!z5$?Hy~eCPkGIw%7zb%Mn2>O6QGIch7w9{k_o!;P zWH+@^grjNIvwUwkXnoJN@3Jx2H%(|u|s5ahele?7K ze8xhaZIb7G#g0zHj4w~06g^6zv=S4YTx5g|Sj^I6qL`2iEnO{h$<7P_^~5{up?w?c zzR!B!k7%L5S&>`BWo_~%$S9FCv6hFxEUIWnNDIi0nC#g;X{!31eFp$h1+BGi^|0q7 z(YQI(6qk|Tv$zWG;ci$(6Z{JmK^ahGxc_9mfdHFGH#6n&-Wyt(oAQsLQ^gL2LCiN* z?(iuPiUQ)w*A3Zb{@}ExHnxGx<1mcV%nhQ|b+v%?-!PK#M_h{Qjr9L0QGp#25{s-x6>bG>2^)*SSZfNcodHY;{1gG`fT_$IpDdL5M_w7ib$=LM;fMYgQCLZ zg4cFcTwY$Ks7A6B2eyQ<&44$U;`)>9o98WY8c^{^$7GC4qv3<)>8K}bk?U3M5&7gD z=H{*t5@qo1WSp=00MYzS;!r((<~Lli|7w!tQU_GcMhkjY=ugMXYJ|qX$uklUBR{e$ zFpH-8CTj9%hMz7hM?_e&NJua*ie0X&@^n24c9zS(ckZPf!RsIawNmn#I!$6jb|Z4} z0)q7~x&)5}RATPy)qjhpPR*>(D%#A6%-@~a`?@*Qh`WysDPG)-0Kh%APOIgk=nN>C?HTf8 zzFm~@iKdF_i{-GIi=WL+v~Kunl`1}W=t^_hJ{AVqb{W*0CHNVSi@h}<2`axwSH4#N zn;itAAc{js%9%LLkg^Q`O# zisX9o=AP_9fnYfqeRl~zNI^6r1misw&?F>KYG_S`jokx|--gkGk1C-^z_M|lAopFJ zf)Vqu)MN0Re|cp9*ngs!*KKn*Q^+2mvx2UjjSJM&Gb5rcr+N(bLRW>V$S?gMPZFTp}G&QvHJ`c{*%IqMM z_$!?~b@@6K@gup>8FoE#KA=E>WMkwu3!>)y*^zU@b+|`HBF|jG;NeAj@s^a@p3UB(N=>81Qg7E7`D815 z->;>vpUt}n+6xiWlqr%}$49NP$D(chvn#U)!;%X2=Vg?j)*()Ni|Lk}ifugEX_w_N zRKwIX=0TVC55^wK?MZycuEK9!UE%|X+!tv+F)M{s81->+?da$tj=8rZf4&h~icw=* zlp6TGh=QYZ%bu;p$=G)Fpv7YkVUmn>tarJ6Cfqc_?>1b}6d(Ulz=pySjDmoPK zDAD(?RygyvB+kC?$LK&P_#8E%FOaoVw~^Szv`eBM*3{Hujo#7UfIUiG~e4TBT&Y< zN(Q?nkjsIhCYfp)tu92w@}#mi$IhrE==BrYY*K0d1_F8C(YhPt z>m@1VTr3Y10=7YoY%kswDP99`PA?u~-4ytLh02an*dg+<1-7>C@!!nP-b_uJ=*_>4r>@bF(ZWwW9lUzW(;B9xALw zlkd`@gm}e=;b*Zfvij53c<0LCQ*$*N#T|;wSW4w;ORt`2mNUno-=QR&E6{%BDcd>a z<5?5KeoAAPzgKT{pkWv*;m2wO=8o#;t15*F~-pclye#)6#J!zb5u|0)1%i5_Z8J7(t&=()k|3*n4;C|uH<6@CYgVn zCj#sZ)(lXtFrxVgjqXzsd}IN3J+sx|H!jm}@AmTDDwm}V zcsm)HloI)SbE?eGt(I>Lky>)`E$CKSsp%~L>#IY=k%dxwi4JtPgu3BYmFiy1=@Dx? z*T@au{MUi^&qlok;m`V%^_E%%1=SuJD%?Jv& zDq~_C_pzJ3DjPIDaU#eNkLh6S`#$$7?afzS5G*fXGf68 z=KKVNFJOGK;^7W_i(1lGAd9hyGjNvA3};`h?Fz_egx{Y%O6b1Yy}s%nKaRkLNZgM6 zlD>9<%(=c1v3NzRLT}x<%7@doNZ>^5HXNw@ou!e1glvgv=Tb}l=4p@_I#v$K8r@QJ z4AiZ#2v%O#7AXO&OY|q}x{@VbVYNU_XLxDJ5tXwj0s=`NtmI~)ySvPTE`zLOHT6WL znEpM8#s@IYE-S;$QjRGRqs{%8$3w?=m zv-R;A{?YH#-aHtU*4iEe{koEG&#O=+Xjw^M0R3e9;mgo_-0~HXW9RVQGmh4)uqS>& zJyE$<)BVVLX^|_SX^8C@srRLO2eyJxx30&Z`aD#q*+Z?5q4T?ReD8E0<@AxJ70;5q zE#J9$v#M!-uj-F0X&&4u-z)#Bq(rfd%KXBD&6-F8ak}y+cHhm@`=#CnQ|q3qYLb07 zn1AEqJ%8)S2#S?s0Gr!=qCe^8#aPQ$!?DEC$@lIE>BAr@;%cY+XPp@>k8wnPfmw1N zN$9~(Zm+YIkrPkvCad`naQJ~NNvr{+zVP#FJCY*_^KR&XX!>japI!iU>1`5)3LD*2 z>bT9ICe!ky99bwr%CVF(FYzJRGT6*wSi39qVCig9WA8yr155G_pd=9tt|Taq zUc5~?6=;$eN$;^f^8*?SF|2xQagXjKCdeQk!RGQ)JP+(H6)!bS^Uw2pDdkybFY7 zyF!P-UhmLpYS}54`+9QutRup~DgGu;E1aT4rWkE_MKn`F7lV>Xc={MgP_9>U2Ra$Oe&+0}`&i6QYq9zcdn>YhB?_#} zwloguSwp}3)O>sv;faAUBbzki?(fI1oO=pfKcvWIWJr69vKO4~0~fg{ zu)?BGr$Ru!+!M#6-jCN3y_oK-S#I#GAE=rypnoh?)r1br5YQjqJ_3Rr@qyLl@m(>S z2hd+}3XJP#kiREdIVqktvT%RRVL*)@825;>X#kGoyl+o!WRPrpuP2C0#ke(0RIBUL z#*jRa>s}30Z*q>wyS4P2D-G1i68#-ai1{*aB^w9~?XeDsDm3D*L`X`Nb2)&9@0Epq zvqXA|^#ntU9^T!hZ||`SoSXQo((pmVCT>`PU|BAwVOpM|GNvXySepJ#nI6DLwRuyx zFWe<+bvgQ0R4OgkCO9>}TnkUXtfjd4`ioQZ%ua=uLEBYAVF5Vb-Ju#5e3r5Dn&9dK z(fh{T-=(qj$6So{n-){%%2LAiuM4WJDr&v~T^A``g09#|x0fdkLVRN3Vfdh9SkBME zn)#wVX;g!xt|xHYPJVY+zdWmoQ)^`XtF2+g8*|^!ZGkdDeNN-r8z*Y9lIuVC!sE3l zYmgm&vr%@T(0R2Plbh$G{->CXmF7fEppr7(4ZsS~1G%f;!JfEa5#jLurxhT=U)% zD(rHQYfK~AVh*xJJ!qj%5DjKX=Aoa_0rqbR7a*S3LJM+p5wGLJyl1q*EaYl29&e6C z2S}4_R*@Nc(~ayA*~G_kt0Sb@+*Cw{VSmBEo$m}Ndx~l6J!uvr_#A^<_!=EqUjx~` zERF8WP}e#2iYN1J`Q5Z!L{_&_{Pqp0Jn3qz&#W8?cMVlJ2XV~5xZ znrz4BiLUGfQf4+gGrnEh#d&UUQ-$nzUFXcLl}voa&UMWW=7wttW}%SP3FpiE3Z$xg z-N;v_DJ{O0dsdcou;(oNUXVJ5N1eCX^X6Kk(#aa+?lz!VkrG7r7UaG(e}dFMm?BoD zKGr%82vk`~RHtjExMZ=UQvx5A0PaL7 zTbZ7&6UYm$R$xeCR2j#ws&W3~1{vz>T99)nL z(2n4}(=^FzQ5%lix;&ZRo@NOPKFDa{3^J2MnAlJm)#iLf&ra-DvUd|&Ma zkeye2{0L~W`uq%#eruATDsrE0jZL?_EskLd4C=i6iEQ#4d9G)3jc6Z4if0zS2MP)= zXe9$QkvNxry#dcq+yutx5arzlL~qmi&ZbxII^bJCxpsj%oVDuXy;pfXME_QDgKC?p zM;!$hbISPaF;yuTU*S}NZcTy$?8zG_i$O8NL!9K)%fEdeuom@qpocy&E+LG`xoJHp zGD89}T5;7?HsgmnlXxt&S-IB;9ol~O`ImHWyOnwqqUi0}%A8ZW`E?eeKW=z`&Z?1z zs$GYgJ-7AfH5Op*Ka}_v1W1H2?TNR{M7KxQkeJ&MZSWJ{7&JYg^VKgKzRQYP*9O75 z?Ht)*59D_a2G1cOF*-CkUiR>_kvQc>Q8(PKLMSn=IVKJy|Ni3_gq(QH!;uq>JJ(?s*Zh+M=~6zSOrf_~C{wyeYLo+&o4$3|9h(EQ z8E{2nJ;UjF{?1$Z8#2_Sh+qw<{LO+2YExT;!VA@u$1jVhn#(XXzM99lspl~sF=@?* z*5+Xy0F9zco0vlNbr#^_bOgX{$8bEtA^#a@8h~-Ldt@zb0!^tpj<+{Y#3G+j^Jeb3P}A#?^Vo&AxCdVjCOwVFW+~cu}UXzXpg{e4k!ocTfv}xC>_`n-Tt85|TkxNqN_fNrU zKwp3^VKC5R{P#-!_w^kxLyJy#>Ur3aMb3xR0%BmCubCVf@dUGu$^bB6QsAuvkSKE` z5CLM#NCn`2l~aCW>Q&r;pOOG8Tj%}J0P{b7ufP74G=K0ukXvzi{|rG623T2_GrBt5 ze+0OqfHal}pF`uHA;{QB2=czXGbQp=`omk_2tWn66($-3_&-CCNs$obm8fN*Ki%iI zEuhl@(-p5(!uijZ@vr}D#ep=-ZpQC!zq??6dNjxru&n-0@npXb)j#~R0|vK7w4kSc zH*VaxzBXDh%J|=!VOz79pNM1-9eW9rhR)&lnIwqhSu&oHRCuHa=4kYL!-ol7`WAaW zH4+l$Se_PmSmH770vRH^#=w@#hJyL|h@qk#J?vx$q-T5`Mp z@s-3)wh3v{N{@YhCO!PqE8Rd9#Kwnq#=oQd!&?6Tduc|z#M32$Fa(>i{$ZJK1n}vb zob9hLP(>u_u*D))=gU$K*!i6nzGV4DD#3TiW>2mGk%KmEA?6>fHq8kbAW@&?_xS&8 ze7`M609e<{%xu#?8z4l^-(PakJ2naH&!7AyFpEe5Jt5iuxOtp+xG6OTUQa7|@I#5uS2vzU%>sdXi@kCDOA5%!i1xk?;Uf`C_jMB!U8A zx~UJwTPQuI5_n6(&W%mN_DfZ64wNHCDs4u)wo81utNN0KKgW@B8rl8qPffKRwkhGW z8u)U!H935^KHNdM3)Bv|Ktn-51Fn=;Z4d6L1Td%tBF4jnbLmpAap756Z9wrgAsCx9 z0pziXBGh%BHb#FbDOKPEFTyLwIjNBZo zcJSScPoeBr-`f#=_;)H%*K3A!BuB(2%e zXWlp!yvM)9UBKXq1i&Jx0Eot)<*d{9L6~5h1OfuSwFfW3M5Mkx;rQdn{wL68manmnStY((yH(!Zb3ECJN-tc|cotxWriOU?B*v-Vc~f)jc8C+|8fhKlynq zdO9bvulS9gw-fi-16ddWP-^A_;`Qoa`Z{Mg8DBnZS8M|FlTrAp;ZgyDnpK`(a1J}sgC zsr~ZiA|opz#p)*7^rwj=EclCy?Zvo)$10W7Cg5XOD#=%{^q*sv1rIClS$dFHu(?Fe zIF{(6%d#L+7E)LcKd+U%5q^Q3F4Vw}C!dv!)l*}&T+{3T~|NbO>*;i9oq@y8V+v+D2Y#^ z5mjFE^glLrs9%Ry8lGX}q0@1r6A=*&DPtyLi0Pt{J2S0mV3RZY^=7Avdcn9COfvJ)Ao{Wbd0C#hPJyiRd`a<<&XZAfz zvlE%)V>K=B0HyTz7GT21C@Ms+D4phQxZCyRNh0#%siqc208~?{n023yzXr@4j?%Ok z8qu_0U|PsMpOE+|&`GdxXcjqG(1l;V#6dyx{ffHxtm4_K*LK_0eZuQW3?J4ZKVu^0 z^BTkSQfL2$tUTsj4+R)wirwVbPd~mfh#r_~9JXKTZfh*b%HX`ie#KjCkW762vNE(c z*-DA~>Bl0k__liC374?C6a8m5X)RYvmPrYi?Pyj7Fy75-9KXi#S{^J7Rk) zSg~a9R;+b&X%SHJ*Ax&7)zy^5;I^mE{FsaKy8qShxl{xEGq*6aA7)#-pA7LLi}6F z4xj#lfhZ>poqyJKn1!`9L**C@H{c=jE>`a5PfNSukBYw35fSnDUGuL_BB<3+Rw#KS zCku;n&=oSvJ_GCtbB89u%O0IT`r zyWR5nNEFx=sgr=I(Q6tZVgZ0`#g)^++DukofiBUfueicJOf+fOwXVwRALfAYeNDm- zz_sE6N(||W>?=1R`$Ma;QXp8%K&#}IxOu=()*9V#uv&=GoQaS>F=)jt)wbKWBxFsr z3=^yWhWm?Csm5BRU)#Jm1fN4<@4>_W<^R(-m_4a65AW|ymsc8 z*TRX?9qh{G%DW?*d__>E`5%&Tu}@fOX2RYM2-16Q*|K_wMs%rSi&6Nd2 z><6rpXB%*{lH4=Z0AsJ;ME{Tn8B{PTVzywkLQlpEOd!dEU5%~k|7zaEVj9U|@uBtq zvOxfnk^1Ea;fmt4sKLM8q$kOQMWX#o{wursQM+@K`c%8)L4M>WA`{OG#cs$$gdfRElC|w6a z&)ROefSz~o{IiYchNzI1Xyy1oR6jt6=>gMp*KKGnc08J00>T#{!Aj@wIVRv}O^S%2 zVbs}(Q1&D>!3E<8f_!Ue(+a)AnBU$6`Rv1ug7f`tNB-}!_V$fICGQa7HpGc3<^KK72konl{tpi^pez*rF%RD{<@e(UGz$&;g0tnPLFDK&;i54Y(x zc#E9*^f!(MlBH|r6T?z4<4@nk*{5aXT-Mw{368vjcSqc06q_j-K4jAbx`ICgu{k=s z$$zira&Vjq_$@JZVGIg-uhOjdL9fP0!gj7?B;sbqcx8`rdnDh)0Z>v&3?u_xmBZPnMh3R-s*v=^&1&%DUWXBIIgoatzSma^^@TN(NwSey!fsxgOn{k+Lu1~#isBNlH5BNJ zk~(==dvb&k0267AtWckDU@@VIvhCdW7ncnP!@+wPTI>~n8nGLnAs=Be!=FPMLttH? zs5}IxV?pfi#;iwkyGGhImL(qThOtg7$voG0l;9)PG|;tv7pPoC*i}tOWzLQjeZ6V%7_|;H-`MM~3G(RF5aJwMWZH+4Yu(GC*zkv+QLzt>N$)-d(OQ z?~{?7)@)@Y;o|+VmND69jbT2`k(Zl#N~yucUWcm8upFiM9|*PN&q{n-Lye3nayJ6< zQEkn|7lE)47eM;zDf|G84}paB+?FdR4Iv1Q1CxwYY4G{Lm&m4fTEFCsVKyM#^FSAH zGnRS4cbZP`x`-SGf->AhWRAP}3sB4PbE#2Al_o$z)OK_dpvNtp^YS8<#U=b+A3iWF zfE-GlPMAixi8%*34j8}4!e(N6Q*KQJ&~78V9^K_dQ<8aMW|mf0ML;nXX$!_12| zLe<+;9NU*4KN6fI_?~X=KGQ6)6>n+gyqp*pe3dvv$$X>aWFN@EKP28_Ov8`;0ZxT1 zLMCdbaQW(O%P{zDfxX@N{F$CHEv`_Aml)zRyP>lobo zb#tAZOcYgxM3l#angi8*)uUwQw1o|xiNGFDF`4p&yqTnFH0>iswD(hJV{vZ5mDRxSlGO$WG;tK`Dz~m!y%QYW)K?IjP0YqHCTf?S zha5}`5&Gcl;KSAQ7d#bEXt5_|8;fw-E;*sHV?ix?OWbQV^zIsA!314hCPI$pV+?1Y9B?R`jk@f&7#nYWtD{AC`X4qsiykHThrYy0#0?Qti^0 zIF$zvuPxZdC`3b0i6EL*<`R3a<33NGEZWa3WrVnkUxszXpjL-SU37flXCHF?{5ZXh z1UA=+aSvm%_nAYsq&YOb+@ul){$g-K!FgnQ#*|9Z7npRvbkb@uD!re)$5w+5PwNJV%7WpUswnR zCVJjS6mY)V0Bj3S@>^P|)f1KtJT0YbB~Up$G4@F`)wHv@RmS1Ob{`pMa1j zBX?Tb7k^Z=gxX9WtqyyTKEQ#LqXYaBpiCC$;)^LSmM6ZwC;u!iF$`CJk4mC#?Blll zdE70eSOAPlH0aW3^rfdp|W~Av26|H2x(!QP;^|=QV zL&M8Ip~Eh!98ka;HP(QI0|~9#=n61}Xm_^DX4~bjSoTrvXTcRP>;Yd&)u#OogPV2C z>taIQhf1u>yk}2zWk!I}nBO1lvZ-4aY2NQ*?NgVaC|Vmkdd0wKeWA?oKrY<%@)tE4 zey1U*t19rYzZ_yy>qQ-nYs5GRVkJ2=&uWeKHDRED4v?xlVdSVYF+*a> zYu(W4GMp{c<*5Kp^s^Y%m7Aa9Uv_b8#85nFjm5+u5#D^KD=)#mn?5b= z2M2PXY0d~vxDHla9#1DCLGzb5M zlG_>IVtYLTf^&J^#PpW&Y*RZQK{AX=_LC@~obw19{jJ(4*66~(@9PO07`eXw5I#~r z=eO76Su}bk4ScKOKyJd{hT&AC?Qd=X$1Sz8w}(;?Y8CFDFi`@OBl1Q(9P7Zo7Qr z6Nk3$rMjjfzCZnnJCM?WGoQjS#)+b#`2fLSMtvE66tu8N4ABcN*u0IikOLSKj5N$l zr9fsZn}No5M!vaB*nVQNvuhE;5MqY^Yl?#8skxR_IQRQGZ*HJscY;Q^%4Seek5lm;(jXOPeLE5%-rcef@Qdrk#`j^A2jaCh`|06Kh;iI`e%> zRjhc)|>X1lxdEVWKR6Z@#4<4wI&M`x>Cd(X&S@< z&kALxa?y$*ra&Xgf3gQw43_S1QObH0fe{-cdI*xe;?Y4qdAX{_$|;Gcr@YJz`EEdU zl2Ua^Y0!ABg0tLi^=X1!8A zHxHl|Nn|D`Q4zdPVay?|%*scie7}lq$?sx ztT=_+0@YR~`r2aSj13Aa$zlf0v%M-4@s<(`m|x3?N|T@8W=Z;?P7B`ds_`l)S(Sql zyh$=|ZHdH{)8<%F;@e2Xs+P2_v8at23OezxcmPtKpP9vIK0T7IPa4%DPH)LyFd+p7 z6aAOLlrmrvxjuv3So1RO4&=*}V_gFAcyyxcW>JlM0e*B7(?q3#E{}I*m;|3E7!CKq zQ@4N?sjD8h!_;7Glc_h87vUcm8^HRavlNAsuUAUtYP}XpTQk{Qp@moPeuwT_peW$D6+OnZo3qCz@oYuoug-tUY zy;6Q!`OGU3cN8~zywW~nxVo6D+~!X7yV8n>Qo(UPsSpe%rZm{&D|hXAaTIKHj&cao z1Q{eh8y^%7*l`W{-oFwHX)dxCm#}^d(y5Bo*_pZGUDUf98YwbMR~A9E2bZ)n_U#OQ z)yy9-5;97z%zw-`!B+TCAzF`KfNEL+RQ#xo7b8nvpk8)>E@_I_ctH2u2JQ#f!p$#B zW4pWPABeHb=VIKes^o?TyOL{uTO}!x_j)9-`NNS;!0p}!?UrZCnmGJ8JGPO=QO{is z7jI%Fe90Qgvq`v7a3cuDI#RX%*i{r!RCI7#038)6Kr;NwhP1tEXE3TWyf*jeRntSDrE9{9$LkzwKZdcjwFp2XzGly%$9RM zv@wygX7U~;;E+-Hih_=FV?9E>7QH$5C_$Z)*-%RJz}V&ryrxm0KC(r#gKBHj^}x?U zk1vf#8p5?1D0TCC9TkH3eE8!nXFc_n@3m`9rtEuXQi3bDUVbt#^!L5k*yVBk=sY)i z!;zF&tq@JEGVm26sm8HIx;lE*g+GTWUy`0IcU_}_lEZ!;R?xH)vP#woOY>ehtAo9P_k4^@H`cG^|0na84?K)Y73*J8)IMxhn1&XRzr z4D~yyFG_XFm3N&TMyFMn6Jj1NW{BBe%lQDe`ce9BdW=5}nyqmzYKSjez^i%PLe|+T z;7F7xHVS`PUg5W$|I6e-8ve+FD5&7Dpx_K_dQ|#K`TlCbba1n?k0`1XLJ~4{Yn{?3 z!MG1@_soPug*`%CrdJ*|U@`|Q)66Uh=@?xa81qavYW<6y0t%&(S`Ub>m%F*)$=P&f zBK4>!e~2s3_totYA}iG$W*|J)W}dHS*eCfq8aDh1@``1Fh9ki!Pj;`}}Ik z?Xf^9Bx*zvv9CUu;3U2XY{KPiMxDI4fl$RfdcAuP@7t*;Xi{S#o7A9yT$m>lA7lx~ zzQbMzThJqAR0z&mX+}IRG4EEIsjTX?Q3=NrEVWE>@Vj_mAV_N8u#J(;>i|_>&CUrae4bKh|yaKd57P~9c%qV zd{1BZ$N5X@M0VVwfs&%&%-Q14+(mLzyyNOcv=XXzjt^{ftb9aJ-?(>rJ~Y0Mpir>f{6!?C1R{xr;=-&d_B~DcK-*W8bsvdt4^|)ASc!21gS{K1W*G ztQ?Q#vUn9a8-7Vv*`oLv0L@|4%ciltAmxg4V!OdtI+Vh3@cEw#hWVp+6PSqYkd;@~ zThaNUlSQ)vqb%xHh#Kqa~LI)r?$#Tl96qTu*SA_?f0X7>1JbaUNoD9<9tI4KYz^oY|hyERC>STa0o&5m}-(WqUa#{PObY?=e zMxSP{_>f7n1#Os@X>u}$WrRo(_wA9NyK+jTHg)1`ou=+@-3x9kS> znXWvYx0qguVNpHs7dJ#B*v9+>zMhjIk)LZ-vfH(N)eegW;pDv&_tM9rmtyd>L;!eI z{uLS~+A&RrGfiG$7r>0T>RnK@-tACmc43YnNVebWor~c!H%Jbl=gB<5B1+~!Op#P2 z-MOr3*-Zi|{Hpa9%d1a@x=*{dn|awM3oBSvisIhFZ`Yu_ubBzEjZW6HKV*ZR>m#5L zZx%*AZSvL5bSn(M=DN*KQ}mepcMWQ!!e2GM0+iRad3ac~{i#At*)? zvm;Pht)`K1pV)p5WFhYuOpbQv=SNirY&}cJ7PP4+c?~VseDY=6hq4uCkKVx^TcMZr zNvSAo=ZC5RCl28*fCo<@D~Cf|*}_Kk#E|+OWZ@ngpBWmRcNW@=*s}HV`IP5pBxPcc z56*TmbK=1|P`WGdnIn7m5@1_3a<2qR7`62JSAr`!!=z@l z>pEoOgZupRI_V4azi|Q^a%ubpM29zZ#9p~ryJ0bbt^I9OY@j`OXx%!-z$rM8bD@y> z3xZq&28%r|Ivc&kGJ_ndp1lu8RLU9q9K{H3R}R0kL6hMlmV&Wk^m3^C&yohcovLm^ zg>0MyEp69|U`-}_pnc=Y(>&Bm1KjM9^)Cw4Td$Dke0k7PjP=WBibJcRu=oC9MsoGD zZ{=`8*($f*GTJ^Mb_{2~V)m|OiO?vZisj^@-_`nASX?d{;N8*#p}iC*E!K+CcEa- z-}MPk(14uYlyW(MvsZWNl;>@}5}Au;{Bzm1=@wqM+#Jctp)4I+s-*+QZYlQQ7?0?I zAm#*AQ8ujg@8{i!1I(96PyeQ^o3B&zfU`AhK>! zdjU{8it^*d1(PxNfCJLX<-*HRBOdgaCXZ=^m&&#g5;Vh0phTH7)5j4z*bmJ#)$aqJ zuF}i0`-2W`k7h5Wh(iQgzo?v`%#s61TPd2LPu&_n^4=)o8lAWvX3>*ZCq(Hs z`+D$L0Jh3_z+={RL+>~K<{lw^NWj#JQ|;jZC}%l(LWI5DYeO9SZd)WYQ?2}X=}l&V zyh&e_Uw_F-zhI&vtLvwG?m3v+rf~nw-pgv40+MKEN5THwRF2Wy2TQ7JyMw=*0`A2Y zaE-8xJY3bne*2;*9KGA}om~L;08STwR8nBT)E7|pXUNW(GE`8+_2ifSOx?;@x&YcD zTJR~$D3T3BG@kHNw{RVOax3!HgzX|w#9Y6!F=?BSt|xY%pA(ICb)hEdIjcU*+hg37 zeC;UfJZ4iFJ{<&(m5KSo2)uCm-t~ItKUGUP&2TISZNk-HL=FSuT|AL|8*C2}?}0jD zQ=>YGGpx{&R*~MOGns9@$+tmtKMqVFcsajhN+^*(;Y;95H>1lIrA(1gDaD~Qc%N)l zy#+UEbco`pWxD+2i4&v9_FZGjs^oN2@{PNT6(^~e51F25cj-L5gx1*aD0e<{qtTYG$!a@2b-=Iw7!E5&WAf<{3>Y!)Dln~SRj$N9@c4AD zHWm&b`(xH#el*(cu;udb4hi|usa~2<0EG&gKWis+=R=-*lIxAlN!APAHK|yuV;;sE zilnj)r|Y1*l?L=ZPsjQp8DRW)qHjpn=c294aJZ7@4+A~EG&Ay1;O0cXv|F1x zH&wfwh`nB-1;+VL)563(!A;ShFC|Nlfb=bj7 z^5Vrj@6p=$W9xNggutM|UazL+`M49IUZ|^^!4wAD{jAVRs`347@`9kLkCz7LB^|M< zo*QQ~KRy#cnl}~w=%rv~>Z>}rT*$M||M}=ld<#4J6GHoi z8U1Z>aJ)ZQU%33M%w+K8`9PdC;~Et|^g8?wG#-6+6iIyssurZh6~A3EOgtb_>|l(r)Q=KiW7C=4IZ8pJl6PjAqXY zJ~C4Yb6dn!#IO(UV;alkWKtrz`zAFF4qUxJvY&Z1K;>cM=HUjH=F*;WHybC^iuKw=?bE%b zP3iQBaAnU+AXfOOmaP5~>e!D~6u90`T@%vO!QnF@R7Bd{uyqZP&h75kd+!W!!R z&&R|BC@MK-*P6H*PpHP(aj|TE2#M}q{1K89YN6-bA$K~{7sh7V^`@`tJvmiCMe#j) z+Rh4u>?aGoM;*zh{76~(3&0hWtMX#{b=Y}s+N229wP=Tw7}gKU@&e0-bj++sN+lYb z(8;b7^F`UmE6XMuY^0EW*mr6Wv88&jDQLtu#j^45#xJfNHX)W$`;Sp;Sz}&qsei)O&uNnnoWH^!z|nDqkI5%SdjBvi1A`Da8VZdi8H4#_@*6Z0Zr}* za-|PCFBmWI?4oTx3Y0t#wW11pF7;9i4vpdCILW)_Q#Fr|AUhXbGCW;+V!P}=g<#)m zbcic@MpZfR`>6he%rYZ^)ri)2f zPn-b|wUl&^pZepj*!2oFcU=DYeS8s@#Gz{PV>wwHfa8NJag-DSgPo*D-bP}-yOkfX z?e2*R91#hyILWJ>jWb2dugC(n-?#DWSUl7a7(5?TRHVAftSM@;$O3)-u8)E_QPb$w z;O7157|k}TyQtlddW}4si$(h1tBwl`Jrw1qLbJVUrvsNfj)btHFE=V5)J|IL2R(6G zcr=Wib3zY0t~Aa5mh8%K6qho6g67oUm+#X*ma>=ID zmnD=9UnyESz3*|+Olp|BIB0VAecGFg$fkogJoUE($Ehf}+M(7fv2i8wuZ86;&3DZ{IVcdgeUqd^g`?pkQRiKzzu6&v0&LKh zy{MftD)?8IRRzgM-}4wI_AsKfpgrNsvp~4)fcvsl8j>)2jM7aZaqoGq+Vu$B`0P4H zU0!7Sr6C<|4?0UstFw^8rd#=is$H{aXABVchP=-Z_f&bd*K^5PnI$du6bbnvMmwUZ26Hy!`p@G~ zkBYCGJ}J{0`){ZVb-@*3WHa4}o_MhOQBsmbyrLt$0sc}8U!wr|d6CuetZQNco@ zXP?)t%&v$tPWStG!qXKg`wJ?qSMnON-ocknzf!#-&0iEyePuB7(pd{yt{C$P0}<5{ z=c}tOn6iEfs4g2dqR*pFqA;#n!^%e{XS6SoP0?i_kSkC+^}!T{pzQm24@m@*K1iA$ zpEG@c)u2v0l~ZTDkO!b+QcAg_r2c&*1S!JiG((F8TJF`#(t8Rk@+)CLwAU*9X`ZP34aM&L`(Aonj=(8gW%QpDXN zvNSdtZiGI;XhN)*%d^M)HF(o}J-rwzIK&?yyJ1NPoAlf1-A^EeX&kMe>n2^gnrt5- zdwfYLAx~gD3G{=A0`x4shq9PlEm#wBvOdxFRBS;VeK9HnNb=%i0L`NRKzcKzhk!5dI}OzsKLI^bh$rU%_10)5 z(xd$?US^9NPn@Pr-wVHJBAt>cmRM+y-PdCk<`VgW6itis4v@fkYy;Cr-2wKLyDGr} zH8j~7FOp}~ah?Ef!U55MSHW#`&i?V4sfNE34wRe&2B)Gk0bE(nmhx@JB{em#s_J=l z*O`y8lAm9$II!xfa6QOhQL0T;57)M5Hw7i;{nNcoApOltdlqEP*>e3X)8Ep{%}d2k zj*nVF7k8wZmbMSdkDA%oxb0gBrgAB0<@uz&_Zb=oE;_Y42Hgz? z#YxW5!eMBdFja|v{LsJ5##IA=>a5?Aul&ak{p+{==Vgc>*53{Be@5m%BlAxi6zrz0 zD{#}%3fGDet|KDd`ES?FRga%Yx_?q_6(!EP1X8m+IMOmGX{uX zFma)!UCFW+yK;@>iz@dUyM^o*ru6l5U;At47)oFiaX&OvR9-r7&b=UJ_aQr7&&8sF zc&FFpXp7KOXgQFNHS0{2E|@iUt`^QWC$!DIBkFZP8xjm~e8K?7aQ0{g6tP-{u~M+L z{cbi|@ncX)>}9@z-+5N#P`h;wQP2RIm7IJ6u#r~t>BxfNAvL}Qtx0V`)Fb?_X2z1G zFVfVlczXw|ZTuL;+Voq+B<)vT&wGyC?pQSqSeZ4&WSMX>4X(Hx6k$x~?_u6G(qP6j zLKW_4$8Fu$;9E3iuotTi4_D%CHGZr&>E{1s##meUy~X>XfV#!6T6=V=`4%B&Jnmjc zg5T?ok_5E6k9hJDwQ0KE&6Hs}v)P_lRs&F#$omJ@-@-f0f>9xSPs3YR@VfhlHEhR* zZK4Mjs>;eQKp_@bei#w4FT>!AT3lohKo(DZbaJ?%%2o$@9>jA>cyv=16eoF910r+tW}LrXgZf*n8fSf|}_oXiQrh`FxJQ5p8$^0) z9>x6_;P5P9u4D<$JIj{7=UEr@@@qS_{x%t-#YWJIoUlwdO+8y%()=y|;LWjs3qc z7%v-Z_U6k+5ai`5@87*Wsyba#`Z&#Ws55-~VNN~Wk>;6|F9iw%bFm^cA7}g3P?%_Sh<)MBm zz>%izHIFA6`eIKt0CG+Buaeo9~Kuo)CGv;}wbTov2E5Y0g@YYcv1XChv(15cy z%As-I{-iAKp0;z^KfC}uB$X^IfP&sU4FFCDRkUy|sgso{OCm_&MAv7U6s*GGk7t!^ z{>DF8DR6ht!wbJ<+S?5TYZsg@xsQM5Kz9_|z5>qf?Ztd@2zsI+4dGH6+&Nk1iqkK0 z6=H`8My<1{viL`)DK*4on>!<%6K-~h=OG(+4e%zjwh z;f}J^Ku8%6u4+Y>hcC?iMf;?J%N}VLa>CNVqjKAGe#|z?ZtsUS-b`>uEpPsL%+tL3 z(*^>~I@T{k-)FdT�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 SL{BvEM@dfocA1P>$o~Pa+BT~I literal 0 HcmV?d00001 diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json new file mode 100644 index 0000000000..541e019122 --- /dev/null +++ b/plugins/github-deployments/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-github-deployments", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.7.4", + "@backstage/core": "^0.7.1", + "@backstage/theme": "^0.2.4", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@octokit/graphql": "^4.6.1", + "moment": "^2.29.1", + "nock": "^13.0.11", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.6.4", + "@backstage/dev-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.8", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts new file mode 100644 index 0000000000..15ba3e542c --- /dev/null +++ b/plugins/github-deployments/src/api/index.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { createApiRef, DiscoveryApi } from '@backstage/core'; +import { graphql } from '@octokit/graphql'; + +export type GithubDeployment = { + environment: string; + state: string; + updatedAt: string; + commit: { + abbreviatedOid: string; + commitUrl: string; + }; +}; + +export interface GithubDeploymentsApi { + listDeployments(options: { + owner: string; + repo: string; + last: number; + }): Promise; +} + +export const githubDeploymentsApiRef = createApiRef({ + id: 'plugin.github-deployments.service', + description: 'Used by the Github Deployments plugin to make requests', +}); + +export type Options = { + discoveryApi: DiscoveryApi; + proxyPath?: string; +}; + +const deploymentsQuery = ` +query lastDeployments($owner: String!, $repo: String!, $last: Int) { + repository(owner: $owner, name: $repo) { + deployments(last: $last) { + nodes { + state + environment + updatedAt + commit { + abbreviatedOid + commitUrl + } + } + } + } +} +`; + +export class GithubDeploymentsApiClient implements GithubDeploymentsApi { + private readonly discoveryApi: DiscoveryApi; + + constructor(options: Options) { + this.discoveryApi = options.discoveryApi; + } + + private async getProxyUrl() { + return await this.discoveryApi.getBaseUrl('proxy'); + } + + async listDeployments(options: { + owner: string; + repo: string; + last: number; + }): Promise { + const proxyUrl = await this.getProxyUrl(); + const graphQlWithBaseURL = graphql.defaults({ + baseUrl: `${proxyUrl}/github/api`, + }); + + const response: any = await graphQlWithBaseURL(deploymentsQuery, options); + return response.repository?.deployments?.nodes?.reverse() || []; + } +} diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx new file mode 100644 index 0000000000..2dc1b57ad2 --- /dev/null +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 React from 'react'; +import { + ApiProvider, + ApiRegistry, + errorApiRef, + UrlPatternDiscovery, + configApiRef, + ConfigReader, +} from '@backstage/core'; + +import { render } from '@testing-library/react'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; +import { githubDeploymentsPlugin } from '../plugin'; +import { GithubDeploymentsCard } from './GithubDeploymentsCard'; + +import { entityStub, responseStub } from '../mocks/mocks'; + +const discoveryApi = UrlPatternDiscovery.compile('http://exampleapi.com'); +const errorApiMock = { post: jest.fn(), error$: jest.fn() }; + +const apis = ApiRegistry.from([ + [configApiRef, new ConfigReader({})], + [errorApiRef, errorApiMock], + [githubDeploymentsApiRef, new GithubDeploymentsApiClient({ discoveryApi })], +]); + +describe('github-deployments', () => { + const worker = setupServer(); + beforeAll(() => worker.listen()); + afterAll(() => worker.close()); + afterEach(() => worker.resetHandlers()); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('export-plugin', () => { + it('should export plugin', () => { + expect(githubDeploymentsPlugin).toBeDefined(); + }); + }); + + describe('GithubDeploymentsCard', () => { + it('should display fetched data', async () => { + worker.use(rest.post('*', (_, res, ctx) => res(ctx.json(responseStub)))); + + const rendered = render( + + + , + ); + + expect(await rendered.findByText('active')).toBeInTheDocument(); + expect(await rendered.findByText('prd')).toBeInTheDocument(); + expect(await rendered.findByText('12345')).toHaveAttribute( + 'href', + 'https://exampleapi.com/123456789', + ); + + expect(await rendered.findByText('pending')).toBeInTheDocument(); + expect(await rendered.findByText('lab')).toBeInTheDocument(); + expect(await rendered.findByText('54321')).toHaveAttribute( + 'href', + 'https://exampleapi.com/543212345', + ); + }); + }); +}); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx new file mode 100644 index 0000000000..5f6e072bfc --- /dev/null +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 React from 'react'; +import { LinearProgress } from '@material-ui/core'; +import { Entity } from '@backstage/catalog-model'; +import { InfoCard, MissingAnnotationEmptyState, useApi } from '@backstage/core'; +import { useAsync } from 'react-use'; +import { githubDeploymentsApiRef } from '../api'; +import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; + +export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug'; + +export const isGithubDeploymentsAvailable = (entity: Entity) => + Boolean(entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION]); + +const GithubDeploymentsComponent = ({ + entity, + last, +}: { + entity: Entity; + last: number; +}) => { + const api = useApi(githubDeploymentsApiRef); + const annotation = + entity.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] ?? ''; + const [owner, repo] = annotation.split('/'); + + const { loading, value, error } = useAsync( + async () => await api.listDeployments({ owner, repo, last }), + ); + + if (loading) { + return ( + + + + ); + } + if (error) { + return ( + + Error occurred while fetching data. + + ); + } + + return ; +}; + +export const GithubDeploymentsCard = ({ + entity, + last, +}: { + entity: Entity; + last?: number; +}) => { + return !isGithubDeploymentsAvailable(entity) ? ( + + ) : ( + + ); +}; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx new file mode 100644 index 0000000000..1d57fc6ea6 --- /dev/null +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 React from 'react'; +import { Table, TableColumn } from '@backstage/core'; +import { GithubDeployment } from '../../api'; +import moment from 'moment'; +import { Box, Typography, Link } from '@material-ui/core'; + +const lastUpdated = (start: string): string => moment(start).fromNow(); + +const State = ({ value }: { value: string }) => { + const colorMap: Record = { + PENDING: 'orange', + IN_PROGRESS: 'orange', + ACTIVE: 'green', + }; + + return ( + + + {value} + + ); +}; + +const columns: TableColumn[] = [ + { + title: 'Environment', + field: 'environment', + highlight: true, + }, + { + title: 'Status', + field: 'environment', + render: (row: any): React.ReactNode => , + }, + { + title: 'Commit', + render: (row: any): React.ReactNode => ( + + {row.commit.abbreviatedOid} + + ), + }, + { + title: 'Last Updated', + render: (row: any): React.ReactNode => lastUpdated(row.updatedAt), + }, +]; + +type GithubDeploymentsTableProps = { + deployments: GithubDeployment[]; +}; + +const GithubDeploymentsTable = ({ + deployments, +}: GithubDeploymentsTableProps) => { + return ( + + ); +}; + +export default GithubDeploymentsTable; diff --git a/plugins/github-deployments/src/index.ts b/plugins/github-deployments/src/index.ts new file mode 100644 index 0000000000..e9860050d6 --- /dev/null +++ b/plugins/github-deployments/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { + githubDeploymentsPlugin as plugin, + EntityGithubDeploymentsCard, +} from './plugin'; diff --git a/plugins/github-deployments/src/mocks/mocks.ts b/plugins/github-deployments/src/mocks/mocks.ts new file mode 100644 index 0000000000..35c87a1f6b --- /dev/null +++ b/plugins/github-deployments/src/mocks/mocks.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 const entityStub = { + metadata: { + namespace: 'default', + annotations: { + 'github.com/project-slug': 'org/repo', + }, + name: 'sample-service', + description: 'Sample service', + uid: 'g0h33dd9-56h7-835b-b63v-7x5da3j64851', + generation: 1, + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'experimental', + }, + relations: [], +}; + +export const responseStub = { + data: { + repository: { + deployments: { + nodes: [ + { + state: 'active', + environment: 'prd', + updatedAt: '2021-03-25T12:08:45Z', + commit: { + commitUrl: 'https://exampleapi.com/123456789', + abbreviatedOid: '12345', + }, + }, + { + state: 'pending', + environment: 'lab', + updatedAt: '2021-03-25T12:08:47Z', + commit: { + commitUrl: 'https://exampleapi.com/543212345', + abbreviatedOid: '54321', + }, + }, + ], + }, + }, + }, +}; + +export const noDataResponseStub = { + data: {}, +}; diff --git a/plugins/github-deployments/src/plugin.test.ts b/plugins/github-deployments/src/plugin.test.ts new file mode 100644 index 0000000000..cac5d58c63 --- /dev/null +++ b/plugins/github-deployments/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { githubDeploymentsPlugin } from './plugin'; + +describe('graphiql', () => { + it('should export plugin', () => { + expect(githubDeploymentsPlugin).toBeDefined(); + }); +}); diff --git a/plugins/github-deployments/src/plugin.ts b/plugins/github-deployments/src/plugin.ts new file mode 100644 index 0000000000..2985688785 --- /dev/null +++ b/plugins/github-deployments/src/plugin.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { + createApiFactory, + createComponentExtension, + createPlugin, + discoveryApiRef, +} from '@backstage/core'; +import { githubDeploymentsApiRef, GithubDeploymentsApiClient } from './api'; + +export const githubDeploymentsPlugin = createPlugin({ + id: 'github-deployments', + apis: [ + createApiFactory({ + api: githubDeploymentsApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory: ({ discoveryApi }) => + new GithubDeploymentsApiClient({ discoveryApi }), + }), + ], +}); + +export const EntityGithubDeploymentsCard = githubDeploymentsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/GithubDeploymentsCard').then( + m => m.GithubDeploymentsCard, + ), + }, + }), +); diff --git a/plugins/github-deployments/src/setupTests.ts b/plugins/github-deployments/src/setupTests.ts new file mode 100644 index 0000000000..0cec5b395d --- /dev/null +++ b/plugins/github-deployments/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index a417d89adb..9c608c113d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4290,6 +4290,15 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" +"@octokit/graphql@^4.6.1": + version "4.6.1" + resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.1.tgz#f975486a46c94b7dbe58a0ca751935edc7e32cc9" + integrity sha512-2lYlvf4YTDgZCTXTW4+OX+9WTLFtEUc6hGm4qM1nlZjzxj+arizM4aHWzBVBCxY9glh7GIs0WEuiSgbVzv8cmA== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + "@octokit/openapi-types@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.2.0.tgz#123e0438a0bc718ccdac3b5a2e69b3dd00daa85b" @@ -17940,6 +17949,11 @@ lodash.once@^4.0.0, lodash.once@^4.1.1: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= +lodash.set@^4.3.2: + version "4.3.2" + resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -19278,6 +19292,16 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +nock@^13.0.11: + version "13.0.11" + resolved "https://registry.npmjs.org/nock/-/nock-13.0.11.tgz#ba733252e720897ca50033205c39db0c7470f331" + integrity sha512-sKZltNkkWblkqqPAsjYW0bm3s9DcHRPiMOyKO/PkfJ+ANHZ2+LA2PLe22r4lLrKgXaiSaDQwW3qGsJFtIpQIeQ== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + lodash.set "^4.3.2" + propagate "^2.0.0" + node-abi@^2.7.0: version "2.19.3" resolved "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d" @@ -21648,6 +21672,11 @@ prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, object-assign "^4.1.1" react-is "^16.8.1" +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + property-expr@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330" From 29e68d271bc9b27bcd1721d7c700b8ab7f3e899d Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 25 Mar 2021 17:41:31 +0000 Subject: [PATCH 02/19] update packages Signed-off-by: Andrew Johnson --- plugins/github-deployments/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 541e019122..99e171bb8a 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.4", - "@backstage/core": "^0.7.1", + "@backstage/core": "^0.7.2", "@backstage/theme": "^0.2.4", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -34,9 +34,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.4", + "@backstage/cli": "^0.6.5", "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.8", + "@backstage/test-utils": "^0.1.9", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", From 1da2c481d20cafd775fffe361aed34e3179bf389 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 26 Mar 2021 11:30:14 +0000 Subject: [PATCH 03/19] useEntity and more tests Signed-off-by: Andrew Johnson --- plugins/github-deployments/README.md | 10 +- plugins/github-deployments/package.json | 2 +- plugins/github-deployments/src/api/index.ts | 2 +- .../components/GithubDeploymentsCard.test.tsx | 35 +++++- .../src/components/GithubDeploymentsCard.tsx | 44 +++++--- .../GithubDeploymentsTable.tsx | 3 +- plugins/github-deployments/src/mocks/mocks.ts | 32 +++--- yarn.lock | 103 ++++++++++++++---- 8 files changed, 166 insertions(+), 65 deletions(-) diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index 2c4a4cb49a..c2c56b02e5 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -1,12 +1,12 @@ -# Github Deployments Plugin +# GitHub Deployments Plugin -The Github Deployments Plugin displays recent deployments from Github. +The GitHub Deployments Plugin displays recent deployments from GitHub. ![github-deployments-card](./docs/github-deployments-card.png) ## Getting Started -1. Install the Github Deployments Plugin +1. Install the GitHub Deployments Plugin ```bash # packages/app @@ -14,7 +14,7 @@ The Github Deployments Plugin displays recent deployments from Github. yarn add @backstage/plugin-github-deployments ``` -2. Add proxy and auth token for Github +2. Add proxy and auth token for GitHub ```yaml # app-config.yaml @@ -57,7 +57,7 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( ); ``` -5. Add the github.com/project-slug annotation to your catalog-info.yaml file: +5. Add the `github.com/project-slug` annotation to your `catalog-info.yaml` file: ```yaml apiVersion: backstage.io/v1alpha1 diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 99e171bb8a..d83b2c8abd 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -22,13 +22,13 @@ "dependencies": { "@backstage/catalog-model": "^0.7.4", "@backstage/core": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.1.3", "@backstage/theme": "^0.2.4", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/graphql": "^4.6.1", "moment": "^2.29.1", - "nock": "^13.0.11", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^15.3.3" diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index 15ba3e542c..4198776b57 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -36,7 +36,7 @@ export interface GithubDeploymentsApi { export const githubDeploymentsApiRef = createApiRef({ id: 'plugin.github-deployments.service', - description: 'Used by the Github Deployments plugin to make requests', + description: 'Used by the GitHub Deployments plugin to make requests', }); export type Options = { diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 2dc1b57ad2..e933592355 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -30,7 +30,14 @@ import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; import { githubDeploymentsPlugin } from '../plugin'; import { GithubDeploymentsCard } from './GithubDeploymentsCard'; -import { entityStub, responseStub } from '../mocks/mocks'; +import { entityStub, noDataResponseStub, responseStub } from '../mocks/mocks'; +import { wrapInTestApp } from '@backstage/test-utils'; + +jest.mock('@backstage/plugin-catalog-react', () => ({ + useEntity: () => { + return entityStub; + }, +})); const discoveryApi = UrlPatternDiscovery.compile('http://exampleapi.com'); const errorApiMock = { post: jest.fn(), error$: jest.fn() }; @@ -62,9 +69,11 @@ describe('github-deployments', () => { worker.use(rest.post('*', (_, res, ctx) => res(ctx.json(responseStub)))); const rendered = render( - - - , + wrapInTestApp( + + + , + ), ); expect(await rendered.findByText('active')).toBeInTheDocument(); @@ -81,5 +90,23 @@ describe('github-deployments', () => { 'https://exampleapi.com/543212345', ); }); + + it('should display empty state when no data', async () => { + worker.use( + rest.post('*', (_, res, ctx) => res(ctx.json(noDataResponseStub))), + ); + + const rendered = render( + wrapInTestApp( + + + , + ), + ); + + expect( + await rendered.findByText('No GitHub deployments available.'), + ).toBeInTheDocument(); + }); }); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 5f6e072bfc..7425370fe7 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -16,10 +16,16 @@ import React from 'react'; import { LinearProgress } from '@material-ui/core'; import { Entity } from '@backstage/catalog-model'; -import { InfoCard, MissingAnnotationEmptyState, useApi } from '@backstage/core'; +import { + EmptyState, + InfoCard, + MissingAnnotationEmptyState, + useApi, +} from '@backstage/core'; import { useAsync } from 'react-use'; import { githubDeploymentsApiRef } from '../api'; import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; +import { useEntity } from '@backstage/plugin-catalog-react'; export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug'; @@ -27,16 +33,14 @@ export const isGithubDeploymentsAvailable = (entity: Entity) => Boolean(entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION]); const GithubDeploymentsComponent = ({ - entity, + projectSlug, last, }: { - entity: Entity; + projectSlug: string; last: number; }) => { const api = useApi(githubDeploymentsApiRef); - const annotation = - entity.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] ?? ''; - const [owner, repo] = annotation.split('/'); + const [owner, repo] = projectSlug.split('/'); const { loading, value, error } = useAsync( async () => await api.listDeployments({ owner, repo, last }), @@ -44,32 +48,38 @@ const GithubDeploymentsComponent = ({ if (loading) { return ( - + ); } if (error) { return ( - - Error occurred while fetching data. + + Error occured while fetching Data ); } + if (!value || value.length === 0) { + return ( + + ); + } return ; }; -export const GithubDeploymentsCard = ({ - entity, - last, -}: { - entity: Entity; - last?: number; -}) => { +export const GithubDeploymentsCard = ({ last }: { last?: number }) => { + const { entity } = useEntity(); + return !isGithubDeploymentsAvailable(entity) ? ( ) : ( - + ); }; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index 1d57fc6ea6..a7b1c5bf9e 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -53,7 +53,6 @@ const columns: TableColumn[] = [ }, { title: 'Status', - field: 'environment', render: (row: any): React.ReactNode => , }, { @@ -81,7 +80,7 @@ const GithubDeploymentsTable = ({
); diff --git a/plugins/github-deployments/src/mocks/mocks.ts b/plugins/github-deployments/src/mocks/mocks.ts index 35c87a1f6b..6547c8ef4a 100644 --- a/plugins/github-deployments/src/mocks/mocks.ts +++ b/plugins/github-deployments/src/mocks/mocks.ts @@ -14,23 +14,25 @@ * limitations under the License. */ export const entityStub = { - metadata: { - namespace: 'default', - annotations: { - 'github.com/project-slug': 'org/repo', + entity: { + metadata: { + namespace: 'default', + annotations: { + 'github.com/project-slug': 'org/repo', + }, + name: 'sample-service', + description: 'Sample service', + uid: 'g0h33dd9-56h7-835b-b63v-7x5da3j64851', + generation: 1, }, - name: 'sample-service', - description: 'Sample service', - uid: 'g0h33dd9-56h7-835b-b63v-7x5da3j64851', - generation: 1, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + type: 'service', + lifecycle: 'experimental', + }, + relations: [], }, - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - spec: { - type: 'service', - lifecycle: 'experimental', - }, - relations: [], }; export const responseStub = { diff --git a/yarn.lock b/yarn.lock index 9c608c113d..94b4d959f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1846,6 +1846,23 @@ uuid "^8.0.0" yup "^0.29.3" +"@backstage/core-api@^0.2.15": + version "0.2.15" + resolved "https://registry.npmjs.org/@backstage/core-api/-/core-api-0.2.15.tgz#24c3e4b1fe0bdee36de26c50b8757f41bb266109" + integrity sha512-OQTmvCGM3hkaLV0+JPF85dZk/X1SXoIFkXZi9pO/DzBPZ1Psuptia9xNXrhnEeYJaCazlCnOCTXS/kdoT1zBVg== + dependencies: + "@backstage/config" "^0.1.4" + "@backstage/theme" "^0.2.5" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@types/prop-types" "^15.7.3" + "@types/react" "^16.9" + prop-types "^15.7.2" + react "^16.12.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + zen-observable "^0.8.15" + "@backstage/core@^0.3.0": version "0.7.3" dependencies: @@ -1972,6 +1989,65 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/core@^0.7.3": + version "0.7.3" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.7.3.tgz#a55f094f0aceab4102ea08a75832c2ec5a70e226" + integrity sha512-uKTCYYAYVtmdOiJrNKe00N0AKeEhxZ+y3qQVY+H96WN9QDql/56XTmoiyPjDU7wK4nDj+yGV4j+xM5uxSXaOCg== + dependencies: + "@backstage/config" "^0.1.4" + "@backstage/core-api" "^0.2.15" + "@backstage/errors" "^0.1.1" + "@backstage/theme" "^0.2.5" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@testing-library/react-hooks" "^3.4.2" + "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" + "@types/react" "^16.9" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-zoom "^2.0.0" + dagre "^0.8.5" + immer "^8.0.1" + lodash "^4.17.15" + material-table "^1.69.1" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^6.6.0" + react-markdown "^5.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^15.4.3" + react-text-truncate "^0.16.0" + react-use "^15.3.3" + remark-gfm "^1.0.0" + zen-observable "^0.8.15" + +"@backstage/plugin-catalog-react@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.1.3.tgz#e4cc6c40616976737893cc91d7e373b9397618d1" + integrity sha512-nNIz+tc72UtwB66L++xA9drv5VV0SxMPh0BNLOFAHAllcR2RMQyvWpDq4uBupUXx/lr6bzihngkW9Jqr4rfz/Q== + dependencies: + "@backstage/catalog-client" "^0.3.8" + "@backstage/catalog-model" "^0.7.3" + "@backstage/core" "^0.7.3" + "@material-ui/core" "^4.11.0" + "@types/react" "^16.9" + react "^16.13.1" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + "@backstage/plugin-catalog@^0.2.1": version "0.5.1" dependencies: @@ -2020,6 +2096,13 @@ react-use "^15.3.3" swr "^0.3.0" +"@backstage/theme@^0.2.5": + version "0.2.5" + resolved "https://registry.npmjs.org/@backstage/theme/-/theme-0.2.5.tgz#657e2ceaaede1b4dbe8abeafa6e168671a27b6da" + integrity sha512-GT7Ok9QSiZWXkxka+vcpweMrOg1lgDkRRjh7aSU97Mcoiq77szdO45o0hZtQChz8qay60vCRlu57ADvUn4QxZw== + dependencies: + "@material-ui/core" "^4.11.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -17949,11 +18032,6 @@ lodash.once@^4.0.0, lodash.once@^4.1.1: resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -19292,16 +19370,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -nock@^13.0.11: - version "13.0.11" - resolved "https://registry.npmjs.org/nock/-/nock-13.0.11.tgz#ba733252e720897ca50033205c39db0c7470f331" - integrity sha512-sKZltNkkWblkqqPAsjYW0bm3s9DcHRPiMOyKO/PkfJ+ANHZ2+LA2PLe22r4lLrKgXaiSaDQwW3qGsJFtIpQIeQ== - dependencies: - debug "^4.1.0" - json-stringify-safe "^5.0.1" - lodash.set "^4.3.2" - propagate "^2.0.0" - node-abi@^2.7.0: version "2.19.3" resolved "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d" @@ -21672,11 +21740,6 @@ prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, object-assign "^4.1.1" react-is "^16.8.1" -propagate@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" - integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== - property-expr@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330" From 6e09602f4e0633602542682e0398692661c4a019 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 26 Mar 2021 12:09:56 +0000 Subject: [PATCH 04/19] package bump Signed-off-by: Andrew Johnson --- plugins/github-deployments/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index d83b2c8abd..c030d33322 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -21,9 +21,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.7.4", - "@backstage/core": "^0.7.2", + "@backstage/core": "^0.7.3", "@backstage/plugin-catalog-react": "^0.1.3", - "@backstage/theme": "^0.2.4", + "@backstage/theme": "^0.2.5", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,7 +34,7 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.5", + "@backstage/cli": "^0.6.6", "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.9", "@testing-library/jest-dom": "^5.10.1", From d2c148b319a06362d53e3b3d4572907e3b9f62cc Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 26 Mar 2021 16:46:47 +0000 Subject: [PATCH 05/19] use client github auth token Signed-off-by: Andrew Johnson --- plugins/github-deployments/README.md | 37 ++++++++++--------- plugins/github-deployments/src/api/index.ts | 32 ++++++++++------ .../components/GithubDeploymentsCard.test.tsx | 15 ++++++-- plugins/github-deployments/src/plugin.ts | 9 +++-- 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index c2c56b02e5..8658eb5985 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -6,7 +6,16 @@ The GitHub Deployments Plugin displays recent deployments from GitHub. ## Getting Started -1. Install the GitHub Deployments Plugin +1. Provide OAuth credentials: + +- Create an [OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) and set env variables for ID and Secret. + +```bash +export AUTH_GITHUB_CLIENT_ID={{YOUR_CLIENT_ID}} +export AUTH_CLIENT_SECRET={{YOUR_CLIENT_SECRET}} +``` + +2. Install the GitHub Deployments Plugin. ```bash # packages/app @@ -14,24 +23,16 @@ The GitHub Deployments Plugin displays recent deployments from GitHub. yarn add @backstage/plugin-github-deployments ``` -2. Add proxy and auth token for GitHub +3. If you use GitHub enterprise then set the host in `app-config.yaml`. ```yaml # app-config.yaml - -proxy: - ... - '/github/api': - target: https://api.github.com - changeOrigin: true - secure: true - headers: - Authorization: - # Content: 'token OAUTH-TOKEN' - $env: GITHUB_OAUTH_TOKEN +integrations: + github: + - host: { { YOUR_GITHUB_HOST_URL } } ``` -3. Add the plugin to the app +4. Add the plugin to the app ```typescript // packages/app/src/plugins.ts @@ -39,25 +40,25 @@ proxy: export { plugin as GithubDeployments } from '@backstage/plugin-github-deployments'; ``` -4. Add the ... to the EntityPage: +5. Add the `EntityGithubDeploymentsCard` to the EntityPage: ```typescript // packages/app/src/components/catalog/EntityPage.tsx import { EntityGithubDeploymentsCard } from '@backstage/plugin-github-deployments'; -const OverviewContent = ({ entity }: { entity: Entity }) => ( +const OverviewContent = () => ( // ... - + // ... ); ``` -5. Add the `github.com/project-slug` annotation to your `catalog-info.yaml` file: +6. Add the `github.com/project-slug` annotation to your `catalog-info.yaml` file: ```yaml apiVersion: backstage.io/v1alpha1 diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index 4198776b57..3942d43f37 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createApiRef, DiscoveryApi } from '@backstage/core'; +import { ConfigApi, createApiRef, OAuthApi } from '@backstage/core'; import { graphql } from '@octokit/graphql'; export type GithubDeployment = { @@ -40,8 +40,8 @@ export const githubDeploymentsApiRef = createApiRef({ }); export type Options = { - discoveryApi: DiscoveryApi; - proxyPath?: string; + configApi: ConfigApi; + githubAuthApi: OAuthApi; }; const deploymentsQuery = ` @@ -63,14 +63,19 @@ query lastDeployments($owner: String!, $repo: String!, $last: Int) { `; export class GithubDeploymentsApiClient implements GithubDeploymentsApi { - private readonly discoveryApi: DiscoveryApi; + private readonly configApi: ConfigApi; + private readonly githubAuthApi: OAuthApi; constructor(options: Options) { - this.discoveryApi = options.discoveryApi; + this.configApi = options.configApi; + this.githubAuthApi = options.githubAuthApi; } - private async getProxyUrl() { - return await this.discoveryApi.getBaseUrl('proxy'); + private getBaseUrl() { + const providerConfigs = + this.configApi.getOptionalConfigArray('integrations.github') ?? []; + const targetProviderConfig = providerConfigs[0]; + return targetProviderConfig?.getOptionalString('apiBaseUrl'); } async listDeployments(options: { @@ -78,12 +83,17 @@ export class GithubDeploymentsApiClient implements GithubDeploymentsApi { repo: string; last: number; }): Promise { - const proxyUrl = await this.getProxyUrl(); - const graphQlWithBaseURL = graphql.defaults({ - baseUrl: `${proxyUrl}/github/api`, + const token = await this.githubAuthApi.getAccessToken(['repo']); + const baseUrl = this.getBaseUrl() || 'https://api.github.com'; + + const graphQLWithAuth = graphql.defaults({ + baseUrl, + headers: { + authorization: `token ${token}`, + }, }); - const response: any = await graphQlWithBaseURL(deploymentsQuery, options); + const response: any = await graphQLWithAuth(deploymentsQuery, options); return response.repository?.deployments?.nodes?.reverse() || []; } } diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index e933592355..73fbbf1dad 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -21,6 +21,8 @@ import { UrlPatternDiscovery, configApiRef, ConfigReader, + ConfigApi, + OAuthApi, } from '@backstage/core'; import { render } from '@testing-library/react'; @@ -39,13 +41,20 @@ jest.mock('@backstage/plugin-catalog-react', () => ({ }, })); -const discoveryApi = UrlPatternDiscovery.compile('http://exampleapi.com'); const errorApiMock = { post: jest.fn(), error$: jest.fn() }; +const configApi: ConfigApi = new ConfigReader({}); +const githubAuthApi: OAuthApi = { + getAccessToken: async _ => 'access_token', +}; + const apis = ApiRegistry.from([ - [configApiRef, new ConfigReader({})], + [configApiRef, configApi], [errorApiRef, errorApiMock], - [githubDeploymentsApiRef, new GithubDeploymentsApiClient({ discoveryApi })], + [ + githubDeploymentsApiRef, + new GithubDeploymentsApiClient({ configApi, githubAuthApi }), + ], ]); describe('github-deployments', () => { diff --git a/plugins/github-deployments/src/plugin.ts b/plugins/github-deployments/src/plugin.ts index 2985688785..438f2f70d2 100644 --- a/plugins/github-deployments/src/plugin.ts +++ b/plugins/github-deployments/src/plugin.ts @@ -14,10 +14,11 @@ * limitations under the License. */ import { + configApiRef, createApiFactory, createComponentExtension, createPlugin, - discoveryApiRef, + githubAuthApiRef, } from '@backstage/core'; import { githubDeploymentsApiRef, GithubDeploymentsApiClient } from './api'; @@ -26,9 +27,9 @@ export const githubDeploymentsPlugin = createPlugin({ apis: [ createApiFactory({ api: githubDeploymentsApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => - new GithubDeploymentsApiClient({ discoveryApi }), + deps: { configApi: configApiRef, githubAuthApi: githubAuthApiRef }, + factory: ({ configApi, githubAuthApi }) => + new GithubDeploymentsApiClient({ configApi, githubAuthApi }), }), ], }); From 1a1c16317a89ac6dca76244d3102e690f159b8fe Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 26 Mar 2021 16:48:13 +0000 Subject: [PATCH 06/19] remove unneeded imports Signed-off-by: Andrew Johnson --- .../src/components/GithubDeploymentsCard.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 73fbbf1dad..7357e96b03 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -18,7 +18,6 @@ import { ApiProvider, ApiRegistry, errorApiRef, - UrlPatternDiscovery, configApiRef, ConfigReader, ConfigApi, From 1f635326457db71cd86514697e644e65f3f00848 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 26 Mar 2021 18:53:39 +0000 Subject: [PATCH 07/19] better 'no deployments' state Signed-off-by: Andrew Johnson --- .../components/GithubDeploymentsCard.test.tsx | 2 +- .../src/components/GithubDeploymentsCard.tsx | 5 ----- .../GithubDeploymentsTable.tsx | 19 ++++++++++++++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 7357e96b03..7a022e99b2 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -113,7 +113,7 @@ describe('github-deployments', () => { ); expect( - await rendered.findByText('No GitHub deployments available.'), + await rendered.findByText('No deployments found for this entity.'), ).toBeInTheDocument(); }); }); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 7425370fe7..4a1c4fcfbb 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -60,11 +60,6 @@ const GithubDeploymentsComponent = ({ ); } - if (!value || value.length === 0) { - return ( - - ); - } return ; }; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index a7b1c5bf9e..8f4806779d 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -17,7 +17,15 @@ import React from 'react'; import { Table, TableColumn } from '@backstage/core'; import { GithubDeployment } from '../../api'; import moment from 'moment'; -import { Box, Typography, Link } from '@material-ui/core'; +import { Box, Typography, Link, makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); const lastUpdated = (start: string): string => moment(start).fromNow(); @@ -76,12 +84,21 @@ type GithubDeploymentsTableProps = { const GithubDeploymentsTable = ({ deployments, }: GithubDeploymentsTableProps) => { + const classes = useStyles(); + return (
+ + No deployments found for this entity. + + + } /> ); }; From 160386633a4ceae209d0a504578d4ee2f3b99879 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 26 Mar 2021 18:58:18 +0000 Subject: [PATCH 08/19] remove import Signed-off-by: Andrew Johnson --- .../src/components/GithubDeploymentsCard.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 4a1c4fcfbb..07784a951a 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -16,12 +16,7 @@ import React from 'react'; import { LinearProgress } from '@material-ui/core'; import { Entity } from '@backstage/catalog-model'; -import { - EmptyState, - InfoCard, - MissingAnnotationEmptyState, - useApi, -} from '@backstage/core'; +import { InfoCard, MissingAnnotationEmptyState, useApi } from '@backstage/core'; import { useAsync } from 'react-use'; import { githubDeploymentsApiRef } from '../api'; import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; From 5982e318a260f904d073ff37d9c121922e389cbd Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 29 Mar 2021 17:01:42 +0100 Subject: [PATCH 09/19] add router Signed-off-by: Andrew Johnson --- plugins/github-deployments/src/Router.tsx | 21 +++++++++++++++++++ .../src/components/GithubDeploymentsCard.tsx | 12 +++++------ plugins/github-deployments/src/index.ts | 1 + 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 plugins/github-deployments/src/Router.tsx diff --git a/plugins/github-deployments/src/Router.tsx b/plugins/github-deployments/src/Router.tsx new file mode 100644 index 0000000000..e48f17b75f --- /dev/null +++ b/plugins/github-deployments/src/Router.tsx @@ -0,0 +1,21 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { Entity } from '@backstage/catalog-model'; + +export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug'; + +export const isGithubDeploymentsAvailable = (entity: Entity) => + Boolean(entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION]); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index 07784a951a..ebbe426caf 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -15,17 +15,15 @@ */ import React from 'react'; import { LinearProgress } from '@material-ui/core'; -import { Entity } from '@backstage/catalog-model'; import { InfoCard, MissingAnnotationEmptyState, useApi } from '@backstage/core'; import { useAsync } from 'react-use'; import { githubDeploymentsApiRef } from '../api'; -import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; import { useEntity } from '@backstage/plugin-catalog-react'; - -export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug'; - -export const isGithubDeploymentsAvailable = (entity: Entity) => - Boolean(entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION]); +import { + GITHUB_PROJECT_SLUG_ANNOTATION, + isGithubDeploymentsAvailable, +} from '../Router'; +import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable'; const GithubDeploymentsComponent = ({ projectSlug, diff --git a/plugins/github-deployments/src/index.ts b/plugins/github-deployments/src/index.ts index e9860050d6..d1bff4e186 100644 --- a/plugins/github-deployments/src/index.ts +++ b/plugins/github-deployments/src/index.ts @@ -17,3 +17,4 @@ export { githubDeploymentsPlugin as plugin, EntityGithubDeploymentsCard, } from './plugin'; +export { isGithubDeploymentsAvailable as isPluginApplicableToEntity } from './Router'; From 9286519051c65eff0126bfaa9808a968a7b33788 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 10:24:52 +0100 Subject: [PATCH 10/19] use msw/graphql and use typed respomse Signed-off-by: Andrew Johnson --- plugins/github-deployments/src/api/index.ts | 15 +++++- .../components/GithubDeploymentsCard.test.tsx | 12 +++-- plugins/github-deployments/src/mocks/mocks.ts | 50 +++++++++---------- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index 3942d43f37..32b7f19a29 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -45,7 +45,7 @@ export type Options = { }; const deploymentsQuery = ` -query lastDeployments($owner: String!, $repo: String!, $last: Int) { +query deployments($owner: String!, $repo: String!, $last: Int) { repository(owner: $owner, name: $repo) { deployments(last: $last) { nodes { @@ -62,6 +62,14 @@ query lastDeployments($owner: String!, $repo: String!, $last: Int) { } `; +export type QueryResponse = { + repository?: { + deployments?: { + nodes?: GithubDeployment[]; + }; + }; +}; + export class GithubDeploymentsApiClient implements GithubDeploymentsApi { private readonly configApi: ConfigApi; private readonly githubAuthApi: OAuthApi; @@ -93,7 +101,10 @@ export class GithubDeploymentsApiClient implements GithubDeploymentsApi { }, }); - const response: any = await graphQLWithAuth(deploymentsQuery, options); + const response: QueryResponse = await graphQLWithAuth( + deploymentsQuery, + options, + ); return response.repository?.deployments?.nodes?.reverse() || []; } } diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 7a022e99b2..fa13e12cd2 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -25,7 +25,6 @@ import { } from '@backstage/core'; import { render } from '@testing-library/react'; -import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; import { githubDeploymentsPlugin } from '../plugin'; @@ -33,6 +32,7 @@ import { GithubDeploymentsCard } from './GithubDeploymentsCard'; import { entityStub, noDataResponseStub, responseStub } from '../mocks/mocks'; import { wrapInTestApp } from '@backstage/test-utils'; +import { graphql } from 'msw'; jest.mock('@backstage/plugin-catalog-react', () => ({ useEntity: () => { @@ -74,7 +74,11 @@ describe('github-deployments', () => { describe('GithubDeploymentsCard', () => { it('should display fetched data', async () => { - worker.use(rest.post('*', (_, res, ctx) => res(ctx.json(responseStub)))); + worker.use( + graphql.query('deployments', (_, res, ctx) => + res(ctx.data(responseStub)), + ), + ); const rendered = render( wrapInTestApp( @@ -101,7 +105,9 @@ describe('github-deployments', () => { it('should display empty state when no data', async () => { worker.use( - rest.post('*', (_, res, ctx) => res(ctx.json(noDataResponseStub))), + graphql.query('deployments', (_, res, ctx) => + res(ctx.data(noDataResponseStub)), + ), ); const rendered = render( diff --git a/plugins/github-deployments/src/mocks/mocks.ts b/plugins/github-deployments/src/mocks/mocks.ts index 6547c8ef4a..a66317dec8 100644 --- a/plugins/github-deployments/src/mocks/mocks.ts +++ b/plugins/github-deployments/src/mocks/mocks.ts @@ -1,3 +1,5 @@ +import { QueryResponse } from '../api'; + /* * Copyright 2021 Spotify AB * @@ -35,35 +37,31 @@ export const entityStub = { }, }; -export const responseStub = { - data: { - repository: { - deployments: { - nodes: [ - { - state: 'active', - environment: 'prd', - updatedAt: '2021-03-25T12:08:45Z', - commit: { - commitUrl: 'https://exampleapi.com/123456789', - abbreviatedOid: '12345', - }, +export const responseStub: QueryResponse = { + repository: { + deployments: { + nodes: [ + { + state: 'active', + environment: 'prd', + updatedAt: '2021-03-25T12:08:45Z', + commit: { + commitUrl: 'https://exampleapi.com/123456789', + abbreviatedOid: '12345', }, - { - state: 'pending', - environment: 'lab', - updatedAt: '2021-03-25T12:08:47Z', - commit: { - commitUrl: 'https://exampleapi.com/543212345', - abbreviatedOid: '54321', - }, + }, + { + state: 'pending', + environment: 'lab', + updatedAt: '2021-03-25T12:08:47Z', + commit: { + commitUrl: 'https://exampleapi.com/543212345', + abbreviatedOid: '54321', }, - ], - }, + }, + ], }, }, }; -export const noDataResponseStub = { - data: {}, -}; +export const noDataResponseStub: QueryResponse = {}; From f3f56dda8dac0186ad34d0d3618407e7ef6a837c Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 10:59:24 +0100 Subject: [PATCH 11/19] remove config baseUrl Signed-off-by: Andrew Johnson --- plugins/github-deployments/README.md | 15 +++------------ plugins/github-deployments/src/api/index.ts | 14 +------------- .../src/components/GithubDeploymentsCard.test.tsx | 5 +---- plugins/github-deployments/src/plugin.ts | 7 +++---- 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index 8658eb5985..46d54cac57 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -23,16 +23,7 @@ export AUTH_CLIENT_SECRET={{YOUR_CLIENT_SECRET}} yarn add @backstage/plugin-github-deployments ``` -3. If you use GitHub enterprise then set the host in `app-config.yaml`. - -```yaml -# app-config.yaml -integrations: - github: - - host: { { YOUR_GITHUB_HOST_URL } } -``` - -4. Add the plugin to the app +3. Add the plugin to the app ```typescript // packages/app/src/plugins.ts @@ -40,7 +31,7 @@ integrations: export { plugin as GithubDeployments } from '@backstage/plugin-github-deployments'; ``` -5. Add the `EntityGithubDeploymentsCard` to the EntityPage: +4. Add the `EntityGithubDeploymentsCard` to the EntityPage: ```typescript // packages/app/src/components/catalog/EntityPage.tsx @@ -58,7 +49,7 @@ const OverviewContent = () => ( ); ``` -6. Add the `github.com/project-slug` annotation to your `catalog-info.yaml` file: +5. Add the `github.com/project-slug` annotation to your `catalog-info.yaml` file: ```yaml apiVersion: backstage.io/v1alpha1 diff --git a/plugins/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index 32b7f19a29..ca69ac3853 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ConfigApi, createApiRef, OAuthApi } from '@backstage/core'; +import { createApiRef, OAuthApi } from '@backstage/core'; import { graphql } from '@octokit/graphql'; export type GithubDeployment = { @@ -40,7 +40,6 @@ export const githubDeploymentsApiRef = createApiRef({ }); export type Options = { - configApi: ConfigApi; githubAuthApi: OAuthApi; }; @@ -71,31 +70,20 @@ export type QueryResponse = { }; export class GithubDeploymentsApiClient implements GithubDeploymentsApi { - private readonly configApi: ConfigApi; private readonly githubAuthApi: OAuthApi; constructor(options: Options) { - this.configApi = options.configApi; this.githubAuthApi = options.githubAuthApi; } - private getBaseUrl() { - const providerConfigs = - this.configApi.getOptionalConfigArray('integrations.github') ?? []; - const targetProviderConfig = providerConfigs[0]; - return targetProviderConfig?.getOptionalString('apiBaseUrl'); - } - async listDeployments(options: { owner: string; repo: string; last: number; }): Promise { const token = await this.githubAuthApi.getAccessToken(['repo']); - const baseUrl = this.getBaseUrl() || 'https://api.github.com'; const graphQLWithAuth = graphql.defaults({ - baseUrl, headers: { authorization: `token ${token}`, }, diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index fa13e12cd2..962c08d1cd 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -50,10 +50,7 @@ const githubAuthApi: OAuthApi = { const apis = ApiRegistry.from([ [configApiRef, configApi], [errorApiRef, errorApiMock], - [ - githubDeploymentsApiRef, - new GithubDeploymentsApiClient({ configApi, githubAuthApi }), - ], + [githubDeploymentsApiRef, new GithubDeploymentsApiClient({ githubAuthApi })], ]); describe('github-deployments', () => { diff --git a/plugins/github-deployments/src/plugin.ts b/plugins/github-deployments/src/plugin.ts index 438f2f70d2..3707b1cf68 100644 --- a/plugins/github-deployments/src/plugin.ts +++ b/plugins/github-deployments/src/plugin.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { - configApiRef, createApiFactory, createComponentExtension, createPlugin, @@ -27,9 +26,9 @@ export const githubDeploymentsPlugin = createPlugin({ apis: [ createApiFactory({ api: githubDeploymentsApiRef, - deps: { configApi: configApiRef, githubAuthApi: githubAuthApiRef }, - factory: ({ configApi, githubAuthApi }) => - new GithubDeploymentsApiClient({ configApi, githubAuthApi }), + deps: { githubAuthApi: githubAuthApiRef }, + factory: ({ githubAuthApi }) => + new GithubDeploymentsApiClient({ githubAuthApi }), }), ], }); From 4b00af9b310adaa45dbf8952e91f8abfb0b714a4 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 13:51:31 +0100 Subject: [PATCH 12/19] use type for row Signed-off-by: Andrew Johnson --- .../GithubDeploymentsTable/GithubDeploymentsTable.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index 8f4806779d..d7bc8e90b3 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -53,7 +53,7 @@ const State = ({ value }: { value: string }) => { ); }; -const columns: TableColumn[] = [ +const columns: TableColumn[] = [ { title: 'Environment', field: 'environment', @@ -61,11 +61,13 @@ const columns: TableColumn[] = [ }, { title: 'Status', - render: (row: any): React.ReactNode => , + render: (row: GithubDeployment): React.ReactNode => ( + + ), }, { title: 'Commit', - render: (row: any): React.ReactNode => ( + render: (row: GithubDeployment): React.ReactNode => ( {row.commit.abbreviatedOid} @@ -73,7 +75,8 @@ const columns: TableColumn[] = [ }, { title: 'Last Updated', - render: (row: any): React.ReactNode => lastUpdated(row.updatedAt), + render: (row: GithubDeployment): React.ReactNode => + lastUpdated(row.updatedAt), }, ]; From 39d7c8718aff2ebd9ae428f4788104807e3afe16 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 17:35:53 +0100 Subject: [PATCH 13/19] update for comments Signed-off-by: Andrew Johnson --- plugins/github-deployments/README.md | 21 ++--- plugins/github-deployments/package.json | 2 +- .../components/GithubDeploymentsCard.test.tsx | 30 +++---- .../src/components/GithubDeploymentsCard.tsx | 9 +- .../GithubDeploymentsTable.tsx | 51 +++++------- plugins/github-deployments/src/index.ts | 4 +- plugins/github-deployments/src/mocks/mocks.ts | 4 +- yarn.lock | 83 ------------------- 8 files changed, 50 insertions(+), 154 deletions(-) diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index 46d54cac57..d736638980 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -4,18 +4,13 @@ The GitHub Deployments Plugin displays recent deployments from GitHub. ![github-deployments-card](./docs/github-deployments-card.png) +## Prerequisites + +- [GitHub Authentication Provider](https://backstage.io/docs/auth/github/provider) + ## Getting Started -1. Provide OAuth credentials: - -- Create an [OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) and set env variables for ID and Secret. - -```bash -export AUTH_GITHUB_CLIENT_ID={{YOUR_CLIENT_ID}} -export AUTH_CLIENT_SECRET={{YOUR_CLIENT_SECRET}} -``` - -2. Install the GitHub Deployments Plugin. +1. Install the GitHub Deployments Plugin. ```bash # packages/app @@ -23,7 +18,7 @@ export AUTH_CLIENT_SECRET={{YOUR_CLIENT_SECRET}} yarn add @backstage/plugin-github-deployments ``` -3. Add the plugin to the app +2. Add the plugin to the app ```typescript // packages/app/src/plugins.ts @@ -31,7 +26,7 @@ yarn add @backstage/plugin-github-deployments export { plugin as GithubDeployments } from '@backstage/plugin-github-deployments'; ``` -4. Add the `EntityGithubDeploymentsCard` to the EntityPage: +3. Add the `EntityGithubDeploymentsCard` to the EntityPage: ```typescript // packages/app/src/components/catalog/EntityPage.tsx @@ -49,7 +44,7 @@ const OverviewContent = () => ( ); ``` -5. Add the `github.com/project-slug` annotation to your `catalog-info.yaml` file: +4. Add the `github.com/project-slug` annotation to your `catalog-info.yaml` file: ```yaml apiVersion: backstage.io/v1alpha1 diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index c030d33322..c2e7fb0fab 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -28,7 +28,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/graphql": "^4.6.1", - "moment": "^2.29.1", + "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^15.3.3" diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 962c08d1cd..93074d4df5 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -24,14 +24,14 @@ import { OAuthApi, } from '@backstage/core'; -import { render } from '@testing-library/react'; -import { setupServer } from 'msw/node'; +import { msw } from '@backstage/test-utils' import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; import { githubDeploymentsPlugin } from '../plugin'; import { GithubDeploymentsCard } from './GithubDeploymentsCard'; import { entityStub, noDataResponseStub, responseStub } from '../mocks/mocks'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { renderInTestApp } from '@backstage/test-utils'; +import { setupServer } from 'msw/node'; import { graphql } from 'msw'; jest.mock('@backstage/plugin-catalog-react', () => ({ @@ -55,9 +55,7 @@ const apis = ApiRegistry.from([ describe('github-deployments', () => { const worker = setupServer(); - beforeAll(() => worker.listen()); - afterAll(() => worker.close()); - afterEach(() => worker.resetHandlers()); + msw.setupDefaultHandlers(worker); beforeEach(() => { jest.resetAllMocks(); @@ -77,12 +75,10 @@ describe('github-deployments', () => { ), ); - const rendered = render( - wrapInTestApp( - - - , - ), + const rendered = await renderInTestApp( + + + , ); expect(await rendered.findByText('active')).toBeInTheDocument(); @@ -107,12 +103,10 @@ describe('github-deployments', () => { ), ); - const rendered = render( - wrapInTestApp( - - - , - ), + const rendered = await renderInTestApp( + + + , ); expect( diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index ebbe426caf..d108dd61ae 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { LinearProgress } from '@material-ui/core'; -import { InfoCard, MissingAnnotationEmptyState, useApi } from '@backstage/core'; +import { InfoCard, MissingAnnotationEmptyState, Progress, ResponseErrorPanel, useApi } from '@backstage/core'; import { useAsync } from 'react-use'; import { githubDeploymentsApiRef } from '../api'; import { useEntity } from '@backstage/plugin-catalog-react'; @@ -42,15 +41,13 @@ const GithubDeploymentsComponent = ({ if (loading) { return ( - + ); } if (error) { return ( - - Error occured while fetching Data - + ); } diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index d7bc8e90b3..66d2fd4f29 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { Table, TableColumn } from '@backstage/core'; +import { StatusPending, StatusRunning, StatusOK, Table, TableColumn, StatusAborted, StatusError } from '@backstage/core'; import { GithubDeployment } from '../../api'; -import moment from 'moment'; +import { DateTime } from 'luxon'; import { Box, Typography, Link, makeStyles } from '@material-ui/core'; const useStyles = makeStyles(theme => ({ @@ -27,31 +27,21 @@ const useStyles = makeStyles(theme => ({ }, })); -const lastUpdated = (start: string): string => moment(start).fromNow(); - -const State = ({ value }: { value: string }) => { - const colorMap: Record = { - PENDING: 'orange', - IN_PROGRESS: 'orange', - ACTIVE: 'green', - }; - - return ( - - - {value} - - ); -}; +const statusIndicator = (value: string): React.ReactNode => { + switch (value) { + case 'PENDING': + return ; + case 'IN_PROGRESS': + return ; + case 'ACTIVE': + return ; + case 'ERROR': + case 'FAILURE': + return ; + default: + return ; + } +} const columns: TableColumn[] = [ { @@ -62,7 +52,10 @@ const columns: TableColumn[] = [ { title: 'Status', render: (row: GithubDeployment): React.ReactNode => ( - + + { statusIndicator(row.state)} + {row.state} + ), }, { @@ -76,7 +69,7 @@ const columns: TableColumn[] = [ { title: 'Last Updated', render: (row: GithubDeployment): React.ReactNode => - lastUpdated(row.updatedAt), + DateTime.fromISO(row.updatedAt).toRelative({ locale: 'en' }), }, ]; diff --git a/plugins/github-deployments/src/index.ts b/plugins/github-deployments/src/index.ts index d1bff4e186..7660aaad3b 100644 --- a/plugins/github-deployments/src/index.ts +++ b/plugins/github-deployments/src/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ export { - githubDeploymentsPlugin as plugin, + githubDeploymentsPlugin, EntityGithubDeploymentsCard, } from './plugin'; -export { isGithubDeploymentsAvailable as isPluginApplicableToEntity } from './Router'; +export { isGithubDeploymentsAvailable } from './Router'; diff --git a/plugins/github-deployments/src/mocks/mocks.ts b/plugins/github-deployments/src/mocks/mocks.ts index a66317dec8..6a394b54bf 100644 --- a/plugins/github-deployments/src/mocks/mocks.ts +++ b/plugins/github-deployments/src/mocks/mocks.ts @@ -1,5 +1,3 @@ -import { QueryResponse } from '../api'; - /* * Copyright 2021 Spotify AB * @@ -15,6 +13,8 @@ import { QueryResponse } from '../api'; * See the License for the specific language governing permissions and * limitations under the License. */ +import { QueryResponse } from '../api'; + export const entityStub = { entity: { metadata: { diff --git a/yarn.lock b/yarn.lock index 94b4d959f4..c595a2283a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1846,23 +1846,6 @@ uuid "^8.0.0" yup "^0.29.3" -"@backstage/core-api@^0.2.15": - version "0.2.15" - resolved "https://registry.npmjs.org/@backstage/core-api/-/core-api-0.2.15.tgz#24c3e4b1fe0bdee36de26c50b8757f41bb266109" - integrity sha512-OQTmvCGM3hkaLV0+JPF85dZk/X1SXoIFkXZi9pO/DzBPZ1Psuptia9xNXrhnEeYJaCazlCnOCTXS/kdoT1zBVg== - dependencies: - "@backstage/config" "^0.1.4" - "@backstage/theme" "^0.2.5" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - prop-types "^15.7.2" - react "^16.12.0" - react-router-dom "6.0.0-beta.0" - react-use "^15.3.3" - zen-observable "^0.8.15" - "@backstage/core@^0.3.0": version "0.7.3" dependencies: @@ -1989,65 +1972,6 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/core@^0.7.3": - version "0.7.3" - resolved "https://registry.npmjs.org/@backstage/core/-/core-0.7.3.tgz#a55f094f0aceab4102ea08a75832c2ec5a70e226" - integrity sha512-uKTCYYAYVtmdOiJrNKe00N0AKeEhxZ+y3qQVY+H96WN9QDql/56XTmoiyPjDU7wK4nDj+yGV4j+xM5uxSXaOCg== - dependencies: - "@backstage/config" "^0.1.4" - "@backstage/core-api" "^0.2.15" - "@backstage/errors" "^0.1.1" - "@backstage/theme" "^0.2.5" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "4.0.0-alpha.45" - "@testing-library/react-hooks" "^3.4.2" - "@types/dagre" "^0.7.44" - "@types/prop-types" "^15.7.3" - "@types/react" "^16.9" - "@types/react-sparklines" "^1.7.0" - "@types/react-text-truncate" "^0.14.0" - classnames "^2.2.6" - clsx "^1.1.0" - d3-selection "^2.0.0" - d3-shape "^2.0.0" - d3-zoom "^2.0.0" - dagre "^0.8.5" - immer "^8.0.1" - lodash "^4.17.15" - material-table "^1.69.1" - prop-types "^15.7.2" - qs "^6.9.4" - rc-progress "^3.0.0" - react "^16.12.0" - react-dom "^16.12.0" - react-helmet "6.1.0" - react-hook-form "^6.6.0" - react-markdown "^5.0.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-sparklines "^1.7.0" - react-syntax-highlighter "^15.4.3" - react-text-truncate "^0.16.0" - react-use "^15.3.3" - remark-gfm "^1.0.0" - zen-observable "^0.8.15" - -"@backstage/plugin-catalog-react@^0.1.3": - version "0.1.3" - resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.1.3.tgz#e4cc6c40616976737893cc91d7e373b9397618d1" - integrity sha512-nNIz+tc72UtwB66L++xA9drv5VV0SxMPh0BNLOFAHAllcR2RMQyvWpDq4uBupUXx/lr6bzihngkW9Jqr4rfz/Q== - dependencies: - "@backstage/catalog-client" "^0.3.8" - "@backstage/catalog-model" "^0.7.3" - "@backstage/core" "^0.7.3" - "@material-ui/core" "^4.11.0" - "@types/react" "^16.9" - react "^16.13.1" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^15.3.3" - "@backstage/plugin-catalog@^0.2.1": version "0.5.1" dependencies: @@ -2096,13 +2020,6 @@ react-use "^15.3.3" swr "^0.3.0" -"@backstage/theme@^0.2.5": - version "0.2.5" - resolved "https://registry.npmjs.org/@backstage/theme/-/theme-0.2.5.tgz#657e2ceaaede1b4dbe8abeafa6e168671a27b6da" - integrity sha512-GT7Ok9QSiZWXkxka+vcpweMrOg1lgDkRRjh7aSU97Mcoiq77szdO45o0hZtQChz8qay60vCRlu57ADvUn4QxZw== - dependencies: - "@material-ui/core" "^4.11.0" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" From 3714e6c374a1255fb4ba24cd64e1857e30c4c5ee Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 17:38:46 +0100 Subject: [PATCH 14/19] update usages Signed-off-by: Andrew Johnson --- packages/app/src/plugins.ts | 2 +- plugins/github-deployments/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index d7ce9025ba..ccc7e2a45c 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -45,4 +45,4 @@ export { plugin as Org } from '@backstage/plugin-org'; export { plugin as Kafka } from '@backstage/plugin-kafka'; export { todoPlugin } from '@backstage/plugin-todo'; export { badgesPlugin } from '@backstage/plugin-badges'; -export { plugin as githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; +export { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index d736638980..ac3cacd359 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -23,7 +23,7 @@ yarn add @backstage/plugin-github-deployments ```typescript // packages/app/src/plugins.ts -export { plugin as GithubDeployments } from '@backstage/plugin-github-deployments'; +import { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; ``` 3. Add the `EntityGithubDeploymentsCard` to the EntityPage: From ceb1088edc9daf13d4a834586a3aee7d4a691f3f Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 17:40:45 +0100 Subject: [PATCH 15/19] spacing Signed-off-by: Andrew Johnson --- .../GithubDeploymentsTable/GithubDeploymentsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index 66d2fd4f29..7a12f45f34 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -53,7 +53,7 @@ const columns: TableColumn[] = [ title: 'Status', render: (row: GithubDeployment): React.ReactNode => ( - { statusIndicator(row.state)} + {statusIndicator(row.state)} {row.state} ), From d4ae83a79d58f5e50a479ceca56d37b4c8967fbf Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 17:46:22 +0100 Subject: [PATCH 16/19] correct readme Signed-off-by: Andrew Johnson --- plugins/github-deployments/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/github-deployments/README.md b/plugins/github-deployments/README.md index ac3cacd359..174d1bc8c7 100644 --- a/plugins/github-deployments/README.md +++ b/plugins/github-deployments/README.md @@ -23,7 +23,7 @@ yarn add @backstage/plugin-github-deployments ```typescript // packages/app/src/plugins.ts -import { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; +export { githubDeploymentsPlugin as GithubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; ``` 3. Add the `EntityGithubDeploymentsCard` to the EntityPage: From 0b1179685bb399dc7d998fb82c44ad93f30df3d4 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 17:58:54 +0100 Subject: [PATCH 17/19] prettier Signed-off-by: Andrew Johnson --- .../src/components/GithubDeploymentsCard.test.tsx | 2 +- .../src/components/GithubDeploymentsCard.tsx | 12 ++++++++---- .../GithubDeploymentsTable.tsx | 12 ++++++++++-- plugins/github-deployments/src/index.ts | 5 +---- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 93074d4df5..c6f5d2a034 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -24,7 +24,7 @@ import { OAuthApi, } from '@backstage/core'; -import { msw } from '@backstage/test-utils' +import { msw } from '@backstage/test-utils'; import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; import { githubDeploymentsPlugin } from '../plugin'; import { GithubDeploymentsCard } from './GithubDeploymentsCard'; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index d108dd61ae..6be4a5a6b3 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -14,7 +14,13 @@ * limitations under the License. */ import React from 'react'; -import { InfoCard, MissingAnnotationEmptyState, Progress, ResponseErrorPanel, useApi } from '@backstage/core'; +import { + InfoCard, + MissingAnnotationEmptyState, + Progress, + ResponseErrorPanel, + useApi, +} from '@backstage/core'; import { useAsync } from 'react-use'; import { githubDeploymentsApiRef } from '../api'; import { useEntity } from '@backstage/plugin-catalog-react'; @@ -46,9 +52,7 @@ const GithubDeploymentsComponent = ({ ); } if (error) { - return ( - - ); + return ; } return ; diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index 7a12f45f34..f5fae7f449 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -14,7 +14,15 @@ * limitations under the License. */ import React from 'react'; -import { StatusPending, StatusRunning, StatusOK, Table, TableColumn, StatusAborted, StatusError } from '@backstage/core'; +import { + StatusPending, + StatusRunning, + StatusOK, + Table, + TableColumn, + StatusAborted, + StatusError, +} from '@backstage/core'; import { GithubDeployment } from '../../api'; import { DateTime } from 'luxon'; import { Box, Typography, Link, makeStyles } from '@material-ui/core'; @@ -41,7 +49,7 @@ const statusIndicator = (value: string): React.ReactNode => { default: return ; } -} +}; const columns: TableColumn[] = [ { diff --git a/plugins/github-deployments/src/index.ts b/plugins/github-deployments/src/index.ts index 7660aaad3b..2ee681332a 100644 --- a/plugins/github-deployments/src/index.ts +++ b/plugins/github-deployments/src/index.ts @@ -13,8 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { - githubDeploymentsPlugin, - EntityGithubDeploymentsCard, -} from './plugin'; +export { githubDeploymentsPlugin, EntityGithubDeploymentsCard } from './plugin'; export { isGithubDeploymentsAvailable } from './Router'; From 6cb8521c88c02d66bbca22cb2157c2d265869e01 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 30 Mar 2021 18:05:16 +0100 Subject: [PATCH 18/19] lint Signed-off-by: Andrew Johnson --- .../src/components/GithubDeploymentsCard.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index c6f5d2a034..678132bd8f 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -24,13 +24,13 @@ import { OAuthApi, } from '@backstage/core'; -import { msw } from '@backstage/test-utils'; +import { msw, renderInTestApp } from '@backstage/test-utils'; import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api'; import { githubDeploymentsPlugin } from '../plugin'; import { GithubDeploymentsCard } from './GithubDeploymentsCard'; import { entityStub, noDataResponseStub, responseStub } from '../mocks/mocks'; -import { renderInTestApp } from '@backstage/test-utils'; + import { setupServer } from 'msw/node'; import { graphql } from 'msw'; From d8a951493acaacd78525ad6d0a24b627d56d27eb Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 31 Mar 2021 15:38:20 +0100 Subject: [PATCH 19/19] change graphql version Signed-off-by: Andrew Johnson --- plugins/github-deployments/package.json | 2 +- yarn.lock | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index c2e7fb0fab..b8761c1385 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -27,7 +27,7 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "@octokit/graphql": "^4.6.1", + "@octokit/graphql": "^4.5.8", "luxon": "^1.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/yarn.lock b/yarn.lock index c595a2283a..a417d89adb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4290,15 +4290,6 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/graphql@^4.6.1": - version "4.6.1" - resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.1.tgz#f975486a46c94b7dbe58a0ca751935edc7e32cc9" - integrity sha512-2lYlvf4YTDgZCTXTW4+OX+9WTLFtEUc6hGm4qM1nlZjzxj+arizM4aHWzBVBCxY9glh7GIs0WEuiSgbVzv8cmA== - dependencies: - "@octokit/request" "^5.3.0" - "@octokit/types" "^6.0.3" - universal-user-agent "^6.0.0" - "@octokit/openapi-types@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.2.0.tgz#123e0438a0bc718ccdac3b5a2e69b3dd00daa85b"