From 6d0f46046ade41b6430d70b4c13da7a4aff00465 Mon Sep 17 00:00:00 2001 From: Nat Burgwyn Date: Tue, 2 Jun 2020 22:40:11 -0400 Subject: [PATCH 01/71] Copy favicons from microsite --- .../app/public/android-chrome-192x192.png | Bin 4352 -> 19209 bytes .../app/public/android-chrome-512x512.png | Bin 0 -> 57367 bytes packages/app/public/apple-touch-icon.png | Bin 4311 -> 18045 bytes packages/app/public/favicon-16x16.png | Bin 890 -> 883 bytes packages/app/public/favicon-32x32.png | Bin 1296 -> 1686 bytes packages/app/public/favicon.ico | Bin 15086 -> 15086 bytes packages/app/public/favicon.svg | 17 +++++++++++++++++ 7 files changed, 17 insertions(+) create mode 100644 packages/app/public/android-chrome-512x512.png create mode 100644 packages/app/public/favicon.svg diff --git a/packages/app/public/android-chrome-192x192.png b/packages/app/public/android-chrome-192x192.png index 3d56edbb0dce7b1c12f644090e3522d9b992fd1d..4660f988c1a3031fa3d959da5e7bcfc34cdb8ba0 100644 GIT binary patch literal 19209 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 CM64_T delta 4171 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 diff --git a/packages/app/public/android-chrome-512x512.png b/packages/app/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..d9b0a6ba78cbea75762c47ebc2ad57d62dc94df6 GIT binary patch literal 57367 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 literal 890 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>bCLfvgo}k(8*fF!X_4rCyA}qqt8F+F!NSBPz&MHTnhDTc#w2fd zm&jS0zDNK$>?NMQuI!Jvh51e8KDs3?1PU$oba4!kxSX7DfFb9NjLe*vJ2nN3_X-3a zJQjE0;NaryZfWoCI>>rNRb}at!>JVym^KyheByf8HK~A6HB~g!w3Ii*`LO9$0j8{5 zR=cXQez9>F^GH_o9O}I}fj8BF^ZH~1Q{(LkESwQhk+*NO-M(pU;bviZnD=RMVo`5L zL2-J*hYukkAzway`?)FK#IZ0z|cb1&`{UVGQ`l-%GB7(*g)IB(8|DoT~O&AiiX_$l+3hB z+#1S!W`X0RDkP#LD6w3jpeR2rGbdG{q_QAYA+w+)nSr5V&f`x!9ED*T8mIhEpYePe z#K5e~t(VL#tSsz3S%g_w!KK0Ea0;{X<`9L`H?EvGa^{H45%$v!9t*tm7+#4BmV9zD RoeFdlgQu&X%Q~loCID><@&Nz< diff --git a/packages/app/public/favicon-32x32.png b/packages/app/public/favicon-32x32.png index e2707f2d1e6f17270c8728efcb7230310aefdefc..c0915ece75949f3d917134f55193949927edc633 100644 GIT binary patch delta 1482 zcmbQhHH~*dd_B{J0G|+71_lP90Rin z5;AvE{r9JZH&1Fh*$MFRDd_9wUOHUz;DWNbN%)c(ahsRrT{&W3kSV~&FQcJW{`{Jn ztyR>jdDS28saTkXEts0HeWg*Tzk-3TmaAjb$~kpk9!bc_%IRpuu3udB=9YU?rHHtg zNn~)wu^su>kJlI9J6C-7Y}mr7P89{>vNDm&XE*$M5xr)Ckf4yFp?=P}{aL5?*cW7Y zb=3Lwx4PDqrtIBN{`5-N!fC2j<|5+aiQ88d-a6%6S(tfZSK#zsc|C14YYYF0?Y@02 z1vgI?-ahS8RaEunwyw9kZ+~n2))n4e4X(8%F>4o;KfRiB;Xq9NxABCT`O*ul72h?nWgO!-Q=sW7jW=S-Vi*&olkVHqX|Y!0A1y z2RHjq>WJUGEc4{9xJ^sV&kmea6$aZ4O;9C2(4|PfwGig1lpC zZtDI`*=P2p9o}k}pCP5Jq-x2JHj_ooZeO|8TJi~TU8>o{p$=luK{f-Tf76Li!C7!;n>`z!(ISuviB=rh0 zFfgfmx;TbdoK8+iNJvRaOH94&%KS!VPRyMi9*0afxj4JnH4X6&6DCe-;KdW3&F+1Sk@t>XJ$v`?<wDp-~= zt$uJ~#fupc3$ESR@uS5>yM#4tp69lcnwl$*Wrl5hxleNDxeTr~mo{(8jVUfN{`85v zo~yfSve%bDQHd$jriRauhzJb}oqqkwwX5b93!@g^zj5Wxr5Vds-=1~t+P#Z6r(VtN z$!hY>^v{p~&mt}!+uS)>&3~rV+H|W`oMFaJ>E~wIrsq|;B)_uR{gYcv&&l|pR^G=+ zp38i_r|ZTi?k<{o|5fC&jJyq}A~%=a{k5cv_qBOFTh6ULsjs)aJ(!{P=ElzK?Pss8 ztp4^cdT-kQ#^!du>AMzaSrnDbe|a(a_Hx#!oz49-40JpM0IY z{rx<}ldri9HI&k|YxmxW&{o;EZjur6=i4F zn?7$}wQS2CnO$o(Oxw1JFF=YxHcfE8$Gg{Zz?7s~;u=wsl30>zm0UF0kA+>@$UxV? n6o^6$EUip{$XwgN$jZPV!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081LL6ppAc7|0tRMAnGIgd zg6z^8JOwj#q}O=JZuFK~=P9+;LuS2~^m;G3O_H2?+zGJ^OOSp?2T3kG1g zVM@V8fDE8|G%ltHTmhQlXeyDF0onim|F2@Y_Zg^Ya!HV1Fsfcg9$t3QwWm%T+c7yr zSo+SdBOWgTFUhZ5wf@Za>?9@@!B2}5I5ux*o4bZt_Iy>fhPHmdzk9D@mA3D z-b1QO^Xm4#vs8O5aif`C$5xTay+V5KtFQMexN-zUf-kVn{F#3FqLkW`ecY)gA1(-e zZIZmSj3pxNvo(KzUwghIYv`tv3oUuil%F>NI*T#M+udaFdh=h+CN7 zRPLi&;zFRbr>Bc!h{fsTgau3w9*Z|H)j8VbaJ96!x+psM#5e_U1vhRvlfp7#>g4B# zS0{W*VRdEVQq$H<4pmsRXw|ZHjA08qLmS1KxAm=CCfBj@e7kQWV@GBNOOn*=2O$R% zxRPegd>oRH$e9$CSlYa`ckSH08<{6O&FBtooHu!Mw|2+FXMOfEHa1&7YJd0?c5FS+ z0{QqgTz^dd{$*x5xbA}Zk(a)V`4h~WnhM!NJTyAq_D4HLSair9Ja9rpQdCy>bjv~o zKAGr`JU*PAo+mX|X8LfjzLcCfQ!MhP!m`!2#eVNZ6i%E@PioZW2-VUkXj#&JNa@-m zp$SczOH+L!4mmEK)3|n#(3%CSq`I4CMLDxA+f}upsm+at)wIa>KtfVdejekgRmjRy*V;jCKX z8c~vxSdwa$T$Bo=7>o=IEp!bHbqy^;3{9;}jjfCgv<(cc3=G%>mENIf$jwj5OsmAL zq0DC%s6Yy@3W+EQN-S3>D9TUE%t=)!sVqoU$Sf#HW?-n8^Y{}FM`4(T#wq{PXFQ(< zF)%B0>m_pwD+_y17GV}vaA`0(oWiWUIYi;~jVmXPoH-(Mg#C1b#{w@shF9W(C7+y3 Rrvj~D@O1TaS?83{1OQGQaIF9U diff --git a/packages/app/public/favicon.ico b/packages/app/public/favicon.ico index 5b582704a10d20240034385f14c210db96d51603..5e45e5dfbde6f39603d5be60d933c1af14dffb1e 100644 GIT binary patch literal 15086 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 D+pS%} literal 15086 zcmeI2d2AF_9LJ{=UG5^DSg*Ti8pU{EyyAuN zh&M`6142+i#ef%xF&Z`4phlws8xbwKw0^$3^S0C1Id*4fh=1%a`RqG?@AsYS&6}BZ zoGPc<88ylwt#@8L$Z?K#9H+kCEe~{@P1Fq}Rr}1Ij&nO5M$iWrXDX;ZXT$PI<+rj2 zDtn-^2h!XF6BpN3pWZyA_r&HQ%t$5KNP16!OJgt@pL1a>)IkPHGBgi@Y48HP3yWYj z>{sMK*VC^9+GCQ%?@#z1mcR@c43UP$VelsW3mqPIl7Al#j~v8tjDBrH5@>rqg^OW# z@HMp6z!LE4w)1Ofuk{_EvodM@W*_L7+y;F+Lv`lRH3@b^=-2jK+v(+-OH04D;~!{( zZr3Eb9xFn-^e^L@4!f?>)UR#$9nOG)G1v4&SJb&<&ZUp(TT^iEo=IE3wqZH+H=!pi z8eD~@C&6e8OINV}dl_G!G)n(cd@h4&9;TC@3s1l%kj5~!(LN3ehW5+Ao_GAVDM*TZ5~16J}?t>-(3Z-!CZ)%_Z3r? zlz!cJ!}PD9y_V^&8rAkKqEAgrxI1nd`n7g$hGL#S%H6!A z^=mye1$vh&tVsW)_3t3=2QU%RnD6b)N?O0UCVnN*C3dhB%2E11C+6dzwX)$k^4iCm zlZ;-!bTjQ~&HD_Uvnc(3>*p#w`@u=D8vL}Ix|eD11!-9v{a!5xQ_vi5^xCQJy~lVR z63&;aUvlmOTer7-b;6px!zH2rstElLdIKh$PeT7~5&EA>ShIJya`g{kuus5yzS#MP zy#W)>$JM_ZIyOa8bDeBmC3SzmvENAg$*vvsr2A*E5^0k%1;iifNMiv*=t8dRMxu z*8loGpku9P@KX63pRMpUyaduc78?PdqoRbJHd_ZLsR|{W7h2zf7Bz+Hnf;OdLBkaZPH)XEWFi zeu_#*aBS=0L@4}EYggPQiC3}z*B;;-R|fh+*u76{?;AjC!FwanbIM#82YZ085pOrR z5MBp;Cwv-?^X=?7`VRLUaF!R1*4-b#B-QL zcbNNw!}!DD$AI|nP~TVmJQfuHO8RK#+RJ4q>G`k$ys?$nJLz-4nb=&bbMH}~`1&u+ z73yPIQ2eP@KU17Q<`1wnG@1^?sfC5`D{KdydyUY?^zo#n#@`R$ufdM5@8bm{mahFn z;CMI?dK4PFW=f4e9Cw`?cKkVAhb_8PO8owedJov&<@AgaG`~a%35u_G@UubNC2j(J z%hb7MZ+ki4FN5OSF@nnPq30loYjNZMOh6rry10Q|zqs+g!T&r6I)L{a?D`T2Op@9|x5;61XmIaQw=N{|o--mJwU7apM1jpVq&pLEPhx zYtHuO_f^pGUK_%Pw7ms-?|K332}zjr_k;cssE^kAgNHiAQ-#eF># z-c#65Mzkel8o8H+&ZTZ^2iD1bU?6zUv&r;*2UdDmLH=pb8bo86XP;(#^}pUps&j1~ zY0&)j7j2E|htJJmo)z`Kay|RVjK9_(55YBXKWKfTaW})TGv6FK3!khz#!^yUQ!?ZK zC;73tn*7h*B=T>A@iSV7_r+%p%!fnrU2?x({}bH$0i@4>{I|kU5M;(*-#=EvB3J{8 ztGY)($0x{)zu!J=#%Z4*N4MViYu?nlUf)`mgJQSB6bLip@7;g<;;;dvX(fbx&oTab zUohV7dkm@8Zt`CX+MhvY{CCpfOIQa#f!QDTk@udj6uX%5H~mejb5Hm7`Yye@7j$gr s!c{JFNN2*y5T={v@+|-E({Wa9gK6CKM)ODZG0;F+H04p*TW9?K1Dt;TIRF3v diff --git a/packages/app/public/favicon.svg b/packages/app/public/favicon.svg new file mode 100644 index 0000000000..535eea5543 --- /dev/null +++ b/packages/app/public/favicon.svg @@ -0,0 +1,17 @@ + + + favicon + + + + + From d3899c79d98e40d97679829d061ad85eb56c4fc0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2020 07:18:23 +0000 Subject: [PATCH 02/71] build(deps): bump yaml from 1.9.2 to 1.10.0 Bumps [yaml](https://github.com/eemeli/yaml) from 1.9.2 to 1.10.0. - [Release notes](https://github.com/eemeli/yaml/releases) - [Commits](https://github.com/eemeli/yaml/compare/v1.9.2...v1.10.0) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0dd8044248..f87a2986f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -892,14 +892,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2", "@babel/runtime@^7.9.6": - version "7.10.2" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz#d103f21f2602497d38348a32e008637d506db839" - integrity sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2", "@babel/runtime@^7.9.6": version "7.10.3" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== @@ -19298,25 +19291,11 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@*, yaml@^1.9.2: - version "1.9.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.9.2.tgz#f0cfa865f003ab707663e4f04b3956957ea564ed" - integrity sha512-HPT7cGGI0DuRcsO51qC1j9O16Dh1mZ2bnXwsi0jrSpsLz0WxOLSLXfkABVl6bZO629py3CU+OMJtpNHDLB97kg== - dependencies: - "@babel/runtime" "^7.9.2" - -yaml@^1.10.0: +yaml@*, yaml@^1.10.0, yaml@^1.7.2, yaml@^1.9.2: version "1.10.0" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== -yaml@^1.7.2: - version "1.8.3" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a" - integrity sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw== - dependencies: - "@babel/runtime" "^7.8.7" - yargs-parser@18.x, yargs-parser@^18.1.1: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" From d42d1eaa1a21e48ae28a8600d88f2194b1fadd5c Mon Sep 17 00:00:00 2001 From: tudi2d Date: Mon, 29 Jun 2020 14:14:25 +0200 Subject: [PATCH 03/71] Add FavouriteEntity component --- .../components/CatalogPage/CatalogPage.tsx | 26 ++-------- .../EntityMetadataCard/EntityMetadataCard.tsx | 9 +++- .../FavouriteEntity/FavouriteEntity.tsx | 50 +++++++++++++++++++ 3 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ef77cf573f..5dbf886df2 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -24,17 +24,9 @@ import { } from '@backstage/core'; import CatalogLayout from './CatalogLayout'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; -import { - Button, - Link, - makeStyles, - Typography, - withStyles, -} from '@material-ui/core'; +import { Button, Link, makeStyles, Typography } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; -import Star from '@material-ui/icons/Star'; -import StarOutline from '@material-ui/icons/StarBorder'; import React, { FC } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { CatalogFilter } from '../CatalogFilter/CatalogFilter'; @@ -47,6 +39,7 @@ import { filterGroups, labeledEntityTypes, } from '../../data/filters'; +import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -77,12 +70,6 @@ export const CatalogPage: FC<{}> = () => { const styles = useStyles(); - const YellowStar = withStyles({ - root: { - color: '#f3ba37', - }, - })(Star); - const actions = [ (rowData: Entity) => { const location = findLocationForEntityMeta(rowData.metadata); @@ -119,14 +106,7 @@ export const CatalogPage: FC<{}> = () => { hidden: location?.type !== 'github', }; }, - (rowData: Entity) => { - const isStarred = isStarredEntity(rowData); - return { - icon: isStarred ? YellowStar : StarOutline, - tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites', - onClick: () => toggleStarredEntity(rowData), - }; - }, + (rowData: Entity) => , ]; return ( diff --git a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx b/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx index 57082a9c91..f1e1589b3b 100644 --- a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx +++ b/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx @@ -17,13 +17,20 @@ import { Entity } from '@backstage/catalog-model'; import { InfoCard, StructuredMetadataTable } from '@backstage/core'; import React, { FC } from 'react'; +import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; type Props = { entity: Entity; }; +const InfoCardHeaderStyle = { display: 'inline-flex', alignItems: 'baseline' }; + export const EntityMetadataCard: FC = ({ entity }) => ( - + }} + headerStyle={InfoCardHeaderStyle} + > ); diff --git a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx new file mode 100644 index 0000000000..38dc654fea --- /dev/null +++ b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2020 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, { ComponentProps } from 'react'; +import { IconButton, Tooltip, withStyles } from '@material-ui/core'; +import StarBorder from '@material-ui/icons/StarBorder'; +import Star from '@material-ui/icons/Star'; +import { useEntities } from '../../hooks/useEntities'; +import { Entity } from '@backstage/catalog-model'; + +type Props = ComponentProps & { entity: Entity }; + +const YellowStar = withStyles({ + root: { + color: '#f3ba37', + }, +})(Star); + +/** + * IconButton for showing if a current entity is starred and adding/removing it from the favourite entities + * @param props MaterialUI IconButton props extended by required `entity` prop + */ +export const FavouriteEntity: React.FC = props => { + const { toggleStarredEntity, isStarredEntity } = useEntities(); + const isStarred = isStarredEntity(props.entity); + return ( + toggleStarredEntity(props.entity)} + > + + {isStarred ? : } + + + ); +}; From 6bfa4d23c7645d091d01f893fb0e18d3857c4352 Mon Sep 17 00:00:00 2001 From: tudi2d Date: Mon, 29 Jun 2020 14:39:57 +0200 Subject: [PATCH 04/71] Fix material-table action for starring entity --- .../src/components/CatalogPage/CatalogPage.tsx | 14 ++++++++++++-- .../FavouriteEntity/FavouriteEntity.tsx | 18 ++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 5dbf886df2..bad303bd53 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -39,7 +39,10 @@ import { filterGroups, labeledEntityTypes, } from '../../data/filters'; -import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; +import { + favouriteEntityIcon, + favouriteEntityTooltip, +} from '../FavouriteEntity/FavouriteEntity'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -106,7 +109,14 @@ export const CatalogPage: FC<{}> = () => { hidden: location?.type !== 'github', }; }, - (rowData: Entity) => , + (rowData: Entity) => { + const isStarred = isStarredEntity(rowData); + return { + icon: favouriteEntityIcon(isStarred), + tooltip: favouriteEntityTooltip(isStarred), + onClick: () => toggleStarredEntity(rowData), + }; + }, ]; return ( diff --git a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx index 38dc654fea..3ea5b68d4b 100644 --- a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx +++ b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentProps } from 'react'; -import { IconButton, Tooltip, withStyles } from '@material-ui/core'; +import { IconButton, Tooltip, withStyles, SvgIcon } from '@material-ui/core'; import StarBorder from '@material-ui/icons/StarBorder'; import Star from '@material-ui/icons/Star'; import { useEntities } from '../../hooks/useEntities'; @@ -29,6 +29,12 @@ const YellowStar = withStyles({ }, })(Star); +export const favouriteEntityTooltip = (isStarred: boolean) => + isStarred ? 'Remove from favorites' : 'Add to favorites'; + +export const favouriteEntityIcon = (isStarred: boolean) => + isStarred ? YellowStar : StarBorder; + /** * IconButton for showing if a current entity is starred and adding/removing it from the favourite entities * @param props MaterialUI IconButton props extended by required `entity` prop @@ -37,13 +43,9 @@ export const FavouriteEntity: React.FC = props => { const { toggleStarredEntity, isStarredEntity } = useEntities(); const isStarred = isStarredEntity(props.entity); return ( - toggleStarredEntity(props.entity)} - > - - {isStarred ? : } + toggleStarredEntity(props.entity)}> + + ); From 255777e83cde55f83df07f449c2b4020513b2f32 Mon Sep 17 00:00:00 2001 From: tudi2d Date: Tue, 30 Jun 2020 11:50:51 +0200 Subject: [PATCH 05/71] Move favourite star to title in header of EntityPage --- .../EntityMetadataCard/EntityMetadataCard.tsx | 9 +-------- .../src/components/EntityPage/EntityPage.tsx | 19 +++++++++++++++++-- .../FavouriteEntity/FavouriteEntity.tsx | 6 +++++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx b/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx index f1e1589b3b..57082a9c91 100644 --- a/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx +++ b/plugins/catalog/src/components/EntityMetadataCard/EntityMetadataCard.tsx @@ -17,20 +17,13 @@ import { Entity } from '@backstage/catalog-model'; import { InfoCard, StructuredMetadataTable } from '@backstage/core'; import React, { FC } from 'react'; -import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; type Props = { entity: Entity; }; -const InfoCardHeaderStyle = { display: 'inline-flex', alignItems: 'baseline' }; - export const EntityMetadataCard: FC = ({ entity }) => ( - }} - headerStyle={InfoCardHeaderStyle} - > + ); diff --git a/plugins/catalog/src/components/EntityPage/EntityPage.tsx b/plugins/catalog/src/components/EntityPage/EntityPage.tsx index e5db5b9230..1c56924589 100644 --- a/plugins/catalog/src/components/EntityPage/EntityPage.tsx +++ b/plugins/catalog/src/components/EntityPage/EntityPage.tsx @@ -28,7 +28,7 @@ import { useApi, } from '@backstage/core'; import { SentryIssuesWidget } from '@backstage/plugin-sentry'; -import { Grid } from '@material-ui/core'; +import { Grid, Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; import React, { FC, useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; @@ -37,6 +37,7 @@ import { catalogApiRef } from '../..'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { EntityMetadataCard } from '../EntityMetadataCard/EntityMetadataCard'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; +import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; const REDIRECT_DELAY = 1000; function headerProps( @@ -63,6 +64,16 @@ export const getPageTheme = (entity?: Entity): PageTheme => { return pageTheme[themeKey] ?? pageTheme.home; }; +const EntityPageTitle: FC<{ title: string; entity: Entity | undefined }> = ({ + entity, + title, +}) => ( + + {title} + {entity && } + +); + export const EntityPage: FC<{}> = () => { const { optionalNamespaceAndName, kind } = useParams() as { optionalNamespaceAndName: string; @@ -138,7 +149,11 @@ export const EntityPage: FC<{}> = () => { return ( -
+
} + pageTitleOverride={headerTitle} + type={headerType} + > {entity && ( <> = props => { const { toggleStarredEntity, isStarredEntity } = useEntities(); const isStarred = isStarredEntity(props.entity); return ( - toggleStarredEntity(props.entity)}> + toggleStarredEntity(props.entity)} + > From 7cef0de42d5a58645250168189b34559d3d3afa8 Mon Sep 17 00:00:00 2001 From: David Dyball Date: Tue, 30 Jun 2020 11:12:06 +0100 Subject: [PATCH 06/71] [deployment] Kubernetes Manifests --- install/kubernetes/app.yaml | 27 +++++++++++++++++++++++++ install/kubernetes/backend.yaml | 27 +++++++++++++++++++++++++ install/kubernetes/ingress.yaml | 20 +++++++++++++++++++ install/kubernetes/service.yaml | 35 +++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 install/kubernetes/app.yaml create mode 100644 install/kubernetes/backend.yaml create mode 100644 install/kubernetes/ingress.yaml create mode 100644 install/kubernetes/service.yaml diff --git a/install/kubernetes/app.yaml b/install/kubernetes/app.yaml new file mode 100644 index 0000000000..465d491907 --- /dev/null +++ b/install/kubernetes/app.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backstage + labes: + app: backstage + component: frontend +spec: + replicas: 1 + selector: + matchLabels: + app: backstage + component: frontend + template: + metadata: + labels: + app: backstage + component: frontend + spec: + containers: + - name: app + image: spotify/backstage:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + name: app + protocol: TCP diff --git a/install/kubernetes/backend.yaml b/install/kubernetes/backend.yaml new file mode 100644 index 0000000000..d762f30331 --- /dev/null +++ b/install/kubernetes/backend.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backstage-backend + labes: + app: backstage + component: backend +spec: + replicas: 1 + selector: + matchLabels: + app: backstage + component: backend + template: + metadata: + labels: + app: backstage + component: backend + spec: + containers: + - name: app + image: spotify/backstage-backend:latest + imagePullPolicy: Always + ports: + - containerPort: 7000 + name: backend + protocol: TCP diff --git a/install/kubernetes/ingress.yaml b/install/kubernetes/ingress.yaml new file mode 100644 index 0000000000..d2ecae74c6 --- /dev/null +++ b/install/kubernetes/ingress.yaml @@ -0,0 +1,20 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: backstage + labels: + app: backstage + component: ingress +spec: + rules: + - host: + http: + paths: + - backend: + serviceName: backstage + servicePort: frontend + path: / + - backend: + serviceName: backstage-backend + servicePort: backend + path: /backend diff --git a/install/kubernetes/service.yaml b/install/kubernetes/service.yaml new file mode 100644 index 0000000000..bbbc3d6e17 --- /dev/null +++ b/install/kubernetes/service.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: Service +metadata: + name: backstage + labels: + app: backstage + component: frontend +spec: + type: ClusterIP + selector: + app: backstage + component: frontend + ports: + - name: frontend + port: 80 + protocol: TCP + targetPort: app +--- +apiVersion: v1 +kind: Service +metadata: + name: backstage-backend + labels: + app: backstage + component: backend +spec: + type: ClusterIP + selector: + app: backstage + component: backend + ports: + - name: backend + port: 7000 + protocol: TCP + targetPort: backend From 5c8074ba2a88da6f8811c93c3b7a82f05f538737 Mon Sep 17 00:00:00 2001 From: tudi2d Date: Tue, 30 Jun 2020 12:22:16 +0200 Subject: [PATCH 07/71] Replace icon & tooltip for star in changed CatalogTable --- .../src/components/CatalogTable/CatalogTable.tsx | 15 ++++++--------- .../FavouriteEntity/FavouriteEntity.tsx | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index b507b96f05..27793c81b1 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -18,14 +18,16 @@ import { Table, TableColumn, TableProps } from '@backstage/core'; import { Link } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import GitHub from '@material-ui/icons/GitHub'; -import Star from '@material-ui/icons/Star'; -import StarOutline from '@material-ui/icons/StarBorder'; import { Alert } from '@material-ui/lab'; import React from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; import { findLocationForEntityMeta } from '../../data/utils'; import { useStarredEntities } from '../../hooks/useStarredEntites'; import { entityRoute } from '../../routes'; +import { + favouriteEntityIcon, + favouriteEntityTooltip, +} from '../FavouriteEntity/FavouriteEntity'; const columns: TableColumn[] = [ { @@ -125,13 +127,8 @@ export const CatalogTable = ({ const isStarred = isStarredEntity(rowData); return { cellStyle: { paddingLeft: '1em' }, - icon: () => - isStarred ? ( - - ) : ( - - ), - tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites', + icon: favouriteEntityIcon(isStarred), + tooltip: favouriteEntityTooltip(isStarred), onClick: () => toggleStarredEntity(rowData), }; }, diff --git a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx index 0bb033b3e2..0f608836a9 100644 --- a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx +++ b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx @@ -18,7 +18,7 @@ import React, { ComponentProps } from 'react'; import { IconButton, Tooltip, withStyles, SvgIcon } from '@material-ui/core'; import StarBorder from '@material-ui/icons/StarBorder'; import Star from '@material-ui/icons/Star'; -import { useEntities } from '../../hooks/useEntities'; +import { useStarredEntities } from '../../hooks/useStarredEntites'; import { Entity } from '@backstage/catalog-model'; type Props = ComponentProps & { entity: Entity }; @@ -40,7 +40,7 @@ export const favouriteEntityIcon = (isStarred: boolean) => * @param props MaterialUI IconButton props extended by required `entity` prop */ export const FavouriteEntity: React.FC = props => { - const { toggleStarredEntity, isStarredEntity } = useEntities(); + const { toggleStarredEntity, isStarredEntity } = useStarredEntities(); const isStarred = isStarredEntity(props.entity); return ( Date: Tue, 30 Jun 2020 12:36:06 +0200 Subject: [PATCH 08/71] Fix type error breaking build --- .../catalog/src/components/CatalogTable/CatalogTable.tsx | 2 +- .../src/components/FavouriteEntity/FavouriteEntity.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 27793c81b1..b5afbcaa0b 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -127,7 +127,7 @@ export const CatalogTable = ({ const isStarred = isStarredEntity(rowData); return { cellStyle: { paddingLeft: '1em' }, - icon: favouriteEntityIcon(isStarred), + icon: () => favouriteEntityIcon(isStarred), tooltip: favouriteEntityTooltip(isStarred), onClick: () => toggleStarredEntity(rowData), }; diff --git a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx index 0f608836a9..bd2c71891d 100644 --- a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx +++ b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentProps } from 'react'; -import { IconButton, Tooltip, withStyles, SvgIcon } from '@material-ui/core'; +import { IconButton, Tooltip, withStyles } from '@material-ui/core'; import StarBorder from '@material-ui/icons/StarBorder'; import Star from '@material-ui/icons/Star'; import { useStarredEntities } from '../../hooks/useStarredEntites'; @@ -33,7 +33,7 @@ export const favouriteEntityTooltip = (isStarred: boolean) => isStarred ? 'Remove from favorites' : 'Add to favorites'; export const favouriteEntityIcon = (isStarred: boolean) => - isStarred ? YellowStar : StarBorder; + isStarred ? : ; /** * IconButton for showing if a current entity is starred and adding/removing it from the favourite entities @@ -49,7 +49,7 @@ export const FavouriteEntity: React.FC = props => { onClick={() => toggleStarredEntity(props.entity)} > - + {favouriteEntityIcon(isStarred)} ); From de9b1e3ee036bb6b494249cab9f175cd09794ddf Mon Sep 17 00:00:00 2001 From: David Dyball Date: Tue, 30 Jun 2020 12:06:57 +0100 Subject: [PATCH 09/71] Add a Kubernetes Helm chart --- install/kubernetes/backstage/.helmignore | 22 ++++ install/kubernetes/backstage/Chart.yaml | 5 + install/kubernetes/backstage/README.md | 51 ++++++++ .../backstage/templates/_helpers.tpl | 80 ++++++++++++ .../backstage/templates/deployment.yaml | 119 ++++++++++++++++++ .../backstage/templates/ingress.yaml | 83 ++++++++++++ .../backstage/templates/service.yaml | 37 ++++++ install/kubernetes/backstage/values.yaml | 95 ++++++++++++++ 8 files changed, 492 insertions(+) create mode 100644 install/kubernetes/backstage/.helmignore create mode 100644 install/kubernetes/backstage/Chart.yaml create mode 100644 install/kubernetes/backstage/README.md create mode 100644 install/kubernetes/backstage/templates/_helpers.tpl create mode 100644 install/kubernetes/backstage/templates/deployment.yaml create mode 100644 install/kubernetes/backstage/templates/ingress.yaml create mode 100644 install/kubernetes/backstage/templates/service.yaml create mode 100644 install/kubernetes/backstage/values.yaml diff --git a/install/kubernetes/backstage/.helmignore b/install/kubernetes/backstage/.helmignore new file mode 100644 index 0000000000..50af031725 --- /dev/null +++ b/install/kubernetes/backstage/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/install/kubernetes/backstage/Chart.yaml b/install/kubernetes/backstage/Chart.yaml new file mode 100644 index 0000000000..23ea41ec41 --- /dev/null +++ b/install/kubernetes/backstage/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Spotify Backstage +name: backstage +version: 0.1.1-alpha.12 diff --git a/install/kubernetes/backstage/README.md b/install/kubernetes/backstage/README.md new file mode 100644 index 0000000000..e8d809c228 --- /dev/null +++ b/install/kubernetes/backstage/README.md @@ -0,0 +1,51 @@ +# Backstage Helm Chart + +## App/Frontend Values + +| Parameter | Description | Default | +| --------------------------- | --------------------------------------------------------- | ------------------- | +| app.enabled | Whether to render the frontend app config or not | `true` | +| app.nameOverride | Override the name given to the app/frontend | `""` | +| app.fullnameOverride | Override the full name of the app/frontend | `""` | +| app.replicaCount | The number of replicas for the app/frontend | `1` | +| app.image.repository | The image repository of the app/frontend container | `spotify/backstage` | +| app.image.tag | The app/frontend tag to pull | `latest` | +| app.image.pullPolicy | Image pull policy | `Always` | +| app.service.type | The service type for the app/frontend service | `ClusterIP` | +| app.service.port | The port for the app/frontend | `80` | +| app.ingress.enabled | Whether to create ingress or not | `false` | +| app.ingress.annotations | Annotations for the app/frontend ingress | `{}` | +| app.ingress.hosts[].host | Hostname for the app/frontend | `backstage.local` | +| app.ingress.hosts[].paths[] | Path name to serve the app/frontend on | `["/"]` | +| app.imagePullSecrets[] | Any image secrets you need to pull `app.image.repository` | `[]` | +| app.podSecurityContext | Security context for the app/frontend pods | `{}` | +| app.securityContext | Security context settings for the deployment | `{}` | +| app.resources | Kubernetes Pod resource requests/limits | `{}` | +| app.nodeSelector | Node selectors for scheduling app/frontend pods | `{}` | +| app.tolerations | Tolerations for scheduling app/frontend pods | `{}` | +| app.affinity | Affinity setttings for scheduling app/frontend pods | `{}` | + +## Backend Values + +| Parameter | Description | Default | +| ------------------------------- | ------------------------------------------------------------- | ------------------- | +| backend.enabled | Whether to render the backend config or not | `true` | +| backend.nameOverride | Override the name given to the backend | `""` | +| backend.fullnameOverride | Override the full name of the backend | `""` | +| backend.replicaCount | The number of replicas for the backend | `1` | +| backend.image.repository | The image repository of the backend container | `spotify/backstage` | +| backend.image.tag | The backend tag to pull | `latest` | +| backend.image.pullPolicy | Image pull policy | `Always` | +| backend.service.type | The service type for the backend service | `ClusterIP` | +| backend.service.port | The port for the backend | `80` | +| backend.ingress.enabled | Whether to create ingress or not | `false` | +| backend.ingress.annotations | Annotations for the backend ingress | `{}` | +| backend.ingress.hosts[].host | Hostname for the backend | `backstage.local` | +| backend.ingress.hosts[].paths[] | Path name to serve the backend on | `["/"]` | +| backend.imagePullSecrets[] | Any image secrets you need to pull `backend.image.repository` | `[]` | +| backend.podSecurityContext | Security context for the backend pods | `{}` | +| backend.securityContext | Security context settings for the deployment | `{}` | +| backend.resources | Kubernetes Pod resource requests/limits | `{}` | +| backend.nodeSelector | Node selectors for scheduling backend pods | `{}` | +| backend.tolerations | Tolerations for scheduling backend pods | `{}` | +| backend.affinity | Affinity setttings for scheduling backend pods | `{}` | diff --git a/install/kubernetes/backstage/templates/_helpers.tpl b/install/kubernetes/backstage/templates/_helpers.tpl new file mode 100644 index 0000000000..af630ab9cd --- /dev/null +++ b/install/kubernetes/backstage/templates/_helpers.tpl @@ -0,0 +1,80 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "backstage.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "backstage.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "backstage.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common App labels +*/}} +{{- define "backstage.app.labels" -}} +app.kubernetes.io/name: {{ include "backstage.name" . }}-app +helm.sh/chart: {{ include "backstage.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Common Backend labels +*/}} +{{- define "backstage.backend.labels" -}} +app.kubernetes.io/name: {{ include "backstage.name" . }}-backend +helm.sh/chart: {{ include "backstage.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Create the name of the service account to use for the app +*/}} +{{- define "backstage.app.serviceAccountName" -}} +{{- if .Values.app.serviceAccount.create -}} + {{ default "default" .Values.app.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.app.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Create the name of the service account to use for the backend +*/}} +{{- define "backstage.backend.serviceAccountName" -}} +{{- if .Values.backend.serviceAccount.create -}} + {{ default default .Values.backend.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.backend.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/install/kubernetes/backstage/templates/deployment.yaml b/install/kubernetes/backstage/templates/deployment.yaml new file mode 100644 index 0000000000..85446772b7 --- /dev/null +++ b/install/kubernetes/backstage/templates/deployment.yaml @@ -0,0 +1,119 @@ +{{- if .Values.app.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "backstage.fullname" . }}-app + labels: +{{ include "backstage.app.labels" . | indent 4 }} +spec: + replicas: {{ .Values.app.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "backstage.name" . }}-app + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "backstage.name" . }}-app + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + {{- with .Values.app.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ template "backstage.app.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.app.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-app + securityContext: + {{- toYaml .Values.app.securityContext | nindent 12 }} + image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}" + imagePullPolicy: {{ .Values.app.image.pullPolicy }} + ports: + - name: app + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.app.resources | nindent 12 }} + {{- with .Values.app.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.app.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.app.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} +{{- if .Values.backend.enabled }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "backstage.fullname" . }}-backend + labels: +{{ include "backstage.backend.labels" . | indent 4 }} +spec: + replicas: {{ .Values.backend.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "backstage.name" . }}-backend + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "backstage.name" . }}-backend + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + {{- with .Values.backend.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ template "backstage.backend.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.backend.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }}-backend + securityContext: + {{- toYaml .Values.backend.securityContext | nindent 12 }} + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + ports: + - name: backend + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: backend + readinessProbe: + httpGet: + path: / + port: backend + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + {{- with .Values.backend.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/install/kubernetes/backstage/templates/ingress.yaml b/install/kubernetes/backstage/templates/ingress.yaml new file mode 100644 index 0000000000..77d304cb20 --- /dev/null +++ b/install/kubernetes/backstage/templates/ingress.yaml @@ -0,0 +1,83 @@ +{{- if .Values.app.ingress.enabled -}} +{{- $fullName := include "backstage.fullname" . -}} +{{- $svcPort := .Values.app.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }}-app + labels: +{{ include "backstage.app.labels" . | indent 4 }} + {{- with .Values.app.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.app.ingress.tls }} + tls: + {{- range .Values.app.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.app.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }}-app + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} +{{ end }} +{{- if .Values.backend.ingress.enabled }} +--- +{{- $fullName := include "backstage.fullname" . -}} +{{- $svcPort := .Values.backend.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion }} +apiVersion: networking.k8s.io/v1beta1 +{{- else }} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }}-backend + labels: +{{ include "backstage.backend.labels" . | indent 4 }} + {{- with .Values.backend.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.backend.ingress.tls }} + tls: + {{- range .Values.backend.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.backend.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }}-backend + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} +{{ end }} diff --git a/install/kubernetes/backstage/templates/service.yaml b/install/kubernetes/backstage/templates/service.yaml new file mode 100644 index 0000000000..77a113ae5c --- /dev/null +++ b/install/kubernetes/backstage/templates/service.yaml @@ -0,0 +1,37 @@ +{{- if .Values.app.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "backstage.fullname" . }}-app + labels: +{{ include "backstage.app.labels" . | indent 4 }} +spec: + type: {{ .Values.app.service.type }} + ports: + - port: {{ .Values.app.service.port }} + targetPort: app + protocol: TCP + name: app + selector: + app.kubernetes.io/name: {{ include "backstage.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} +{{- if .Values.app.enabled }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "backstage.fullname" . }}-backend + labels: +{{ include "backstage.backend.labels" . | indent 4 }} +spec: + type: {{ .Values.backend.service.type }} + ports: + - port: {{ .Values.backend.service.port }} + targetPort: backend + protocol: TCP + name: backend + selector: + app.kubernetes.io/name: {{ include "backstage.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/install/kubernetes/backstage/values.yaml b/install/kubernetes/backstage/values.yaml new file mode 100644 index 0000000000..fd4dd66b53 --- /dev/null +++ b/install/kubernetes/backstage/values.yaml @@ -0,0 +1,95 @@ +app: + enabled: true + nameOverride: "" + fullnameOverride: "" + replicaCount: 1 + serviceAccount: + create: false + Name: "" + image: + repository: spotify/backstage + tag: latest + pullPolicy: Always + service: + type: ClusterIP + port: 80 + ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: "nginx" + hosts: + - host: backstage.local + paths: + - / + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + imagePullSecrets: [] + podSecurityContext: {} + # fsGroup: 2000 + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + nodeSelector: {} + tolerations: [] + affinity: {} + +backend: + enabled: false + nameOverride: "" + fullnameOverride: "" + replicaCount: 1 + serviceAccount: + create: false + Name: "" + image: + repository: spotify/backstage-backend + tag: latest + pullPolicy: IfNotPresent + service: + type: ClusterIP + port: 7000 + ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: "nginx" + hosts: + - host: backstage.local + paths: + - /backend + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + imagePullSecrets: [] + podSecurityContext: {} + # fsGroup: 2000 + securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + nodeSelector: {} + tolerations: [] + affinity: {} From 71aa8d9d18da4b54a0f5f0e0f4d3a3c4ad8d8642 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 14:00:28 +0200 Subject: [PATCH 10/71] Scroll to anchor link --- plugins/techdocs/src/reader/components/Reader.tsx | 13 ++++++++++--- ...istener.test.ts => addLinkClickListener.test.ts} | 6 +++--- ...{addEventListener.ts => addLinkClickListener.ts} | 10 ++++------ .../src/reader/transformers/rewriteDocLinks.test.ts | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) rename plugins/techdocs/src/reader/transformers/{addEventListener.test.ts => addLinkClickListener.test.ts} (89%) rename plugins/techdocs/src/reader/transformers/{addEventListener.ts => addLinkClickListener.ts} (81%) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 12cec30c80..935def0dd4 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -25,7 +25,7 @@ import { Header, Content, ItemCard } from '@backstage/core'; import transformer, { addBaseUrl, rewriteDocLinks, - addEventListener, + addLinkClickListener, removeMkdocsHeader, modifyCss, } from '../transformers'; @@ -92,8 +92,15 @@ export const Reader = () => { if (transformedElement) { divElement.shadowRoot.appendChild(transformedElement); transformer(divElement.shadowRoot.children[0], [ - addEventListener({ - onClick: navigate, + addLinkClickListener({ + onClick: url => { + const parsedUrl = new URL(url); + navigate(parsedUrl.pathname); + + divElement.shadowRoot + ?.querySelector(parsedUrl.hash) + ?.scrollIntoView(); + }, }), ]); } diff --git a/plugins/techdocs/src/reader/transformers/addEventListener.test.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts similarity index 89% rename from plugins/techdocs/src/reader/transformers/addEventListener.test.ts rename to plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts index e87ab81ab5..0554ff85e9 100644 --- a/plugins/techdocs/src/reader/transformers/addEventListener.test.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts @@ -15,14 +15,14 @@ */ import { createTestShadowDom, FIXTURES } from '../../test-utils'; -import { addEventListener } from '../transformers'; +import { addLinkClickListener } from '.'; -describe('addEventListener', () => { +describe('addLinkClickListener', () => { it('calls onClick when a link has been clicked', () => { const fn = jest.fn(); const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { transformers: [ - addEventListener({ + addLinkClickListener({ onClick: fn, }), ], diff --git a/plugins/techdocs/src/reader/transformers/addEventListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts similarity index 81% rename from plugins/techdocs/src/reader/transformers/addEventListener.ts rename to plugins/techdocs/src/reader/transformers/addLinkClickListener.ts index 32b9d6b0b9..fe0a11d9ae 100644 --- a/plugins/techdocs/src/reader/transformers/addEventListener.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts @@ -16,22 +16,20 @@ import type { Transformer } from './index'; -type AddEventListenerOptions = { +type addLinkClickListenerOptions = { onClick: (newUrl: string) => void; }; -export const addEventListener = ({ +export const addLinkClickListener = ({ onClick, -}: AddEventListenerOptions): Transformer => { +}: addLinkClickListenerOptions): Transformer => { return dom => { Array.from(dom.getElementsByTagName('a')).forEach(elem => { elem.addEventListener('click', (e: MouseEvent) => { e.preventDefault(); const target = e.target as HTMLAnchorElement; if (target?.getAttribute('href')) { - onClick( - target.getAttribute('href')!.replace(window.location.origin, ''), - ); + onClick(target.getAttribute('href')!); } }); }); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index 83ca846142..a064511fd0 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -34,7 +34,7 @@ describe('rewriteDocLinks', () => { ]); }); - it('should transform a href with licalhost as baseUrl', () => { + it('should transform a href with localhost as baseUrl', () => { const shadowDom = createTestShadowDom( ` Test From ef1c5813bf902c8df067af61581d900540b4c201 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 14:04:33 +0200 Subject: [PATCH 11/71] Change type name --- .../techdocs/src/reader/transformers/addLinkClickListener.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts index fe0a11d9ae..83fb79a0b4 100644 --- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts @@ -16,13 +16,13 @@ import type { Transformer } from './index'; -type addLinkClickListenerOptions = { +type AddLinkClickListenerOptions = { onClick: (newUrl: string) => void; }; export const addLinkClickListener = ({ onClick, -}: addLinkClickListenerOptions): Transformer => { +}: AddLinkClickListenerOptions): Transformer => { return dom => { Array.from(dom.getElementsByTagName('a')).forEach(elem => { elem.addEventListener('click', (e: MouseEvent) => { From 6f9270e7656f1dc638df70307b7a95f96ad2c2c2 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 14:11:57 +0200 Subject: [PATCH 12/71] Updated export from --- plugins/techdocs/src/reader/transformers/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index f49f496277..f35d6aa478 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -16,7 +16,7 @@ export * from './addBaseUrl'; export * from './rewriteDocLinks'; -export * from './addEventListener'; +export * from './addLinkClickListener'; export * from './removeMkdocsHeader'; export * from './modifyCss'; From 45a981ca0442dabb1c43081f05c0e778dd293906 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Tue, 30 Jun 2020 15:39:35 +0200 Subject: [PATCH 13/71] Rewrite urlParser to urlFormater --- .../techdocs/src/reader/components/Reader.tsx | 23 +++-- .../src/reader/transformers/addBaseUrl.ts | 12 +-- .../transformers/addLinkClickListener.ts | 4 +- .../reader/transformers/rewriteDocLinks.ts | 8 +- .../techdocs/src/reader/urlFormatter.test.ts | 84 +++++++++++++++++++ .../reader/{urlParser.ts => urlFormatter.ts} | 28 ++++--- plugins/techdocs/src/reader/urlParser.test.ts | 61 -------------- 7 files changed, 130 insertions(+), 90 deletions(-) create mode 100644 plugins/techdocs/src/reader/urlFormatter.test.ts rename plugins/techdocs/src/reader/{urlParser.ts => urlFormatter.ts} (50%) delete mode 100644 plugins/techdocs/src/reader/urlParser.test.ts diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 935def0dd4..6398d86515 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -30,7 +30,7 @@ import transformer, { modifyCss, } from '../transformers'; import { docStorageURL } from '../../config'; -import URLParser from '../urlParser'; +import URLFormatter from '../urlFormatter'; const useFetch = (url: string) => { const state = useAsync(async () => { @@ -45,7 +45,7 @@ const useFetch = (url: string) => { const useEnforcedTrailingSlash = (): void => { React.useEffect(() => { const actualUrl = window.location.href; - const expectedUrl = new URLParser(window.location.href, '.').parse(); + const expectedUrl = new URLFormatter(window.location.href).formatBaseURL(); if (actualUrl !== expectedUrl) { window.history.replaceState({}, document.title, expectedUrl); @@ -58,10 +58,9 @@ export const Reader = () => { const { componentId, '*': path } = useParams(); const shadowDomRef = useShadowDom(); const navigate = useNavigate(); - const normalizedUrl = new URLParser( + const normalizedUrl = new URLFormatter( `${docStorageURL}${location.pathname.replace('/docs', '')}`, - '.', - ).parse(); + ).formatBaseURL(); const state = useFetch(`${normalizedUrl}index.html`); useEnforcedTrailingSlash(); @@ -89,13 +88,23 @@ export const Reader = () => { ]); divElement.shadowRoot.innerHTML = ''; + if (transformedElement) { divElement.shadowRoot.appendChild(transformedElement); transformer(divElement.shadowRoot.children[0], [ + dom => { + setTimeout(() => { + if (window.location.hash) { + const hash = window.location.hash.slice(1); + divElement.shadowRoot?.getElementById(hash)?.scrollIntoView(); + } + }, 200); + return dom; + }, addLinkClickListener({ - onClick: url => { + onClick: (_: MouseEvent, url: string) => { const parsedUrl = new URL(url); - navigate(parsedUrl.pathname); + navigate(`${parsedUrl.pathname}${parsedUrl.hash}`); divElement.shadowRoot ?.querySelector(parsedUrl.hash) diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index 653f0b2f71..dc041e9a71 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import URLParser from '../urlParser'; +import URLFormatter from '../urlFormatter'; import type { Transformer } from './index'; type AddBaseUrlOptions = { @@ -36,11 +36,13 @@ export const addBaseUrl = ({ Array.from(list) .filter(elem => !!elem.getAttribute(attributeName)) .forEach((elem: T) => { - const newUrl = new URLParser( + const urlFormatter = new URLFormatter( `${docStorageURL}/${componentId}/${path}`, - elem.getAttribute(attributeName)!, - ).parse(); - elem.setAttribute(attributeName, newUrl); + ); + elem.setAttribute( + attributeName, + urlFormatter.formatURL(elem.getAttribute(attributeName)!), + ); }); }; diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts index 83fb79a0b4..93e4b67a1b 100644 --- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts @@ -17,7 +17,7 @@ import type { Transformer } from './index'; type AddLinkClickListenerOptions = { - onClick: (newUrl: string) => void; + onClick: (e: MouseEvent, newUrl: string) => void; }; export const addLinkClickListener = ({ @@ -29,7 +29,7 @@ export const addLinkClickListener = ({ e.preventDefault(); const target = e.target as HTMLAnchorElement; if (target?.getAttribute('href')) { - onClick(target.getAttribute('href')!); + onClick(e, target.getAttribute('href')!); } }); }); diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts index 7b8495b6b7..4bf4ab3898 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import URLParser from '../urlParser'; +import URLFormatter from '../urlFormatter'; import type { Transformer } from './index'; export const rewriteDocLinks = (): Transformer => { @@ -26,12 +26,10 @@ export const rewriteDocLinks = (): Transformer => { Array.from(list) .filter(elem => elem.hasAttribute(attributeName)) .forEach((elem: T) => { + const urlFormatter = new URLFormatter(window.location.href); elem.setAttribute( attributeName, - new URLParser( - window.location.href, - elem.getAttribute(attributeName)!, - ).parse(), + urlFormatter.formatURL(elem.getAttribute(attributeName)!), ); }); }; diff --git a/plugins/techdocs/src/reader/urlFormatter.test.ts b/plugins/techdocs/src/reader/urlFormatter.test.ts new file mode 100644 index 0000000000..2659f9f4dc --- /dev/null +++ b/plugins/techdocs/src/reader/urlFormatter.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2020 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 URLFormatter from './urlFormatter'; + +describe('URLFormatter', () => { + describe('formatURL', () => { + it('should not change an absolute url', () => { + const formatter = new URLFormatter('https://www.google.com/'); + expect(formatter.formatURL('https://www.mkdocs.org/')).toEqual( + 'https://www.mkdocs.org/', + ); + }); + + it('should convert a relative url to an absolute url', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/', + ); + expect(formatter.formatURL('../../support/installing/')).toEqual( + 'https://www.mkdocs.org/support/installing/', + ); + }); + + it('should add a trailing slash', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started', + ); + expect(formatter.formatURL('./getting-started')).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/', + ); + }); + + it('should not add a trailing slash', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/', + ); + expect(formatter.formatURL('.')).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/', + ); + }); + + it('should not add multiple hashes', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/#hash1', + ); + expect(formatter.formatURL('./#hash2')).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/#hash2', + ); + }); + }); + + describe('formatBaseURL', () => { + it('should keep query params in URL', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/?query=hello+world', + ); + expect(formatter.formatBaseURL()).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/?query=hello+world', + ); + }); + + it('should keep hash in URL', () => { + const formatter = new URLFormatter( + 'https://www.mkdocs.org/user-guide/getting-started/#hash', + ); + expect(formatter.formatBaseURL()).toEqual( + 'https://www.mkdocs.org/user-guide/getting-started/#hash', + ); + }); + }); +}); diff --git a/plugins/techdocs/src/reader/urlParser.ts b/plugins/techdocs/src/reader/urlFormatter.ts similarity index 50% rename from plugins/techdocs/src/reader/urlParser.ts rename to plugins/techdocs/src/reader/urlFormatter.ts index b1429b7635..166f0d9963 100644 --- a/plugins/techdocs/src/reader/urlParser.ts +++ b/plugins/techdocs/src/reader/urlFormatter.ts @@ -14,18 +14,26 @@ * limitations under the License. */ -const normalizeBaseURL = (baseURL: string): string => { - const url = new URL(baseURL); - url.pathname = url.pathname.replace(/([^/])$/, '$1/'); - return url.toString(); -}; +export default class URLFormatter { + constructor(public baseURL: string) {} -export default class URLParser { - constructor(public baseURL: string, public pathname: string) { - this.baseURL = normalizeBaseURL(baseURL); + formatBaseURL(): string { + return this.normalizeURL(this.baseURL); } - parse(): string { - return new URL(this.pathname, this.baseURL).toString(); + formatURL(pathname: string): string { + return this.normalizeURL(new URL(pathname, this.baseURL).toString()); + } + + private normalizeURL(urlString: string): string { + const url = new URL(urlString); + const filename: string = url.pathname.split('/').pop() ?? url.pathname; + const isDir: boolean = filename.includes('.') === false; + + if (isDir) { + url.pathname = url.pathname.replace(/([^/])$/, '$1/'); + } + + return url.toString(); } } diff --git a/plugins/techdocs/src/reader/urlParser.test.ts b/plugins/techdocs/src/reader/urlParser.test.ts deleted file mode 100644 index 4322fc42e1..0000000000 --- a/plugins/techdocs/src/reader/urlParser.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020 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 URLParser from './urlParser'; - -describe('URLParser', () => { - it('should not change an absolute url', () => { - const urlParser = new URLParser( - 'https://www.google.com/', - 'https://www.mkdocs.org/', - ); - - expect(urlParser.parse()).toEqual('https://www.mkdocs.org/'); - }); - - it('should convert a relative url to an absolute url', () => { - const urlParser = new URLParser( - 'https://www.mkdocs.org/user-guide/getting-started/', - '../../support/installing/', - ); - - expect(urlParser.parse()).toEqual( - 'https://www.mkdocs.org/support/installing/', - ); - }); - - it('should add a trailing slash', () => { - const urlParser = new URLParser( - 'https://www.mkdocs.org/user-guide/getting-started', - '.', - ); - - expect(urlParser.parse()).toEqual( - 'https://www.mkdocs.org/user-guide/getting-started/', - ); - }); - - it('should not add a trailing slash', () => { - const urlParser = new URLParser( - 'https://www.mkdocs.org/user-guide/getting-started/', - '.', - ); - - expect(urlParser.parse()).toEqual( - 'https://www.mkdocs.org/user-guide/getting-started/', - ); - }); -}); From c5bdbdfe85ee6615dbfe593df99bb0b58d3b04d9 Mon Sep 17 00:00:00 2001 From: ellieseastream <67265053+ellieseastream@users.noreply.github.com> Date: Tue, 30 Jun 2020 16:08:15 +0200 Subject: [PATCH 14/71] Create adr006-avoid-react-fc --- docs/architecture-decisions/adr006-avoid-react-fc | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/architecture-decisions/adr006-avoid-react-fc diff --git a/docs/architecture-decisions/adr006-avoid-react-fc b/docs/architecture-decisions/adr006-avoid-react-fc new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/docs/architecture-decisions/adr006-avoid-react-fc @@ -0,0 +1 @@ + From 9f475bacd3253faa9cb38d5618606c608d45a1f0 Mon Sep 17 00:00:00 2001 From: ellieseastream <67265053+ellieseastream@users.noreply.github.com> Date: Tue, 30 Jun 2020 16:10:57 +0200 Subject: [PATCH 15/71] Rename adr006-avoid-react-fc to adr006-avoid-react-fc.md --- .../{adr006-avoid-react-fc => adr006-avoid-react-fc.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/architecture-decisions/{adr006-avoid-react-fc => adr006-avoid-react-fc.md} (100%) diff --git a/docs/architecture-decisions/adr006-avoid-react-fc b/docs/architecture-decisions/adr006-avoid-react-fc.md similarity index 100% rename from docs/architecture-decisions/adr006-avoid-react-fc rename to docs/architecture-decisions/adr006-avoid-react-fc.md From b926b4ef7b91bf275ba7832d54df61857838faac Mon Sep 17 00:00:00 2001 From: ellieseastream <67265053+ellieseastream@users.noreply.github.com> Date: Tue, 30 Jun 2020 17:08:39 +0200 Subject: [PATCH 16/71] Added example of changing existing React.FC code --- .../adr006-avoid-react-fc.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 8b13789179..4b28d72361 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -1 +1,28 @@ +# ADR006: Avoid React.FC +## Context + +Facebook has removed ```React.FC``` from their base template for a Typescript project. The reason for this was that it was found to be an unnecessary feature with next to no benefits in combination with a few downsides. + +The main reasons were: +- **children props** were implicitly added +- **Generic Type** lacked support + +## Decision + +To keep our codebase up to date, we have decided that React.FC should be avoided in our codebase when adding new code. + +Here is an example: +```` +type MyType = { text: string } + +// avoid React.FC +const ComponentWithReactFC = React.FC = ({text} =>
{text}
) + +// do this instead +const ComponentWithoutReactFC = ({text} : MyType) =>
{text}
+```` + +## Consequences + +We will gradually remove the current usage of React.FC from our codebase. From 8e602b52bf6273513b68e67d1f16f41f6e53610d Mon Sep 17 00:00:00 2001 From: David Dyball Date: Tue, 30 Jun 2020 19:00:32 +0100 Subject: [PATCH 17/71] Fix container name on backend yaml --- install/kubernetes/backend.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/kubernetes/backend.yaml b/install/kubernetes/backend.yaml index d762f30331..1f215dccf8 100644 --- a/install/kubernetes/backend.yaml +++ b/install/kubernetes/backend.yaml @@ -18,7 +18,7 @@ spec: component: backend spec: containers: - - name: app + - name: backend image: spotify/backstage-backend:latest imagePullPolicy: Always ports: From c26b7a0d52d1704b3cc98aa1d4e9e131f1254cb8 Mon Sep 17 00:00:00 2001 From: David Dyball Date: Tue, 30 Jun 2020 19:00:57 +0100 Subject: [PATCH 18/71] Fix spelling --- install/kubernetes/backend.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/kubernetes/backend.yaml b/install/kubernetes/backend.yaml index 1f215dccf8..669426fecd 100644 --- a/install/kubernetes/backend.yaml +++ b/install/kubernetes/backend.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: backstage-backend - labes: + labels: app: backstage component: backend spec: From 404f7c1deab570782439ce84c480f219f5bf5d51 Mon Sep 17 00:00:00 2001 From: David Dyball Date: Tue, 30 Jun 2020 19:01:38 +0100 Subject: [PATCH 19/71] Fix typo in app.yaml --- install/kubernetes/app.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/kubernetes/app.yaml b/install/kubernetes/app.yaml index 465d491907..ed4108f11b 100644 --- a/install/kubernetes/app.yaml +++ b/install/kubernetes/app.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: backstage - labes: + labels: app: backstage component: frontend spec: From 38734afa4c87e44620ba55edf2364739f5cd17be Mon Sep 17 00:00:00 2001 From: Christina Marie Rahaim Date: Thu, 18 Jun 2020 16:33:01 -0400 Subject: [PATCH 20/71] bumping react-router and react-router-dom --- packages/app/package.json | 5 ++-- .../default-app/packages/app/package.json.hbs | 5 ++-- .../plugins/welcome/package.json.hbs | 2 +- packages/core-api/package.json | 2 +- packages/core/package.json | 4 +-- packages/dev-utils/package.json | 4 +-- packages/test-utils/package.json | 4 +-- plugins/catalog/package.json | 4 +-- plugins/circleci/package.json | 4 +-- plugins/gitops-profiles/package.json | 2 +- plugins/graphiql/package.json | 2 +- plugins/lighthouse/package.json | 2 +- plugins/register-component/package.json | 4 +-- plugins/scaffolder/package.json | 2 +- plugins/techdocs/package.json | 4 +-- plugins/welcome/package.json | 2 +- yarn.lock | 27 +++++++++---------- 17 files changed, 40 insertions(+), 39 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 46769e7dfb..e924d9bc04 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,12 +21,13 @@ "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", + "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^16.12.0", "react-dom": "^16.12.0", "react-hot-loader": "^4.12.21", - "react-router": "6.0.0-alpha.5", - "react-router-dom": "6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0", "zen-observable": "^0.8.15" }, diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs index 31e6baa63e..26cce2c5e6 100644 --- a/packages/cli/templates/default-app/packages/app/package.json.hbs +++ b/packages/cli/templates/default-app/packages/app/package.json.hbs @@ -10,11 +10,12 @@ "@backstage/core": "^{{version}}", "@backstage/test-utils": "^{{version}}", "@backstage/theme": "^{{version}}", + "history": "^5.0.0", "plugin-welcome": "0.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router": "6.0.0-alpha.5", - "react-router-dom": "6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs index b686abab9a..52e7a04f72 100644 --- a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs +++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs @@ -26,7 +26,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^14.2.0", - "react-router-dom": "6.0.0-alpha.5" + "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { "@backstage/cli": "^{{version}}", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index f9f206daf5..4e488206f1 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -36,7 +36,7 @@ "@types/react": "^16.9", "prop-types": "^15.7.2", "react": "^16.12.0", - "react-router-dom": "6.0.0-alpha.5", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0", "zen-observable": "^0.8.15" }, diff --git a/packages/core/package.json b/packages/core/package.json index e49e4616ae..562f3894fa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,8 +47,8 @@ "react-dom": "^16.12.0", "react-helmet": "6.1.0", "react-hook-form": "^5.7.2", - "react-router": "6.0.0-alpha.5", - "react-router-dom": "6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-sparklines": "^1.7.0", "react-syntax-highlighter": "^12.2.1", "react-use": "^14.2.0" diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index fce1340386..1a08f0fb22 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -42,8 +42,8 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-hot-loader": "^4.12.21", - "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5" + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { "@types/jest": "^25.2.2", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index f0ed4905d5..08214b285a 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -40,8 +40,8 @@ "@types/react": "^16.9", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" }, "devDependencies": { diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 818a8c4bc6..06c83136f5 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -32,8 +32,8 @@ "moment": "^2.26.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0", "swr": "^0.2.2" }, diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 8eede54580..89f6335dcc 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -40,8 +40,8 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", - "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 67a76ef872..b96843d412 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -28,7 +28,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router-dom": "6.0.0-alpha.5", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index af971f9160..45cad52ac8 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -53,7 +53,7 @@ "@types/jest": "^25.2.2", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "react-router-dom": "6.0.0-alpha.5" + "react-router-dom": "6.0.0-beta.0" }, "files": [ "dist" diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index bb79e57179..6f3a67ef64 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -29,7 +29,7 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-markdown": "^4.3.1", - "react-router-dom": "6.0.0-alpha.5", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 738cabea4b..de57e8a233 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -31,8 +31,8 @@ "react": "^16.13.1", "react-dom": "^16.13.1", "react-hook-form": "^5.7.2", - "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 1bccd6a738..83a80aef56 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -30,7 +30,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router-dom": "6.0.0-alpha.5", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0", "swr": "^0.2.2" }, diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index ca8e957a46..6b879a2d78 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -29,8 +29,8 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index c4b9b5e5bd..df33c222f4 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -28,7 +28,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router-dom": "6.0.0-alpha.5", + "react-router-dom": "6.0.0-beta.0", "react-use": "^14.2.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 180b26a6eb..8d52b405bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9761,10 +9761,10 @@ highlight.js@~9.15.0, highlight.js@~9.15.1: resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== -history@5.0.0-beta.9: - version "5.0.0-beta.9" - resolved "https://registry.npmjs.org/history/-/history-5.0.0-beta.9.tgz#fe230706c18c5f7f132001e55215e71b4aaab6d6" - integrity sha512-iLpu0fzu3iM041KDMNsawyB6YZjPLB+Bn+Pvq2lMnY7xxpxDIYvEz7r4et3Na8FthWzbYeukjl74ZKGWXcLhIA== +history@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08" + integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg== dependencies: "@babel/runtime" "^7.7.6" @@ -15450,20 +15450,19 @@ react-redux@^7.1.1: prop-types "^15.7.2" react-is "^16.9.0" -react-router-dom@6.0.0-alpha.5, react-router-dom@^6.0.0-alpha.5: - version "6.0.0-alpha.5" - resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0-alpha.5.tgz#3c3e22226ee610eb91042a351741ce3f53596323" - integrity sha512-xo3VM55aE563uyZBPoUplfCPOYKJmTP2oA8wamm0k4K07e/6T4x4DDunS5Gu2VIy+m2+5mZp8n0rT6S+tYCb6Q== +react-router-dom@6.0.0-beta.0: + version "6.0.0-beta.0" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0-beta.0.tgz#9dcc8555365f22f7fbd09f26b6b82543f3eb97d6" + integrity sha512-36yNNGMT8RB9FRPL9nKJi6HKDkgOakU+o/2hHpSzR6e37gN70MpOU6QQlmif4oAWWBwjyGc3ZNOMFCsFuHUY5w== dependencies: - history "5.0.0-beta.9" prop-types "^15.7.2" + react-router "6.0.0-beta.0" -react-router@6.0.0-alpha.5, react-router@^6.0.0-alpha.5: - version "6.0.0-alpha.5" - resolved "https://registry.npmjs.org/react-router/-/react-router-6.0.0-alpha.5.tgz#c98805e50dc0e64787aa8aa4fa6753b435f2496b" - integrity sha512-cDj70bTUAgcfx6b5Fx1+wVlBSDVZGo8N+GUDk/yNFDCyGLfAsFlRpS3BhQqx8c49w2cCW+OrXxFhB4cbLZxWJw== +react-router@6.0.0-beta.0: + version "6.0.0-beta.0" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.0.0-beta.0.tgz#3e11f39b6ded4412c2fed9e4f989dd4c8156724d" + integrity sha512-VgMdfpVcmFQki/LZuLh8E/MNACekDetz4xqft+a6fBZvvJnVqKbLqebF7hyoawGrV1HcO5tVaUang2Og4W2j1Q== dependencies: - history "5.0.0-beta.9" prop-types "^15.7.2" react-side-effect@^2.1.0: From b2ff69e2d30788122ffc77218787491f8b3a2e7b Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 1 Jul 2020 10:01:57 +0200 Subject: [PATCH 21/71] fix(transformer): add trailing slash to test output --- .../src/reader/transformers/rewriteDocLinks.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts index a064511fd0..b4ceab0b38 100644 --- a/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts +++ b/plugins/techdocs/src/reader/transformers/rewriteDocLinks.test.ts @@ -49,9 +49,9 @@ describe('rewriteDocLinks', () => { expect(getSample(shadowDom, 'a', 'href', 6)).toEqual([ 'http://example.org/', - 'http://localhost/example', - 'http://localhost/example-docs', - 'http://localhost/example-docs/example-page', + 'http://localhost/example/', + 'http://localhost/example-docs/', + 'http://localhost/example-docs/example-page/', ]); }); }); From 5e7eeda1d0e1ef788b959e82892346a12743a17c Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 1 Jul 2020 11:53:56 +0200 Subject: [PATCH 22/71] fix(transformers): add slash at end of path if missing --- .../reader/transformers/addBaseUrl.test.ts | 27 ++++++++++++++++++- .../src/reader/transformers/addBaseUrl.ts | 5 +++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index b9cf9745ee..4b1477e9c0 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -62,7 +62,7 @@ describe('addBaseUrl', () => { ]); }); - it('includes path option', () => { + it('includes path option without slash', () => { const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { transformers: [ addBaseUrl({ @@ -86,4 +86,29 @@ describe('addBaseUrl', () => { 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/javascripts/vendor.d710d30a.min.js', ]); }); + + it('includes path option with slash', () => { + const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + transformers: [ + addBaseUrl({ + docStorageURL: DOC_STORAGE_URL, + componentId: 'example-docs', + path: 'examplepath/', + }), + ], + }); + + expect(getSample(shadowDom, 'img', 'src')).toEqual([ + 'https://example-host.storage.googleapis.com/example-docs/examplepath/img/win-py-install.png', + 'https://example-host.storage.googleapis.com/example-docs/examplepath/img/initial-layout.png', + ]); + expect(getSample(shadowDom, 'link', 'href')).toEqual([ + 'https://www.mkdocs.org/', + 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/images/favicon.png', + ]); + expect(getSample(shadowDom, 'script', 'src')).toEqual([ + 'https://www.google-analytics.com/analytics.js', + 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/javascripts/vendor.d710d30a.min.js', + ]); + }); }); diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index dc041e9a71..91986db729 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -37,8 +37,11 @@ export const addBaseUrl = ({ .filter(elem => !!elem.getAttribute(attributeName)) .forEach((elem: T) => { const urlFormatter = new URLFormatter( - `${docStorageURL}/${componentId}/${path}`, + path.length < 1 || path.endsWith('/') + ? `${docStorageURL}/${componentId}/${path}` + : `${docStorageURL}/${componentId}/${path}/`, ); + elem.setAttribute( attributeName, urlFormatter.formatURL(elem.getAttribute(attributeName)!), From 434994bfee1810cc2ceb06a872150be7b7144a52 Mon Sep 17 00:00:00 2001 From: ellinors Date: Wed, 1 Jul 2020 14:28:07 +0200 Subject: [PATCH 23/71] Code change to remove React.FC in SentryIssueTable.tsx, testing it out --- .../src/components/SentryIssuesTable/SentryIssuesTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index cebd5563f4..aae8009d98 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import { Table, TableColumn } from '@backstage/core'; import { SentryIssue } from '../../data/sentry-issue'; import { format } from 'timeago.js'; @@ -60,7 +60,7 @@ type SentryIssuesTableProps = { sentryIssues: SentryIssue[]; }; -const SentryIssuesTable: FC = ({ sentryIssues }) => { +const SentryIssuesTable = ({ sentryIssues }: SentryIssuesTableProps) => { return ( Date: Wed, 1 Jul 2020 14:53:51 +0200 Subject: [PATCH 24/71] Removed React.FC from welcome page --- plugins/welcome/src/components/WelcomePage/WelcomePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx index ce151737b3..c77d7eacd1 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC } from 'react'; +import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { Typography, @@ -38,7 +38,7 @@ import { configApiRef, } from '@backstage/core'; -const WelcomePage: FC<{}> = () => { +const WelcomePage = ({}) => { const appTitle = useApi(configApiRef).getOptionalString('app.title') ?? 'Backstage'; const profile = { givenName: '' }; From b6ec4298bc19879008c3853a71a06b1cb2a36c27 Mon Sep 17 00:00:00 2001 From: ellieseastream <67265053+ellieseastream@users.noreply.github.com> Date: Wed, 1 Jul 2020 16:21:07 +0200 Subject: [PATCH 25/71] Added PR link and edited example. --- .../adr006-avoid-react-fc.md | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 4b28d72361..53cc3df7ca 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -1,4 +1,4 @@ -# ADR006: Avoid React.FC +# ADR006: Avoid React.FC and React.SFC ## Context @@ -8,21 +8,35 @@ The main reasons were: - **children props** were implicitly added - **Generic Type** lacked support +Read more about the removal in [this PR](https://github.com/facebook/create-react-app/pull/8177). + ## Decision To keep our codebase up to date, we have decided that React.FC should be avoided in our codebase when adding new code. Here is an example: ```` -type MyType = { text: string } +/* Avoid this: */ +type BadProps = { text: string; }; -// avoid React.FC -const ComponentWithReactFC = React.FC = ({text} =>
{text}
) +const BadComponent: FC = ({ text, children }) => ( +
+
{text}
+ {children} +
+) -// do this instead -const ComponentWithoutReactFC = ({text} : MyType) =>
{text}
+ +/* Do this instead: */ +type GoodProps = { text: string; children?: React.ReactNode }; +const GoodComponent = ({ text, children }: GoodProps) => ( +
+
{text}
+ {children} +
+) ```` ## Consequences -We will gradually remove the current usage of React.FC from our codebase. +We will gradually remove the current usage of React.FC and React.SFC from our codebase. From 4491e52567f7418e6baf45616f76cbde8e9d2aa6 Mon Sep 17 00:00:00 2001 From: ellieseastream <67265053+ellieseastream@users.noreply.github.com> Date: Wed, 1 Jul 2020 16:22:04 +0200 Subject: [PATCH 26/71] Update adr006-avoid-react-fc.md --- docs/architecture-decisions/adr006-avoid-react-fc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 53cc3df7ca..af8db8795d 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -12,7 +12,7 @@ Read more about the removal in [this PR](https://github.com/facebook/create-reac ## Decision -To keep our codebase up to date, we have decided that React.FC should be avoided in our codebase when adding new code. +To keep our codebase up to date, we have decided that React.FC and React.SFC should be avoided in our codebase when adding new code. Here is an example: ```` From cec455c0e42d7a3f582cd14cbbe36288bf2a1b92 Mon Sep 17 00:00:00 2001 From: ellieseastream <67265053+ellieseastream@users.noreply.github.com> Date: Wed, 1 Jul 2020 17:30:28 +0200 Subject: [PATCH 27/71] Update adr006-avoid-react-fc.md --- .../adr006-avoid-react-fc.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index af8db8795d..89234c0697 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -2,23 +2,22 @@ ## Context -Facebook has removed ```React.FC``` from their base template for a Typescript project. The reason for this was that it was found to be an unnecessary feature with next to no benefits in combination with a few downsides. +Facebook has removed `React.FC` from their base template for a Typescript project. The reason for this was that it was found to be an unnecessary feature with next to no benefits in combination with a few downsides. The main reasons were: - **children props** were implicitly added -- **Generic Type** lacked support +- **Generic Type** were not supported on children Read more about the removal in [this PR](https://github.com/facebook/create-react-app/pull/8177). ## Decision -To keep our codebase up to date, we have decided that React.FC and React.SFC should be avoided in our codebase when adding new code. +To keep our codebase up to date, we have decided that `React.FC` and `React.SFC` should be avoided in our codebase when adding new code. Here is an example: -```` +```ts /* Avoid this: */ type BadProps = { text: string; }; - const BadComponent: FC = ({ text, children }) => (
{text}
@@ -26,17 +25,16 @@ const BadComponent: FC = ({ text, children }) => (
) - /* Do this instead: */ -type GoodProps = { text: string; children?: React.ReactNode }; +type GoodProps = { text: string; children?: React.ReactNode; }; const GoodComponent = ({ text, children }: GoodProps) => (
{text}
{children}
) -```` +``` ## Consequences -We will gradually remove the current usage of React.FC and React.SFC from our codebase. +We will gradually remove the current usage of `React.FC` and `React.SFC` from our codebase. From 9ef217e6b876753b3f98f7bbbdeb35c43a338311 Mon Sep 17 00:00:00 2001 From: ellinors Date: Wed, 1 Jul 2020 17:37:51 +0200 Subject: [PATCH 28/71] removed unnecessary curly brackets --- plugins/welcome/src/components/WelcomePage/WelcomePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx index c77d7eacd1..272297ed45 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx @@ -38,7 +38,7 @@ import { configApiRef, } from '@backstage/core'; -const WelcomePage = ({}) => { +const WelcomePage = () => { const appTitle = useApi(configApiRef).getOptionalString('app.title') ?? 'Backstage'; const profile = { givenName: '' }; From 0c2ba42e4e1fca7c88e0d0e92ba3bcc03700c80a Mon Sep 17 00:00:00 2001 From: Ivan Shmidt Date: Thu, 2 Jul 2020 01:48:03 +0200 Subject: [PATCH 29/71] fix: favicon styles incapsulation --- packages/app/public/favicon.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/public/favicon.svg b/packages/app/public/favicon.svg index 535eea5543..351dcc8809 100644 --- a/packages/app/public/favicon.svg +++ b/packages/app/public/favicon.svg @@ -1,13 +1,13 @@ - + - favicon + Backstage favicon
+ + + + < + + + Build Details + + +
+ + + + Branch + + {details?.build.branch} + + + + Message + + {details?.build.message} + + + + Commit ID + + {details?.build.commitId} + + + + Status + + + + + + + + Author + + {details?.author} + + + + Links + + + + {details?.overviewUrl && ( + + )} + {details?.logUrl && ( + + )} + + + + +
+ + + ); +}; diff --git a/plugins/github-actions/src/components/BuildDetailsPage/index.ts b/plugins/github-actions/src/components/BuildDetailsPage/index.ts new file mode 100644 index 0000000000..d59d0fc396 --- /dev/null +++ b/plugins/github-actions/src/components/BuildDetailsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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 { BuildDetailsPage } from './BuildDetailsPage'; diff --git a/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx b/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx new file mode 100644 index 0000000000..a04189debc --- /dev/null +++ b/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2020 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 { Link } from '@backstage/core'; +import { + LinearProgress, + makeStyles, + Table, + TableBody, + TableCell, + TableRow, + Theme, + Typography, +} from '@material-ui/core'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { BuildsClient } from '../../apis/builds'; +import { BuildStatusIndicator } from '../BuildStatusIndicator'; + +const client = BuildsClient.create(); + +const useStyles = makeStyles(theme => ({ + root: { + // height: 400, + }, + title: { + paddingBottom: theme.spacing(1), + }, +})); + +export const BuildInfoCard = () => { + const classes = useStyles(); + const status = useAsync(() => client.listBuilds('entity:spotify:backstage')); + + let content: JSX.Element; + + if (status.loading) { + content = ; + } else if (status.error) { + content = ( + + Failed to load builds, {status.error.message} + + ); + } else { + const [build] = + status.value?.filter(({ branch }) => branch === 'master') ?? []; + + content = ( + + + + + Message + + + + {build?.message} + + + + + + Commit ID + + {build?.commitId} + + + + Status + + + + + + +
+ ); + } + + return ( +
+ + Master Build + + {content} +
+ ); +}; diff --git a/plugins/github-actions/src/components/BuildInfoCard/index.ts b/plugins/github-actions/src/components/BuildInfoCard/index.ts new file mode 100644 index 0000000000..a6409e682a --- /dev/null +++ b/plugins/github-actions/src/components/BuildInfoCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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 { BuildInfoCard } from './BuildInfoCard'; diff --git a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx b/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx new file mode 100644 index 0000000000..fc784108d4 --- /dev/null +++ b/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx @@ -0,0 +1,128 @@ +/* + * Copyright 2020 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 { Link } from '@backstage/core'; +import { + LinearProgress, + makeStyles, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Theme, + Tooltip, + Typography, +} from '@material-ui/core'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { BuildsClient } from '../../apis/builds'; +import { BuildStatusIndicator } from '../BuildStatusIndicator'; + +const client = BuildsClient.create(); + +const LongText = ({ text, max }: { text: string; max: number }) => { + if (text.length < max) { + return {text}; + } + return ( + + {text.slice(0, max)}... + + ); +}; + +const useStyles = makeStyles(theme => ({ + root: { + padding: theme.spacing(2), + }, + title: { + padding: theme.spacing(1, 0, 2, 0), + }, +})); + +const PageContents = () => { + const { loading, error, value } = useAsync(() => + client.listBuilds('entity:spotify:backstage'), + ); + + if (loading) { + return ; + } + + if (error) { + return ( + + Failed to load builds, {error.message}{' '} + + ); + } + + return ( + + + + + Status + Branch + Message + Commit + + + + {value!.map(build => ( + + + + + + + + + + + + + + + + + + + {build.commitId.slice(0, 10)} + + + + ))} + +
+
+ ); +}; + +export const BuildListPage = () => { + const classes = useStyles(); + return ( +
+ + CI/CD Builds + + +
+ ); +}; diff --git a/plugins/github-actions/src/components/BuildListPage/index.ts b/plugins/github-actions/src/components/BuildListPage/index.ts new file mode 100644 index 0000000000..2134913e51 --- /dev/null +++ b/plugins/github-actions/src/components/BuildListPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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 { BuildListPage } from './BuildListPage'; diff --git a/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx b/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx new file mode 100644 index 0000000000..332a03d67a --- /dev/null +++ b/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2020 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 { IconComponent } from '@backstage/core'; +import { makeStyles, Theme } from '@material-ui/core'; +import ProgressIcon from '@material-ui/icons/Autorenew'; +import SuccessIcon from '@material-ui/icons/CheckCircle'; +import FailureIcon from '@material-ui/icons/Error'; +import UnknownIcon from '@material-ui/icons/Help'; +import React from 'react'; +import { BuildStatus } from '../../apis/builds'; + +type Props = { + status?: BuildStatus; +}; + +type StatusStyle = { + icon: IconComponent; + color: string; +}; + +const styles: { [key in BuildStatus]: StatusStyle } = { + [BuildStatus.Null]: { + icon: UnknownIcon, + color: '#f49b20', + }, + [BuildStatus.Success]: { + icon: SuccessIcon, + color: '#1db855', + }, + [BuildStatus.Failure]: { + icon: FailureIcon, + color: '#CA001B', + }, + [BuildStatus.Pending]: { + icon: UnknownIcon, + color: '#5BC0DE', + }, + [BuildStatus.Running]: { + icon: ProgressIcon, + color: '#BEBEBE', + }, +}; + +const useStyles = makeStyles({ + icon: style => ({ + color: style.color, + }), +}); + +export const BuildStatusIndicator = ({ status }: Props) => { + const style = (status && styles[status]) || styles[BuildStatus.Null]; + const classes = useStyles(style); + const Icon = style.icon; + + return ( +
+ +
+ ); +}; diff --git a/plugins/github-actions/src/components/BuildStatusIndicator/index.ts b/plugins/github-actions/src/components/BuildStatusIndicator/index.ts new file mode 100644 index 0000000000..520de525ec --- /dev/null +++ b/plugins/github-actions/src/components/BuildStatusIndicator/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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 { BuildStatusIndicator } from './BuildStatusIndicator'; diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts new file mode 100644 index 0000000000..3a0a0fe2d3 --- /dev/null +++ b/plugins/github-actions/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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 { plugin } from './plugin'; diff --git a/plugins/github-actions/src/plugin.test.ts b/plugins/github-actions/src/plugin.test.ts new file mode 100644 index 0000000000..fa1075cbc8 --- /dev/null +++ b/plugins/github-actions/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 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 { plugin } from './plugin'; + +describe('github-actions', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/github-actions/src/plugin.ts b/plugins/github-actions/src/plugin.ts new file mode 100644 index 0000000000..638c8d2c8c --- /dev/null +++ b/plugins/github-actions/src/plugin.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 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 { createPlugin, createRouteRef } from '@backstage/core'; +import { BuildDetailsPage } from './components/BuildDetailsPage'; +import { BuildListPage } from './components/BuildListPage'; + +// TODO(freben): This is just a demo route for now +export const rootRouteRef = createRouteRef({ + path: '/github-actions', + title: 'GitHub Actions', +}); +export const buildRouteRef = createRouteRef({ + path: '/github-actions/builds/:buildUri', + title: 'GitHub Actions Build', +}); + +export const plugin = createPlugin({ + id: 'github-actions', + register({ router }) { + router.addRoute(rootRouteRef, BuildListPage); + router.addRoute(buildRouteRef, BuildDetailsPage); + }, +}); diff --git a/plugins/github-actions/src/setupTests.ts b/plugins/github-actions/src/setupTests.ts new file mode 100644 index 0000000000..8553642152 --- /dev/null +++ b/plugins/github-actions/src/setupTests.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 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'; + +require('jest-fetch-mock').enableMocks(); From ea0e5db06bf71f7c2afff0471a0499ec6947d9f2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 26 Jun 2020 17:25:59 +0200 Subject: [PATCH 37/71] cli: add build-workspace command --- packages/cli/src/commands/buildWorkspace.ts | 29 +++++++++++++++++++++ packages/cli/src/index.ts | 5 ++++ packages/cli/src/lib/packager/index.ts | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 packages/cli/src/commands/buildWorkspace.ts diff --git a/packages/cli/src/commands/buildWorkspace.ts b/packages/cli/src/commands/buildWorkspace.ts new file mode 100644 index 0000000000..624f104b3f --- /dev/null +++ b/packages/cli/src/commands/buildWorkspace.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 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 fs from 'fs-extra'; +import { Command } from 'commander'; +import { createDistWorkspace } from '../lib/packager'; + +export default async (dir: string, _cmd: Command, packages: string[]) => { + if (!(await fs.pathExists(dir))) { + throw new Error(`Target workspace directory doesn't exist, '${dir}'`); + } + + await createDistWorkspace(packages, { + targetDir: dir, + }); +}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index bed32bcf8b..f29a1c1005 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -140,6 +140,11 @@ const main = (argv: string[]) => { .description('Delete cache directories') .action(lazyAction(() => import('./commands/clean/clean'), 'default')); + program + .command('build-workspace ...') + .description('Builds a temporary dist workspace from the provided packages') + .action(lazyAction(() => import('./commands/buildWorkspace'), 'default')); + program.on('command:*', () => { console.log(); console.log( diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index 5e309ab953..d3ba1f6b12 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -59,7 +59,7 @@ type Options = { */ export async function createDistWorkspace( packageNames: string[], - options: Options, + options: Options = {}, ) { const targetDir = options.targetDir ?? From c01de0d489e7f5b9e075d709741e791a39a93dfc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 29 Jun 2020 13:47:13 +0200 Subject: [PATCH 38/71] cli: add --skip-install option to create-app --- .../cli/src/commands/create-app/createApp.ts | 17 ++++++++--------- packages/cli/src/index.ts | 4 ++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index f3ac37667f..eed078342f 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -17,6 +17,7 @@ import fs from 'fs-extra'; import { promisify } from 'util'; import chalk from 'chalk'; +import { Command } from 'commander'; import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; @@ -40,7 +41,7 @@ async function checkExists(rootDir: string, name: string) { }); } -export async function createTemporaryAppFolder(tempDir: string) { +async function createTemporaryAppFolder(tempDir: string) { await Task.forItem('creating', 'temporary directory', async () => { try { await fs.mkdir(tempDir); @@ -76,11 +77,7 @@ async function buildApp(appDir: string) { await runCmd('yarn build'); } -export async function moveApp( - tempDir: string, - destination: string, - id: string, -) { +async function moveApp(tempDir: string, destination: string, id: string) { await Task.forItem('moving', id, async () => { await fs.move(tempDir, destination).catch(error => { throw new Error( @@ -90,7 +87,7 @@ export async function moveApp( }); } -export default async () => { +export default async (cmd: Command): Promise => { const questions: Question[] = [ { type: 'input', @@ -130,8 +127,10 @@ export default async () => { Task.section('Moving to final location'); await moveApp(tempDir, appDir, answers.name); - Task.section('Building the app'); - await buildApp(appDir); + if (!cmd.skipInstall) { + Task.section('Building the app'); + await buildApp(appDir); + } Task.log(); Task.log( diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f29a1c1005..9a102ab7d3 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -25,6 +25,10 @@ const main = (argv: string[]) => { program .command('create-app') .description('Creates a new app in a new directory') + .option( + '--skip-install', + 'Skip the install and builds steps after creating the app', + ) .action( lazyAction(() => import('./commands/create-app/createApp'), 'default'), ); From 3c447e36dcb622f993527f4fea876638ee799bee Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 01:03:57 +0200 Subject: [PATCH 39/71] cli: update e2e-test to use build-workspace and inline more scripts --- packages/cli/e2e-test/cli-e2e-test.js | 225 +++++++++++++++++++--- packages/cli/e2e-test/createTestApp.js | 44 ----- packages/cli/e2e-test/createTestPlugin.js | 44 ----- packages/cli/e2e-test/helpers.js | 6 +- 4 files changed, 199 insertions(+), 120 deletions(-) delete mode 100644 packages/cli/e2e-test/createTestApp.js delete mode 100644 packages/cli/e2e-test/createTestPlugin.js diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index c8374af0eb..ff0e9193b0 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -16,45 +16,210 @@ const os = require('os'); const fs = require('fs-extra'); -const { resolve: resolvePath } = require('path'); +const { resolve: resolvePath, join: joinPath } = require('path'); const Browser = require('zombie'); - +const { execFile: execFileCb } = require('child_process'); +const { promisify } = require('util'); const { spawnPiped, handleError, waitForPageWithText, + waitFor, waitForExit, print, } = require('./helpers'); - -const createTestApp = require('./createTestApp'); -const createTestPlugin = require('./createTestPlugin'); - -Browser.localhost('localhost', 3000); - -async function createTempDir() { - return fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); -} +const execFile = promisify(execFileCb); async function main() { - process.env.BACKSTAGE_E2E_CLI_TEST = 'true'; + const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); + print(`CLI E2E test root: ${rootDir}\n`); - const workDir = process.env.CI ? process.cwd() : await createTempDir(); + print('Building dist workspace'); + const workspaceDir = await buildDistWorkspace('workspace', rootDir); - process.stdout.write(`Initial directory: ${process.cwd()}\n`); - process.chdir(workDir); - process.stdout.write(`Working directory: ${process.cwd()}\n`); + print('Creating a Backstage App'); + const appDir = await createApp('test-app', workspaceDir, rootDir); - await createTestApp(); - - const appDir = resolvePath(workDir, 'test-app'); - process.chdir(appDir); - process.stdout.write(`App directory: ${appDir}\n`); - - await createTestPlugin(); + print('Creating a Backstage Plugin'); + const pluginName = await createPlugin('test-plugin', appDir); print('Starting the app'); - const startApp = spawnPiped(['yarn', 'start']); + await testAppServe(pluginName, appDir); + + print('All tests successful, removing test dir'); + await fs.remove(rootDir); +} + +/** + * Builds a dist workspace that contains the cli and core packages + */ +async function buildDistWorkspace(workspaceName, rootDir) { + const workspaceDir = resolvePath(rootDir, workspaceName); + await fs.ensureDir(workspaceDir); + + print(`Preparing workspace`); + await execFile('yarn', [ + 'backstage-cli', + 'build-workspace', + workspaceDir, + '@backstage/cli', + '@backstage/core', + '@backstage/dev-utils', + '@backstage/test-utils', + ]); + + print('Pinning yarn version in workspace'); + await pinYarnVersion(workspaceDir); + + print('Installing workspace dependencies'); + await execFile('yarn', ['install', '--production', '--frozen-lockfile'], { + cwd: workspaceDir, + }); + + return workspaceDir; +} + +/** + * Pin the yarn version in a directory to the one we're using in the Backstage repo + */ +async function pinYarnVersion(dir) { + const repoRoot = resolvePath(__dirname, '../../..'); + + const yarnRc = await fs.readFile(resolvePath(repoRoot, '.yarnrc'), 'utf8'); + const yarnRcLines = yarnRc.split('\n'); + const yarnPathLine = yarnRcLines.find(line => line.startsWith('yarn-path')); + const [, localYarnPath] = yarnPathLine.match(/"(.*)"/); + const yarnPath = resolvePath(repoRoot, localYarnPath); + + await fs.writeFile(resolvePath(dir, '.yarnrc'), `yarn-path "${yarnPath}"\n`); +} + +/** + * Creates a new app inside rootDir called test-app, using packages from the workspaceDir + */ +async function createApp(appName, workspaceDir, rootDir) { + const child = spawnPiped( + [ + resolvePath(workspaceDir, 'packages/cli/bin/backstage-cli'), + 'create-app', + '--skip-install', + ], + { + cwd: rootDir, + }, + ); + + try { + let stdout = ''; + child.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + + await waitFor(() => stdout.includes('Enter a name for the app')); + child.stdin.write(`${appName}\n`); + + print('Waiting for app create script to be done'); + await waitForExit(child); + + const appDir = resolvePath(rootDir, appName); + + print('Rewriting module resolutions of app to use workspace packages'); + await overrideModuleResolutions(appDir, workspaceDir); + + print('Pinning yarn version and registry in app'); + await pinYarnVersion(appDir); + await fs.writeFile( + resolvePath(appDir, '.npmrc'), + 'registry=https://registry.npmjs.org/\n', + ); + + print('Test app created'); + + for (const cmd of ['install', 'tsc', 'build', 'lint:all', 'test:all']) { + print(`Running 'yarn ${cmd}' in newly created app`); + await execFile('yarn', [cmd], { cwd: appDir }); + } + + print(`Running 'yarn test:e2e:ci' in newly created app`); + await execFile('yarn', ['test:e2e:ci'], { + cwd: resolvePath(appDir, 'packages', 'app'), + env: { + ...process.env, + APP_CONFIG_app_baseUrl: '"http://localhost:3001"', + }, + }); + + return appDir; + } finally { + child.kill(); + } +} + +/** + * This points dependency resolutions into the workspace for each package that is present there + */ +async function overrideModuleResolutions(appDir, workspaceDir) { + const pkgJsonPath = resolvePath(appDir, 'package.json'); + const pkgJson = await fs.readJson(pkgJsonPath); + + pkgJson.resolutions = pkgJson.resolutions || {}; + pkgJson.dependencies = pkgJson.dependencies || {}; + + const packageNames = await fs.readdir(resolvePath(workspaceDir, 'packages')); + for (const name of packageNames) { + const pkgPath = joinPath('..', 'workspace', 'packages', name); + + pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`; + pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`; + delete pkgJson.devDependencies[`@backstage/${name}`]; + } + fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); +} + +/** + * Uses create-plugin command to create a new plugin in the app + */ +async function createPlugin(pluginName, appDir) { + const child = spawnPiped(['yarn', 'create-plugin'], { + cwd: appDir, + }); + + try { + let stdout = ''; + child.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + + await waitFor(() => stdout.includes('Enter an ID for the plugin')); + child.stdin.write(`${pluginName}\n`); + + // await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); + // child.stdin.write('@someuser\n'); + + print('Waiting for plugin create script to be done'); + await waitForExit(child); + + const pluginDir = resolvePath(appDir, 'plugins', pluginName); + for (const cmd of [['lint'], ['test', '--no-watch']]) { + print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`); + await execFile('yarn', cmd, { cwd: pluginDir }); + } + + return pluginName; + } finally { + child.kill(); + } +} + +/** + * Start serving the newly created app and make sure that the create plugin is rendering correctly + */ +async function testAppServe(pluginName, appDir) { + const startApp = spawnPiped(['yarn', 'start'], { + cwd: appDir, + detached: true, + }); + Browser.localhost('localhost', 3000); try { const browser = new Browser(); @@ -62,19 +227,19 @@ async function main() { await waitForPageWithText(browser, '/', 'Welcome to Backstage'); await waitForPageWithText( browser, - '/test-plugin', - 'Welcome to test-plugin!', + `/${pluginName}`, + `Welcome to ${pluginName}!`, ); print('Both App and Plugin loaded correctly'); + } catch (error) { + throw new Error(`App serve test failed, ${error}`); } finally { - startApp.kill(); + // Kill entire process group, otherwise we'll end up with hanging serve processes + process.kill(-startApp.pid, 'SIGTERM'); } await waitForExit(startApp); - - print('All tests done'); - process.exit(0); } process.on('unhandledRejection', handleError); diff --git a/packages/cli/e2e-test/createTestApp.js b/packages/cli/e2e-test/createTestApp.js deleted file mode 100644 index 5437804975..0000000000 --- a/packages/cli/e2e-test/createTestApp.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 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. - */ - -const { resolve: resolvePath } = require('path'); -const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); - -async function createTestApp() { - const cliPath = resolvePath(__dirname, '../bin/backstage-cli'); - - print('Creating a Backstage App'); - const createApp = spawnPiped(['node', cliPath, 'create-app']); - - try { - let stdout = ''; - createApp.stdout.on('data', data => { - stdout = stdout + data.toString('utf8'); - }); - - await waitFor(() => stdout.includes('Enter a name for the app')); - createApp.stdin.write('test-app\n'); - - print('Waiting for app create script to be done'); - await waitForExit(createApp); - - print('Test app created'); - } finally { - createApp.kill(); - } -} - -module.exports = createTestApp; diff --git a/packages/cli/e2e-test/createTestPlugin.js b/packages/cli/e2e-test/createTestPlugin.js deleted file mode 100644 index 59e8d6dd77..0000000000 --- a/packages/cli/e2e-test/createTestPlugin.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 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. - */ - -const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); - -async function createTestPlugin() { - print('Creating a Backstage Plugin'); - const createPlugin = spawnPiped(['yarn', 'create-plugin']); - - try { - let stdout = ''; - createPlugin.stdout.on('data', data => { - stdout = stdout + data.toString('utf8'); - }); - - await waitFor(() => stdout.includes('Enter an ID for the plugin')); - createPlugin.stdin.write('test-plugin\n'); - - // await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); - // createPlugin.stdin.write('@someuser\n'); - - print('Waiting for plugin create script to be done'); - await waitForExit(createPlugin); - - print('Test plugin created'); - } finally { - createPlugin.kill(); - } -} - -module.exports = createTestPlugin; diff --git a/packages/cli/e2e-test/helpers.js b/packages/cli/e2e-test/helpers.js index d7f3f28a7a..1292ba19bf 100644 --- a/packages/cli/e2e-test/helpers.js +++ b/packages/cli/e2e-test/helpers.js @@ -43,13 +43,15 @@ function spawnPiped(cmd, options) { process.exit(code); } }); + + const logPrefix = cmd.map(s => s.replace(/.+\//, '')).join(' '); child.stdout.on( 'data', - pipeWithPrefix(process.stdout, `[${cmd.join(' ')}].out: `), + pipeWithPrefix(process.stdout, `[${logPrefix}].out: `), ); child.stderr.on( 'data', - pipeWithPrefix(process.stderr, `[${cmd.join(' ')}].err: `), + pipeWithPrefix(process.stderr, `[${logPrefix}].err: `), ); return child; From 3fb76b575b6a927e77ffe871ae56c9c90318cf31 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 01:19:40 +0200 Subject: [PATCH 40/71] cli: remove special handling of yarn install --- .../cli/src/commands/create-app/createApp.ts | 4 +- .../commands/create-plugin/createPlugin.ts | 6 +- packages/cli/src/lib/tasks.ts | 108 +----------------- 3 files changed, 5 insertions(+), 113 deletions(-) diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index eed078342f..4f4134957d 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -22,7 +22,7 @@ import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; import os from 'os'; -import { Task, templatingTask, installWithLocalDeps } from '../../lib/tasks'; +import { Task, templatingTask } from '../../lib/tasks'; import { paths } from '../../lib/paths'; import { version } from '../../lib/version'; @@ -72,7 +72,7 @@ async function buildApp(appDir: string) { }); }; - await installWithLocalDeps(appDir); + await runCmd('yarn install'); await runCmd('yarn tsc'); await runCmd('yarn build'); } diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 07f68165a5..1898285a22 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -28,7 +28,7 @@ import { } from '../../lib/codeowners'; import { paths } from '../../lib/paths'; import { version } from '../../lib/version'; -import { Task, templatingTask, installWithLocalDeps } from '../../lib/tasks'; +import { Task, templatingTask } from '../../lib/tasks'; const exec = promisify(execCb); @@ -142,9 +142,7 @@ async function cleanUp(tempDir: string) { } async function buildPlugin(pluginFolder: string) { - await installWithLocalDeps(paths.targetRoot); - - const commands = ['yarn tsc', 'yarn build']; + const commands = ['yarn install', 'yarn tsc', 'yarn build']; for (const command of commands) { await Task.forItem('executing', command, async () => { process.chdir(pluginFolder); diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index f4cb31fd69..0753301b78 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -18,13 +18,8 @@ import chalk from 'chalk'; import fs from 'fs-extra'; import handlebars from 'handlebars'; import ora from 'ora'; -import { resolve as resolvePath, basename, dirname } from 'path'; +import { basename, dirname } from 'path'; import recursive from 'recursive-readdir'; -import { promisify } from 'util'; -import { exec as execCb } from 'child_process'; -import { paths } from './paths'; - -const exec = promisify(execCb); const TASK_NAME_MAX_LENGTH = 14; @@ -108,104 +103,3 @@ export async function templatingTask( } } } - -// List of local packages that we need to modify as a part of an E2E test -const PATCH_PACKAGES = [ - 'cli', - 'config', - 'config-loader', - 'core', - 'core-api', - 'dev-utils', - 'test-utils', - 'test-utils-core', - 'theme', -]; - -// This runs a `yarn install` task, but with special treatment for e2e tests -export async function installWithLocalDeps(dir: string) { - // This makes us install any package inside this repo as a local file dependency. - // For example, instead of trying to fetch @backstage/core from npm, we point it - // to /packages/core. This makes yarn use a simple file copy to install it instead. - if (process.env.BACKSTAGE_E2E_CLI_TEST) { - Task.section('Linking packages locally for e2e tests'); - - const pkgJsonPath = resolvePath(dir, 'package.json'); - const pkgJson = await fs.readJson(pkgJsonPath); - - pkgJson.resolutions = pkgJson.resolutions || {}; - pkgJson.dependencies = pkgJson.dependencies || {}; - - if (!pkgJson.resolutions[`@backstage/${PATCH_PACKAGES[0]}`]) { - for (const name of PATCH_PACKAGES) { - await Task.forItem( - 'adding', - `@backstage/${name} link to package.json`, - async () => { - const pkgPath = paths.resolveOwnRoot('packages', name); - // Add to both resolutions and dependencies, or transitive dependencies will still be fetched from the registry. - pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`; - pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`; - delete pkgJson.devDependencies[`@backstage/${name}`]; - - await fs - .writeJSON(pkgJsonPath, pkgJson, { encoding: 'utf8', spaces: 2 }) - .catch(error => { - throw new Error( - `Failed to add resolutions to package.json: ${error.message}`, - ); - }); - }, - ); - } - } - } - - await Task.forItem('executing', 'yarn install', async () => { - await exec('yarn install', { cwd: dir }).catch(error => { - process.stdout.write(error.stderr); - process.stdout.write(error.stdout); - throw new Error( - `Could not execute command ${chalk.cyan('yarn install')}`, - ); - }); - }); - - // This takes care of pointing all the installed packages from this repo to - // dist instead of the local src, using the field overrides in publishConfig. - // Without this we get type checking errors in the e2e test - if (process.env.BACKSTAGE_E2E_CLI_TEST) { - Task.section('Patching local dependencies for e2e tests'); - - for (const name of PATCH_PACKAGES) { - await Task.forItem( - 'patching', - `node_modules/@backstage/${name} package.json`, - async () => { - const depJsonPath = resolvePath( - dir, - 'node_modules/@backstage', - name, - 'package.json', - ); - const depJson = await fs.readJson(depJsonPath); - - // We want dist to be used for e2e tests - for (const key of Object.keys(depJson.publishConfig)) { - if (key !== 'access') { - depJson[key] = depJson.publishConfig[key]; - } - } - - await fs - .writeJSON(depJsonPath, depJson, { encoding: 'utf8', spaces: 2 }) - .catch(error => { - throw new Error( - `Failed to add resolutions to package.json: ${error.message}`, - ); - }); - }, - ); - } - } -} From 192868ffefef163a347529f2c6bdc0fe7e4443e8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 01:20:07 +0200 Subject: [PATCH 41/71] github/workflows: remove redundant cli checks --- .github/workflows/cli-win.yml | 18 ------------------ .github/workflows/cli.yml | 18 ------------------ 2 files changed, 36 deletions(-) diff --git a/.github/workflows/cli-win.yml b/.github/workflows/cli-win.yml index 00c7c26a6c..3e93c7a6d9 100644 --- a/.github/workflows/cli-win.yml +++ b/.github/workflows/cli-win.yml @@ -45,21 +45,3 @@ jobs: - name: verify app and plugin creation working-directory: ${{ runner.temp }} run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js - env: - BACKSTAGE_E2E_CLI_TEST: true - - name: lint newly created app and plugin - run: yarn lint:all - working-directory: ${{ runner.temp }}/test-app - env: - BACKSTAGE_E2E_CLI_TEST: true - - name: test newly created app and plugin - run: yarn test:all - working-directory: ${{ runner.temp }}/test-app - env: - BACKSTAGE_E2E_CLI_TEST: true - - name: e2e test newly created app - run: yarn test:e2e:ci - working-directory: ${{ runner.temp }}/test-app/packages/app - env: - APP_CONFIG_app_baseUrl: '"http://localhost:3001"' - BACKSTAGE_E2E_CLI_TEST: true diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 71b207c0d3..2f6ec865d1 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -48,21 +48,3 @@ jobs: run: | sudo sysctl fs.inotify.max_user_watches=524288 node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js - env: - BACKSTAGE_E2E_CLI_TEST: true - - name: lint newly created app and plugin - run: yarn lint:all - working-directory: ${{ runner.temp }}/test-app - env: - BACKSTAGE_E2E_CLI_TEST: true - - name: test newly created app and plugin - run: yarn test:all - working-directory: ${{ runner.temp }}/test-app - env: - BACKSTAGE_E2E_CLI_TEST: true - - name: e2e test newly created app - run: yarn test:e2e:ci - working-directory: ${{ runner.temp }}/test-app/packages/app - env: - APP_CONFIG_app_baseUrl: '"http://localhost:3001"' - BACKSTAGE_E2E_CLI_TEST: true From dd26e15fd4e095eaefaa8999e584b32b82954eb8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 01:31:23 +0200 Subject: [PATCH 42/71] github/workflows: use correct working directory for e2e test --- .github/workflows/cli-win.yml | 1 - .github/workflows/cli.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/cli-win.yml b/.github/workflows/cli-win.yml index 3e93c7a6d9..1355ed251e 100644 --- a/.github/workflows/cli-win.yml +++ b/.github/workflows/cli-win.yml @@ -43,5 +43,4 @@ jobs: - run: yarn tsc - run: yarn build - name: verify app and plugin creation - working-directory: ${{ runner.temp }} run: node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 2f6ec865d1..7704aa36cd 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -44,7 +44,6 @@ jobs: - run: yarn tsc - run: yarn build - name: verify app and plugin creation - working-directory: ${{ runner.temp }} run: | sudo sysctl fs.inotify.max_user_watches=524288 node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js From 2aca5bb6d45954966e29a87c95ffc90e458eead6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 09:56:17 +0200 Subject: [PATCH 43/71] cli: refactor e2e-test to use runPlain with fix for windows --- packages/cli/e2e-test/cli-e2e-test.js | 15 +++++++-------- packages/cli/e2e-test/helpers.js | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index ff0e9193b0..b238c726a9 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -18,17 +18,15 @@ const os = require('os'); const fs = require('fs-extra'); const { resolve: resolvePath, join: joinPath } = require('path'); const Browser = require('zombie'); -const { execFile: execFileCb } = require('child_process'); -const { promisify } = require('util'); const { spawnPiped, + runPlain, handleError, waitForPageWithText, waitFor, waitForExit, print, } = require('./helpers'); -const execFile = promisify(execFileCb); async function main() { const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); @@ -58,7 +56,8 @@ async function buildDistWorkspace(workspaceName, rootDir) { await fs.ensureDir(workspaceDir); print(`Preparing workspace`); - await execFile('yarn', [ + await runPlain([ + 'yarn', 'backstage-cli', 'build-workspace', workspaceDir, @@ -72,7 +71,7 @@ async function buildDistWorkspace(workspaceName, rootDir) { await pinYarnVersion(workspaceDir); print('Installing workspace dependencies'); - await execFile('yarn', ['install', '--production', '--frozen-lockfile'], { + await runPlain(['yarn', 'install', '--production', '--frozen-lockfile'], { cwd: workspaceDir, }); @@ -137,11 +136,11 @@ async function createApp(appName, workspaceDir, rootDir) { for (const cmd of ['install', 'tsc', 'build', 'lint:all', 'test:all']) { print(`Running 'yarn ${cmd}' in newly created app`); - await execFile('yarn', [cmd], { cwd: appDir }); + await runPlain(['yarn', cmd], { cwd: appDir }); } print(`Running 'yarn test:e2e:ci' in newly created app`); - await execFile('yarn', ['test:e2e:ci'], { + await runPlain(['yarn', 'test:e2e:ci'], { cwd: resolvePath(appDir, 'packages', 'app'), env: { ...process.env, @@ -202,7 +201,7 @@ async function createPlugin(pluginName, appDir) { const pluginDir = resolvePath(appDir, 'plugins', pluginName); for (const cmd of [['lint'], ['test', '--no-watch']]) { print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`); - await execFile('yarn', cmd, { cwd: pluginDir }); + await runPlain(['yarn', ...cmd], { cwd: pluginDir }); } return pluginName; diff --git a/packages/cli/e2e-test/helpers.js b/packages/cli/e2e-test/helpers.js index 1292ba19bf..4f5152e0b1 100644 --- a/packages/cli/e2e-test/helpers.js +++ b/packages/cli/e2e-test/helpers.js @@ -14,9 +14,10 @@ * limitations under the License. */ -const childProcess = require('child_process'); +const { spawn, execFile: execFileCb } = require('child_process'); +const { promisify } = require('util'); -const { spawn } = childProcess; +const execFile = promisify(execFileCb); const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/; @@ -57,6 +58,21 @@ function spawnPiped(cmd, options) { return child; } +async function runPlain(cmd, options) { + try { + const { stdout } = await execFile(cmd[0], cmd.slice(1), { + ...options, + shell: true, + }); + return stdout.trim(); + } catch (error) { + if (error.stderr) { + process.stderr.write(error.stderr); + } + throw error; + } +} + function handleError(err) { process.stdout.write(`${err.name}: ${err.stack || err.message}\n`); if (typeof err.code === 'number') { @@ -150,6 +166,7 @@ function print(msg) { module.exports = { spawnPiped, + runPlain, handleError, waitFor, waitForExit, From 7cd04c77fac7d830c03699739f578e99135d9740 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 12:53:30 +0200 Subject: [PATCH 44/71] cli: use node to execude backstage-cli bin in e2e test --- packages/cli/e2e-test/cli-e2e-test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index b238c726a9..67cb71802f 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -99,6 +99,7 @@ async function pinYarnVersion(dir) { async function createApp(appName, workspaceDir, rootDir) { const child = spawnPiped( [ + 'node', resolvePath(workspaceDir, 'packages/cli/bin/backstage-cli'), 'create-app', '--skip-install', From c499656a690079eaed00a58cbbc2e565666fd6c8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 14:41:18 +0200 Subject: [PATCH 45/71] cli: use tree-kill to kill serve task in e2e test --- packages/cli/e2e-test/cli-e2e-test.js | 4 ++-- packages/cli/package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index 67cb71802f..e6a8b2e201 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -16,6 +16,7 @@ const os = require('os'); const fs = require('fs-extra'); +const killTree = require('tree-kill'); const { resolve: resolvePath, join: joinPath } = require('path'); const Browser = require('zombie'); const { @@ -217,7 +218,6 @@ async function createPlugin(pluginName, appDir) { async function testAppServe(pluginName, appDir) { const startApp = spawnPiped(['yarn', 'start'], { cwd: appDir, - detached: true, }); Browser.localhost('localhost', 3000); @@ -236,7 +236,7 @@ async function testAppServe(pluginName, appDir) { throw new Error(`App serve test failed, ${error}`); } finally { // Kill entire process group, otherwise we'll end up with hanging serve processes - process.kill(-startApp.pid, 'SIGTERM'); + killTree(startApp.pid); } await waitForExit(startApp); diff --git a/packages/cli/package.json b/packages/cli/package.json index f74c1b1fb0..1959e9cc07 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -112,6 +112,7 @@ "@types/webpack-dev-server": "^3.10.0", "del": "^5.1.0", "nodemon": "^2.0.2", + "tree-kill": "^1.2.2", "ts-node": "^8.6.2", "zombie": "^6.1.4" }, diff --git a/yarn.lock b/yarn.lock index adc4a02cd4..7a9b6c0bd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17999,6 +17999,11 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" From 837aa9ab0c384197f299068678a83d3a2ea6eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 2 Jul 2020 14:56:53 +0200 Subject: [PATCH 46/71] fix(sidebar): do not let sidebar items collapse and shrink --- packages/core/src/layout/Sidebar/Bar.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/layout/Sidebar/Bar.tsx b/packages/core/src/layout/Sidebar/Bar.tsx index 1aadf9fd3e..4d81c398b7 100644 --- a/packages/core/src/layout/Sidebar/Bar.tsx +++ b/packages/core/src/layout/Sidebar/Bar.tsx @@ -44,6 +44,9 @@ const useStyles = makeStyles(theme => ({ easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.shortest, }), + '& > *': { + flexShrink: 0, + }, }, drawerOpen: { width: sidebarConfig.drawerWidthOpen, From 88d71cd34684706d1227b42665090d920a538c2b Mon Sep 17 00:00:00 2001 From: David Dyball Date: Thu, 2 Jul 2020 14:07:20 +0100 Subject: [PATCH 47/71] Use Config for Filter Org List Header --- plugins/catalog/src/components/CatalogPage/CatalogPage.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index e59b753363..c1e02b3c28 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -19,6 +19,7 @@ import { ContentHeader, identityApiRef, SupportButton, + configApiRef, useApi, } from '@backstage/core'; import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder'; @@ -51,6 +52,8 @@ const CatalogPageContents = () => { const userId = useApi(identityApiRef).getUserId(); const [selectedTab, setSelectedTab] = useState(); const [selectedSidebarItem, setSelectedSidebarItem] = useState(); + const orgName = + useApi(configApiRef).getOptionalString('organization.name') ?? 'Company'; const tabs = useMemo( () => [ @@ -79,7 +82,7 @@ const CatalogPageContents = () => { ); const filterGroups = useMemo( - () => [ + (org: orgName) => [ { name: 'Personal', items: [ @@ -98,7 +101,7 @@ const CatalogPageContents = () => { ], }, { - name: 'Company', // TODO: Replace with Company name, read from app config. + name: org, items: [ { id: 'all', From d78d9f20a7a994298285ada3de8491b7de64b933 Mon Sep 17 00:00:00 2001 From: David Dyball Date: Thu, 2 Jul 2020 14:48:37 +0100 Subject: [PATCH 48/71] Fix variable --- plugins/catalog/src/components/CatalogPage/CatalogPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index c1e02b3c28..e58b2d407b 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -82,7 +82,7 @@ const CatalogPageContents = () => { ); const filterGroups = useMemo( - (org: orgName) => [ + () => [ { name: 'Personal', items: [ @@ -101,7 +101,7 @@ const CatalogPageContents = () => { ], }, { - name: org, + name: orgName, items: [ { id: 'all', @@ -111,7 +111,7 @@ const CatalogPageContents = () => { ], }, ], - [isStarredEntity, userId], + [isStarredEntity, userId, orgName], ); return ( From ab6385cfb1266a6c873a860a6db0fd4a7c51d2a7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Jul 2020 19:00:45 +0200 Subject: [PATCH 49/71] cli: never treat success as a failure in e2e test --- packages/cli/e2e-test/cli-e2e-test.js | 10 +++++++++- packages/cli/e2e-test/helpers.js | 6 ------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/cli/e2e-test/cli-e2e-test.js b/packages/cli/e2e-test/cli-e2e-test.js index e6a8b2e201..ef5d6ab243 100644 --- a/packages/cli/e2e-test/cli-e2e-test.js +++ b/packages/cli/e2e-test/cli-e2e-test.js @@ -221,6 +221,7 @@ async function testAppServe(pluginName, appDir) { }); Browser.localhost('localhost', 3000); + let successful = false; try { const browser = new Browser(); @@ -232,6 +233,7 @@ async function testAppServe(pluginName, appDir) { ); print('Both App and Plugin loaded correctly'); + successful = true; } catch (error) { throw new Error(`App serve test failed, ${error}`); } finally { @@ -239,7 +241,13 @@ async function testAppServe(pluginName, appDir) { killTree(startApp.pid); } - await waitForExit(startApp); + try { + await waitForExit(startApp); + } catch (error) { + if (!successful) { + throw error; + } + } } process.on('unhandledRejection', handleError); diff --git a/packages/cli/e2e-test/helpers.js b/packages/cli/e2e-test/helpers.js index 4f5152e0b1..da2ec343ab 100644 --- a/packages/cli/e2e-test/helpers.js +++ b/packages/cli/e2e-test/helpers.js @@ -38,12 +38,6 @@ function spawnPiped(cmd, options) { ...options, }); child.on('error', handleError); - child.on('exit', code => { - if (code) { - print(`Child '${cmd.join(' ')}' exited with code ${code}`); - process.exit(code); - } - }); const logPrefix = cmd.map(s => s.replace(/.+\//, '')).join(' '); child.stdout.on( From d6b26d2c95e264adda644c8cb273f361e31b2236 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Jul 2020 05:26:03 +0200 Subject: [PATCH 50/71] chore(scaffolder): some more docs for the PR --- .../src/scaffolder/jobs/logger.test.ts | 1 + .../src/scaffolder/stages/templater/types.ts | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts b/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts index 5905d26ebb..12f893c146 100644 --- a/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/jobs/logger.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { makeLogStream } from './logger'; + describe('Logger', () => { const mockMeta = { test: 'blob' }; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts index cdefa7821e..519c24e381 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/types.ts @@ -18,11 +18,28 @@ import type { Writable } from 'stream'; import Docker from 'dockerode'; import { JsonValue } from '@backstage/config'; +/** + * Currently the required template values. The owner + * and where to store the result from templating + */ export type RequiredTemplateValues = { owner: string; storePath: string; }; +/** + * The returned directory from the templater which is ready + * to pass to the next stage of the scaffolder which is publishing + */ +export type TemplaterRunResult = { + resultDir: string; +}; + +/** + * The values that the templater will recieve. The directory of the + * skeleton, with the values from the frontend. A dedicated log stream and a docker + * client to run any templater on top of your directory. + */ export type TemplaterRunOptions = { directory: string; values: RequiredTemplateValues & Record; @@ -32,7 +49,7 @@ export type TemplaterRunOptions = { export type TemplaterBase = { // runs the templating with the values and returns the directory to push the VCS - run(opts: TemplaterRunOptions): Promise; + run(opts: TemplaterRunOptions): Promise; }; export type TemplaterConfig = { From 40fcbc4b0fba5e9ee4099990d829cdf1ec1f91a2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2020 06:01:40 +0000 Subject: [PATCH 51/71] build(deps): bump css-loader from 3.5.3 to 3.6.0 Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 3.5.3 to 3.6.0. - [Release notes](https://github.com/webpack-contrib/css-loader/releases) - [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/css-loader/compare/v3.5.3...v3.6.0) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 67 +++++++++++++++++++++---------------------------------- 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/yarn.lock b/yarn.lock index adc4a02cd4..be1e744f12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3553,6 +3553,11 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339" integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA== +"@types/json-schema@^7.0.4": + version "7.0.5" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -4419,7 +4424,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.5.5: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5: version "6.12.2" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== @@ -6714,41 +6719,23 @@ css-in-js-utils@^2.0.0: hyphenate-style-name "^1.0.2" isobject "^3.0.1" -css-loader@^3.0.0: - version "3.4.2" - resolved "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz#d3fdb3358b43f233b78501c5ed7b1c6da6133202" - integrity sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA== +css-loader@^3.0.0, css-loader@^3.5.3: + version "3.6.0" + resolved "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" + integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" icss-utils "^4.1.1" loader-utils "^1.2.3" normalize-path "^3.0.0" - postcss "^7.0.23" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.1.1" - postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.2" - schema-utils "^2.6.0" - -css-loader@^3.5.3: - version "3.5.3" - resolved "https://registry.npmjs.org/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf" - integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw== - dependencies: - camelcase "^5.3.1" - cssesc "^3.0.0" - icss-utils "^4.1.1" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.27" + postcss "^7.0.32" postcss-modules-extract-imports "^2.0.0" postcss-modules-local-by-default "^3.0.2" postcss-modules-scope "^2.2.0" postcss-modules-values "^3.0.0" - postcss-value-parser "^4.0.3" - schema-utils "^2.6.6" + postcss-value-parser "^4.1.0" + schema-utils "^2.7.0" semver "^6.3.0" css-modules-loader-core@^1.1.0: @@ -14497,7 +14484,7 @@ postcss-modules-scope@1.1.0: css-selector-tokenizer "^0.7.0" postcss "^6.0.1" -postcss-modules-scope@^2.1.1, postcss-modules-scope@^2.2.0: +postcss-modules-scope@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== @@ -14684,20 +14671,15 @@ postcss-value-parser@^3.0.0: resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" - integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== - -postcss-value-parser@^4.0.3: +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -"postcss@5 - 7", postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.23, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.27" - resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" - integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== +"postcss@5 - 7", postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.32" + resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" + integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -16388,12 +16370,13 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.4, schema-utils@^2.6.5, schema-utils@^2.6.6: - version "2.6.6" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c" - integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA== +schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.4, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== dependencies: - ajv "^6.12.0" + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" ajv-keywords "^3.4.1" screenfull@^5.0.0: From ea58f885e2504f62b53206de9c411fb4d8647948 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 3 Jul 2020 11:17:15 +0200 Subject: [PATCH 52/71] fix(tests): fix fixures (#1505) Co-authored-by: Emma Indal --- .../reader/transformers/addBaseUrl.test.ts | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index 4b1477e9c0..8629dce0d3 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -63,52 +63,72 @@ describe('addBaseUrl', () => { }); it('includes path option without slash', () => { - const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - transformers: [ - addBaseUrl({ - docStorageURL: DOC_STORAGE_URL, - componentId: 'example-docs', - path: 'examplepath', - }), - ], - }); + const shadowDom = createTestShadowDom( + ` + + + + + + + `, + { + transformers: [ + addBaseUrl({ + docStorageURL: DOC_STORAGE_URL, + componentId: 'example-docs', + path: 'examplepath', + }), + ], + }, + ); expect(getSample(shadowDom, 'img', 'src')).toEqual([ - 'https://example-host.storage.googleapis.com/example-docs/examplepath/img/win-py-install.png', - 'https://example-host.storage.googleapis.com/example-docs/examplepath/img/initial-layout.png', + 'https://example-host.storage.googleapis.com/example-docs/img/win-py-install.png', + 'https://example-host.storage.googleapis.com/example-docs/img/initial-layout.png', ]); expect(getSample(shadowDom, 'link', 'href')).toEqual([ 'https://www.mkdocs.org/', - 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/images/favicon.png', + 'https://example-host.storage.googleapis.com/example-docs/assets/images/favicon.png', ]); expect(getSample(shadowDom, 'script', 'src')).toEqual([ 'https://www.google-analytics.com/analytics.js', - 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/javascripts/vendor.d710d30a.min.js', + 'https://example-host.storage.googleapis.com/example-docs/assets/javascripts/vendor.d710d30a.min.js', ]); }); it('includes path option with slash', () => { - const shadowDom = createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { - transformers: [ - addBaseUrl({ - docStorageURL: DOC_STORAGE_URL, - componentId: 'example-docs', - path: 'examplepath/', - }), - ], - }); + const shadowDom = createTestShadowDom( + ` + + + + + + + `, + { + transformers: [ + addBaseUrl({ + docStorageURL: DOC_STORAGE_URL, + componentId: 'example-docs', + path: 'examplepath/', + }), + ], + }, + ); expect(getSample(shadowDom, 'img', 'src')).toEqual([ - 'https://example-host.storage.googleapis.com/example-docs/examplepath/img/win-py-install.png', - 'https://example-host.storage.googleapis.com/example-docs/examplepath/img/initial-layout.png', + 'https://example-host.storage.googleapis.com/example-docs/img/win-py-install.png', + 'https://example-host.storage.googleapis.com/example-docs/img/initial-layout.png', ]); expect(getSample(shadowDom, 'link', 'href')).toEqual([ 'https://www.mkdocs.org/', - 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/images/favicon.png', + 'https://example-host.storage.googleapis.com/example-docs/assets/images/favicon.png', ]); expect(getSample(shadowDom, 'script', 'src')).toEqual([ 'https://www.google-analytics.com/analytics.js', - 'https://example-host.storage.googleapis.com/example-docs/examplepath/assets/javascripts/vendor.d710d30a.min.js', + 'https://example-host.storage.googleapis.com/example-docs/assets/javascripts/vendor.d710d30a.min.js', ]); }); }); From cabd540abbaefbe78da61f5a03a798217d8fbe2f Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Wed, 1 Jul 2020 21:21:53 +0200 Subject: [PATCH 53/71] refactor(techdocs): basic techdocs not found, split out components from reader --- plugins/techdocs/src/plugin.ts | 3 +- .../techdocs/src/reader/components/Reader.tsx | 42 +++----------- .../reader/components/TechDocsHome.test.tsx | 36 ++++++++++++ .../src/reader/components/TechDocsHome.tsx | 55 +++++++++++++++++++ .../components/TechDocsNotFound.test.tsx | 26 +++++++++ .../reader/components/TechDocsNotFound.tsx | 34 ++++++++++++ .../components/TechDocsPageWrapper.test.tsx | 34 ++++++++++++ .../reader/components/TechDocsPageWrapper.tsx | 37 +++++++++++++ 8 files changed, 232 insertions(+), 35 deletions(-) create mode 100644 plugins/techdocs/src/reader/components/TechDocsHome.test.tsx create mode 100644 plugins/techdocs/src/reader/components/TechDocsHome.tsx create mode 100644 plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx create mode 100644 plugins/techdocs/src/reader/components/TechDocsNotFound.tsx create mode 100644 plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx create mode 100644 plugins/techdocs/src/reader/components/TechDocsPageWrapper.tsx diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 89a5ddeb73..a96f7e5502 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -30,6 +30,7 @@ */ import { createPlugin, createRouteRef } from '@backstage/core'; +import { TechDocsHome } from './reader/components/TechDocsHome'; import { Reader } from './reader/components/Reader'; export const rootRouteRef = createRouteRef({ @@ -45,7 +46,7 @@ export const rootDocsRouteRef = createRouteRef({ export const plugin = createPlugin({ id: 'techdocs', register({ router }) { - router.addRoute(rootRouteRef, Reader); + router.addRoute(rootRouteRef, TechDocsHome); router.addRoute(rootDocsRouteRef, Reader); }, }); diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 6398d86515..aa42eaffc7 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -19,9 +19,6 @@ import { useShadowDom } from '..'; import { useAsync } from 'react-use'; import { useLocation, useParams, useNavigate } from 'react-router-dom'; -import { Grid } from '@material-ui/core'; -import { Header, Content, ItemCard } from '@backstage/core'; - import transformer, { addBaseUrl, rewriteDocLinks, @@ -31,10 +28,13 @@ import transformer, { } from '../transformers'; import { docStorageURL } from '../../config'; import URLFormatter from '../urlFormatter'; +import { TechDocsNotFound } from './TechDocsNotFound'; +import { TechDocsPageWrapper } from './TechDocsPageWrapper'; const useFetch = (url: string) => { const state = useAsync(async () => { const response = await fetch(url); + if (response.status === 404) return '404'; const raw = await response.text(); return raw; }, [url]); @@ -116,39 +116,13 @@ export const Reader = () => { } }, [shadowDomRef, state, componentId, path, navigate]); + if (state.value === '404') return ; + return ( <> -
- - - {componentId ? ( -
- ) : ( - - - navigate('/docs/mkdocs')} - tags={['Developer Tool']} - title="MkDocs" - label="Read Docs" - description="MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. " - /> - - - navigate('/docs/backstage-microsite')} - tags={['Service']} - title="Backstage" - label="Read Docs" - description="Getting started guides, API Overview, documentation around how to Create a Plugin and more. " - /> - - - )} - + +
+ ); }; diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx new file mode 100644 index 0000000000..2140541033 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2020 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 { TechDocsHome } from './TechDocsHome'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; + +describe('TechDocs Home', () => { + it('should render a TechDocs home page', async () => { + const { getByTestId, queryByText } = render( + wrapInTestApp(), + ); + + // Header + expect(queryByText('Documentation')).toBeInTheDocument(); + expect( + queryByText(/Documentation available in Backstage/i), + ).toBeInTheDocument(); + + // Explore Content + expect(getByTestId('docs-explore')).toBeDefined(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx new file mode 100644 index 0000000000..9e51b5f582 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2020 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 { useNavigate } from 'react-router-dom'; +import { Grid } from '@material-ui/core'; +import { ItemCard } from '@backstage/core'; +import { TechDocsPageWrapper } from './TechDocsPageWrapper'; + +export const TechDocsHome = () => { + const navigate = useNavigate(); + + return ( + <> + + + + navigate('/docs/mkdocs')} + tags={['Developer Tool']} + title="MkDocs" + label="Read Docs" + description="MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. " + /> + + + navigate('/docs/backstage-microsite')} + tags={['Service']} + title="Backstage" + label="Read Docs" + description="Getting started guides, API Overview, documentation around how to Create a Plugin and more. " + /> + + + + + ); +}; diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx new file mode 100644 index 0000000000..1d3fbaf0d5 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2020 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 { TechDocsNotFound } from './TechDocsNotFound'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; + +describe('TechDocs Not Found', () => { + it('should render a Documentation not found page', async () => { + const { queryByText } = render(wrapInTestApp()); + expect(queryByText(/Error: Documentation not found/i)).toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx new file mode 100644 index 0000000000..2a961b5bc0 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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 { Typography, Button } from '@material-ui/core'; +import { TechDocsPageWrapper } from './TechDocsPageWrapper'; + +export const TechDocsNotFound = () => { + return ( + + Error: Documentation not found + Path: {window.location.pathname} + + + ); +}; diff --git a/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx new file mode 100644 index 0000000000..d4cc49c919 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsPageWrapper.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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 { TechDocsPageWrapper } from './TechDocsPageWrapper'; +import { TechDocsHome } from './TechDocsHome'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; + +describe('TechDocs Page Wrapper', () => { + it('should render a TechDocs Page Wrapper', async () => { + const { queryByText } = render( + wrapInTestApp( + + + , + ), + ); + expect(queryByText(/test-title/i)).toBeInTheDocument(); + expect(queryByText(/test-subtitle/i)).toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsPageWrapper.tsx b/plugins/techdocs/src/reader/components/TechDocsPageWrapper.tsx new file mode 100644 index 0000000000..abb702535b --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsPageWrapper.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2020 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 { Header, Content } from '@backstage/core'; + +type TechDocsPageWrapperProps = { + title: string; + subtitle: string; + children: any; +}; + +export const TechDocsPageWrapper = ({ + children, + title, + subtitle, +}: TechDocsPageWrapperProps) => { + return ( + <> +
+ {children} + + ); +}; From 6f3a8ee90a30d7c037db77adc2885fc5baa5ad1b Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 3 Jul 2020 11:37:37 +0200 Subject: [PATCH 54/71] fix(test): remove async Co-authored-by: Bilawal Hameed --- plugins/techdocs/src/reader/components/TechDocsHome.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx index 2140541033..dd31e6a318 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.test.tsx @@ -19,7 +19,7 @@ import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; describe('TechDocs Home', () => { - it('should render a TechDocs home page', async () => { + it('should render a TechDocs home page', () => { const { getByTestId, queryByText } = render( wrapInTestApp(), ); From 3e3dff511bcc4a0f78a504ba96c045e38a044c17 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 3 Jul 2020 11:38:27 +0200 Subject: [PATCH 55/71] fix(test): change to lowercase Co-authored-by: Bilawal Hameed --- .../techdocs/src/reader/components/TechDocsNotFound.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index 1d3fbaf0d5..7792164b14 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -21,6 +21,6 @@ import { wrapInTestApp } from '@backstage/test-utils'; describe('TechDocs Not Found', () => { it('should render a Documentation not found page', async () => { const { queryByText } = render(wrapInTestApp()); - expect(queryByText(/Error: Documentation not found/i)).toBeInTheDocument(); + expect(queryByText(/error: documentation not found/i)).toBeInTheDocument(); }); }); From 3229b16ce8e51622377f679ba0d1b1d3eea9b283 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 3 Jul 2020 11:50:31 +0200 Subject: [PATCH 56/71] fix(techdocs-home): move docs sites mock data to array --- .../src/reader/components/TechDocsHome.tsx | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index 9e51b5f582..3f9763912c 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -20,6 +20,32 @@ import { Grid } from '@material-ui/core'; import { ItemCard } from '@backstage/core'; import { TechDocsPageWrapper } from './TechDocsPageWrapper'; +type DocumentationSite = { + title: string; + description: string; + tags: Array; + path: string; + btnLabel: string; +}; + +const documentationSites: Array = [ + { + title: 'MkDocs', + description: + "MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. ", + tags: ['Developer Tool'], + path: '/docs/mkdocs', + btnLabel: 'Read Docs', + }, + { + title: 'Backstage Docs', + description: + 'Getting started guides, API Overview, documentation around how to Create a Plugin and more. ', + tags: ['Service'], + path: '/docs/backstage-microsite', + btnLabel: 'Read Docs', + }, +]; export const TechDocsHome = () => { const navigate = useNavigate(); @@ -30,24 +56,17 @@ export const TechDocsHome = () => { subtitle="Documentation available in Backstage" > - - navigate('/docs/mkdocs')} - tags={['Developer Tool']} - title="MkDocs" - label="Read Docs" - description="MkDocs is a fast, simple and downright gorgeous static site generator that's geared towards building project documentation. " - /> - - - navigate('/docs/backstage-microsite')} - tags={['Service']} - title="Backstage" - label="Read Docs" - description="Getting started guides, API Overview, documentation around how to Create a Plugin and more. " - /> - + {documentationSites.map((site: DocumentationSite, index: number) => ( + + navigate(site.path)} + tags={site.tags} + title={site.title} + label={site.btnLabel} + description={site.description} + /> + + ))} From eaf1d0a09dc4e08b6526b8af5c9b9a5e9d6e4739 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 3 Jul 2020 11:51:19 +0200 Subject: [PATCH 57/71] fix(techdocs-404): throw error --- plugins/techdocs/src/reader/components/Reader.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index aa42eaffc7..72ef752e34 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -34,7 +34,9 @@ import { TechDocsPageWrapper } from './TechDocsPageWrapper'; const useFetch = (url: string) => { const state = useAsync(async () => { const response = await fetch(url); - if (response.status === 404) return '404'; + if (response.status === 404) { + return new Error('Page not found'); + } const raw = await response.text(); return raw; }, [url]); @@ -116,7 +118,7 @@ export const Reader = () => { } }, [shadowDomRef, state, componentId, path, navigate]); - if (state.value === '404') return ; + if (state.value instanceof Error) return ; return ( <> From 8945b51114f3996ce641c1aebdd9ec29dfb9c876 Mon Sep 17 00:00:00 2001 From: Emma Indal Date: Fri, 3 Jul 2020 12:06:14 +0200 Subject: [PATCH 58/71] fix(useFetch): use AsyncState type --- plugins/techdocs/src/reader/components/Reader.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 72ef752e34..995d9dc99f 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -17,6 +17,8 @@ import React from 'react'; import { useShadowDom } from '..'; import { useAsync } from 'react-use'; +import { AsyncState } from 'react-use/lib/useAsync'; + import { useLocation, useParams, useNavigate } from 'react-router-dom'; import transformer, { @@ -31,7 +33,7 @@ import URLFormatter from '../urlFormatter'; import { TechDocsNotFound } from './TechDocsNotFound'; import { TechDocsPageWrapper } from './TechDocsPageWrapper'; -const useFetch = (url: string) => { +const useFetch = (url: string): AsyncState => { const state = useAsync(async () => { const response = await fetch(url); if (response.status === 404) { @@ -69,7 +71,11 @@ export const Reader = () => { React.useEffect(() => { const divElement = shadowDomRef.current; - if (divElement?.shadowRoot && state.value) { + if ( + divElement?.shadowRoot && + state.value && + !(state.value instanceof Error) + ) { const transformedElement = transformer(state.value, [ addBaseUrl({ docStorageURL, From 468ca0f9b3388878a30f05996d81af0351f943ce Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Fri, 3 Jul 2020 16:03:38 +0200 Subject: [PATCH 59/71] fix(techdocs): fixed race condition between page loads --- .../techdocs/src/reader/components/Reader.tsx | 21 ++++++++----- .../techdocs/src/reader/hooks/shadowDom.ts | 30 ++++++++++++++----- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 995d9dc99f..bd7ac17a6a 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -35,15 +35,22 @@ import { TechDocsPageWrapper } from './TechDocsPageWrapper'; const useFetch = (url: string): AsyncState => { const state = useAsync(async () => { - const response = await fetch(url); - if (response.status === 404) { - return new Error('Page not found'); + const request = await fetch(url); + if (request.status === 404) { + return [request.url, new Error('Page not found')]; } - const raw = await response.text(); - return raw; + const response = await request.text(); + return [request.url, response]; }, [url]); - return state; + const [fetchedUrl, fetchedValue] = state.value ?? []; + + if (url !== fetchedUrl) { + // Fixes a race condition between two pages + return { loading: true }; + } + + return Object.assign(state, fetchedValue ? { value: fetchedValue } : {}); }; const useEnforcedTrailingSlash = (): void => { @@ -60,7 +67,7 @@ const useEnforcedTrailingSlash = (): void => { export const Reader = () => { const location = useLocation(); const { componentId, '*': path } = useParams(); - const shadowDomRef = useShadowDom(); + const [ref, shadowRoot] = useShadowDom(componentId, path); const navigate = useNavigate(); const normalizedUrl = new URLFormatter( `${docStorageURL}${location.pathname.replace('/docs', '')}`, diff --git a/plugins/techdocs/src/reader/hooks/shadowDom.ts b/plugins/techdocs/src/reader/hooks/shadowDom.ts index 5edb8f4e4d..90b287e343 100644 --- a/plugins/techdocs/src/reader/hooks/shadowDom.ts +++ b/plugins/techdocs/src/reader/hooks/shadowDom.ts @@ -17,14 +17,30 @@ import { useEffect, useRef } from 'react'; import type { RefObject } from 'react'; -type IShadowDOMRefObject = RefObject; -export const useShadowDom: () => IShadowDOMRefObject = () => { - const ref: IShadowDOMRefObject = useRef(null); +type IUseShadowDOM = ( + componentId: string, + path: string, +) => [RefObject, ShadowRoot?]; + +export const useShadowDom: IUseShadowDOM = (componentId, path) => { + const ref = useRef(null); useEffect(() => { - const divElement = ref.current; - divElement?.attachShadow({ mode: 'open' }); - }, [ref]); + const innerDivElement = document.createElement('div'); + innerDivElement.attachShadow({ mode: 'open' }); - return ref; + const outerDivElement = ref.current; + outerDivElement?.appendChild(innerDivElement); + + return function cancel() { + outerDivElement?.removeChild(innerDivElement); + }; + }, [componentId, path]); + + const shadowRoot = + ref.current?.children && ref.current?.children[0].shadowRoot + ? ref.current?.children[0].shadowRoot + : undefined; + + return [ref, shadowRoot]; }; From 96c24c687b4e3e32461a97531aac9acc68dd91fb Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Fri, 3 Jul 2020 16:04:00 +0200 Subject: [PATCH 60/71] chore(techdocs): updated tests --- .../src/reader/hooks/shadowDom.test.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx b/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx index 1d58bf26e7..b8a6a78c1f 100644 --- a/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx +++ b/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx @@ -19,26 +19,31 @@ import { renderWithEffects } from '@backstage/test-utils'; import { useShadowDom } from './shadowDom'; const ComponentWithoutHook = () => { - return
; + return
; }; const ComponentWithHook = () => { - const ref = useShadowDom(); - return
; + const [ref] = useShadowDom('mkdocs', 'about/license/'); + return
; }; describe('useShadowDom', () => { it('does not create a Shadow DOM instance', async () => { const rendered = await renderWithEffects(); - const divElement = rendered.getByTestId('shadow-dom'); - expect(divElement.shadowRoot).not.toBeInstanceOf(ShadowRoot); + const outerDivElement = rendered.getByTestId('root'); + expect(outerDivElement.shadowRoot).not.toBeInstanceOf(ShadowRoot); + expect(outerDivElement.children.length).toBe(0); }); it('create a Shadow DOM instance', async () => { const rendered = await renderWithEffects(); - const divElement = rendered.getByTestId('shadow-dom'); - expect(divElement.shadowRoot).toBeInstanceOf(ShadowRoot); + const outerDivElement = rendered.getByTestId('root'); + expect(outerDivElement.shadowRoot).not.toBeInstanceOf(ShadowRoot); + expect(outerDivElement.children.length).toBe(1); + + const innerDivElement = outerDivElement.children[0]; + expect(innerDivElement.shadowRoot).toBeInstanceOf(ShadowRoot); }); }); From 95ebc8eba9e44ba2c19a56552bee2b4db054c6a2 Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Fri, 3 Jul 2020 16:04:26 +0200 Subject: [PATCH 61/71] fix: moved useEnforcingTrailingSlash upper in the hook chain --- plugins/techdocs/src/reader/components/Reader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index bd7ac17a6a..d3aadae6aa 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -65,6 +65,8 @@ const useEnforcedTrailingSlash = (): void => { }; export const Reader = () => { + useEnforcedTrailingSlash(); + const location = useLocation(); const { componentId, '*': path } = useParams(); const [ref, shadowRoot] = useShadowDom(componentId, path); @@ -74,8 +76,6 @@ export const Reader = () => { ).formatBaseURL(); const state = useFetch(`${normalizedUrl}index.html`); - useEnforcedTrailingSlash(); - React.useEffect(() => { const divElement = shadowDomRef.current; if ( From 52f4d37b8ea9ad212dba5fb0dc640131547eeab9 Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Fri, 3 Jul 2020 16:05:12 +0200 Subject: [PATCH 62/71] fix(techdocs): cleaned up useEffect in Reader --- .../techdocs/src/reader/components/Reader.tsx | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index d3aadae6aa..31ccbbb57a 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -77,66 +77,71 @@ export const Reader = () => { const state = useFetch(`${normalizedUrl}index.html`); React.useEffect(() => { - const divElement = shadowDomRef.current; - if ( - divElement?.shadowRoot && - state.value && - !(state.value instanceof Error) - ) { - const transformedElement = transformer(state.value, [ - addBaseUrl({ - docStorageURL, - componentId, - path, - }), - rewriteDocLinks(), - modifyCss({ - cssTransforms: { - '.md-main__inner': [{ 'margin-top': '0' }], - '.md-sidebar': [{ top: '0' }, { width: '20rem' }], - '.md-typeset': [{ 'font-size': '1rem' }], - '.md-nav': [{ 'font-size': '1rem' }], - '.md-grid': [{ 'max-width': '80vw' }], - }, - }), - removeMkdocsHeader(), - ]); - - divElement.shadowRoot.innerHTML = ''; - - if (transformedElement) { - divElement.shadowRoot.appendChild(transformedElement); - transformer(divElement.shadowRoot.children[0], [ - dom => { - setTimeout(() => { - if (window.location.hash) { - const hash = window.location.hash.slice(1); - divElement.shadowRoot?.getElementById(hash)?.scrollIntoView(); - } - }, 200); - return dom; - }, - addLinkClickListener({ - onClick: (_: MouseEvent, url: string) => { - const parsedUrl = new URL(url); - navigate(`${parsedUrl.pathname}${parsedUrl.hash}`); - - divElement.shadowRoot - ?.querySelector(parsedUrl.hash) - ?.scrollIntoView(); - }, - }), - ]); - } + if (!shadowRoot) { + return; // Shadow DOM isn't ready } - }, [shadowDomRef, state, componentId, path, navigate]); + + if (state.loading) { + return; // Page isn't ready + } + + // Pre-render + const transformedElement = transformer(state.value as string, [ + addBaseUrl({ + docStorageURL, + componentId, + path, + }), + rewriteDocLinks(), + modifyCss({ + cssTransforms: { + '.md-main__inner': [{ 'margin-top': '0' }], + '.md-sidebar': [{ top: '0' }, { width: '20rem' }], + '.md-typeset': [{ 'font-size': '1rem' }], + '.md-nav': [{ 'font-size': '1rem' }], + '.md-grid': [{ 'max-width': '80vw' }], + }, + }), + removeMkdocsHeader(), + ]); + + if (!transformedElement) { + return; // An unexpected error occurred + } + + Array.from(shadowRoot.children).forEach(child => + shadowRoot.removeChild(child), + ); + shadowRoot.appendChild(transformedElement); + + // Post-render + transformer(shadowRoot.children[0], [ + dom => { + setTimeout(() => { + if (window.location.hash) { + const hash = window.location.hash.slice(1); + shadowRoot?.getElementById(hash)?.scrollIntoView(); + } + }, 200); + return dom; + }, + addLinkClickListener({ + onClick: (_: MouseEvent, url: string) => { + const parsedUrl = new URL(url); + navigate(`${parsedUrl.pathname}${parsedUrl.hash}`); + + shadowRoot?.querySelector(parsedUrl.hash)?.scrollIntoView(); + }, + }), + ]); + }, [componentId, path, shadowRoot, state]); // eslint-disable-line react-hooks/exhaustive-deps if (state.value instanceof Error) return ; return ( <> -
+
); From a662bc1639cfb91c19849481d0c37b6bd11b14ad Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Fri, 3 Jul 2020 16:18:34 +0200 Subject: [PATCH 63/71] fix: remove re-creating new shadow-dom on page change --- .../techdocs/src/reader/components/Reader.tsx | 2 +- .../src/reader/hooks/shadowDom.test.tsx | 15 ++++------- .../techdocs/src/reader/hooks/shadowDom.ts | 27 +++++-------------- 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 31ccbbb57a..4f89b9296f 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -69,7 +69,7 @@ export const Reader = () => { const location = useLocation(); const { componentId, '*': path } = useParams(); - const [ref, shadowRoot] = useShadowDom(componentId, path); + const [ref, shadowRoot] = useShadowDom(); const navigate = useNavigate(); const normalizedUrl = new URLFormatter( `${docStorageURL}${location.pathname.replace('/docs', '')}`, diff --git a/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx b/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx index b8a6a78c1f..31956ccb06 100644 --- a/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx +++ b/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx @@ -23,7 +23,7 @@ const ComponentWithoutHook = () => { }; const ComponentWithHook = () => { - const [ref] = useShadowDom('mkdocs', 'about/license/'); + const [ref] = useShadowDom(); return
; }; @@ -31,19 +31,14 @@ describe('useShadowDom', () => { it('does not create a Shadow DOM instance', async () => { const rendered = await renderWithEffects(); - const outerDivElement = rendered.getByTestId('root'); - expect(outerDivElement.shadowRoot).not.toBeInstanceOf(ShadowRoot); - expect(outerDivElement.children.length).toBe(0); + const divElement = rendered.getByTestId('root'); + expect(divElement.shadowRoot).not.toBeInstanceOf(ShadowRoot); }); it('create a Shadow DOM instance', async () => { const rendered = await renderWithEffects(); - const outerDivElement = rendered.getByTestId('root'); - expect(outerDivElement.shadowRoot).not.toBeInstanceOf(ShadowRoot); - expect(outerDivElement.children.length).toBe(1); - - const innerDivElement = outerDivElement.children[0]; - expect(innerDivElement.shadowRoot).toBeInstanceOf(ShadowRoot); + const divElement = rendered.getByTestId('root'); + expect(divElement.shadowRoot).toBeInstanceOf(ShadowRoot); }); }); diff --git a/plugins/techdocs/src/reader/hooks/shadowDom.ts b/plugins/techdocs/src/reader/hooks/shadowDom.ts index 90b287e343..93e4c1d375 100644 --- a/plugins/techdocs/src/reader/hooks/shadowDom.ts +++ b/plugins/techdocs/src/reader/hooks/shadowDom.ts @@ -17,30 +17,15 @@ import { useEffect, useRef } from 'react'; import type { RefObject } from 'react'; -type IUseShadowDOM = ( - componentId: string, - path: string, -) => [RefObject, ShadowRoot?]; +type IUseShadowDOM = () => [RefObject, ShadowRoot?]; -export const useShadowDom: IUseShadowDOM = (componentId, path) => { +export const useShadowDom: IUseShadowDOM = () => { const ref = useRef(null); useEffect(() => { - const innerDivElement = document.createElement('div'); - innerDivElement.attachShadow({ mode: 'open' }); + const divElement = ref.current; + divElement?.attachShadow({ mode: 'open' }); + }, []); - const outerDivElement = ref.current; - outerDivElement?.appendChild(innerDivElement); - - return function cancel() { - outerDivElement?.removeChild(innerDivElement); - }; - }, [componentId, path]); - - const shadowRoot = - ref.current?.children && ref.current?.children[0].shadowRoot - ? ref.current?.children[0].shadowRoot - : undefined; - - return [ref, shadowRoot]; + return [ref, ref.current?.shadowRoot || undefined]; }; From 07e5d2ce8a3943f8884989d721c6aee5879a2902 Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Fri, 3 Jul 2020 16:25:09 +0200 Subject: [PATCH 64/71] chore: revert data-testid back to shadow-dom --- plugins/techdocs/src/reader/hooks/shadowDom.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx b/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx index 31956ccb06..c4fab0dbc6 100644 --- a/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx +++ b/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx @@ -19,26 +19,26 @@ import { renderWithEffects } from '@backstage/test-utils'; import { useShadowDom } from './shadowDom'; const ComponentWithoutHook = () => { - return
; + return
; }; const ComponentWithHook = () => { const [ref] = useShadowDom(); - return
; + return
; }; describe('useShadowDom', () => { it('does not create a Shadow DOM instance', async () => { const rendered = await renderWithEffects(); - const divElement = rendered.getByTestId('root'); + const divElement = rendered.getByTestId('shadow-dom'); expect(divElement.shadowRoot).not.toBeInstanceOf(ShadowRoot); }); it('create a Shadow DOM instance', async () => { const rendered = await renderWithEffects(); - const divElement = rendered.getByTestId('root'); + const divElement = rendered.getByTestId('shadow-dom'); expect(divElement.shadowRoot).toBeInstanceOf(ShadowRoot); }); }); From 9e4c092d6b0ed6735c4371b34a8303ef6c7274df Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Fri, 3 Jul 2020 16:26:54 +0200 Subject: [PATCH 65/71] chore: ref to shadowDomRef --- plugins/techdocs/src/reader/components/Reader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 4f89b9296f..42be2732d0 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -69,7 +69,7 @@ export const Reader = () => { const location = useLocation(); const { componentId, '*': path } = useParams(); - const [ref, shadowRoot] = useShadowDom(); + const [shadowDomRef, shadowRoot] = useShadowDom(); const navigate = useNavigate(); const normalizedUrl = new URLFormatter( `${docStorageURL}${location.pathname.replace('/docs', '')}`, @@ -141,7 +141,7 @@ export const Reader = () => { return ( <> -
+
); From 8b1d4bd9374446077ab30f479c9ac088afd3189d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Suzana=20Ferreira?= Date: Sun, 5 Jul 2020 07:21:22 +0100 Subject: [PATCH 66/71] Fixes SupportButton console warning (#1530) --- .../SupportButton/SupportButton.test.tsx | 66 +++++++++++++++++++ .../SupportButton/SupportButton.tsx | 7 +- 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/components/SupportButton/SupportButton.test.tsx diff --git a/packages/core/src/components/SupportButton/SupportButton.test.tsx b/packages/core/src/components/SupportButton/SupportButton.test.tsx new file mode 100644 index 0000000000..6d6b9fd477 --- /dev/null +++ b/packages/core/src/components/SupportButton/SupportButton.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2020 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 { + render, + act, + RenderResult, + waitFor, + fireEvent, +} from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { SupportButton } from './SupportButton'; + +const SUPPORT_BUTTON_ID = 'support-button'; +const POPOVER_ID = 'support-button-popover'; + +describe('', () => { + it('renders without exploding', async () => { + let renderResult: RenderResult; + + await act(async () => { + renderResult = render(wrapInTestApp()); + }); + + await waitFor(() => + expect(renderResult.getByTestId(SUPPORT_BUTTON_ID)).toBeInTheDocument(), + ); + }); + + it('shows popover on click', async () => { + let renderResult: RenderResult; + + await act(async () => { + renderResult = render(wrapInTestApp()); + }); + + let button: HTMLElement; + + await waitFor(() => { + expect(renderResult.getByTestId(SUPPORT_BUTTON_ID)).toBeInTheDocument(); + button = renderResult.getByTestId(SUPPORT_BUTTON_ID); + }); + + await act(async () => { + fireEvent.click(button); + }); + + await waitFor(() => { + expect(renderResult.getByTestId(POPOVER_ID)).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/core/src/components/SupportButton/SupportButton.tsx b/packages/core/src/components/SupportButton/SupportButton.tsx index f8c09f5073..537ee3d6ea 100644 --- a/packages/core/src/components/SupportButton/SupportButton.tsx +++ b/packages/core/src/components/SupportButton/SupportButton.tsx @@ -87,6 +87,7 @@ export const SupportButton: FC = ({ Support = ({ Support} secondary={
{slackChannels.map((channel, i) => ( @@ -137,7 +139,8 @@ export const SupportButton: FC = ({ Contact} secondary={
{contactEmails.map((em, index) => ( From 2ff4071209a5d0713ee8082578611ae74f6f98d8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2020 14:27:58 +0200 Subject: [PATCH 67/71] build(deps): bump @testing-library/react from 10.4.1 to 10.4.3 (#1480) Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 10.4.1 to 10.4.3. - [Release notes](https://github.com/testing-library/react-testing-library/releases) - [Changelog](https://github.com/testing-library/react-testing-library/blob/master/CHANGELOG.md) - [Commits](https://github.com/testing-library/react-testing-library/compare/v10.4.1...v10.4.3) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 626df67178..143c2569f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3137,9 +3137,9 @@ "@types/testing-library__react-hooks" "^3.0.0" "@testing-library/react@^10.4.1": - version "10.4.1" - resolved "https://registry.npmjs.org/@testing-library/react/-/react-10.4.1.tgz#d38dee4abab172c06f6cf8894c51190e6c503f61" - integrity sha512-QX31fRDGLnOdBYoQ95VEOYgRahaPfsI+toOaYhlvuGNFQrcagZv/KLWCIctRGB0h1PTsQt3JpLBbbLGM63yy5Q== + version "10.4.3" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-10.4.3.tgz#c6f356688cffc51f6b35385583d664bb11a161f4" + integrity sha512-A/ydYXcwAcfY7vkPrfUkUTf9HQLL3/GtixTefcu3OyGQtAYQ7XBQj1S9FWbLEhfWa0BLwFwTBFS3Ao1O0tbMJg== dependencies: "@babel/runtime" "^7.10.3" "@testing-library/dom" "^7.17.1" From 33cc696090091519a0761cb5543b4baa5395f22a Mon Sep 17 00:00:00 2001 From: Jannik Hell Date: Sun, 5 Jul 2020 18:23:55 +0200 Subject: [PATCH 68/71] Fix path to example app directory --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f3740c36d..2d7499e03d 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Our vision for Backstage is for it to become the trusted standard toolbox (read: The Backstage platform consists of a number of different components: -- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/example-app`) and you can easily get started with your own app by [creating one](docs/create-an-app.md). +- **app** - Main web application that users interact with. It's built up by a number of different _Plugins_. This repo contains an example implementation of an app (located in `packages/app`) and you can easily get started with your own app by [creating one](docs/create-an-app.md). - [**plugins**](https://github.com/spotify/backstage/tree/master/plugins) - Each plugin is treated as a self-contained web app and can include almost any type of content. Plugins all use a common set of platform API's and reusable UI components. Plugins can fetch data either from the _backend_ or through any RESTful API exposed through the _proxy_. - [**service catalog**](https://github.com/spotify/backstage/tree/master/packages/backend) - Service that holds the model of your software ecosystem, including organisational information and what team owns what software. The backend also has a Plugin model for extending its graph. - **proxy** \* - Terminates HTTPS and exposes any RESTful API to Plugins. From 55982ba3e9d82852ae4b075e48f6e6fe4c44e768 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Jul 2020 11:14:29 +0200 Subject: [PATCH 69/71] chore(scaffolder): moving the mocks up a level --- .../{stages/publish => }/__mocks__/@octokit/rest/index.ts | 0 .../scaffolder/{stages/publish => }/__mocks__/nodegit/index.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename plugins/scaffolder-backend/src/scaffolder/{stages/publish => }/__mocks__/@octokit/rest/index.ts (100%) rename plugins/scaffolder-backend/src/scaffolder/{stages/publish => }/__mocks__/nodegit/index.ts (100%) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/@octokit/rest/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/@octokit/rest/index.ts rename to plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/nodegit/index.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/stages/publish/__mocks__/nodegit/index.ts rename to plugins/scaffolder-backend/src/scaffolder/__mocks__/nodegit/index.ts From 7693a8d484e27809a0972d1a8426ffc7f431ca1e Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Jul 2020 11:15:53 +0200 Subject: [PATCH 70/71] chore(scaffolder): fixing linting --- .../src/scaffolder/stages/templater/cookiecutter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index 3e94d67c6e..1f2ae56959 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -18,6 +18,7 @@ import { JsonValue } from '@backstage/config'; import { runDockerContainer } from './helpers'; import { TemplaterBase, TemplaterRunOptions } from '.'; import path from 'path'; + export class CookieCutter implements TemplaterBase { private async fetchTemplateCookieCutter( directory: string, From 66016baf5c7876847fe13a33f34821dc9774fefd Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 6 Jul 2020 11:34:46 +0200 Subject: [PATCH 71/71] chore(scaffolder): fixing typescript woes --- .../src/scaffolder/stages/templater/cookiecutter.test.ts | 4 ++-- .../src/scaffolder/stages/templater/cookiecutter.ts | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts index 27e4cd4b01..31bcdf189b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts @@ -131,13 +131,13 @@ describe('CookieCutter Templater', () => { component_id: 'newthing', }; - const returnPath = await cookie.run({ + const { resultDir } = await cookie.run({ directory: tempdir, values, dockerClient: mockDocker, }); - expect(returnPath.startsWith(`${tempdir}-result`)).toBeTruthy(); + expect(resultDir.startsWith(`${tempdir}-result`)).toBeTruthy(); }); it('should pass through the streamer to the run docker helper', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index 1f2ae56959..fff349cf82 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -18,6 +18,7 @@ import { JsonValue } from '@backstage/config'; import { runDockerContainer } from './helpers'; import { TemplaterBase, TemplaterRunOptions } from '.'; import path from 'path'; +import { TemplaterRunResult } from './types'; export class CookieCutter implements TemplaterBase { private async fetchTemplateCookieCutter( @@ -34,7 +35,7 @@ export class CookieCutter implements TemplaterBase { } } - public async run(options: TemplaterRunOptions): Promise { + public async run(options: TemplaterRunOptions): Promise { // First lets grab the default cookiecutter.json file const cookieCutterJson = await this.fetchTemplateCookieCutter( options.directory, @@ -66,6 +67,8 @@ export class CookieCutter implements TemplaterBase { dockerClient: options.dockerClient, }); - return path.resolve(resultDir, options.values.component_id as string); + return { + resultDir: path.resolve(resultDir, options.values.component_id as string), + }; } }